}
}
+ /**
+ * Creates an object that consumes code points one at a time and returns intermediate prefix
+ * matches. Returns null if no match exists.
+ *
+ * @return An instance of {@link ParseState}, or null if the starting code point is not a
+ * prefix for any entry in the trie.
+ */
+ public ParseState openParseState(int startingCp) {
+ // Check to see whether this is a valid starting character. If not, return null.
+ if (_ignoreCase) {
+ startingCp = UCharacter.foldCase(startingCp, true);
+ }
+ int count = Character.charCount(startingCp);
+ char ch1 = (count == 1) ? (char) startingCp : Character.highSurrogate(startingCp);
+ if (!_root.hasChildFor(ch1)) {
+ return null;
+ }
+
+ return new ParseState(_root);
+ }
+
+ /**
+ * ParseState is mutable, not thread-safe, and intended to be used internally by parsers for
+ * consuming values from this trie.
+ */
+ public class ParseState {
+ private Node node;
+ private int offset;
+ private Node.StepResult result;
+
+ ParseState(Node start) {
+ node = start;
+ offset = 0;
+ result = start.new StepResult();
+ }
+
+ /**
+ * Consumes a code point and walk to the next node in the trie.
+ *
+ * @param cp The code point to consume.
+ */
+ public void accept(int cp) {
+ assert node != null;
+ if (_ignoreCase) {
+ cp = UCharacter.foldCase(cp, true);
+ }
+ int count = Character.charCount(cp);
+ char ch1 = (count == 1) ? (char) cp : Character.highSurrogate(cp);
+ node.takeStep(ch1, offset, result);
+ if (count == 2 && result.node != null) {
+ char ch2 = Character.lowSurrogate(cp);
+ result.node.takeStep(ch2, result.offset, result);
+ }
+ node = result.node;
+ offset = result.offset;
+ }
+
+ /**
+ * Gets the exact prefix matches for all code points that have been consumed so far.
+ *
+ * @return The matches.
+ */
+ public Iterator<V> getCurrentMatches() {
+ if (node != null && offset == node.charCount()) {
+ return node.values();
+ }
+ return null;
+ }
+
+ /**
+ * Checks whether any more code points can be consumed.
+ *
+ * @return true if no more code points can be consumed; false otherwise.
+ */
+ public boolean atEnd() {
+ return node == null || (node.charCount() == offset && node._children == null);
+ }
+ }
+
public static class CharIterator implements Iterator<Character> {
private boolean _ignoreCase;
private CharSequence _text;
_children = children;
}
+ public int charCount() {
+ return _text == null ? 0 : _text.length;
+ }
+
+ public boolean hasChildFor(char ch) {
+ for (int i=0; _children != null && i < _children.size(); i++) {
+ Node child = _children.get(i);
+ if (ch < child._text[0]) break;
+ if (ch == child._text[0]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public Iterator<V> values() {
if (_values == null) {
return null;
return match;
}
+ public class StepResult {
+ public Node node;
+ public int offset;
+ }
+ public void takeStep(char ch, int offset, StepResult result) {
+ assert offset <= charCount();
+ if (offset == charCount()) {
+ // Go to a child node
+ for (int i=0; _children != null && i < _children.size(); i++) {
+ Node child = _children.get(i);
+ if (ch < child._text[0]) break;
+ if (ch == child._text[0]) {
+ // Found a matching child node
+ result.node = child;
+ result.offset = 1;
+ return;
+ }
+ }
+ // No matching children; fall through
+ } else if (_text[offset] == ch) {
+ // Return to this node; increase offset
+ result.node = this;
+ result.offset = offset + 1;
+ return;
+ }
+ // No matches
+ result.node = null;
+ result.offset = -1;
+ return;
+ }
+
private void add(char[] text, int offset, V value) {
if (text.length == offset) {
_values = addValue(_values, value);
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/**
+ * Performs manipulations on affix patterns: the prefix and suffix strings associated with a decimal
+ * format pattern. For example:
+ *
+ * <table>
+ * <tr><th>Affix Pattern</th><th>Example Unescaped (Formatted) String</th></tr>
+ * <tr><td>abc</td><td>abc</td></tr>
+ * <tr><td>ab-</td><td>ab−</td></tr>
+ * <tr><td>ab'-'</td><td>ab-</td></tr>
+ * <tr><td>ab''</td><td>ab'</td></tr>
+ * </table>
+ *
+ * To manually iterate over tokens in a literal string, use the following pattern, which is designed
+ * to be efficient.
+ *
+ * <pre>
+ * long tag = 0L;
+ * while (AffixPatternUtils.hasNext(tag, patternString)) {
+ * tag = AffixPatternUtils.nextToken(tag, patternString);
+ * int typeOrCp = AffixPatternUtils.getTypeOrCp(tag);
+ * switch (typeOrCp) {
+ * case AffixPatternUtils.TYPE_MINUS_SIGN:
+ * // Current token is a minus sign.
+ * break;
+ * case AffixPatternUtils.TYPE_PLUS_SIGN:
+ * // Current token is a plus sign.
+ * break;
+ * case AffixPatternUtils.TYPE_PERCENT:
+ * // Current token is a percent sign.
+ * break;
+ * case AffixPatternUtils.TYPE_PERMILLE:
+ * // Current token is a permille sign.
+ * break;
+ * case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
+ * // Current token is a single currency sign.
+ * break;
+ * case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+ * // Current token is a double currency sign.
+ * break;
+ * case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+ * // Current token is a triple currency sign.
+ * break;
+ * case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
+ * // Current token has four or more currency signs.
+ * break;
+ * default:
+ * // Current token is an arbitrary code point.
+ * // The variable typeOrCp is the code point.
+ * break;
+ * }
+ * }
+ * </pre>
+ */
+public class AffixPatternUtils {
+
+ private static final int STATE_BASE = 0;
+ private static final int STATE_FIRST_QUOTE = 1;
+ private static final int STATE_INSIDE_QUOTE = 2;
+ private static final int STATE_AFTER_QUOTE = 3;
+ private static final int STATE_FIRST_CURR = 4;
+ private static final int STATE_SECOND_CURR = 5;
+ private static final int STATE_THIRD_CURR = 6;
+ private static final int STATE_OVERFLOW_CURR = 7;
+
+ private static final int TYPE_CODEPOINT = 0;
+
+ /** Represents a minus sign symbol '-'. */
+ public static final int TYPE_MINUS_SIGN = -1;
+
+ /** Represents a plus sign symbol '+'. */
+ public static final int TYPE_PLUS_SIGN = -2;
+
+ /** Represents a percent sign symbol '%'. */
+ public static final int TYPE_PERCENT = -3;
+
+ /** Represents a permille sign symbol '‰'. */
+ public static final int TYPE_PERMILLE = -4;
+
+ /** Represents a single currency symbol '¤'. */
+ public static final int TYPE_CURRENCY_SINGLE = -5;
+
+ /** Represents a double currency symbol '¤¤'. */
+ public static final int TYPE_CURRENCY_DOUBLE = -6;
+
+ /** Represents a triple currency symbol '¤¤¤'. */
+ public static final int TYPE_CURRENCY_TRIPLE = -7;
+
+ /** Represents a sequence of four or more currency symbols. */
+ public static final int TYPE_CURRENCY_OVERFLOW = -15;
+
+ /**
+ * Checks whether the specified affix pattern has any unquoted currency symbols ("¤").
+ *
+ * @param patternString The string to check for currency symbols.
+ * @return true if the literal has at least one unquoted currency symbol; false otherwise.
+ */
+ public static boolean hasCurrencySymbols(CharSequence patternString) {
+ if (patternString == null) return false;
+ int offset = 0;
+ int state = STATE_BASE;
+ boolean result = false;
+ for (; offset < patternString.length(); ) {
+ int cp = Character.codePointAt(patternString, offset);
+ switch (state) {
+ case STATE_BASE:
+ if (cp == '¤') {
+ result = true;
+ } else if (cp == '\'') {
+ state = STATE_INSIDE_QUOTE;
+ }
+ break;
+ case STATE_INSIDE_QUOTE:
+ if (cp == '\'') {
+ state = STATE_BASE;
+ }
+ break;
+ default:
+ throw new AssertionError();
+ }
+ offset += Character.charCount(cp);
+ }
+
+ if (state == STATE_INSIDE_QUOTE) {
+ throw new IllegalArgumentException("Unterminated quote: \"" + patternString + "\"");
+ } else {
+ return result;
+ }
+ }
+
+ /**
+ * Estimates the number of code points present in an unescaped version of the affix pattern string
+ * (one that would be returned by {@link #unescape}), assuming that all interpolated symbols
+ * consume one code point and that currencies consume as many code points as their symbol width.
+ * Used for computing padding width.
+ *
+ * @param patternString The original string whose width will be estimated.
+ * @return The length of the unescaped string.
+ */
+ public static int unescapedLength(CharSequence patternString) {
+ if (patternString == null) return 0;
+ int state = STATE_BASE;
+ int offset = 0;
+ int length = 0;
+ for (; offset < patternString.length(); ) {
+ int cp = Character.codePointAt(patternString, offset);
+
+ switch (state) {
+ case STATE_BASE:
+ if (cp == '\'') {
+ // First quote
+ state = STATE_FIRST_QUOTE;
+ } else {
+ // Unquoted symbol
+ length++;
+ }
+ break;
+ case STATE_FIRST_QUOTE:
+ if (cp == '\'') {
+ // Repeated quote
+ length++;
+ state = STATE_BASE;
+ } else {
+ // Quoted code point
+ length++;
+ state = STATE_INSIDE_QUOTE;
+ }
+ break;
+ case STATE_INSIDE_QUOTE:
+ if (cp == '\'') {
+ // End of quoted sequence
+ state = STATE_AFTER_QUOTE;
+ } else {
+ // Quoted code point
+ length++;
+ }
+ break;
+ case STATE_AFTER_QUOTE:
+ if (cp == '\'') {
+ // Double quote inside of quoted sequence
+ length++;
+ state = STATE_INSIDE_QUOTE;
+ } else {
+ // Unquoted symbol
+ length++;
+ }
+ break;
+ default:
+ throw new AssertionError();
+ }
+
+ offset += Character.charCount(cp);
+ }
+
+ switch (state) {
+ case STATE_FIRST_QUOTE:
+ case STATE_INSIDE_QUOTE:
+ throw new IllegalArgumentException("Unterminated quote: \"" + patternString + "\"");
+ default:
+ break;
+ }
+
+ return length;
+ }
+
+ /**
+ * Takes a string and escapes (quotes) characters that have special meaning in the affix pattern
+ * syntax. This function does not reverse-lookup symbols.
+ *
+ * <p>Example input: "-$x"; example output: "'-'$x"
+ *
+ * @param input The string to be escaped.
+ * @param output The string builder to which to append the escaped string.
+ * @return The number of chars (UTF-16 code units) appended to the output.
+ */
+ public static int escape(CharSequence input, StringBuilder output) {
+ if (input == null) return 0;
+ int state = STATE_BASE;
+ int offset = 0;
+ int startLength = output.length();
+ for (; offset < input.length(); ) {
+ int cp = Character.codePointAt(input, offset);
+
+ switch (cp) {
+ case '\'':
+ output.append("''");
+ break;
+
+ case '-':
+ case '+':
+ case '%':
+ case '‰':
+ case '¤':
+ if (state == STATE_BASE) {
+ output.append('\'');
+ output.appendCodePoint(cp);
+ state = STATE_INSIDE_QUOTE;
+ } else {
+ output.appendCodePoint(cp);
+ }
+ break;
+
+ default:
+ if (state == STATE_INSIDE_QUOTE) {
+ output.append('\'');
+ output.appendCodePoint(cp);
+ state = STATE_BASE;
+ } else {
+ output.appendCodePoint(cp);
+ }
+ break;
+ }
+ offset += Character.charCount(cp);
+ }
+
+ if (state == STATE_INSIDE_QUOTE) {
+ output.append('\'');
+ }
+
+ return output.length() - startLength;
+ }
+
+ /**
+ * Executes the unescape state machine. Replaces the unquoted characters "-", "+", "%", and "‰"
+ * with their localized equivalents. Replaces "¤", "¤¤", and "¤¤¤" with the three argument
+ * strings.
+ *
+ * <p>Example input: "'-'¤x"; example output: "-$x"
+ *
+ * @param affixPattern The original string to be unescaped.
+ * @param symbols An instance of {@link DecimalFormatSymbols} for the locale of interest.
+ * @param currency1 The string to replace "¤".
+ * @param currency2 The string to replace "¤¤".
+ * @param currency3 The string to replace "¤¤¤".
+ * @param minusSign The string to replace "-". If null, symbols.getMinusSignString() is used.
+ * @param output The {@link NumberStringBuilder} to which the result will be appended.
+ */
+ public static void unescape(
+ CharSequence affixPattern,
+ DecimalFormatSymbols symbols,
+ String currency1,
+ String currency2,
+ String currency3,
+ String minusSign,
+ NumberStringBuilder output) {
+ if (affixPattern == null || affixPattern.length() == 0) return;
+ if (minusSign == null) minusSign = symbols.getMinusSignString();
+ long tag = 0L;
+ while (hasNext(tag, affixPattern)) {
+ tag = nextToken(tag, affixPattern);
+ int typeOrCp = getTypeOrCp(tag);
+ switch (typeOrCp) {
+ case TYPE_MINUS_SIGN:
+ output.append(minusSign, Field.SIGN);
+ break;
+ case TYPE_PLUS_SIGN:
+ output.append(symbols.getPlusSignString(), Field.SIGN);
+ break;
+ case TYPE_PERCENT:
+ output.append(symbols.getPercentString(), Field.PERCENT);
+ break;
+ case TYPE_PERMILLE:
+ output.append(symbols.getPerMillString(), Field.PERMILLE);
+ break;
+ case TYPE_CURRENCY_SINGLE:
+ output.append(currency1, Field.CURRENCY);
+ break;
+ case TYPE_CURRENCY_DOUBLE:
+ output.append(currency2, Field.CURRENCY);
+ break;
+ case TYPE_CURRENCY_TRIPLE:
+ output.append(currency3, Field.CURRENCY);
+ break;
+ case TYPE_CURRENCY_OVERFLOW:
+ output.append("\uFFFD", Field.CURRENCY);
+ break;
+ default:
+ output.appendCodePoint(typeOrCp, null);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns the next token from the affix pattern.
+ *
+ * @param tag A bitmask used for keeping track of state from token to token. The initial value
+ * should be 0L.
+ * @param patternString The affix pattern.
+ * @return The bitmask tag to pass to the next call of this method to retrieve the following
+ * token.
+ * @throws IllegalArgumentException If there are no tokens left or if there is a syntax error in
+ * the pattern string.
+ * @see #hasNext
+ */
+ public static long nextToken(long tag, CharSequence patternString) {
+ int offset = getOffset(tag);
+ int state = getState(tag);
+ for (; offset < patternString.length(); ) {
+ int cp = Character.codePointAt(patternString, offset);
+ int count = Character.charCount(cp);
+
+ switch (state) {
+ case STATE_BASE:
+ switch (cp) {
+ case '\'':
+ state = STATE_FIRST_QUOTE;
+ offset += count;
+ // continue to the next code point
+ break;
+ case '-':
+ return makeTag(offset + count, TYPE_MINUS_SIGN, STATE_BASE, 0);
+ case '+':
+ return makeTag(offset + count, TYPE_PLUS_SIGN, STATE_BASE, 0);
+ case '%':
+ return makeTag(offset + count, TYPE_PERCENT, STATE_BASE, 0);
+ case '‰':
+ return makeTag(offset + count, TYPE_PERMILLE, STATE_BASE, 0);
+ case '¤':
+ state = STATE_FIRST_CURR;
+ offset += count;
+ // continue to the next code point
+ break;
+ default:
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_BASE, cp);
+ }
+ break;
+ case STATE_FIRST_QUOTE:
+ if (cp == '\'') {
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_BASE, cp);
+ } else {
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+ }
+ case STATE_INSIDE_QUOTE:
+ if (cp == '\'') {
+ state = STATE_AFTER_QUOTE;
+ offset += count;
+ // continue to the next code point
+ break;
+ } else {
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+ }
+ case STATE_AFTER_QUOTE:
+ if (cp == '\'') {
+ return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+ } else {
+ state = STATE_BASE;
+ // re-evaluate this code point
+ break;
+ }
+ case STATE_FIRST_CURR:
+ if (cp == '¤') {
+ state = STATE_SECOND_CURR;
+ offset += count;
+ // continue to the next code point
+ break;
+ } else {
+ return makeTag(offset, TYPE_CURRENCY_SINGLE, STATE_BASE, 0);
+ }
+ case STATE_SECOND_CURR:
+ if (cp == '¤') {
+ state = STATE_THIRD_CURR;
+ offset += count;
+ // continue to the next code point
+ break;
+ } else {
+ return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
+ }
+ case STATE_THIRD_CURR:
+ if (cp == '¤') {
+ state = STATE_OVERFLOW_CURR;
+ offset += count;
+ // continue to the next code point
+ break;
+ } else {
+ return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
+ }
+ case STATE_OVERFLOW_CURR:
+ if (cp == '¤') {
+ offset += count;
+ // continue to the next code point and loop back to this state
+ break;
+ } else {
+ return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
+ }
+ default:
+ throw new AssertionError();
+ }
+ }
+ // End of string
+ switch (state) {
+ case STATE_BASE:
+ // We shouldn't get here if hasNext() was followed.
+ throw new IllegalArgumentException();
+ case STATE_FIRST_QUOTE:
+ case STATE_INSIDE_QUOTE:
+ // For consistent behavior with the JDK and ICU 58, throw an exception here.
+ throw new IllegalArgumentException(
+ "Unterminated quote in pattern affix: \"" + patternString + "\"");
+ case STATE_AFTER_QUOTE:
+ // We shouldn't get here if hasNext() was followed.
+ throw new IllegalArgumentException();
+ case STATE_FIRST_CURR:
+ return makeTag(offset, TYPE_CURRENCY_SINGLE, STATE_BASE, 0);
+ case STATE_SECOND_CURR:
+ return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
+ case STATE_THIRD_CURR:
+ return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
+ case STATE_OVERFLOW_CURR:
+ return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Returns whether the affix pattern string has any more tokens to be retrieved from a call to
+ * {@link #nextToken}.
+ *
+ * @param tag The bitmask tag of the previous token, as returned by {@link #nextToken}.
+ * @param string The affix pattern.
+ * @return true if there are more tokens to consume; false otherwise.
+ */
+ public static boolean hasNext(long tag, CharSequence string) {
+ int state = getState(tag);
+ int offset = getOffset(tag);
+ // Special case: the last character in string is an end quote.
+ if (state == STATE_INSIDE_QUOTE
+ && offset == string.length() - 1
+ && string.charAt(offset) == '\'') {
+ return false;
+ } else if (state != STATE_BASE) {
+ return true;
+ } else {
+ return offset < string.length();
+ }
+ }
+
+ /**
+ * This function helps determine the identity of the token consumed by {@link #nextToken}.
+ * Converts from a bitmask tag, based on a call to {@link #nextToken}, to its corresponding symbol
+ * type or code point.
+ *
+ * @param tag The bitmask tag of the current token, as returned by {@link #nextToken}.
+ * @return If less than zero, a symbol type corresponding to one of the <code>TYPE_</code>
+ * constants, such as {@link #TYPE_MINUS_SIGN}. If greater than or equal to zero, a literal
+ * code point.
+ */
+ public static int getTypeOrCp(long tag) {
+ int type = getType(tag);
+ return (type == 0) ? getCodePoint(tag) : -type;
+ }
+
+ private static long makeTag(int offset, int type, int state, int cp) {
+ long tag = 0L;
+ tag |= offset;
+ tag |= (-(long) type) << 32;
+ tag |= ((long) state) << 36;
+ tag |= ((long) cp) << 40;
+ return tag;
+ }
+
+ static int getOffset(long tag) {
+ return (int) (tag & 0xffffffff);
+ }
+
+ static int getType(long tag) {
+ return (int) ((tag >>> 32) & 0xf);
+ }
+
+ static int getState(long tag) {
+ return (int) ((tag >>> 36) & 0xf);
+ }
+
+ static int getCodePoint(long tag) {
+ return (int) (tag >>> 40);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import com.ibm.icu.impl.number.Format.BeforeTargetAfterFormat;
+import com.ibm.icu.impl.number.Format.SingularFormat;
+import com.ibm.icu.impl.number.Format.TargetFormat;
+import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier;
+import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat;
+import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier;
+import com.ibm.icu.impl.number.formatters.MeasureFormat;
+import com.ibm.icu.impl.number.formatters.PaddingFormat;
+import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import com.ibm.icu.impl.number.formatters.RoundingFormat;
+import com.ibm.icu.impl.number.formatters.ScientificFormat;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.ULocale;
+
+public class Endpoint {
+ // public static Format from(DecimalFormatSymbols symbols, Properties properties)
+ // throws ParseException {
+ // Format format = new PositiveIntegerFormat(symbols, properties);
+ // // TODO: integer-only format
+ // format = new PositiveDecimalFormat((SelfContainedFormat) format, symbols, properties);
+ // if (properties.useCompactDecimalFormat()) {
+ // format = CompactDecimalFormat.getInstance((SingularFormat) format, symbols, properties);
+ // } else {
+ // format =
+ // PositiveNegativeAffixFormat.getInstance((SingularFormat) format, symbols, properties);
+ // }
+ // if (properties.useRoundingInterval()) {
+ // format = new IntervalRoundingFormat((SingularFormat) format, properties);
+ // } else if (properties.useSignificantDigits()) {
+ // format = new SignificantDigitsFormat((SingularFormat) format, properties);
+ // } else if (properties.useFractionFormat()) {
+ // format = new RoundingFormat((SingularFormat) format, properties);
+ // }
+ // return format;
+ // }
+
+ public static Format fromBTA(Properties properties) {
+ return fromBTA(properties, getSymbols());
+ }
+
+ public static SingularFormat fromBTA(Properties properties, Locale locale) {
+ return fromBTA(properties, getSymbols(locale));
+ }
+
+ public static SingularFormat fromBTA(Properties properties, ULocale uLocale) {
+ return fromBTA(properties, getSymbols(uLocale));
+ }
+
+ public static SingularFormat fromBTA(String pattern) {
+ return fromBTA(getProperties(pattern), getSymbols());
+ }
+
+ public static SingularFormat fromBTA(String pattern, Locale locale) {
+ return fromBTA(getProperties(pattern), getSymbols(locale));
+ }
+
+ public static SingularFormat fromBTA(String pattern, ULocale uLocale) {
+ return fromBTA(getProperties(pattern), getSymbols(uLocale));
+ }
+
+ public static SingularFormat fromBTA(String pattern, DecimalFormatSymbols symbols) {
+ return fromBTA(getProperties(pattern), symbols);
+ }
+
+ public static SingularFormat fromBTA(Properties properties, DecimalFormatSymbols symbols) {
+
+ if (symbols == null) throw new IllegalArgumentException("symbols must not be null");
+
+ // TODO: This fast track results in an improvement of about 10ns during formatting. See if
+ // there is a way to implement it more elegantly.
+ boolean canUseFastTrack = true;
+ PluralRules rules = getPluralRules(symbols.getULocale(), properties);
+ BeforeTargetAfterFormat format = new Format.BeforeTargetAfterFormat(rules);
+ TargetFormat target = new PositiveDecimalFormat(symbols, properties);
+ format.setTargetFormat(target);
+ // TODO: integer-only format?
+ if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(MagnitudeMultiplier.getInstance(properties));
+ }
+ if (BigDecimalMultiplier.useMultiplier(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(BigDecimalMultiplier.getInstance(properties));
+ }
+ if (MeasureFormat.useMeasureFormat(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(MeasureFormat.getInstance(symbols, properties));
+ }
+ if (CurrencyFormat.useCurrency(properties)) {
+ canUseFastTrack = false;
+ if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+ format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties));
+ } else if (ScientificFormat.useScientificNotation(properties)) {
+ // TODO: Should the currency rounder or scientific rounder be used in this case?
+ // For now, default to using the scientific rounder.
+ format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+ format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties));
+ } else {
+ format.addBeforeFormat(CurrencyFormat.getCurrencyRounder(symbols, properties));
+ format.addBeforeFormat(CurrencyFormat.getCurrencyModifier(symbols, properties));
+ }
+ } else {
+ if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties));
+ } else if (ScientificFormat.useScientificNotation(properties)) {
+ canUseFastTrack = false;
+ format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+ format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties));
+ } else {
+ format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+ format.addBeforeFormat(RoundingFormat.getDefaultOrNoRounder(properties));
+ }
+ }
+ if (PaddingFormat.usePadding(properties)) {
+ canUseFastTrack = false;
+ format.addAfterFormat(PaddingFormat.getInstance(properties));
+ }
+ if (canUseFastTrack) {
+ return new Format.PositiveNegativeRounderTargetFormat(
+ PositiveNegativeAffixFormat.getInstance(symbols, properties),
+ RoundingFormat.getDefaultOrNoRounder(properties),
+ target);
+ } else {
+ return format;
+ }
+ }
+
+ public static String staticFormat(FormatQuantity input, Properties properties) {
+ return staticFormat(input, properties, getSymbols());
+ }
+
+ public static String staticFormat(FormatQuantity input, Properties properties, Locale locale) {
+ return staticFormat(input, properties, getSymbols(locale));
+ }
+
+ public static String staticFormat(FormatQuantity input, Properties properties, ULocale uLocale) {
+ return staticFormat(input, properties, getSymbols(uLocale));
+ }
+
+ public static String staticFormat(FormatQuantity input, String pattern) {
+ return staticFormat(input, getProperties(pattern), getSymbols());
+ }
+
+ public static String staticFormat(FormatQuantity input, String pattern, Locale locale) {
+ return staticFormat(input, getProperties(pattern), getSymbols(locale));
+ }
+
+ public static String staticFormat(FormatQuantity input, String pattern, ULocale uLocale) {
+ return staticFormat(input, getProperties(pattern), getSymbols(uLocale));
+ }
+
+ public static String staticFormat(
+ FormatQuantity input, String pattern, DecimalFormatSymbols symbols) {
+ return staticFormat(input, getProperties(pattern), symbols);
+ }
+
+ public static String staticFormat(
+ FormatQuantity input, Properties properties, DecimalFormatSymbols symbols) {
+ PluralRules rules = null;
+ ModifierHolder mods = Format.threadLocalModifierHolder.get().clear();
+ NumberStringBuilder sb = Format.threadLocalStringBuilder.get().clear();
+ int length = 0;
+
+ // Pre-processing
+ if (!input.isNaN()) {
+ if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) {
+ MagnitudeMultiplier.getInstance(properties).before(input, mods, rules);
+ }
+ if (BigDecimalMultiplier.useMultiplier(properties)) {
+ BigDecimalMultiplier.getInstance(properties).before(input, mods, rules);
+ }
+ if (MeasureFormat.useMeasureFormat(properties)) {
+ rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+ MeasureFormat.getInstance(symbols, properties).before(input, mods, rules);
+ }
+ if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+ rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+ CompactDecimalFormat.apply(input, mods, rules, symbols, properties);
+ } else if (CurrencyFormat.useCurrency(properties)) {
+ rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+ CurrencyFormat.getCurrencyRounder(symbols, properties).before(input, mods, rules);
+ CurrencyFormat.getCurrencyModifier(symbols, properties).before(input, mods, rules);
+ } else if (ScientificFormat.useScientificNotation(properties)) {
+ // TODO: Is it possible to combine significant digits with currency?
+ PositiveNegativeAffixFormat.getInstance(symbols, properties).before(input, mods, rules);
+ ScientificFormat.getInstance(symbols, properties).before(input, mods, rules);
+ } else {
+ PositiveNegativeAffixFormat.apply(input, mods, symbols, properties);
+ RoundingFormat.getDefaultOrNoRounder(properties).before(input, mods, rules);
+ }
+ }
+
+ // Primary format step
+ length += new PositiveDecimalFormat(symbols, properties).target(input, sb, 0);
+ length += mods.applyStrong(sb, 0, length);
+
+ // Post-processing
+ if (PaddingFormat.usePadding(properties)) {
+ length += PaddingFormat.getInstance(properties).after(mods, sb, 0, length);
+ }
+ length += mods.applyAll(sb, 0, length);
+
+ return sb.toString();
+ }
+
+ private static final ThreadLocal<Map<ULocale, DecimalFormatSymbols>> threadLocalSymbolsCache =
+ new ThreadLocal<Map<ULocale, DecimalFormatSymbols>>() {
+ @Override
+ protected Map<ULocale, DecimalFormatSymbols> initialValue() {
+ return new HashMap<ULocale, DecimalFormatSymbols>();
+ }
+ };
+
+ private static DecimalFormatSymbols getSymbols() {
+ ULocale uLocale = ULocale.getDefault();
+ return getSymbols(uLocale);
+ }
+
+ private static DecimalFormatSymbols getSymbols(Locale locale) {
+ ULocale uLocale = ULocale.forLocale(locale);
+ return getSymbols(uLocale);
+ }
+
+ private static DecimalFormatSymbols getSymbols(ULocale uLocale) {
+ if (uLocale == null) uLocale = ULocale.getDefault();
+ DecimalFormatSymbols symbols = threadLocalSymbolsCache.get().get(uLocale);
+ if (symbols == null) {
+ symbols = DecimalFormatSymbols.getInstance(uLocale);
+ threadLocalSymbolsCache.get().put(uLocale, symbols);
+ }
+ return symbols;
+ }
+
+ private static final ThreadLocal<Map<String, Properties>> threadLocalPropertiesCache =
+ new ThreadLocal<Map<String, Properties>>() {
+ @Override
+ protected Map<String, Properties> initialValue() {
+ return new HashMap<String, Properties>();
+ }
+ };
+
+ private static Properties getProperties(String pattern) {
+ if (pattern == null) pattern = "#";
+ Properties properties = threadLocalPropertiesCache.get().get(pattern);
+ if (properties == null) {
+ properties = PatternString.parseToProperties(pattern);
+ threadLocalPropertiesCache.get().put(pattern.intern(), properties);
+ }
+ return properties;
+ }
+
+ private static final ThreadLocal<Map<ULocale, PluralRules>> threadLocalRulesCache =
+ new ThreadLocal<Map<ULocale, PluralRules>>() {
+ @Override
+ protected Map<ULocale, PluralRules> initialValue() {
+ return new HashMap<ULocale, PluralRules>();
+ }
+ };
+
+ private static PluralRules getPluralRules(ULocale uLocale, Properties properties) {
+ // Backwards compatibility: CurrencyPluralInfo wraps its own copy of PluralRules
+ if (properties.getCurrencyPluralInfo() != null) {
+ return properties.getCurrencyPluralInfo().getPluralRules();
+ }
+
+ if (uLocale == null) uLocale = ULocale.getDefault();
+ PluralRules rules = threadLocalRulesCache.get().get(uLocale);
+ if (rules == null) {
+ rules = PluralRules.forLocale(uLocale);
+ threadLocalRulesCache.get().put(uLocale, rules);
+ }
+ return rules;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+/**
+ * This is a small interface I made to assist with converting from a formatter pipeline object to a
+ * pattern string. It allows classes to "export" themselves to a property bag, which in turn can be
+ * passed to {@link PatternString#propertiesToString(Properties)} to generate the pattern string.
+ *
+ * <p>Depending on the new API we expose, this process might not be necessary if we persist the
+ * property bag in the current DecimalFormat shim.
+ */
+public interface Exportable {
+ public void export(Properties properties);
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.text.AttributedCharacterIterator;
+import java.text.FieldPosition;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+
+import com.ibm.icu.text.PluralRules;
+
+// TODO: Get a better name for this base class.
+public abstract class Format {
+
+ protected static final ThreadLocal<NumberStringBuilder> threadLocalStringBuilder =
+ new ThreadLocal<NumberStringBuilder>() {
+ @Override
+ protected NumberStringBuilder initialValue() {
+ return new NumberStringBuilder();
+ }
+ };
+
+ protected static final ThreadLocal<ModifierHolder> threadLocalModifierHolder =
+ new ThreadLocal<ModifierHolder>() {
+ @Override
+ protected ModifierHolder initialValue() {
+ return new ModifierHolder();
+ }
+ };
+
+ public String format(FormatQuantity... inputs) {
+ // Setup
+ Deque<FormatQuantity> inputDeque = new ArrayDeque<FormatQuantity>();
+ inputDeque.addAll(Arrays.asList(inputs));
+ ModifierHolder modDeque = threadLocalModifierHolder.get().clear();
+ NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+
+ // Primary "recursion" step, calling the implementation's process method
+ int length = process(inputDeque, modDeque, sb, 0);
+
+ // Resolve remaining affixes
+ length += modDeque.applyAll(sb, 0, length);
+ return sb.toString();
+ }
+
+ /** A Format that works on only one number. */
+ public abstract static class SingularFormat extends Format implements Exportable {
+
+ public String format(FormatQuantity input) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ return sb.toString();
+ }
+
+ public void format(FormatQuantity input, StringBuffer output) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ output.append(sb);
+ }
+
+ public String format(FormatQuantity input, FieldPosition fp) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ sb.populateFieldPosition(fp, 0);
+ return sb.toString();
+ }
+
+ public void format(FormatQuantity input, StringBuffer output, FieldPosition fp) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ sb.populateFieldPosition(fp, output.length());
+ output.append(sb);
+ }
+
+ public AttributedCharacterIterator formatToCharacterIterator(FormatQuantity input) {
+ NumberStringBuilder sb = formatToStringBuilder(input);
+ return sb.getIterator();
+ }
+
+ private NumberStringBuilder formatToStringBuilder(FormatQuantity input) {
+ // Setup
+ ModifierHolder modDeque = threadLocalModifierHolder.get().clear();
+ NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+
+ // Primary "recursion" step, calling the implementation's process method
+ int length = process(input, modDeque, sb, 0);
+
+ // Resolve remaining affixes
+ length += modDeque.applyAll(sb, 0, length);
+ return sb;
+ }
+
+ @Override
+ public int process(
+ Deque<FormatQuantity> input,
+ ModifierHolder mods,
+ NumberStringBuilder string,
+ int startIndex) {
+ return process(input.removeFirst(), mods, string, startIndex);
+ }
+
+ public abstract int process(
+ FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex);
+ }
+
+ public static class BeforeTargetAfterFormat extends SingularFormat {
+ // The formatters are kept as individual fields to avoid extra object creation overhead.
+ private BeforeFormat before1 = null;
+ private BeforeFormat before2 = null;
+ private BeforeFormat before3 = null;
+ private TargetFormat target = null;
+ private AfterFormat after1 = null;
+ private AfterFormat after2 = null;
+ private AfterFormat after3 = null;
+ private final PluralRules rules;
+
+ public BeforeTargetAfterFormat(PluralRules rules) {
+ this.rules = rules;
+ }
+
+ public void addBeforeFormat(BeforeFormat before) {
+ if (before1 == null) {
+ before1 = before;
+ } else if (before2 == null) {
+ before2 = before;
+ } else if (before3 == null) {
+ before3 = before;
+ } else {
+ throw new IllegalArgumentException("Only three BeforeFormats are allowed at a time");
+ }
+ }
+
+ public void setTargetFormat(TargetFormat target) {
+ this.target = target;
+ }
+
+ public void addAfterFormat(AfterFormat after) {
+ if (after1 == null) {
+ after1 = after;
+ } else if (after2 == null) {
+ after2 = after;
+ } else if (after3 == null) {
+ after3 = after;
+ } else {
+ throw new IllegalArgumentException("Only three AfterFormats are allowed at a time");
+ }
+ }
+
+ @Override
+ public String format(FormatQuantity input) {
+ ModifierHolder mods = threadLocalModifierHolder.get().clear();
+ NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+ int length = process(input, mods, sb, 0);
+ length += mods.applyAll(sb, 0, length);
+ return sb.toString();
+ }
+
+ @Override
+ public int process(
+ FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex) {
+ // Special case: modifiers are skipped for NaN
+ int length = 0;
+ if (!input.isNaN()) {
+ if (before1 != null) {
+ before1.before(input, mods, rules);
+ }
+ if (before2 != null) {
+ before2.before(input, mods, rules);
+ }
+ if (before3 != null) {
+ before3.before(input, mods, rules);
+ }
+ }
+ length = target.target(input, string, startIndex);
+ length += mods.applyStrong(string, startIndex, startIndex + length);
+ if (after1 != null) {
+ length += after1.after(mods, string, startIndex, startIndex + length);
+ }
+ if (after2 != null) {
+ length += after2.after(mods, string, startIndex, startIndex + length);
+ }
+ if (after3 != null) {
+ length += after3.after(mods, string, startIndex, startIndex + length);
+ }
+ return length;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ if (before1 != null) {
+ before1.export(properties);
+ }
+ if (before2 != null) {
+ before2.export(properties);
+ }
+ if (before3 != null) {
+ before3.export(properties);
+ }
+ target.export(properties);
+ if (after1 != null) {
+ after1.export(properties);
+ }
+ if (after2 != null) {
+ after2.export(properties);
+ }
+ if (after3 != null) {
+ after3.export(properties);
+ }
+ }
+ }
+
+ public static class PositiveNegativeRounderTargetFormat extends SingularFormat {
+ private final Modifier.PositiveNegativeModifier positiveNegative;
+ private final Rounder rounder;
+ private final TargetFormat target;
+
+ public PositiveNegativeRounderTargetFormat(
+ Modifier.PositiveNegativeModifier positiveNegative, Rounder rounder, TargetFormat target) {
+ this.positiveNegative = positiveNegative;
+ this.rounder = rounder;
+ this.target = target;
+ }
+
+ @Override
+ public String format(FormatQuantity input) {
+ NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+ process(input, null, sb, 0);
+ return sb.toString();
+ }
+
+ @Override
+ public int process(
+ FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex) {
+ // Special case: modifiers are skipped for NaN
+ Modifier mod = null;
+ rounder.apply(input);
+ if (!input.isNaN() && positiveNegative != null) {
+ mod = positiveNegative.getModifier(input.isNegative());
+ }
+ int length = target.target(input, string, startIndex);
+ if (mod != null) {
+ length += mod.apply(string, 0, length);
+ }
+ return length;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ rounder.export(properties);
+ positiveNegative.export(properties);
+ target.export(properties);
+ }
+ }
+
+ public abstract static class BeforeFormat implements Exportable {
+ protected abstract void before(FormatQuantity input, ModifierHolder mods);
+
+ @SuppressWarnings("unused")
+ public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+ before(input, mods);
+ }
+ }
+
+ public static interface TargetFormat extends Exportable {
+ public abstract int target(FormatQuantity input, NumberStringBuilder string, int startIndex);
+ }
+
+ public static interface AfterFormat extends Exportable {
+ public abstract int after(
+ ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex);
+ }
+
+ // Instead of Dequeue<BigDecimal>, it could be Deque<Quantity> where
+ // we control the API of Quantity
+ public abstract int process(
+ Deque<FormatQuantity> inputs,
+ ModifierHolder outputMods,
+ NumberStringBuilder outputString,
+ int startIndex);
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.text.PluralRules;
+
+/**
+ * An interface representing a number to be processed by the decimal formatting pipeline. Includes
+ * methods for rounding, plural rules, and decimal digit extraction.
+ *
+ * <p>By design, this is NOT IMMUTABLE and NOT THREAD SAFE. It is intended to be an intermediate
+ * object holding state during a pass through the decimal formatting pipeline.
+ *
+ * <p>Implementations of this interface are free to use any internal storage mechanism.
+ *
+ * <p>TODO: Should I change this to an abstract class so that logic for min/max digits doesn't need
+ * to be copied to every implementation?
+ */
+public interface FormatQuantity extends PluralRules.IFixedDecimal {
+
+ /**
+ * Sets the minimum and maximum digits that this {@link FormatQuantity} should generate. This
+ * method does not perform rounding.
+ *
+ * @param minInt The minimum number of integer digits.
+ * @param maxInt The maximum number of integer digits.
+ * @param minFrac The minimum number of fraction digits.
+ * @param maxFrac The maximum number of fraction digits.
+ */
+ public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac);
+
+ /**
+ * Rounds the number to a specified interval, such as 0.05.
+ *
+ * <p>If rounding to a power of ten, use the more efficient {@link #roundToMagnitude} instead.
+ *
+ * @param roundingInterval The increment to which to round.
+ * @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior
+ * if null.
+ */
+ public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext);
+
+ /**
+ * Rounds the number to a specified magnitude (power of ten).
+ *
+ * @param roundingMagnitude The power of ten to which to round. For example, a value of -2 will
+ * round to 2 decimal places.
+ * @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior
+ * if null.
+ */
+ public void roundToMagnitude(int roundingMagnitude, MathContext mathContext);
+
+ /**
+ * Rounds the number to an infinite number of decimal points. This has no effect except for
+ * forcing the double in {@link FormatQuantityBCD} to adopt its exact representation.
+ */
+ public void roundToInfinity();
+
+ /**
+ * Multiply the internal value.
+ *
+ * @param multiplicand The value by which to multiply.
+ */
+ public void multiplyBy(BigDecimal multiplicand);
+
+ /**
+ * Scales the number by a power of ten. For example, if the value is currently "1234.56", calling
+ * this method with delta=-3 will change the value to "1.23456".
+ *
+ * @param delta The number of magnitudes of ten to change by.
+ */
+ public void adjustMagnitude(int delta);
+
+ /**
+ * @return The power of ten corresponding to the most significant nonzero digit.
+ * @throws ArithmeticException If the value represented is zero.
+ */
+ public int getMagnitude() throws ArithmeticException;
+
+ /** @return Whether the value represented by this {@link FormatQuantity} is zero. */
+ public boolean isZero();
+
+ /** @return Whether the value represented by this {@link FormatQuantity} is less than zero. */
+ public boolean isNegative();
+
+ /** @return Whether the value represented by this {@link FormatQuantity} is infinite. */
+ @Override
+ public boolean isInfinite();
+
+ /** @return Whether the value represented by this {@link FormatQuantity} is not a number. */
+ @Override
+ public boolean isNaN();
+
+ /** @return The value contained in this {@link FormatQuantity} approximated as a double. */
+ public double toDouble();
+
+ public BigDecimal toBigDecimal();
+
+ public int maxRepresentableDigits();
+
+ // TODO: Should this method be removed, since FormatQuantity implements IFixedDecimal now?
+ /**
+ * Computes the plural form for this number based on the specified set of rules.
+ *
+ * @param rules A {@link PluralRules} object representing the set of rules.
+ * @return The {@link StandardPlural} according to the PluralRules. If the plural form is not in
+ * the set of standard plurals, {@link StandardPlural#OTHER} is returned instead.
+ */
+ public StandardPlural getStandardPlural(PluralRules rules);
+
+ // /**
+ // * @return The number of fraction digits, always in the closed interval [minFrac, maxFrac].
+ // * @see #setIntegerFractionLength(int, int, int, int)
+ // */
+ // public int fractionCount();
+ //
+ // /**
+ // * @return The number of integer digits, always in the closed interval [minInt, maxInt].
+ // * @see #setIntegerFractionLength(int, int, int, int)
+ // */
+ // public int integerCount();
+ //
+ // /**
+ // * @param index The index of the fraction digit relative to the decimal place, or 1 minus the
+ // * digit's power of ten.
+ // * @return The digit at the specified index. Undefined if index is greater than maxInt or less
+ // * than 0.
+ // * @see #fractionCount()
+ // */
+ // public byte getFractionDigit(int index);
+ //
+ // /**
+ // * @param index The index of the integer digit relative to the decimal place, or the digit's power
+ // * of ten.
+ // * @return The digit at the specified index. Undefined if index is greater than maxInt or less
+ // * than 0.
+ // * @see #integerCount()
+ // */
+ // public byte getIntegerDigit(int index);
+
+ /**
+ * Gets the digit at the specified magnitude. For example, if the represented number is 12.3,
+ * getDigit(-1) returns 3, since 3 is the digit corresponding to 10^-1.
+ *
+ * @param magnitude The magnitude of the digit.
+ * @return The digit at the specified magnitude.
+ */
+ public byte getDigit(int magnitude);
+
+ /**
+ * Gets the largest power of ten that needs to be displayed. The value returned by this function
+ * will be bounded between minInt and maxInt.
+ *
+ * @return The highest-magnitude digit to be displayed.
+ */
+ public int getUpperDisplayMagnitude();
+
+ /**
+ * Gets the smallest power of ten that needs to be displayed. The value returned by this function
+ * will be bounded between -minFrac and -maxFrac.
+ *
+ * @return The lowest-magnitude digit to be displayed.
+ */
+ public int getLowerDisplayMagnitude();
+
+ public FormatQuantity clone();
+
+ public void copyFrom(FormatQuantity other);
+
+ /**
+ * This method is for internal testing only and should be removed before release.
+ *
+ * @internal
+ */
+ public long getPositionFingerprint();
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.Operand;
+
+/**
+ * This is an older implementation of FormatQuantity. A newer, faster implementation is
+ * FormatQuantity2. I kept this implementation around because it was useful for testing purposes
+ * (being able to compare the output of one implementation with the other).
+ *
+ * <p>This class is NOT IMMUTABLE and NOT THREAD SAFE and is intended to be used by a single thread
+ * to format a number through a formatter, which is thread-safe.
+ */
+public class FormatQuantity1 implements FormatQuantity {
+ // Four positions: left optional '(', left required '[', right required ']', right optional ')'.
+ // These four positions determine which digits are displayed in the output string. They do NOT
+ // affect rounding. These positions are internal-only and can be specified only by the public
+ // endpoints like setFractionLength, setIntegerLength, and setSignificantDigits, among others.
+ //
+ // * Digits between lReqPos and rReqPos are in the "required zone" and are always displayed.
+ // * Digits between lOptPos and rOptPos but outside the required zone are in the "optional zone"
+ // and are displayed unless they are trailing off the left or right edge of the number and
+ // have a numerical value of zero. In order to be "trailing", the digits need to be beyond
+ // the decimal point in their respective directions.
+ // * Digits outside of the "optional zone" are never displayed.
+ //
+ // See the table below for illustrative examples.
+ //
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ // | lOptPos | lReqPos | rReqPos | rOptPos | number | positions | en-US string |
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ // | 5 | 2 | -1 | -5 | 1234.567 | ( 12[34.5]67 ) | 1,234.567 |
+ // | 3 | 2 | -1 | -5 | 1234.567 | 1(2[34.5]67 ) | 234.567 |
+ // | 3 | 2 | -1 | -2 | 1234.567 | 1(2[34.5]6)7 | 234.56 |
+ // | 6 | 4 | 2 | -5 | 123456789. | 123(45[67]89. ) | 456,789. |
+ // | 6 | 4 | 2 | 1 | 123456789. | 123(45[67]8)9. | 456,780. |
+ // | -1 | -1 | -3 | -4 | 0.123456 | 0.1([23]4)56 | .0234 |
+ // | 6 | 4 | -2 | -2 | 12.3 | ( [ 12.3 ]) | 0012.30 |
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ //
+ private int lOptPos = Integer.MAX_VALUE;
+ private int lReqPos = 0;
+ private int rReqPos = 0;
+ private int rOptPos = Integer.MIN_VALUE;
+
+ // Internally, attempt to use a long to store the number. A long can hold numbers between 18 and
+ // 19 digits, covering the vast majority of use cases. We store three values: the long itself,
+ // the "scale" of the long (the power of 10 represented by the rightmost digit in the long), and
+ // the "precision" (the number of digits in the long). "primary" and "primaryScale" are the only
+ // two variables that are required for representing the number in memory. "primaryPrecision" is
+ // saved only for the sake of performance enhancements when performing certain operations. It can
+ // always be re-computed from "primary" and "primaryScale".
+ private long primary;
+ private int primaryScale;
+ private int primaryPrecision;
+
+ // If the decimal can't fit into the long, fall back to a BigDecimal.
+ private BigDecimal fallback;
+
+ // Other properties
+ private int flags;
+ private static final int NEGATIVE_FLAG = 1;
+ private static final int INFINITY_FLAG = 2;
+ private static final int NAN_FLAG = 4;
+ private static final long[] POWERS_OF_TEN = {
+ 1L,
+ 10L,
+ 100L,
+ 1000L,
+ 10000L,
+ 100000L,
+ 1000000L,
+ 10000000L,
+ 100000000L,
+ 1000000000L,
+ 10000000000L,
+ 100000000000L,
+ 1000000000000L,
+ 10000000000000L,
+ 100000000000000L,
+ 1000000000000000L,
+ 10000000000000000L,
+ 100000000000000000L,
+ 1000000000000000000L
+ };
+
+ @Override
+ public int maxRepresentableDigits() {
+ return Integer.MAX_VALUE;
+ }
+
+ public FormatQuantity1(long input) {
+ if (input < 0) {
+ setNegative(true);
+ input *= -1;
+ }
+
+ primary = input;
+ primaryScale = 0;
+ primaryPrecision = computePrecision(primary);
+ fallback = null;
+ }
+
+ /**
+ * Creates a FormatQuantity from the given double value. Internally attempts several strategies
+ * for converting the double to an exact representation, falling back on a BigDecimal if it fails
+ * to do so.
+ *
+ * @param input The double to represent by this FormatQuantity.
+ */
+ public FormatQuantity1(double input) {
+ if (input < 0) {
+ setNegative(true);
+ input *= -1;
+ }
+
+ // First try reading from IEEE bits. This is trivial only for doubles in [2^52, 2^64). If it
+ // fails, we wasted only a few CPU cycles.
+ long ieeeBits = Double.doubleToLongBits(input);
+ int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+ if (exponent >= 52 && exponent <= 63) {
+ // We can convert this double directly to a long.
+ long mantissa = (ieeeBits & 0x000fffffffffffffL) + 0x0010000000000000L;
+ primary = (mantissa << (exponent - 52));
+ primaryScale = 0;
+ primaryPrecision = computePrecision(primary);
+ return;
+ }
+
+ // Now try parsing the string produced by Double.toString().
+ String temp = Double.toString(input);
+ try {
+ if (temp.length() == 3 && temp.equals("0.0")) {
+ // Case 1: Zero.
+ primary = 0L;
+ primaryScale = 0;
+ primaryPrecision = 0;
+ } else if (temp.indexOf('E') != -1) {
+ // Case 2: Exponential notation.
+ assert temp.indexOf('.') == 1;
+ int expPos = temp.indexOf('E');
+ primary = Long.parseLong(temp.charAt(0) + temp.substring(2, expPos));
+ primaryScale = Integer.parseInt(temp.substring(expPos + 1)) - (expPos - 1) + 1;
+ primaryPrecision = expPos - 1;
+ } else if (temp.charAt(0) == '0') {
+ // Case 3: Fraction-only number.
+ assert temp.indexOf('.') == 1;
+ primary = Long.parseLong(temp.substring(2)); // ignores leading zeros
+ primaryScale = 2 - temp.length();
+ primaryPrecision = computePrecision(primary);
+ } else if (temp.charAt(temp.length() - 1) == '0') {
+ // Case 4: Integer-only number.
+ assert temp.indexOf('.') == temp.length() - 2;
+ int rightmostNonzeroDigitIndex = temp.length() - 3;
+ while (temp.charAt(rightmostNonzeroDigitIndex) == '0') {
+ rightmostNonzeroDigitIndex -= 1;
+ }
+ primary = Long.parseLong(temp.substring(0, rightmostNonzeroDigitIndex + 1));
+ primaryScale = temp.length() - rightmostNonzeroDigitIndex - 3;
+ primaryPrecision = rightmostNonzeroDigitIndex + 1;
+ } else if (temp.equals("Infinity")) {
+ // Case 5: Infinity.
+ primary = 0;
+ setInfinity(true);
+ } else if (temp.equals("NaN")) {
+ // Case 6: NaN.
+ primary = 0;
+ setNaN(true);
+ } else {
+ // Case 7: Number with both a fraction and an integer.
+ int decimalPos = temp.indexOf('.');
+ primary = Long.parseLong(temp.substring(0, decimalPos) + temp.substring(decimalPos + 1));
+ primaryScale = decimalPos - temp.length() + 1;
+ primaryPrecision = temp.length() - 1;
+ }
+ } catch (NumberFormatException e) {
+ // The digits of the double can't fit into the long.
+ primary = -1;
+ fallback = new BigDecimal(temp);
+ }
+ }
+
+ static final double LOG_2_OF_TEN = 3.32192809489;
+
+ public FormatQuantity1(double input, boolean fast) {
+ if (input < 0) {
+ setNegative(true);
+ input *= -1;
+ }
+
+ // Our strategy is to read all digits that are *guaranteed* to be valid without delving into
+ // the IEEE rounding rules. This strategy might not end up with a perfect representation of
+ // the fractional part of the double.
+ long ieeeBits = Double.doubleToLongBits(input);
+ int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+ long mantissa = (ieeeBits & 0x000fffffffffffffL) + 0x0010000000000000L;
+ if (exponent > 63) {
+ throw new IllegalArgumentException(); // FIXME
+ } else if (exponent >= 52) {
+ primary = (mantissa << (exponent - 52));
+ primaryScale = 0;
+ primaryPrecision = computePrecision(primary);
+ return;
+ } else if (exponent >= 0) {
+ int shift = 52 - exponent;
+ primary = (mantissa >> shift); // integer part
+ int fractionCount = (int) (shift / LOG_2_OF_TEN);
+ long fraction = (mantissa - (primary << shift)) + 1L; // TODO: Explain the +1L
+ primary *= POWERS_OF_TEN[fractionCount];
+ for (int i = 0; i < fractionCount; i++) {
+ long times10 = (fraction * 10L);
+ long digit = times10 >> shift;
+ assert digit >= 0 && digit < 10;
+ primary += digit * POWERS_OF_TEN[fractionCount - i - 1];
+ fraction = times10 & ((1L << shift) - 1);
+ }
+ primaryScale = -fractionCount;
+ primaryPrecision = computePrecision(primary);
+ } else {
+ throw new IllegalArgumentException(); // FIXME
+ }
+ }
+
+ public FormatQuantity1(BigDecimal decimal) {
+ if (decimal.compareTo(BigDecimal.ZERO) < 0) {
+ setNegative(true);
+ decimal = decimal.negate();
+ }
+
+ primary = -1;
+ if (decimal.compareTo(BigDecimal.ZERO) == 0) {
+ fallback = BigDecimal.ZERO;
+ } else {
+ fallback = decimal;
+ }
+ }
+
+ public FormatQuantity1(FormatQuantity1 other) {
+ copyFrom(other);
+ }
+
+ @Override
+ public FormatQuantity clone() {
+ return new FormatQuantity1(this);
+ }
+
+ /**
+ * Make the internal state of this FormatQuantity equal to another FormatQuantity.
+ *
+ * @param other The template FormatQuantity. All properties from this FormatQuantity will be
+ * copied into this FormatQuantity.
+ */
+ @Override
+ public void copyFrom(FormatQuantity other) {
+ // TODO: Check before casting
+ FormatQuantity1 _other = (FormatQuantity1) other;
+ lOptPos = _other.lOptPos;
+ lReqPos = _other.lReqPos;
+ rReqPos = _other.rReqPos;
+ rOptPos = _other.rOptPos;
+ primary = _other.primary;
+ primaryScale = _other.primaryScale;
+ primaryPrecision = _other.primaryPrecision;
+ fallback = _other.fallback;
+ flags = _other.flags;
+ }
+
+ @Override
+ public long getPositionFingerprint() {
+ long fingerprint = 0;
+ fingerprint ^= lOptPos;
+ fingerprint ^= (lReqPos << 16);
+ fingerprint ^= ((long) rReqPos << 32);
+ fingerprint ^= ((long) rOptPos << 48);
+ return fingerprint;
+ }
+
+ /**
+ * Utility method to compute the number of digits ("precision") in a long.
+ *
+ * @param input The long (which can't contain more than 19 digits).
+ * @return The precision of the long.
+ */
+ private static int computePrecision(long input) {
+ int precision = 0;
+ while (input > 0) {
+ input /= 10;
+ precision++;
+ }
+ return precision;
+ }
+
+ /**
+ * Changes the internal representation from a long to a BigDecimal. Used only for operations that
+ * don't support longs.
+ */
+ private void convertToBigDecimal() {
+ if (primary == -1) {
+ return;
+ }
+
+ fallback = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+ primary = -1;
+ }
+
+ @Override
+ public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
+ // Graceful failures for bogus input
+ minInt = Math.max(0, minInt);
+ maxInt = Math.max(0, maxInt);
+ minFrac = Math.max(0, minFrac);
+ maxFrac = Math.max(0, maxFrac);
+
+ // The minima must be less than or equal to the maxima
+ if (maxInt < minInt) {
+ minInt = maxInt;
+ }
+ if (maxFrac < minFrac) {
+ minFrac = maxFrac;
+ }
+
+ // Displaying neither integer nor fraction digits is not allowed
+ if (maxInt == 0 && maxFrac == 0) {
+ maxInt = Integer.MAX_VALUE;
+ maxFrac = Integer.MAX_VALUE;
+ }
+
+ // Save values into internal state
+ // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
+ lOptPos = maxInt;
+ lReqPos = minInt;
+ rReqPos = -minFrac;
+ rOptPos = -maxFrac;
+ }
+
+ @Override
+ public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext) {
+ BigDecimal d =
+ (primary == -1) ? fallback : new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+ if (isNegative()) d = d.negate();
+ d = d.divide(roundingInterval, 0, mathContext.getRoundingMode()).multiply(roundingInterval);
+ if (isNegative()) d = d.negate();
+ fallback = d;
+ primary = -1;
+ }
+
+ @Override
+ public void roundToMagnitude(int roundingMagnitude, MathContext mathContext) {
+ if (roundingMagnitude < -1000) {
+ roundToInfinity();
+ return;
+ }
+ if (primary == -1) {
+ if (isNegative()) fallback = fallback.negate();
+ fallback = fallback.setScale(-roundingMagnitude, mathContext.getRoundingMode());
+ if (isNegative()) fallback = fallback.negate();
+ // Enforce the math context.
+ fallback = fallback.round(mathContext);
+ } else {
+ int relativeScale = primaryScale - roundingMagnitude;
+ if (relativeScale < -18) {
+ // No digits will remain after rounding the number.
+ primary = 0L;
+ primaryScale = roundingMagnitude;
+ primaryPrecision = 0;
+ } else if (relativeScale < 0) {
+ // This is the harder case, when we need to perform the rounding logic.
+ // First check if the rightmost digits are already zero, where we can skip rounding.
+ if ((primary % POWERS_OF_TEN[0 - relativeScale]) == 0) {
+ // No rounding is necessary.
+ } else {
+ // TODO: Make this more efficient. Temporarily, convert to a BigDecimal and back again.
+ BigDecimal temp = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+ if (isNegative()) temp = temp.negate();
+ temp = temp.setScale(-roundingMagnitude, mathContext.getRoundingMode());
+ if (isNegative()) temp = temp.negate();
+ temp = temp.scaleByPowerOfTen(-roundingMagnitude);
+ primary = temp.longValueExact(); // should never throw
+ primaryScale = roundingMagnitude;
+ primaryPrecision = computePrecision(primary);
+ }
+ } else {
+ // No rounding is necessary. All digits are to the left of the rounding magnitude.
+ }
+ // Enforce the math context.
+ primary = new BigDecimal(primary).round(mathContext).longValueExact();
+ primaryPrecision = computePrecision(primary);
+ }
+ }
+
+ @Override
+ public void roundToInfinity() {
+ // noop
+ }
+
+ /**
+ * Multiply the internal number by the specified multiplicand. This method forces the internal
+ * representation into a BigDecimal. If you are multiplying by a power of 10, use {@link
+ * #adjustMagnitude} instead.
+ *
+ * @param multiplicand The number to be passed to {@link BigDecimal#multiply}.
+ */
+ @Override
+ public void multiplyBy(BigDecimal multiplicand) {
+ convertToBigDecimal();
+ fallback = fallback.multiply(multiplicand);
+ if (fallback.compareTo(BigDecimal.ZERO) < 0) {
+ setNegative(!isNegative());
+ fallback = fallback.negate();
+ }
+ }
+
+ /**
+ * Divide the internal number by the specified quotient. This method forces the internal
+ * representation into a BigDecimal. If you are dividing by a power of 10, use {@link
+ * #adjustMagnitude} instead.
+ *
+ * @param divisor The number to be passed to {@link BigDecimal#divide}.
+ * @param scale The scale of the final rounded number. More negative means more decimal places.
+ * @param mathContext The math context to use if rounding is necessary.
+ */
+ private void divideBy(BigDecimal divisor, int scale, MathContext mathContext) {
+ convertToBigDecimal();
+ // Negate the scale because BigDecimal's scale is defined as the inverse of our scale
+ fallback = fallback.divide(divisor, -scale, mathContext.getRoundingMode());
+ if (fallback.compareTo(BigDecimal.ZERO) < 0) {
+ setNegative(!isNegative());
+ fallback = fallback.negate();
+ }
+ }
+
+ @Override
+ public boolean isZero() {
+ if (primary == -1) {
+ return fallback.compareTo(BigDecimal.ZERO) == 0;
+ } else {
+ return primary == 0;
+ }
+ }
+
+ /** @return The power of ten of the highest digit represented by this FormatQuantity */
+ @Override
+ public int getMagnitude() throws ArithmeticException {
+ int scale = (primary == -1) ? scaleBigDecimal(fallback) : primaryScale;
+ int precision = (primary == -1) ? precisionBigDecimal(fallback) : primaryPrecision;
+ if (precision == 0) {
+ throw new ArithmeticException("Magnitude is not well-defined for zero");
+ } else {
+ return scale + precision - 1;
+ }
+ }
+
+ /**
+ * Changes the magnitude of this FormatQuantity. If the indices of the represented digits had been
+ * previously specified, those indices are moved relative to the FormatQuantity.
+ *
+ * <p>This method does NOT perform rounding.
+ *
+ * @param delta The number of powers of ten to shift (positive shifts to the left).
+ */
+ @Override
+ public void adjustMagnitude(int delta) {
+ if (primary == -1) {
+ fallback = fallback.scaleByPowerOfTen(delta);
+ } else {
+ primaryScale = addOrMaxValue(primaryScale, delta);
+ }
+ }
+
+ private static int addOrMaxValue(int a, int b) {
+ // Check for overflow, and return min/max value if overflow occurs.
+ if (b < 0 && a + b > a) {
+ return Integer.MIN_VALUE;
+ } else if (b > 0 && a + b < a) {
+ return Integer.MAX_VALUE;
+ }
+ return a + b;
+ }
+
+ /** @return If the number represented by this FormatQuantity is less than zero */
+ @Override
+ public boolean isNegative() {
+ return (flags & NEGATIVE_FLAG) != 0;
+ }
+
+ private void setNegative(boolean isNegative) {
+ flags = (flags & (~NEGATIVE_FLAG)) | (isNegative ? NEGATIVE_FLAG : 0);
+ }
+
+ @Override
+ public boolean isInfinite() {
+ return (flags & INFINITY_FLAG) != 0;
+ }
+
+ private void setInfinity(boolean isInfinity) {
+ flags = (flags & (~INFINITY_FLAG)) | (isInfinity ? INFINITY_FLAG : 0);
+ }
+
+ @Override
+ public boolean isNaN() {
+ return (flags & NAN_FLAG) != 0;
+ }
+
+ private void setNaN(boolean isNaN) {
+ flags = (flags & (~NAN_FLAG)) | (isNaN ? NAN_FLAG : 0);
+ }
+
+ /**
+ * Returns a representation of this FormatQuantity as a double, with possible loss of information.
+ */
+ @Override
+ public double toDouble() {
+ double result;
+ if (primary == -1) {
+ result = fallback.doubleValue();
+ } else {
+ // TODO: Make this more efficient
+ result = primary;
+ for (int i = 0; i < primaryScale; i++) {
+ result *= 10.;
+ }
+ for (int i = 0; i > primaryScale; i--) {
+ result /= 10.;
+ }
+ }
+ return isNegative() ? -result : result;
+ }
+
+ @Override
+ public BigDecimal toBigDecimal() {
+ BigDecimal result;
+ if (primary != -1) {
+ result = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+ } else {
+ result = fallback;
+ }
+ return isNegative() ? result.negate() : result;
+ }
+
+ /** @return */
+ @Override
+ public StandardPlural getStandardPlural(PluralRules rules) {
+ if (rules == null) {
+ // Fail gracefully if the user didn't provide a PluralRules
+ return StandardPlural.OTHER;
+ } else {
+ // TODO: Avoid converting to a double for the sake of PluralRules
+ String ruleString = rules.select(toDouble());
+ return StandardPlural.orOtherFromString(ruleString);
+ }
+ }
+
+ @Override
+ public double getPluralOperand(Operand operand) {
+ // TODO: This is a temporary hack.
+ return new PluralRules.FixedDecimal(toDouble()).getPluralOperand(operand);
+ }
+
+ /** @return */
+ public boolean hasNextFraction() {
+ if (rReqPos < 0) {
+ // We are in the required zone.
+ return true;
+ } else if (rOptPos >= 0) {
+ // We are in the forbidden zone.
+ return false;
+ } else {
+ // We are in the optional zone.
+ if (primary == -1) {
+ return fallback.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) > 0;
+ } else {
+ if (primaryScale <= -19) {
+ // The number is a fraction so small that it consists of only fraction digits.
+ return primary > 0;
+ } else if (primaryScale < 0) {
+ // Check if we have a fraction part.
+ long factor = POWERS_OF_TEN[0 - primaryScale];
+ return ((primary % factor) != 0);
+ } else {
+ // The lowest digit in the long has magnitude greater than -1.
+ return false;
+ }
+ }
+ }
+ }
+
+ /** @return */
+ public byte nextFraction() {
+ byte returnValue;
+ if (primary == -1) {
+ BigDecimal temp = fallback.multiply(BigDecimal.TEN);
+ returnValue = temp.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue();
+ fallback = fallback.setScale(0, RoundingMode.FLOOR).add(temp.remainder(BigDecimal.ONE));
+ } else {
+ if (primaryScale <= -20) {
+ // The number is a fraction so small that it has no first fraction digit.
+ primaryScale += 1;
+ returnValue = 0;
+ } else if (primaryScale < 0) {
+ // Extract the fraction digit out of the middle of the long.
+ long factor = POWERS_OF_TEN[0 - primaryScale - 1];
+ long temp1 = primary / factor;
+ long temp2 = primary % factor;
+ returnValue = (byte) (temp1 % 10); // not necessarily nonzero
+ primary = ((temp1 / 10) * factor) + temp2;
+ primaryScale += 1;
+ if (temp1 != 0) {
+ primaryPrecision -= 1;
+ }
+ } else {
+ // The lowest digit in the long has magnitude greater than -1.
+ returnValue = 0;
+ }
+ }
+
+ // Update digit brackets
+ if (lOptPos < 0) {
+ lOptPos += 1;
+ }
+ if (lReqPos < 0) {
+ lReqPos += 1;
+ }
+ if (rReqPos < 0) {
+ rReqPos += 1;
+ }
+ if (rOptPos < 0) {
+ rOptPos += 1;
+ }
+
+ assert returnValue >= 0;
+ return returnValue;
+ }
+
+ /** @return */
+ public boolean hasNextInteger() {
+ if (lReqPos > 0) {
+ // We are in the required zone.
+ return true;
+ } else if (lOptPos <= 0) {
+ // We are in the forbidden zone.
+ return false;
+ } else {
+ // We are in the optional zone.
+ if (primary == -1) {
+ return fallback.setScale(0, RoundingMode.FLOOR).compareTo(BigDecimal.ZERO) > 0;
+ } else {
+ if (primaryScale < -18) {
+ // The number is a fraction so small that it has no integer part.
+ return false;
+ } else if (primaryScale < 0) {
+ // Check if we have an integer part.
+ long factor = POWERS_OF_TEN[0 - primaryScale];
+ return ((primary % factor) != primary); // equivalent: ((primary / 10) != 0)
+ } else {
+ // The lowest digit in the long has magnitude of at least 0.
+ return primary != 0;
+ }
+ }
+ }
+ }
+
+ private int integerCount() {
+ int digitsRemaining;
+ if (primary == -1) {
+ digitsRemaining = precisionBigDecimal(fallback) + scaleBigDecimal(fallback);
+ } else {
+ digitsRemaining = primaryPrecision + primaryScale;
+ }
+ return Math.min(Math.max(digitsRemaining, lReqPos), lOptPos);
+ }
+
+ private int fractionCount() {
+ // TODO: This is temporary.
+ FormatQuantity1 copy = (FormatQuantity1) this.clone();
+ int fractionCount = 0;
+ while (copy.hasNextFraction()) {
+ copy.nextFraction();
+ fractionCount++;
+ }
+ return fractionCount;
+ }
+
+ @Override
+ public int getUpperDisplayMagnitude() {
+ return integerCount() - 1;
+ }
+
+ @Override
+ public int getLowerDisplayMagnitude() {
+ return -fractionCount();
+ }
+
+ // @Override
+ // public byte getIntegerDigit(int index) {
+ // return getDigitPos(index);
+ // }
+ //
+ // @Override
+ // public byte getFractionDigit(int index) {
+ // return getDigitPos(-index - 1);
+ // }
+
+ @Override
+ public byte getDigit(int magnitude) {
+ // TODO: This is temporary.
+ FormatQuantity1 copy = (FormatQuantity1) this.clone();
+ if (magnitude < 0) {
+ for (int p = -1; p > magnitude; p--) {
+ copy.nextFraction();
+ }
+ return copy.nextFraction();
+ } else {
+ for (int p = 0; p < magnitude; p++) {
+ copy.nextInteger();
+ }
+ return copy.nextInteger();
+ }
+ }
+
+ /** @return */
+ public byte nextInteger() {
+ byte returnValue;
+ if (primary == -1) {
+ returnValue = fallback.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue();
+ BigDecimal temp = fallback.divide(BigDecimal.TEN).setScale(0, RoundingMode.FLOOR);
+ fallback = fallback.remainder(BigDecimal.ONE).add(temp);
+ } else {
+ if (primaryScale < -18) {
+ // The number is a fraction so small that it has no integer part.
+ returnValue = 0;
+ } else if (primaryScale < 0) {
+ // Extract the integer digit out of the middle of the long. In many ways, this is the heart
+ // of the digit iterator algorithm.
+ long factor = POWERS_OF_TEN[0 - primaryScale];
+ if ((primary % factor) != primary) { // equivalent: ((primary / 10) != 0)
+ returnValue = (byte) ((primary / factor) % 10);
+ long temp = (primary / 10);
+ primary = temp - (temp % factor) + (primary % factor);
+ primaryPrecision -= 1;
+ } else {
+ returnValue = 0;
+ }
+ } else if (primaryScale == 0) {
+ // Fast-path for primaryScale == 0 (otherwise equivalent to previous step).
+ if (primary != 0) {
+ returnValue = (byte) (primary % 10);
+ primary /= 10;
+ primaryPrecision -= 1;
+ } else {
+ returnValue = 0;
+ }
+ } else {
+ // The lowest digit in the long has magnitude greater than 0.
+ primaryScale -= 1;
+ returnValue = 0;
+ }
+ }
+
+ // Update digit brackets
+ if (lOptPos > 0) {
+ lOptPos -= 1;
+ }
+ if (lReqPos > 0) {
+ lReqPos -= 1;
+ }
+ if (rReqPos > 0) {
+ rReqPos -= 1;
+ }
+ if (rOptPos > 0) {
+ rOptPos -= 1;
+ }
+
+ assert returnValue >= 0;
+ return returnValue;
+ }
+
+ /**
+ * Helper method to compute the precision of a BigDecimal by our definition of precision, which is
+ * that the number zero gets precision zero.
+ *
+ * @param decimal The BigDecimal whose precision to compute.
+ * @return The precision by our definition.
+ */
+ private static int precisionBigDecimal(BigDecimal decimal) {
+ if (decimal.compareTo(BigDecimal.ZERO) == 0) {
+ return 0;
+ } else {
+ return decimal.precision();
+ }
+ }
+
+ /**
+ * Helper method to compute the scale of a BigDecimal by our definition of scale, which is that
+ * deeper fractions result in negative scales as opposed to positive scales.
+ *
+ * @param decimal The BigDecimal whose scale to compute.
+ * @return The scale by our definition.
+ */
+ private static int scaleBigDecimal(BigDecimal decimal) {
+ return -decimal.scale();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<FormatQuantity1 ");
+ if (primary == -1) {
+ sb.append(lOptPos > 1000 ? "max" : lOptPos);
+ sb.append(":");
+ sb.append(lReqPos);
+ sb.append(":");
+ sb.append(rReqPos);
+ sb.append(":");
+ sb.append(rOptPos < -1000 ? "min" : rOptPos);
+ sb.append(" ");
+ sb.append(fallback.toString());
+ } else {
+ String digits = Long.toString(primary);
+ int iDec = digits.length() + primaryScale;
+ int iLP = iDec - toRange(lOptPos, -1000, 1000);
+ int iLB = iDec - toRange(lReqPos, -1000, 1000);
+ int iRB = iDec - toRange(rReqPos, -1000, 1000);
+ int iRP = iDec - toRange(rOptPos, -1000, 1000);
+ iDec = Math.max(Math.min(iDec, digits.length() + 1), -1);
+ iLP = Math.max(Math.min(iLP, digits.length() + 1), -1);
+ iLB = Math.max(Math.min(iLB, digits.length() + 1), -1);
+ iRB = Math.max(Math.min(iRB, digits.length() + 1), -1);
+ iRP = Math.max(Math.min(iRP, digits.length() + 1), -1);
+
+ for (int i = -1; i <= digits.length() + 1; i++) {
+ if (i == iLP) sb.append('(');
+ if (i == iLB) sb.append('[');
+ if (i == iDec) sb.append('.');
+ if (i == iRB) sb.append(']');
+ if (i == iRP) sb.append(')');
+ if (i >= 0 && i < digits.length()) sb.append(digits.charAt(i));
+ else sb.append('\u00A0');
+ }
+ }
+ sb.append(">");
+ return sb.toString();
+ }
+
+ private static int toRange(int i, int lo, int hi) {
+ if (i < lo) {
+ return lo;
+ } else if (i > hi) {
+ return hi;
+ } else {
+ return i;
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public final class FormatQuantity2 extends FormatQuantityBCD {
+
+ /**
+ * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+ * to one digit. For example, the number "12345" in BCD is "0x12345".
+ *
+ * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+ * like setting the digit to zero.
+ */
+ private long bcd;
+
+ @Override
+ public int maxRepresentableDigits() {
+ return 16;
+ }
+
+ public FormatQuantity2(long input) {
+ setToLong(input);
+ }
+
+ public FormatQuantity2(int input) {
+ setToInt(input);
+ }
+
+ public FormatQuantity2(double input) {
+ setToDouble(input);
+ }
+
+ public FormatQuantity2(BigInteger input) {
+ setToBigInteger(input);
+ }
+
+ public FormatQuantity2(BigDecimal input) {
+ setToBigDecimal(input);
+ }
+
+ public FormatQuantity2(FormatQuantity2 other) {
+ copyFrom(other);
+ }
+
+ @Override
+ protected byte getDigitPos(int position) {
+ if (position < 0 || position >= 16) return 0;
+ return (byte) ((bcd >>> (position * 4)) & 0xf);
+ }
+
+ @Override
+ protected void setDigitPos(int position, byte value) {
+ assert position >= 0 && position < 16;
+ int shift = position * 4;
+ bcd = bcd & ~(0xfL << shift) | ((long) value << shift);
+ }
+
+ @Override
+ protected void shiftLeft(int numDigits) {
+ assert precision + numDigits <= 16;
+ bcd <<= (numDigits * 4);
+ scale -= numDigits;
+ precision += numDigits;
+ }
+
+ @Override
+ protected void shiftRight(int numDigits) {
+ bcd >>>= (numDigits * 4);
+ scale += numDigits;
+ precision -= numDigits;
+ }
+
+ @Override
+ protected void setBcdToZero() {
+ bcd = 0L;
+ scale = 0;
+ precision = 0;
+ isApproximate = false;
+ origDouble = 0;
+ origDelta = 0;
+ }
+
+ @Override
+ protected void readIntToBcd(int n) {
+ long result = 0L;
+ int i = 16;
+ for (; n != 0; n /= 10, i--) {
+ result = (result >>> 4) + (((long) n % 10) << 60);
+ }
+ // ints can't overflow the 16 digits in the BCD, so scale is always zero
+ bcd = result >>> (i * 4);
+ scale = 0;
+ precision = 16 - i;
+ }
+
+ @Override
+ protected void readLongToBcd(long n) {
+ long result = 0L;
+ int i = 16;
+ for (; n != 0L; n /= 10L, i--) {
+ result = (result >>> 4) + ((n % 10) << 60);
+ }
+ int adjustment = (i > 0) ? i : 0;
+ bcd = result >>> (adjustment * 4);
+ scale = (i < 0) ? -i : 0;
+ precision = 16 - i;
+ }
+
+ @Override
+ protected void readBigIntegerToBcd(BigInteger n) {
+ long result = 0L;
+ int i = 16;
+ for (; n.signum() != 0; i--) {
+ BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+ result = (result >>> 4) + (temp[1].longValue() << 60);
+ n = temp[0];
+ }
+ int adjustment = (i > 0) ? i : 0;
+ bcd = result >>> (adjustment * 4);
+ scale = (i < 0) ? -i : 0;
+ }
+
+ @Override
+ protected BigDecimal bcdToBigDecimal() {
+ long tempLong = 0L;
+ for (int shift = (precision - 1); shift >= 0; shift--) {
+ tempLong = tempLong * 10 + getDigitPos(shift);
+ }
+ BigDecimal result = BigDecimal.valueOf(tempLong);
+ result = result.scaleByPowerOfTen(scale);
+ if (isNegative()) result = result.negate();
+ return result;
+ }
+
+ @Override
+ protected void compact() {
+ // Special handling for 0
+ if (bcd == 0L) {
+ scale = 0;
+ precision = 0;
+ return;
+ }
+
+ // Compact the number (remove trailing zeros)
+ int delta = Long.numberOfTrailingZeros(bcd) / 4;
+ bcd >>>= delta * 4;
+ scale += delta;
+
+ // Compute precision
+ precision = 16 - (Long.numberOfLeadingZeros(bcd) / 4);
+ }
+
+ @Override
+ protected void copyBcdFrom(FormatQuantity _other) {
+ FormatQuantity2 other = (FormatQuantity2) _other;
+ bcd = other.bcd;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "<FormatQuantity2 %s:%d:%d:%s %016XE%d>",
+ (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+ lReqPos,
+ rReqPos,
+ (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+ bcd,
+ scale);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public final class FormatQuantity3 extends FormatQuantityBCD {
+
+ /**
+ * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+ * to one digit. For example, the number "12345" in BCD is "0x12345".
+ *
+ * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+ * like setting the digit to zero.
+ */
+ private byte[] bcd = new byte[100];
+
+ @Override
+ public int maxRepresentableDigits() {
+ return Integer.MAX_VALUE;
+ }
+
+ public FormatQuantity3(long input) {
+ setToLong(input);
+ }
+
+ public FormatQuantity3(int input) {
+ setToInt(input);
+ }
+
+ public FormatQuantity3(double input) {
+ setToDouble(input);
+ }
+
+ public FormatQuantity3(BigInteger input) {
+ setToBigInteger(input);
+ }
+
+ public FormatQuantity3(BigDecimal input) {
+ setToBigDecimal(input);
+ }
+
+ public FormatQuantity3(FormatQuantity3 other) {
+ copyFrom(other);
+ }
+
+ @Override
+ protected byte getDigitPos(int position) {
+ if (position < 0 || position > precision) return 0;
+ return bcd[position];
+ }
+
+ @Override
+ protected void setDigitPos(int position, byte value) {
+ assert position >= 0;
+ ensureCapacity(position + 1);
+ bcd[position] = value;
+ }
+
+ @Override
+ protected void shiftLeft(int numDigits) {
+ ensureCapacity(precision + numDigits);
+ int i = precision + numDigits - 1;
+ for (; i >= numDigits; i--) {
+ bcd[i] = bcd[i - numDigits];
+ }
+ for (; i >= 0; i--) {
+ bcd[i] = 0;
+ }
+ scale -= numDigits;
+ precision += numDigits;
+ }
+
+ @Override
+ protected void shiftRight(int numDigits) {
+ int i = 0;
+ for (; i < precision - numDigits; i++) {
+ bcd[i] = bcd[i + numDigits];
+ }
+ for (; i < precision; i++) {
+ bcd[i] = 0;
+ }
+ scale += numDigits;
+ precision -= numDigits;
+ }
+
+ @Override
+ protected void setBcdToZero() {
+ for (int i = 0; i < precision; i++) {
+ bcd[i] = (byte) 0;
+ }
+ scale = 0;
+ precision = 0;
+ isApproximate = false;
+ origDouble = 0;
+ origDelta = 0;
+ }
+
+ @Override
+ protected void readIntToBcd(int n) {
+ int i = 0;
+ for (; n != 0L; n /= 10L, i++) {
+ bcd[i] = (byte) (n % 10);
+ }
+ scale = 0;
+ precision = i;
+ }
+
+ private static final byte[] LONG_MIN_VALUE =
+ new byte[] {8, 0, 8, 5, 7, 7, 4, 5, 8, 6, 3, 0, 2, 7, 3, 3, 2, 2, 9};
+
+ @Override
+ protected void readLongToBcd(long n) {
+ if (n == Long.MIN_VALUE) {
+ // Can't consume via the normal path.
+ System.arraycopy(LONG_MIN_VALUE, 0, bcd, 0, LONG_MIN_VALUE.length);
+ scale = 0;
+ precision = LONG_MIN_VALUE.length;
+ return;
+ }
+ int i = 0;
+ for (; n != 0L; n /= 10L, i++) {
+ bcd[i] = (byte) (n % 10);
+ }
+ scale = 0;
+ precision = i;
+ }
+
+ @Override
+ protected void readBigIntegerToBcd(BigInteger n) {
+ int i = 0;
+ for (; n.signum() != 0; i++) {
+ BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+ ensureCapacity(i + 1);
+ bcd[i] = temp[1].byteValue();
+ n = temp[0];
+ }
+ scale = 0;
+ precision = i;
+ }
+
+ @Override
+ protected BigDecimal bcdToBigDecimal() {
+ // Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
+ return new BigDecimal(toDumbString());
+ }
+
+ private String toDumbString() {
+ StringBuilder sb = new StringBuilder();
+ if (isNegative()) sb.append('-');
+ if (precision == 0) {
+ sb.append('0');
+ return sb.toString();
+ }
+ for (int i = precision - 1; i >= 0; i--) {
+ sb.append(getDigitPos(i));
+ }
+ if (scale != 0) {
+ sb.append('E');
+ sb.append(scale);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ protected void compact() {
+ // Special handling for 0
+ boolean isZero = true;
+ for (int i = 0; i < precision; i++) {
+ if (bcd[i] != 0) {
+ isZero = false;
+ break;
+ }
+ }
+ if (isZero) {
+ scale = 0;
+ precision = 0;
+ return;
+ }
+
+ // Compact the number (remove trailing zeros)
+ int delta = 0;
+ for (; bcd[delta] == 0; delta++) ;
+ shiftRight(delta);
+
+ // Compute precision
+ int leading = precision - 1;
+ for (; leading >= 0 && bcd[leading] == 0; leading--) ;
+ precision = leading + 1;
+ }
+
+ private void ensureCapacity(int capacity) {
+ if (bcd.length >= capacity) return;
+ byte[] bcd1 = new byte[capacity * 2];
+ System.arraycopy(bcd, 0, bcd1, 0, bcd.length);
+ bcd = bcd1;
+ }
+
+ @Override
+ protected void copyBcdFrom(FormatQuantity _other) {
+ FormatQuantity3 other = (FormatQuantity3) _other;
+ System.arraycopy(other.bcd, 0, bcd, 0, bcd.length);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 30; i >= 0; i--) {
+ sb.append(bcd[i]);
+ }
+ return String.format(
+ "<FormatQuantity3 %s:%d:%d:%s %s%s%d>",
+ (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+ lReqPos,
+ rReqPos,
+ (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+ sb,
+ "E",
+ scale);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public final class FormatQuantity4 extends FormatQuantityBCD {
+
+ /**
+ * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+ * to one digit. For example, the number "12345" in BCD is "0x12345".
+ *
+ * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+ * like setting the digit to zero.
+ */
+ private byte[] bcdBytes;
+
+ private long bcdLong = 0L;
+
+ private boolean usingBytes = false;;
+
+ @Override
+ public int maxRepresentableDigits() {
+ return Integer.MAX_VALUE;
+ }
+
+ public FormatQuantity4() {
+ setBcdToZero();
+ }
+
+ public FormatQuantity4(long input) {
+ setToLong(input);
+ }
+
+ public FormatQuantity4(int input) {
+ setToInt(input);
+ }
+
+ public FormatQuantity4(double input) {
+ setToDouble(input);
+ }
+
+ public FormatQuantity4(BigInteger input) {
+ setToBigInteger(input);
+ }
+
+ public FormatQuantity4(BigDecimal input) {
+ setToBigDecimal(input);
+ }
+
+ public FormatQuantity4(FormatQuantity4 other) {
+ copyFrom(other);
+ }
+
+ public FormatQuantity4(Number number) {
+ if (number instanceof Long) {
+ setToLong(number.longValue());
+ } else if (number instanceof Integer) {
+ setToInt(number.intValue());
+ } else if (number instanceof Double) {
+ setToDouble(number.doubleValue());
+ } else if (number instanceof BigInteger) {
+ setToBigInteger((BigInteger) number);
+ } else if (number instanceof BigDecimal) {
+ setToBigDecimal((BigDecimal) number);
+ } else if (number instanceof com.ibm.icu.math.BigDecimal) {
+ setToBigDecimal(((com.ibm.icu.math.BigDecimal) number).toBigDecimal());
+ } else {
+ throw new IllegalArgumentException(
+ "Number is of an unsupported type: " + number.getClass().getName());
+ }
+ }
+
+ @Override
+ protected byte getDigitPos(int position) {
+ if (usingBytes) {
+ if (position < 0 || position > precision) return 0;
+ return bcdBytes[position];
+ } else {
+ if (position < 0 || position >= 16) return 0;
+ return (byte) ((bcdLong >>> (position * 4)) & 0xf);
+ }
+ }
+
+ @Override
+ protected void setDigitPos(int position, byte value) {
+ assert position >= 0;
+ if (usingBytes) {
+ ensureCapacity(position + 1);
+ bcdBytes[position] = value;
+ } else if (position >= 16) {
+ switchStorage();
+ ensureCapacity(position + 1);
+ bcdBytes[position] = value;
+ } else {
+ int shift = position * 4;
+ bcdLong = bcdLong & ~(0xfL << shift) | ((long) value << shift);
+ }
+ }
+
+ @Override
+ protected void shiftLeft(int numDigits) {
+ if (!usingBytes && precision + numDigits > 16) {
+ switchStorage();
+ }
+ if (usingBytes) {
+ ensureCapacity(precision + numDigits);
+ int i = precision + numDigits - 1;
+ for (; i >= numDigits; i--) {
+ bcdBytes[i] = bcdBytes[i - numDigits];
+ }
+ for (; i >= 0; i--) {
+ bcdBytes[i] = 0;
+ }
+ } else {
+ bcdLong <<= (numDigits * 4);
+ }
+ scale -= numDigits;
+ precision += numDigits;
+ }
+
+ @Override
+ protected void shiftRight(int numDigits) {
+ if (usingBytes) {
+ int i = 0;
+ for (; i < precision - numDigits; i++) {
+ bcdBytes[i] = bcdBytes[i + numDigits];
+ }
+ for (; i < precision; i++) {
+ bcdBytes[i] = 0;
+ }
+ } else {
+ bcdLong >>>= (numDigits * 4);
+ }
+ scale += numDigits;
+ precision -= numDigits;
+ }
+
+ @Override
+ protected void setBcdToZero() {
+ if (usingBytes) {
+ for (int i = 0; i < precision; i++) {
+ bcdBytes[i] = (byte) 0;
+ }
+ }
+ usingBytes = false;
+ bcdLong = 0L;
+ scale = 0;
+ precision = 0;
+ isApproximate = false;
+ origDouble = 0;
+ origDelta = 0;
+ }
+
+ @Override
+ protected void readIntToBcd(int n) {
+ // ints always fit inside the long implementation.
+ long result = 0L;
+ int i = 16;
+ for (; n != 0; n /= 10, i--) {
+ result = (result >>> 4) + (((long) n % 10) << 60);
+ }
+ usingBytes = false;
+ bcdLong = result >>> (i * 4);
+ scale = 0;
+ precision = 16 - i;
+ }
+
+ @Override
+ protected void readLongToBcd(long n) {
+ if (n >= 10000000000000000L) {
+ ensureCapacity();
+ int i = 0;
+ for (; n != 0L; n /= 10L, i++) {
+ bcdBytes[i] = (byte) (n % 10);
+ }
+ usingBytes = true;
+ scale = 0;
+ precision = i;
+ } else {
+ long result = 0L;
+ int i = 16;
+ for (; n != 0L; n /= 10L, i--) {
+ result = (result >>> 4) + ((n % 10) << 60);
+ }
+ assert i >= 0;
+ usingBytes = false;
+ bcdLong = result >>> (i * 4);
+ scale = 0;
+ precision = 16 - i;
+ }
+ }
+
+ @Override
+ protected void readBigIntegerToBcd(BigInteger n) {
+ ensureCapacity(); // allocate initial byte array
+ int i = 0;
+ for (; n.signum() != 0; i++) {
+ BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+ ensureCapacity(i + 1);
+ bcdBytes[i] = temp[1].byteValue();
+ n = temp[0];
+ }
+ usingBytes = true;
+ scale = 0;
+ precision = i;
+ }
+
+ @Override
+ protected BigDecimal bcdToBigDecimal() {
+ if (usingBytes) {
+ // Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
+ StringBuilder sb = new StringBuilder();
+ if (isNegative()) sb.append('-');
+ assert precision > 0;
+ for (int i = precision - 1; i >= 0; i--) {
+ sb.append(getDigitPos(i));
+ }
+ if (scale != 0) {
+ sb.append('E');
+ sb.append(scale);
+ }
+ return new BigDecimal(sb.toString());
+ } else {
+ long tempLong = 0L;
+ for (int shift = (precision - 1); shift >= 0; shift--) {
+ tempLong = tempLong * 10 + getDigitPos(shift);
+ }
+ BigDecimal result = BigDecimal.valueOf(tempLong);
+ result = result.scaleByPowerOfTen(scale);
+ if (isNegative()) result = result.negate();
+ return result;
+ }
+ }
+
+ @Override
+ protected void compact() {
+ if (usingBytes) {
+ int delta = 0;
+ for (; delta < precision && bcdBytes[delta] == 0; delta++) ;
+ if (delta == precision) {
+ // Number is zero
+ setBcdToZero();
+ return;
+ } else {
+ // Remove trailing zeros
+ shiftRight(delta);
+ }
+
+ // Compute precision
+ int leading = precision - 1;
+ for (; leading >= 0 && bcdBytes[leading] == 0; leading--) ;
+ precision = leading + 1;
+
+ // Switch storage mechanism if possible
+ if (precision <= 16) {
+ switchStorage();
+ }
+
+ } else {
+ if (bcdLong == 0L) {
+ // Number is zero
+ setBcdToZero();
+ return;
+ }
+
+ // Compact the number (remove trailing zeros)
+ int delta = Long.numberOfTrailingZeros(bcdLong) / 4;
+ bcdLong >>>= delta * 4;
+ scale += delta;
+
+ // Compute precision
+ precision = 16 - (Long.numberOfLeadingZeros(bcdLong) / 4);
+ }
+ }
+
+ /** Ensure that a byte array of at least 40 digits is allocated. */
+ private void ensureCapacity() {
+ ensureCapacity(40);
+ }
+
+ private void ensureCapacity(int capacity) {
+ if (bcdBytes == null && capacity > 0) {
+ bcdBytes = new byte[capacity];
+ } else if (bcdBytes.length < capacity) {
+ byte[] bcd1 = new byte[capacity * 2];
+ System.arraycopy(bcdBytes, 0, bcd1, 0, bcdBytes.length);
+ bcdBytes = bcd1;
+ }
+ }
+
+ /** Switches the internal storage mechanism between the 64-bit long and the byte array. */
+ private void switchStorage() {
+ if (usingBytes) {
+ // Change from bytes to long
+ bcdLong = 0L;
+ for (int i = precision - 1; i >= 0; i--) {
+ bcdLong <<= 4;
+ bcdLong |= bcdBytes[i];
+ bcdBytes[i] = 0;
+ }
+ usingBytes = false;
+ } else {
+ // Change from long to bytes
+ ensureCapacity();
+ for (int i = 0; i < precision; i++) {
+ bcdBytes[i] = (byte) (bcdLong & 0xf);
+ bcdLong >>>= 4;
+ }
+ usingBytes = true;
+ }
+ }
+
+ @Override
+ protected void copyBcdFrom(FormatQuantity _other) {
+ FormatQuantity4 other = (FormatQuantity4) _other;
+ if (other.usingBytes) {
+ usingBytes = true;
+ ensureCapacity(other.precision);
+ System.arraycopy(other.bcdBytes, 0, bcdBytes, 0, other.precision);
+ } else {
+ usingBytes = false;
+ bcdLong = other.bcdLong;
+ }
+ }
+
+ /**
+ * Checks whether the bytes stored in this instance are all valid. For internal unit testing only.
+ *
+ * @return An error message if this instance is invalid, or null if this instance is healthy.
+ * @internal
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String checkHealth() {
+ if (usingBytes) {
+ if (bcdLong != 0) return "Value in bcdLong but we are in byte mode";
+ if (precision == 0) return "Zero precision but we are in byte mode";
+ if (precision > bcdBytes.length) return "Precision exceeds length of byte array";
+ if (getDigitPos(precision - 1) == 0) return "Most significant digit is zero in byte mode";
+ if (getDigitPos(0) == 0) return "Least significant digit is zero in long mode";
+ for (int i = 0; i < precision; i++) {
+ if (getDigitPos(i) >= 10) return "Digit exceeding 10 in byte array";
+ if (getDigitPos(i) < 0) return "Digit below 0 in byte array";
+ }
+ for (int i = precision; i < bcdBytes.length; i++) {
+ if (getDigitPos(i) != 0) return "Nonzero digits outside of range in byte array";
+ }
+ } else {
+ if (bcdBytes != null) {
+ for (int i = 0; i < bcdBytes.length; i++) {
+ if (bcdBytes[i] != 0) return "Nonzero digits in byte array but we are in long mode";
+ }
+ }
+ if (precision == 0 && bcdLong != 0) return "Value in bcdLong even though precision is zero";
+ if (precision > 16) return "Precision exceeds length of long";
+ if (precision != 0 && getDigitPos(precision - 1) == 0)
+ return "Most significant digit is zero in long mode";
+ if (precision != 0 && getDigitPos(0) == 0)
+ return "Least significant digit is zero in long mode";
+ for (int i = 0; i < precision; i++) {
+ if (getDigitPos(i) >= 10) return "Digit exceeding 10 in long";
+ if (getDigitPos(i) < 0) return "Digit below 0 in long (?!)";
+ }
+ for (int i = precision; i < 16; i++) {
+ if (getDigitPos(i) != 0) return "Nonzero digits outside of range in long";
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks whether this {@link FormatQuantity4} is using its internal byte array storage mechanism.
+ *
+ * @return true if an internal byte array is being used; false if a long is being used.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public boolean usingBytes() {
+ return usingBytes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ if (usingBytes) {
+ for (int i = precision - 1; i >= 0; i--) {
+ sb.append(bcdBytes[i]);
+ }
+ } else {
+ sb.append(Long.toHexString(bcdLong));
+ }
+ return String.format(
+ "<FormatQuantity4 %s:%d:%d:%s %s %s%s%d>",
+ (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+ lReqPos,
+ rReqPos,
+ (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+ (usingBytes ? "bytes" : "long"),
+ sb,
+ "E",
+ scale);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.text.FieldPosition;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.Operand;
+import com.ibm.icu.text.UFieldPosition;
+
+/**
+ * Represents numbers and digit display properties using Binary Coded Decimal (BCD).
+ *
+ * @implements {@link FormatQuantity}
+ */
+public abstract class FormatQuantityBCD implements FormatQuantity {
+
+ /**
+ * The power of ten corresponding to the least significant digit in the BCD. For example, if this
+ * object represents the number "3.14", the BCD will be "0x314" and the scale will be -2.
+ *
+ * <p>Note that in {@link java.math.BigDecimal}, the scale is defined differently: the number of
+ * digits after the decimal place, which is the negative of our definition of scale.
+ */
+ protected int scale;
+
+ /**
+ * The number of digits in the BCD. For example, "1007" has BCD "0x1007" and precision 4. The
+ * maximum precision is 16 since a long can hold only 16 digits.
+ *
+ * <p>This value must be re-calculated whenever the value in bcd changes by using {@link
+ * #computePrecisionAndCompact()}.
+ */
+ protected int precision;
+
+ /**
+ * A bitmask of properties relating to the number represented by this object.
+ *
+ * @see #NEGATIVE_FLAG
+ * @see #INFINITY_FLAG
+ * @see #NAN_FLAG
+ */
+ protected int flags;
+
+ protected static final int NEGATIVE_FLAG = 1;
+ protected static final int INFINITY_FLAG = 2;
+ protected static final int NAN_FLAG = 4;
+
+ /**
+ * The original number provided by the user and which is represented in BCD. Used when we need to
+ * re-compute the BCD for an exact double representation.
+ */
+ protected double origDouble;
+
+ protected int origDelta;
+ protected boolean isApproximate;
+
+ // Four positions: left optional '(', left required '[', right required ']', right optional ')'.
+ // These four positions determine which digits are displayed in the output string. They do NOT
+ // affect rounding. These positions are internal-only and can be specified only by the public
+ // endpoints like setFractionLength, setIntegerLength, and setSignificantDigits, among others.
+ //
+ // * Digits between lReqPos and rReqPos are in the "required zone" and are always displayed.
+ // * Digits between lOptPos and rOptPos but outside the required zone are in the "optional zone"
+ // and are displayed unless they are trailing off the left or right edge of the number and
+ // have a numerical value of zero. In order to be "trailing", the digits need to be beyond
+ // the decimal point in their respective directions.
+ // * Digits outside of the "optional zone" are never displayed.
+ //
+ // See the table below for illustrative examples.
+ //
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ // | lOptPos | lReqPos | rReqPos | rOptPos | number | positions | en-US string |
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ // | 5 | 2 | -1 | -5 | 1234.567 | ( 12[34.5]67 ) | 1,234.567 |
+ // | 3 | 2 | -1 | -5 | 1234.567 | 1(2[34.5]67 ) | 234.567 |
+ // | 3 | 2 | -1 | -2 | 1234.567 | 1(2[34.5]6)7 | 234.56 |
+ // | 6 | 4 | 2 | -5 | 123456789. | 123(45[67]89. ) | 456,789. |
+ // | 6 | 4 | 2 | 1 | 123456789. | 123(45[67]8)9. | 456,780. |
+ // | -1 | -1 | -3 | -4 | 0.123456 | 0.1([23]4)56 | .0234 |
+ // | 6 | 4 | -2 | -2 | 12.3 | ( [ 12.3 ]) | 0012.30 |
+ // +---------+---------+---------+---------+------------+------------------------+--------------+
+ //
+ protected int lOptPos = Integer.MAX_VALUE;
+ protected int lReqPos = 0;
+ protected int rReqPos = 0;
+ protected int rOptPos = Integer.MIN_VALUE;
+
+ @Override
+ public void copyFrom(FormatQuantity _other) {
+ copyBcdFrom(_other);
+ FormatQuantityBCD other = (FormatQuantityBCD) _other;
+ lOptPos = other.lOptPos;
+ lReqPos = other.lReqPos;
+ rReqPos = other.rReqPos;
+ rOptPos = other.rOptPos;
+ scale = other.scale;
+ precision = other.precision;
+ flags = other.flags;
+ origDouble = other.origDouble;
+ origDelta = other.origDelta;
+ isApproximate = other.isApproximate;
+ }
+
+ public FormatQuantityBCD clear() {
+ lOptPos = Integer.MAX_VALUE;
+ lReqPos = 0;
+ rReqPos = 0;
+ rOptPos = Integer.MIN_VALUE;
+ flags = 0;
+ setBcdToZero(); // sets scale, precision, hasDouble, origDouble, origDelta, and BCD data
+ return this;
+ }
+
+ @Override
+ public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
+ // Graceful failures for bogus input
+ minInt = Math.max(0, minInt);
+ maxInt = Math.max(0, maxInt);
+ minFrac = Math.max(0, minFrac);
+ maxFrac = Math.max(0, maxFrac);
+
+ // The minima must be less than or equal to the maxima
+ if (maxInt < minInt) {
+ minInt = maxInt;
+ }
+ if (maxFrac < minFrac) {
+ minFrac = maxFrac;
+ }
+
+ // Displaying neither integer nor fraction digits is not allowed
+ if (maxInt == 0 && maxFrac == 0) {
+ maxInt = Integer.MAX_VALUE;
+ maxFrac = Integer.MAX_VALUE;
+ }
+
+ // Save values into internal state
+ // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
+ lOptPos = maxInt;
+ lReqPos = minInt;
+ rReqPos = -minFrac;
+ rOptPos = -maxFrac;
+ }
+
+ @Override
+ public long getPositionFingerprint() {
+ long fingerprint = 0;
+ fingerprint ^= lOptPos;
+ fingerprint ^= (lReqPos << 16);
+ fingerprint ^= ((long) rReqPos << 32);
+ fingerprint ^= ((long) rOptPos << 48);
+ return fingerprint;
+ }
+
+ @Override
+ public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext) {
+ // TODO: Avoid converting back and forth to BigDecimal.
+ BigDecimal temp = toBigDecimal();
+ temp =
+ temp.divide(roundingInterval, 0, mathContext.getRoundingMode())
+ .multiply(roundingInterval)
+ .round(mathContext);
+ if (temp.signum() == 0) {
+ setBcdToZero(); // keeps negative flag for -0.0
+ } else {
+ setToBigDecimal(temp);
+ }
+ }
+
+ @Override
+ public void multiplyBy(BigDecimal multiplicand) {
+ BigDecimal temp = toBigDecimal();
+ temp = temp.multiply(multiplicand);
+ setToBigDecimal(temp);
+ }
+
+ @Override
+ public int getMagnitude() throws ArithmeticException {
+ if (precision == 0) {
+ throw new ArithmeticException("Magnitude is not well-defined for zero");
+ } else {
+ return scale + precision - 1;
+ }
+ }
+
+ @Override
+ public void adjustMagnitude(int delta) {
+ if (precision != 0) {
+ scale += delta;
+ origDelta += delta;
+ }
+ }
+
+ @Override
+ public StandardPlural getStandardPlural(PluralRules rules) {
+ if (rules == null) {
+ // Fail gracefully if the user didn't provide a PluralRules
+ return StandardPlural.OTHER;
+ } else {
+ @SuppressWarnings("deprecation")
+ String ruleString = rules.select(this);
+ return StandardPlural.orOtherFromString(ruleString);
+ }
+ }
+
+ @Override
+ public double getPluralOperand(Operand operand) {
+ switch (operand) {
+ case i:
+ return toLong();
+ case f:
+ return toFractionLong(true);
+ case t:
+ return toFractionLong(false);
+ case v:
+ return fractionCount();
+ case w:
+ return fractionCountWithoutTrailingZeros();
+ default:
+ return Math.abs(toDouble());
+ }
+ }
+
+ /**
+ * If the given {@link FieldPosition} is a {@link UFieldPosition}, populates it with the fraction
+ * length and fraction long value. If the argument is not a {@link UFieldPosition}, nothing
+ * happens.
+ *
+ * @param fp The {@link UFieldPosition} to populate.
+ */
+ public void populateUFieldPosition(FieldPosition fp) {
+ if (fp instanceof UFieldPosition) {
+ ((UFieldPosition) fp)
+ .setFractionDigits((int) getPluralOperand(Operand.v), (long) getPluralOperand(Operand.f));
+ }
+ }
+
+ @Override
+ public int getUpperDisplayMagnitude() {
+ int magnitude = scale + precision;
+ int result = (lReqPos > magnitude) ? lReqPos : (lOptPos < magnitude) ? lOptPos : magnitude;
+ return result - 1;
+ }
+
+ @Override
+ public int getLowerDisplayMagnitude() {
+ int magnitude = scale;
+ int result = (rReqPos < magnitude) ? rReqPos : (rOptPos > magnitude) ? rOptPos : magnitude;
+ return result;
+ }
+
+ @Override
+ public byte getDigit(int magnitude) {
+ return getDigitPos(magnitude - scale);
+ }
+
+ private int fractionCount() {
+ return -getLowerDisplayMagnitude();
+ }
+
+ private int fractionCountWithoutTrailingZeros() {
+ return Math.max(-scale, 0);
+ }
+
+ @Override
+ public boolean isNegative() {
+ return (flags & NEGATIVE_FLAG) != 0;
+ }
+
+ @Override
+ public boolean isInfinite() {
+ return (flags & INFINITY_FLAG) != 0;
+ }
+
+ @Override
+ public boolean isNaN() {
+ return (flags & NAN_FLAG) != 0;
+ }
+
+ @Override
+ public boolean isZero() {
+ return precision == 0;
+ }
+
+ @Override
+ public FormatQuantity clone() {
+ if (this instanceof FormatQuantity2) {
+ return new FormatQuantity2((FormatQuantity2) this);
+ } else if (this instanceof FormatQuantity3) {
+ return new FormatQuantity3((FormatQuantity3) this);
+ } else if (this instanceof FormatQuantity4) {
+ return new FormatQuantity4((FormatQuantity4) this);
+ } else {
+ throw new IllegalArgumentException("Don't know how to clone " + this.getClass());
+ }
+ }
+
+ public void setToInt(int n) {
+ setBcdToZero();
+ flags = 0;
+ if (n < 0) {
+ flags |= NEGATIVE_FLAG;
+ n = -n;
+ }
+ if (n != 0) {
+ _setToInt(n);
+ compact();
+ }
+ }
+
+ private void _setToInt(int n) {
+ if (n == Integer.MIN_VALUE) {
+ readLongToBcd(-(long) n);
+ } else {
+ readIntToBcd(n);
+ }
+ }
+
+ public void setToLong(long n) {
+ setBcdToZero();
+ flags = 0;
+ if (n < 0) {
+ flags |= NEGATIVE_FLAG;
+ n = -n;
+ }
+ if (n != 0) {
+ _setToLong(n);
+ compact();
+ }
+ }
+
+ private void _setToLong(long n) {
+ if (n == Long.MIN_VALUE) {
+ readBigIntegerToBcd(BigInteger.valueOf(n).negate());
+ } else if (n <= Integer.MAX_VALUE) {
+ readIntToBcd((int) n);
+ } else {
+ readLongToBcd(n);
+ }
+ }
+
+ public void setToBigInteger(BigInteger n) {
+ setBcdToZero();
+ flags = 0;
+ if (n.signum() == -1) {
+ flags |= NEGATIVE_FLAG;
+ n = n.negate();
+ }
+ if (n.signum() != 0) {
+ _setToBigInteger(n);
+ compact();
+ }
+ }
+
+ private void _setToBigInteger(BigInteger n) {
+ if (n.bitLength() < 32) {
+ readIntToBcd(n.intValue());
+ } else if (n.bitLength() < 64) {
+ readLongToBcd(n.longValue());
+ } else {
+ readBigIntegerToBcd(n);
+ }
+ }
+
+ /**
+ * Sets the internal BCD state to represent the value in the given double.
+ *
+ * @param n The value to consume.
+ */
+ public void setToDouble(double n) {
+ setBcdToZero();
+ flags = 0;
+ // Double.compare() handles +0.0 vs -0.0
+ if (Double.compare(n, 0.0) < 0) {
+ flags |= NEGATIVE_FLAG;
+ n = -n;
+ }
+ if (Double.isNaN(n)) {
+ flags |= NAN_FLAG;
+ } else if (Double.isInfinite(n)) {
+ flags |= INFINITY_FLAG;
+ } else if (n != 0) {
+ _setToDoubleFast(n);
+
+ // TODO: Remove this when finished testing.
+ // isApproximate = true;
+ // origDouble = n;
+ // origDelta = 0;
+ // convertToAccurateDouble();
+
+ compact();
+ }
+ }
+
+ private static final double[] DOUBLE_MULTIPLIERS = {
+ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
+ 1e17, 1e18, 1e19, 1e20, 1e21
+ };
+
+ /**
+ * Uses double multiplication and division to get the number into integer space before converting
+ * to digits. Since double arithmetic is inexact, the resulting digits may not be accurate.
+ */
+ private void _setToDoubleFast(double n) {
+ long ieeeBits = Double.doubleToLongBits(n);
+ int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+
+ // Not all integers can be represented exactly for exponent > 52
+ if (exponent <= 52 && (long) n == n) {
+ _setToLong((long) n);
+ return;
+ }
+
+ isApproximate = true;
+ origDouble = n;
+ origDelta = 0;
+
+ int fracLength = (int) ((52 - exponent) / 3.32192809489);
+ if (fracLength >= 0) {
+ int i = fracLength;
+ // 1e22 is the largest exact double.
+ for (; i >= 22; i -= 22) n *= 1e22;
+ n *= DOUBLE_MULTIPLIERS[i];
+ } else {
+ int i = fracLength;
+ // 1e22 is the largest exact double.
+ for (; i <= -22; i += 22) n /= 1e22;
+ n /= DOUBLE_MULTIPLIERS[-i];
+ }
+ _setToLong(Math.round(n));
+ scale -= fracLength;
+ }
+
+ /**
+ * Uses Double.toString() to obtain an exact accurate representation of the double, overwriting it
+ * into the BCD. This method can be called at any point after {@link #_setToDoubleFast} while
+ * {@link #isApproximate} is still true.
+ */
+ private void convertToAccurateDouble() {
+ double n = origDouble;
+ assert n != 0;
+ int delta = origDelta;
+ setBcdToZero();
+
+ // Call the slow oracle function
+ String temp = Double.toString(n);
+
+ if (temp.indexOf('E') != -1) {
+ // Case 1: Exponential notation.
+ assert temp.indexOf('.') == 1;
+ int expPos = temp.indexOf('E');
+ _setToLong(Long.parseLong(temp.charAt(0) + temp.substring(2, expPos)));
+ scale += Integer.parseInt(temp.substring(expPos + 1)) - (expPos - 1) + 1;
+ } else if (temp.charAt(0) == '0') {
+ // Case 2: Fraction-only number.
+ assert temp.indexOf('.') == 1;
+ _setToLong(Long.parseLong(temp.substring(2)));
+ scale += 2 - temp.length();
+ } else if (temp.charAt(temp.length() - 1) == '0') {
+ // Case 3: Integer-only number.
+ // Note: this path should not normally happen, because integer-only numbers are captured
+ // before the approximate double logic is performed.
+ assert temp.indexOf('.') == temp.length() - 2;
+ assert temp.length() - 2 <= 18;
+ _setToLong(Long.parseLong(temp.substring(0, temp.length() - 2)));
+ // no need to adjust scale
+ } else {
+ // Case 4: Number with both a fraction and an integer.
+ int decimalPos = temp.indexOf('.');
+ _setToLong(Long.parseLong(temp.substring(0, decimalPos) + temp.substring(decimalPos + 1)));
+ scale += decimalPos - temp.length() + 1;
+ }
+ scale += delta;
+ compact();
+ explicitExactDouble = true;
+ }
+
+ /**
+ * Whether this {@link FormatQuantity4} has been explicitly converted to an exact double. true if
+ * backed by a double that was explicitly converted via convertToAccurateDouble; false otherwise.
+ * Used for testing.
+ *
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated public boolean explicitExactDouble = false;
+
+ /**
+ * Sets the internal BCD state to represent the value in the given BigDecimal.
+ *
+ * @param n The value to consume.
+ */
+ public void setToBigDecimal(BigDecimal n) {
+ setBcdToZero();
+ flags = 0;
+ if (n.signum() == -1) {
+ flags |= NEGATIVE_FLAG;
+ n = n.negate();
+ }
+ if (n.signum() != 0) {
+ _setToBigDecimal(n);
+ compact();
+ }
+ }
+
+ private void _setToBigDecimal(BigDecimal n) {
+ int fracLength = n.scale();
+ n = n.scaleByPowerOfTen(fracLength);
+ BigInteger bi = n.toBigInteger();
+ _setToBigInteger(bi);
+ scale -= fracLength;
+ }
+
+ /**
+ * Returns a long approximating the internal BCD. A long can only represent the integral part of
+ * the number.
+ *
+ * @return A double representation of the internal BCD.
+ */
+ protected long toLong() {
+ long result = 0L;
+ for (int magnitude = scale + precision - 1; magnitude >= 0; magnitude--) {
+ result = result * 10 + getDigitPos(magnitude - scale);
+ }
+ return result;
+ }
+
+ /**
+ * This returns a long representing the fraction digits of the number, as required by PluralRules.
+ * For example, if we represent the number "1.20" (including optional and required digits), then
+ * this function returns "20" if includeTrailingZeros is true or "2" if false.
+ */
+ protected long toFractionLong(boolean includeTrailingZeros) {
+ long result = 0L;
+ int magnitude = -1;
+ for (;
+ (magnitude >= scale || (includeTrailingZeros && magnitude >= rReqPos))
+ && magnitude >= rOptPos;
+ magnitude--) {
+ result = result * 10 + getDigitPos(magnitude - scale);
+ }
+ return result;
+ }
+
+ /**
+ * Returns a double approximating the internal BCD. The double may not retain all of the
+ * information encoded in the BCD if the BCD represents a number out of range of a double.
+ *
+ * @return A double representation of the internal BCD.
+ */
+ @Override
+ public double toDouble() {
+ if (isApproximate) {
+ return toDoubleFromOriginal();
+ }
+
+ if (isNaN()) {
+ return Double.NaN;
+ } else if (isInfinite()) {
+ return isNegative() ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
+ }
+
+ long tempLong = 0L;
+ int lostDigits = precision - Math.min(precision, 17);
+ for (int shift = precision - 1; shift >= lostDigits; shift--) {
+ tempLong = tempLong * 10 + getDigitPos(shift);
+ }
+ double result = tempLong;
+ int _scale = scale + lostDigits;
+ if (_scale >= 0) {
+ int i = _scale;
+ for (; i >= 9; i -= 9) result *= 1000000000;
+ for (; i >= 3; i -= 3) result *= 1000;
+ for (; i >= 1; i -= 1) result *= 10;
+ } else {
+ int i = _scale;
+ for (; i <= -9; i += 9) result /= 1000000000;
+ for (; i <= -3; i += 3) result /= 1000;
+ for (; i <= -1; i += 1) result /= 10;
+ }
+ if (isNegative()) result = -result;
+ return result;
+ }
+
+ @Override
+ public BigDecimal toBigDecimal() {
+ if (isApproximate) {
+ // Converting to a BigDecimal requires Double.toString().
+ convertToAccurateDouble();
+ }
+ return bcdToBigDecimal();
+ }
+
+ protected double toDoubleFromOriginal() {
+ double result = origDouble;
+ double delta = origDelta;
+ if (delta >= 0) {
+ for (; delta >= 9; delta -= 9) result *= 1000000000;
+ for (; delta >= 3; delta -= 3) result *= 1000;
+ for (; delta >= 1; delta -= 1) result *= 10;
+ } else {
+ for (; delta <= -9; delta += 9) result /= 1000000000;
+ for (; delta <= -3; delta += 3) result /= 1000;
+ for (; delta <= -1; delta += 1) result /= 10;
+ }
+ if (isNegative()) result *= -1;
+ return result;
+ }
+
+ private static int safeSubtract(int a, int b) {
+ if (b < 0 && a - b < a) return Integer.MAX_VALUE;
+ if (b > 0 && a - b > a) return Integer.MIN_VALUE;
+ return a - b;
+ }
+
+ @Override
+ public void roundToMagnitude(int magnitude, MathContext mathContext) {
+ // The position in the BCD at which rounding will be performed; digits to the right of position
+ // will be rounded away.
+ // TODO: Andy: There was a test failure because of integer overflow here. Should I do
+ // "safe subtraction" everywhere in the code? What's the nicest way to do it?
+ int position = safeSubtract(magnitude, scale);
+
+ // Enforce the number of digits required by the MathContext.
+ int _mcPrecision = mathContext.getPrecision();
+ if (magnitude == Integer.MAX_VALUE
+ || (_mcPrecision > 0 && precision - position > _mcPrecision)) {
+ position = precision - _mcPrecision;
+ }
+
+ if (position <= 0 && !isApproximate) {
+ // All digits are to the left of the rounding magnitude.
+ } else if (precision == 0) {
+ // No rounding for zero.
+ } else {
+ // Perform rounding logic.
+ // "leading" = most significant digit to the right of rounding
+ // "trailing" = least significant digit to the left of rounding
+ byte leadingDigit = getDigitPos(safeSubtract(position, 1));
+ byte trailingDigit = getDigitPos(position);
+
+ // Compute which section of the number we are in.
+ // EDGE means we are at the bottom or top edge, like 1.000 or 1.999 (used by doubles)
+ // LOWER means we are between the bottom edge and the midpoint, like 1.391
+ // MIDPOINT means we are exactly in the middle, like 1.500
+ // UPPER means we are between the midpoint and the top edge, like 1.916
+ int section = RoundingUtils.SECTION_MIDPOINT;
+ if (!isApproximate) {
+ if (leadingDigit < 5) {
+ section = RoundingUtils.SECTION_LOWER;
+ } else if (leadingDigit > 5) {
+ section = RoundingUtils.SECTION_UPPER;
+ } else {
+ for (int p = safeSubtract(position, 2); p >= 0; p--) {
+ if (getDigitPos(p) != 0) {
+ section = RoundingUtils.SECTION_UPPER;
+ break;
+ }
+ }
+ }
+ } else {
+ int p = safeSubtract(position, 2);
+ int minP = Math.max(0, precision - 14);
+ if (leadingDigit == 0) {
+ section = -1;
+ for (; p >= minP; p--) {
+ if (getDigitPos(p) != 0) {
+ section = RoundingUtils.SECTION_LOWER;
+ break;
+ }
+ }
+ } else if (leadingDigit == 4) {
+ for (; p >= minP; p--) {
+ if (getDigitPos(p) != 9) {
+ section = RoundingUtils.SECTION_LOWER;
+ break;
+ }
+ }
+ } else if (leadingDigit == 5) {
+ for (; p >= minP; p--) {
+ if (getDigitPos(p) != 0) {
+ section = RoundingUtils.SECTION_UPPER;
+ break;
+ }
+ }
+ } else if (leadingDigit == 9) {
+ section = -2;
+ for (; p >= minP; p--) {
+ if (getDigitPos(p) != 9) {
+ section = RoundingUtils.SECTION_UPPER;
+ break;
+ }
+ }
+ } else if (leadingDigit < 5) {
+ section = RoundingUtils.SECTION_LOWER;
+ } else {
+ section = RoundingUtils.SECTION_UPPER;
+ }
+
+ boolean roundsAtMidpoint =
+ RoundingUtils.roundsAtMidpoint(mathContext.getRoundingMode().ordinal());
+ if (safeSubtract(position, 1) < precision - 14
+ || (roundsAtMidpoint && section == RoundingUtils.SECTION_MIDPOINT)
+ || (!roundsAtMidpoint && section < 0 /* i.e. at upper or lower edge */)) {
+ // Oops! This means that we have to get the exact representation of the double, because
+ // the zone of uncertainty is along the rounding boundary.
+ convertToAccurateDouble();
+ roundToMagnitude(magnitude, mathContext); // start over
+ return;
+ }
+
+ // Turn off the approximate double flag, since the value is now confirmed to be exact.
+ isApproximate = false;
+ origDouble = 0.0;
+ origDelta = 0;
+
+ if (position <= 0) {
+ // All digits are to the left of the rounding magnitude.
+ return;
+ }
+
+ // Good to continue rounding.
+ if (section == -1) section = RoundingUtils.SECTION_LOWER;
+ if (section == -2) section = RoundingUtils.SECTION_UPPER;
+ }
+
+ boolean roundDown =
+ RoundingUtils.getRoundingDirection(
+ (trailingDigit % 2) == 0,
+ isNegative(),
+ section,
+ mathContext.getRoundingMode().ordinal(),
+ this);
+
+ // Perform truncation
+ if (position >= precision) {
+ setBcdToZero();
+ scale = magnitude;
+ } else {
+ shiftRight(position);
+ }
+
+ // Bubble the result to the higher digits
+ if (!roundDown) {
+ if (trailingDigit == 9) {
+ int bubblePos = 0;
+ // Note: in the long implementation, the most digits BCD can have at this point is 15,
+ // so bubblePos <= 15 and getDigitPos(bubblePos) is safe.
+ for (; getDigitPos(bubblePos) == 9; bubblePos++) {}
+ shiftRight(bubblePos); // shift off the trailing 9s
+ }
+ byte digit0 = getDigitPos(0);
+ assert digit0 != 9;
+ setDigitPos(0, (byte) (digit0 + 1));
+ precision += 1; // in case an extra digit got added
+ }
+
+ compact();
+ }
+ }
+
+ @Override
+ public void roundToInfinity() {
+ if (isApproximate) {
+ convertToAccurateDouble();
+ }
+ }
+
+ /**
+ * Appends a digit, optionally with one or more leading zeros, to the end of the value represented
+ * by this FormatQuantity.
+ *
+ * <p>The primary use of this method is to construct numbers during a parsing loop. It allows
+ * parsing to take advantage of the digit list infrastructure primarily designed for formatting.
+ *
+ * @param value The digit to append.
+ * @param leadingZeros The number of zeros to append before the digit. For example, if the value
+ * in this instance starts as 12.3, and you append a 4 with 1 leading zero, the value becomes
+ * 12.304.
+ * @param appendAsInteger If true, increase the magnitude of existing digits to make room for the
+ * new digit. If false, append to the end like a fraction digit. If true, there must not be
+ * any fraction digits already in the number.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public void appendDigit(byte value, int leadingZeros, boolean appendAsInteger) {
+ assert leadingZeros >= 0;
+
+ // Zero requires special handling to maintain the invariant that the least-significant digit
+ // in the BCD is nonzero.
+ if (value == 0) {
+ if (appendAsInteger && precision != 0) {
+ scale += leadingZeros + 1;
+ }
+ return;
+ }
+
+ // Deal with trailing zeros
+ if (scale > 0) {
+ leadingZeros += scale;
+ if (appendAsInteger) {
+ scale = 0;
+ }
+ }
+
+ // Append digit
+ shiftLeft(leadingZeros + 1);
+ setDigitPos(0, value);
+
+ // Fix scale if in integer mode
+ if (appendAsInteger) {
+ scale += leadingZeros + 1;
+ }
+ }
+
+ /**
+ * Returns a single digit from the BCD list. No internal state is changed by calling this method.
+ *
+ * @param position The position of the digit to pop, counted in BCD units from the least
+ * significant digit. If outside the range supported by the implementation, zero is returned.
+ * @return The digit at the specified location.
+ */
+ protected abstract byte getDigitPos(int position);
+
+ /**
+ * Sets the digit in the BCD list. This method only sets the digit; it is the caller's
+ * responsibility to call {@link #compact} after setting the digit.
+ *
+ * @param position The position of the digit to pop, counted in BCD units from the least
+ * significant digit. If outside the range supported by the implementation, an AssertionError
+ * is thrown.
+ * @param value The digit to set at the specified location.
+ */
+ protected abstract void setDigitPos(int position, byte value);
+
+ /**
+ * Adds zeros to the end of the BCD list. This will result in an invalid BCD representation; it is
+ * the caller's responsibility to do further manipulation and then call {@link #compact}.
+ *
+ * @param numDigits The number of zeros to add.
+ */
+ protected abstract void shiftLeft(int numDigits);
+
+ protected abstract void shiftRight(int numDigits);
+
+ /**
+ * Sets the internal representation to zero. Clears any values stored in scale, precision,
+ * hasDouble, origDouble, origDelta, and BCD data.
+ */
+ protected abstract void setBcdToZero();
+
+ /**
+ * Sets the internal BCD state to represent the value in the given int. The int is guaranteed to
+ * be either positive. The internal state is guaranteed to be empty when this method is called.
+ *
+ * @param n The value to consume.
+ */
+ protected abstract void readIntToBcd(int input);
+
+ /**
+ * Sets the internal BCD state to represent the value in the given long. The long is guaranteed to
+ * be either positive. The internal state is guaranteed to be empty when this method is called.
+ *
+ * @param n The value to consume.
+ */
+ protected abstract void readLongToBcd(long input);
+
+ /**
+ * Sets the internal BCD state to represent the value in the given BigInteger. The BigInteger is
+ * guaranteed to be positive, and it is guaranteed to be larger than Long.MAX_VALUE. The internal
+ * state is guaranteed to be empty when this method is called.
+ *
+ * @param n The value to consume.
+ */
+ protected abstract void readBigIntegerToBcd(BigInteger input);
+
+ /**
+ * Returns a BigDecimal encoding the internal BCD value.
+ *
+ * @return A BigDecimal representation of the internal BCD.
+ */
+ protected abstract BigDecimal bcdToBigDecimal();
+
+ protected abstract void copyBcdFrom(FormatQuantity _other);
+
+ /**
+ * Removes trailing zeros from the BCD (adjusting the scale as required) and then computes the
+ * precision. The precision is the number of digits in the number up through the greatest nonzero
+ * digit.
+ *
+ * <p>This method must always be called when bcd changes in order for assumptions to be correct in
+ * methods like {@link #fractionCount()}.
+ */
+ protected abstract void compact();
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/** @author sffc */
+public class FormatQuantitySelector {
+ public static FormatQuantityBCD from(int input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(long input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(double input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(BigInteger input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(BigDecimal input) {
+ return new FormatQuantity4(input);
+ }
+
+ public static FormatQuantityBCD from(com.ibm.icu.math.BigDecimal input) {
+ return from(input.toBigDecimal());
+ }
+
+ public static FormatQuantityBCD from(Number number) {
+ if (number instanceof Long) {
+ return from(number.longValue());
+ } else if (number instanceof Integer) {
+ return from(number.intValue());
+ } else if (number instanceof Double) {
+ return from(number.doubleValue());
+ } else if (number instanceof BigInteger) {
+ return from((BigInteger) number);
+ } else if (number instanceof BigDecimal) {
+ return from((BigDecimal) number);
+ } else if (number instanceof com.ibm.icu.math.BigDecimal) {
+ return from((com.ibm.icu.math.BigDecimal) number);
+ } else {
+ throw new IllegalArgumentException(
+ "Number is of an unsupported type: " + number.getClass().getName());
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
+import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import com.ibm.icu.impl.number.modifiers.SimpleModifier;
+
+/**
+ * A Modifier is an immutable object that can be passed through the formatting pipeline until it is
+ * finally applied to the string builder. A Modifier usually contains a prefix and a suffix that are
+ * applied, but it could contain something else, like a {@link com.ibm.icu.text.SimpleFormatter}
+ * pattern.
+ *
+ * @see PositiveNegativeAffixModifier
+ * @see ConstantAffixModifier
+ * @see GeneralPluralModifier
+ * @see SimpleModifier
+ */
+public interface Modifier {
+
+ /**
+ * Apply this Modifier to the string builder.
+ *
+ * @param output The string builder to which to apply this modifier.
+ * @param leftIndex The left index of the string within the builder. Equal to 0 when only one
+ * number is being formatted.
+ * @param rightIndex The right index of the string within the string builder. Equal to length-1
+ * when only one number is being formatted.
+ * @return The number of characters (UTF-16 code units) that were added to the string builder.
+ */
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex);
+
+ /**
+ * The number of characters that {@link #apply} would add to the string builder.
+ *
+ * @return The number of characters (UTF-16 code units) that would be added to a string builder.
+ */
+ public int length();
+
+ /**
+ * Whether this modifier is strong. If a modifier is strong, it should always be applied
+ * immediately and not allowed to bubble up. With regard to padding, strong modifiers are
+ * considered to be on the inside of the prefix and suffix.
+ *
+ * @return Whether the modifier is strong.
+ */
+ public boolean isStrong();
+
+ /**
+ * Gets the prefix string associated with this modifier, defined as the string that will be
+ * inserted at leftIndex when {@link #apply} is called.
+ *
+ * @return The prefix string. Will not be null.
+ */
+ public String getPrefix();
+
+ /**
+ * Gets the prefix string associated with this modifier, defined as the string that will be
+ * inserted at rightIndex when {@link #apply} is called.
+ *
+ * @return The suffix string. Will not be null.
+ */
+ public String getSuffix();
+
+ /**
+ * An interface for a modifier that contains both a positive and a negative form. Note that a
+ * class implementing {@link PositiveNegativeModifier} is not necessarily a {@link Modifier}
+ * itself. Rather, it returns a {@link Modifier} when {@link #getModifier} is called.
+ */
+ public static interface PositiveNegativeModifier extends Exportable {
+ /**
+ * Converts this {@link PositiveNegativeModifier} to a {@link Modifier} given the negative sign.
+ *
+ * @param isNegative true if the negative form of this modifier should be used; false if the
+ * positive form should be used.
+ * @return A Modifier corresponding to the negative sign.
+ */
+ public Modifier getModifier(boolean isNegative);
+ }
+
+ /**
+ * An interface for a modifier that contains both a positive and a negative form for all six
+ * standard plurals. Note that a class implementing {@link PositiveNegativePluralModifier} is not
+ * necessarily a {@link Modifier} itself. Rather, it returns a {@link Modifier} when {@link
+ * #getModifier} is called.
+ */
+ public static interface PositiveNegativePluralModifier extends Exportable {
+ /**
+ * Converts this {@link PositiveNegativePluralModifier} to a {@link Modifier} given the negative
+ * sign and the standard plural.
+ *
+ * @param plural The StandardPlural to use.
+ * @param isNegative true if the negative form of this modifier should be used; false if the
+ * positive form should be used.
+ * @return A Modifier corresponding to the negative sign.
+ */
+ public Modifier getModifier(StandardPlural plural, boolean isNegative);
+ }
+
+ /**
+ * An interface for a modifier that is represented internally by a prefix string and a suffix
+ * string.
+ */
+ public static interface AffixModifier extends Modifier {}
+
+ /**
+ * A starter implementation with defaults for some of the basic methods.
+ *
+ * <p>Implements {@link PositiveNegativeModifier} only so that instances of this class can be used when
+ * a {@link PositiveNegativeModifier} is required.
+ */
+ public abstract static class BaseModifier extends Format.BeforeFormat
+ implements Modifier, PositiveNegativeModifier {
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ mods.add(this);
+ }
+
+ @Override
+ public Modifier getModifier(boolean isNegative) {
+ return this;
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.util.ArrayDeque;
+
+public class ModifierHolder {
+ private ArrayDeque<Modifier> mods = new ArrayDeque<Modifier>();
+
+ // Using five separate fields instead of the ArrayDeque saves about 10ns at the expense of
+ // worse code.
+ // TODO: Decide which implementation to use.
+
+ // private Modifier mod1 = null;
+ // private Modifier mod2 = null;
+ // private Modifier mod3 = null;
+ // private Modifier mod4 = null;
+ // private Modifier mod5 = null;
+
+ public ModifierHolder clear() {
+ // mod1 = null;
+ // mod2 = null;
+ // mod3 = null;
+ // mod4 = null;
+ // mod5 = null;
+ mods.clear();
+ return this;
+ }
+
+ public void add(Modifier modifier) {
+ // if (mod1 == null) {
+ // mod1 = modifier;
+ // } else if (mod2 == null) {
+ // mod2 = modifier;
+ // } else if (mod3 == null) {
+ // mod3 = modifier;
+ // } else if (mod4 == null) {
+ // mod4 = modifier;
+ // } else if (mod5 == null) {
+ // mod5 = modifier;
+ // } else {
+ // throw new IndexOutOfBoundsException();
+ // }
+ if (modifier != null) mods.addFirst(modifier);
+ }
+
+ public Modifier peekLast() {
+ return mods.peekLast();
+ }
+
+ public Modifier removeLast() {
+ return mods.removeLast();
+ }
+
+ public int applyAll(NumberStringBuilder string, int leftIndex, int rightIndex) {
+ int addedLength = 0;
+ // if (mod5 != null) {
+ // addedLength += mod5.apply(string, leftIndex, rightIndex + addedLength);
+ // mod5 = null;
+ // }
+ // if (mod4 != null) {
+ // addedLength += mod4.apply(string, leftIndex, rightIndex + addedLength);
+ // mod4 = null;
+ // }
+ // if (mod3 != null) {
+ // addedLength += mod3.apply(string, leftIndex, rightIndex + addedLength);
+ // mod3 = null;
+ // }
+ // if (mod2 != null) {
+ // addedLength += mod2.apply(string, leftIndex, rightIndex + addedLength);
+ // mod2 = null;
+ // }
+ // if (mod1 != null) {
+ // addedLength += mod1.apply(string, leftIndex, rightIndex + addedLength);
+ // mod1 = null;
+ // }
+ while (!mods.isEmpty()) {
+ Modifier mod = mods.removeFirst();
+ addedLength += mod.apply(string, leftIndex, rightIndex + addedLength);
+ }
+ return addedLength;
+ }
+
+ public int applyStrong(NumberStringBuilder string, int leftIndex, int rightIndex) {
+ int addedLength = 0;
+ while (!mods.isEmpty() && mods.peekFirst().isStrong()) {
+ Modifier mod = mods.removeFirst();
+ addedLength += mod.apply(string, leftIndex, rightIndex + addedLength);
+ }
+ return addedLength;
+ }
+
+ public int totalLength() {
+ int length = 0;
+ // if (mod1 != null) length += mod1.length();
+ // if (mod2 != null) length += mod2.length();
+ // if (mod3 != null) length += mod3.length();
+ // if (mod4 != null) length += mod4.length();
+ // if (mod5 != null) length += mod5.length();
+ for (Modifier mod : mods) {
+ if (mod == null) continue;
+ length += mod.length();
+ }
+ return length;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.text.FieldPosition;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.NumberFormat.Field;
+
+public class NumberStringBuilder implements CharSequence {
+ private char[] chars;
+ private Field[] fields;
+ private int zero;
+ private int length;
+
+ public NumberStringBuilder() {
+ this(40);
+ }
+
+ public NumberStringBuilder(int capacity) {
+ chars = new char[capacity];
+ fields = new Field[capacity];
+ zero = capacity / 2;
+ length = 0;
+ }
+
+ @Override
+ public int length() {
+ return length;
+ }
+
+ @Override
+ public char charAt(int index) {
+ if (index < 0 || index > length) {
+ throw new IndexOutOfBoundsException();
+ }
+ return chars[zero + index];
+ }
+
+ /**
+ * Appends the specified codePoint to the end of the string.
+ *
+ * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise.
+ */
+ public int appendCodePoint(int codePoint, Field field) {
+ return insertCodePoint(length, codePoint, field);
+ }
+
+ /**
+ * Inserts the specified codePoint at the specified index in the string.
+ *
+ * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise.
+ */
+ public int insertCodePoint(int index, int codePoint, Field field) {
+ int count = Character.charCount(codePoint);
+ int position = prepareForInsert(index, count);
+ Character.toChars(codePoint, chars, position);
+ fields[position] = field;
+ if (count == 2) fields[position + 1] = field;
+ return count;
+ }
+
+ /**
+ * Appends the specified CharSequence to the end of the string.
+ *
+ * @return The number of chars added, which is the length of CharSequence.
+ */
+ public int append(CharSequence sequence, Field field) {
+ return insert(length, sequence, field);
+ }
+
+ /**
+ * Inserts the specified CharSequence at the specified index in the string.
+ *
+ * @return The number of chars added, which is the length of CharSequence.
+ */
+ public int insert(int index, CharSequence sequence, Field field) {
+ if (sequence.length() == 0) {
+ // Nothing to insert.
+ return 0;
+ } else if (sequence.length() == 1) {
+ // Fast path: on a single-char string, using insertCodePoint below is 70% faster than the
+ // CharSequence method: 12.2 ns versus 41.9 ns for five operations on my Linux x86-64.
+ return insertCodePoint(index, sequence.charAt(0), field);
+ } else {
+ return insert(index, sequence, 0, sequence.length(), field);
+ }
+ }
+
+ /**
+ * Inserts the specified CharSequence at the specified index in the string, reading from the
+ * CharSequence from start (inclusive) to end (exclusive).
+ *
+ * @return The number of chars added, which is the length of CharSequence.
+ */
+ public int insert(int index, CharSequence sequence, int start, int end, Field field) {
+ int count = end - start;
+ int position = prepareForInsert(index, count);
+ for (int i = 0; i < count; i++) {
+ chars[position + i] = sequence.charAt(start + i);
+ fields[position + i] = field;
+ }
+ return count;
+ }
+
+ /**
+ * Appends the chars in the specified char array to the end of the string, and associates them
+ * with the fields in the specified field array, which must have the same length as chars.
+ *
+ * @return The number of chars added, which is the length of the char array.
+ */
+ public int append(char[] chars, Field[] fields) {
+ return insert(length, chars, fields);
+ }
+
+ /**
+ * Inserts the chars in the specified char array at the specified index in the string, and
+ * associates them with the fields in the specified field array, which must have the same length
+ * as chars.
+ *
+ * @return The number of chars added, which is the length of the char array.
+ */
+ public int insert(int index, char[] chars, Field[] fields) {
+ assert fields == null || chars.length == fields.length;
+ int count = chars.length;
+ if (count == 0) return 0; // nothing to insert
+ int position = prepareForInsert(index, count);
+ for (int i = 0; i < count; i++) {
+ this.chars[position + i] = chars[i];
+ this.fields[position + i] = fields == null ? null : fields[i];
+ }
+ return count;
+ }
+
+ /**
+ * Appends the contents of another {@link NumberStringBuilder} to the end of this instance.
+ *
+ * @return The number of chars added, which is the length of the other {@link
+ * NumberStringBuilder}.
+ */
+ public int append(NumberStringBuilder other) {
+ return insert(length, other);
+ }
+
+ /**
+ * Inserts the contents of another {@link NumberStringBuilder} into this instance at the given
+ * index.
+ *
+ * @return The number of chars added, which is the length of the other {@link
+ * NumberStringBuilder}.
+ */
+ public int insert(int index, NumberStringBuilder other) {
+ assert this != other;
+ int count = other.length;
+ if (count == 0) return 0; // nothing to insert
+ int position = prepareForInsert(index, count);
+ for (int i = 0; i < count; i++) {
+ this.chars[position + i] = other.chars[other.zero + i];
+ this.fields[position + i] = other.fields[other.zero + i];
+ }
+ return count;
+ }
+
+ /**
+ * Shifts around existing data if necessary to make room for new characters.
+ *
+ * @param index The location in the string where the operation is to take place.
+ * @param count The number of chars (UTF-16 code units) to be inserted at that location.
+ * @return The position in the char array to insert the chars.
+ */
+ private int prepareForInsert(int index, int count) {
+ if (index == 0 && zero - count >= 0) {
+ // Append to start
+ zero -= count;
+ length += count;
+ return zero;
+ } else if (index == length && zero + length + count < chars.length) {
+ // Append to end
+ length += count;
+ return zero + length - count;
+ } else {
+ // Move chars around and/or allocate more space
+ return prepareForInsertHelper(index, count);
+ }
+ }
+
+ private int prepareForInsertHelper(int index, int count) {
+ // Keeping this code out of prepareForInsert() increases the speed of append operations.
+ if (length + count > chars.length) {
+ char[] newChars = new char[(length + count) * 2];
+ Field[] newFields = new Field[(length + count) * 2];
+ int newZero = newChars.length / 2 - (length + count) / 2;
+ System.arraycopy(chars, zero, newChars, newZero, index);
+ System.arraycopy(chars, zero + index, newChars, newZero + index + count, length - index);
+ System.arraycopy(fields, zero, newFields, newZero, index);
+ System.arraycopy(fields, zero + index, newFields, newZero + index + count, length - index);
+ chars = newChars;
+ fields = newFields;
+ zero = newZero;
+ length += count;
+ } else {
+ int newZero = chars.length / 2 - (length + count) / 2;
+ System.arraycopy(chars, zero, chars, newZero, length);
+ System.arraycopy(chars, newZero + index, chars, newZero + index + count, length - index);
+ System.arraycopy(fields, zero, fields, newZero, length);
+ System.arraycopy(fields, newZero + index, fields, newZero + index + count, length - index);
+ zero = newZero;
+ length += count;
+ }
+ return zero + index;
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ if (start < 0 || end > length || end < start) {
+ throw new IndexOutOfBoundsException();
+ }
+ NumberStringBuilder other = this.clone();
+ other.zero = zero + start;
+ other.length = end - start;
+ return other;
+ }
+
+ /**
+ * Returns the string represented by the characters in this string builder.
+ *
+ * <p>For a string intended be used for debugging, use {@link #toDebugString}.
+ */
+ @Override
+ public String toString() {
+ return new String(chars, zero, length);
+ }
+
+ private static final Map<Field, Character> fieldToDebugChar = new HashMap<Field, Character>();
+
+ static {
+ fieldToDebugChar.put(NumberFormat.Field.SIGN, '-');
+ fieldToDebugChar.put(NumberFormat.Field.INTEGER, 'i');
+ fieldToDebugChar.put(NumberFormat.Field.FRACTION, 'f');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT, 'e');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SIGN, '+');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SYMBOL, 'E');
+ fieldToDebugChar.put(NumberFormat.Field.DECIMAL_SEPARATOR, '.');
+ fieldToDebugChar.put(NumberFormat.Field.GROUPING_SEPARATOR, ',');
+ fieldToDebugChar.put(NumberFormat.Field.PERCENT, '%');
+ fieldToDebugChar.put(NumberFormat.Field.PERMILLE, '‰');
+ fieldToDebugChar.put(NumberFormat.Field.CURRENCY, '$');
+ }
+
+ /**
+ * Returns a string that includes field information, for debugging purposes.
+ *
+ * <p>For example, if the string is "-12.345", the debug string will be something like
+ * "<NumberStringBuilder [-123.45] [-iii.ff]>"
+ *
+ * @return A string for debugging purposes.
+ */
+ public String toDebugString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<NumberStringBuilder [");
+ sb.append(this.toString());
+ sb.append("] [");
+ for (int i = zero; i < zero + length; i++) {
+ if (fields[i] == null) {
+ sb.append('n');
+ } else {
+ sb.append(fieldToDebugChar.get(fields[i]));
+ }
+ }
+ sb.append("]>");
+ return sb.toString();
+ }
+
+ /** @return A new array containing the contents of this string builder. */
+ public char[] toCharArray() {
+ return Arrays.copyOfRange(chars, zero, zero + length);
+ }
+
+ /** @return A new array containing the field values of this string builder. */
+ public Field[] toFieldArray() {
+ return Arrays.copyOfRange(fields, zero, zero + length);
+ }
+
+ /**
+ * @return Whether the contents and field values of this string builder are equal to the given
+ * chars and fields.
+ * @see #toCharArray
+ * @see #toFieldArray
+ */
+ public boolean contentEquals(char[] chars, Field[] fields) {
+ if (chars.length != length) return false;
+ if (fields.length != length) return false;
+ for (int i = 0; i < length; i++) {
+ if (this.chars[zero + i] != chars[i]) return false;
+ if (this.fields[zero + i] != fields[i]) return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param other The instance to compare.
+ * @return Whether the contents of this instance is currently equal to the given instance.
+ */
+ public boolean contentEquals(NumberStringBuilder other) {
+ if (length != other.length) return false;
+ for (int i = 0; i < length; i++) {
+ if (chars[zero + i] != other.chars[other.zero + i]) return false;
+ if (fields[zero + i] != other.fields[other.zero + i]) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Populates the given {@link FieldPosition} based on this string builder.
+ *
+ * @param fp The FieldPosition to populate.
+ * @param offset An offset to add to the field position index; can be zero.
+ */
+ public void populateFieldPosition(FieldPosition fp, int offset) {
+ java.text.Format.Field rawField = fp.getFieldAttribute();
+
+ if (rawField == null) {
+ // Backwards compatibility: read from fp.getField()
+ if (fp.getField() == NumberFormat.INTEGER_FIELD) {
+ rawField = NumberFormat.Field.INTEGER;
+ } else if (fp.getField() == NumberFormat.FRACTION_FIELD) {
+ rawField = NumberFormat.Field.FRACTION;
+ } else {
+ // No field is set
+ return;
+ }
+ }
+
+ if (!(rawField instanceof com.ibm.icu.text.NumberFormat.Field)) {
+ throw new IllegalArgumentException(
+ "You must pass an instance of com.ibm.icu.text.NumberFormat.Field as your FieldPosition attribute. You passed: "
+ + rawField.getClass().toString());
+ }
+ /* com.ibm.icu.text.NumberFormat. */ Field field = (Field) rawField;
+
+ boolean seenStart = false;
+ int fractionStart = -1;
+ for (int i = zero; i <= zero + length; i++) {
+ Field _field = (i < zero + length) ? fields[i] : null;
+ if (seenStart && field != _field) {
+ // Special case: GROUPING_SEPARATOR counts as an INTEGER.
+ if (field == NumberFormat.Field.INTEGER && _field == NumberFormat.Field.GROUPING_SEPARATOR)
+ continue;
+ fp.setEndIndex(i - zero + offset);
+ break;
+ } else if (!seenStart && field == _field) {
+ fp.setBeginIndex(i - zero + offset);
+ seenStart = true;
+ }
+ if (_field == NumberFormat.Field.INTEGER || _field == NumberFormat.Field.DECIMAL_SEPARATOR) {
+ fractionStart = i - zero + 1;
+ }
+ }
+
+ // Backwards compatibility: FRACTION needs to start after INTEGER if empty
+ if (field == NumberFormat.Field.FRACTION && !seenStart) {
+ fp.setBeginIndex(fractionStart);
+ fp.setEndIndex(fractionStart);
+ }
+ }
+
+ public AttributedCharacterIterator getIterator() {
+ AttributedString as = new AttributedString(toString());
+ Field current = null;
+ int currentStart = -1;
+ for (int i = 0; i < length; i++) {
+ Field field = fields[i + zero];
+ if (current == NumberFormat.Field.INTEGER && field == NumberFormat.Field.GROUPING_SEPARATOR) {
+ // Special case: GROUPING_SEPARATOR counts as an INTEGER.
+ as.addAttribute(
+ NumberFormat.Field.GROUPING_SEPARATOR, NumberFormat.Field.GROUPING_SEPARATOR, i, i + 1);
+ } else if (current != field) {
+ if (current != null) {
+ as.addAttribute(current, current, currentStart, i);
+ }
+ current = field;
+ currentStart = i;
+ }
+ }
+ if (current != null) {
+ as.addAttribute(current, current, currentStart, length);
+ }
+ return as.getIterator();
+ }
+
+ @Override
+ public NumberStringBuilder clone() {
+ NumberStringBuilder other = new NumberStringBuilder(chars.length);
+ other.zero = zero;
+ other.length = length;
+ System.arraycopy(chars, zero, other.chars, zero, length);
+ System.arraycopy(fields, zero, other.fields, zero, length);
+ return other;
+ }
+
+ public NumberStringBuilder clear() {
+ zero = chars.length / 2;
+ length = 0;
+ return this;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.number.Modifier.AffixModifier;
+import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat.IProperties;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/**
+ * A class to convert from a bag of prefix/suffix properties into a positive and negative {@link
+ * Modifier}. This is a standard implementation used by {@link PositiveNegativeAffixFormat}, {@link
+ * CompactDecimalFormat}, {@link Parse}, and others.
+ *
+ * <p>This class is is intended to be an efficient generator for instances of Modifier by a single
+ * thread during construction of a formatter or during static formatting. It uses internal caching
+ * to avoid creating new Modifier objects when possible. It is NOT THREAD SAFE and NOT IMMUTABLE.
+ *
+ * <p>The thread-local instance of this class provided by {@link #getThreadLocalInstance} should be
+ * used in most cases instead of constructing a new instance of the object.
+ *
+ * <p>This class also handles the logic of assigning positive signs, negative signs, and currency
+ * signs according to the LDML specification.
+ */
+public class PNAffixGenerator {
+ public static class Result {
+ public AffixModifier positive = null;
+ public AffixModifier negative = null;
+ }
+
+ protected static final ThreadLocal<PNAffixGenerator> threadLocalInstance =
+ new ThreadLocal<PNAffixGenerator>() {
+ @Override
+ protected PNAffixGenerator initialValue() {
+ return new PNAffixGenerator();
+ }
+ };
+
+ public static PNAffixGenerator getThreadLocalInstance() {
+ return threadLocalInstance.get();
+ }
+
+ // These instances are used internally and cached to avoid object creation. The resultInstance
+ // also serves as a 1-element cache to avoid creating objects when subsequent calls have
+ // identical prefixes and suffixes. This happens, for example, when consuming CDF data.
+ private Result resultInstance = new Result();
+ private NumberStringBuilder sb1 = new NumberStringBuilder();
+ private NumberStringBuilder sb2 = new NumberStringBuilder();
+ private NumberStringBuilder sb3 = new NumberStringBuilder();
+ private NumberStringBuilder sb4 = new NumberStringBuilder();
+
+ /**
+ * Generates modifiers using default currency symbols.
+ *
+ * @param symbols The symbols to interpolate for minus, plus, percent, permille, and currency.
+ * @param properties The bag of properties to convert.
+ * @return The positive and negative {@link Modifier}.
+ */
+ public Result getModifiers(
+ DecimalFormatSymbols symbols, PositiveNegativeAffixFormat.IProperties properties) {
+ // If this method is used, the user doesn't care about currencies. Default the currency symbols
+ // to the information we can get from the DecimalFormatSymbols instance.
+ return getModifiers(
+ symbols,
+ symbols.getCurrencySymbol(),
+ symbols.getInternationalCurrencySymbol(),
+ symbols.getCurrencySymbol(),
+ properties);
+ }
+
+ /**
+ * Generates modifiers using the specified currency symbol for all three lengths of currency
+ * placeholders in the pattern string.
+ *
+ * @param symbols The symbols to interpolate for minus, plus, percent, and permille.
+ * @param currencySymbol The currency symbol.
+ * @param properties The bag of properties to convert.
+ * @return The positive and negative {@link Modifier}.
+ */
+ public Result getModifiers(
+ DecimalFormatSymbols symbols,
+ String currencySymbol,
+ PositiveNegativeAffixFormat.IProperties properties) {
+ // If this method is used, the user doesn't cares about currencies but doesn't care about
+ // supporting all three sizes of currency placeholders. Use the one provided string for all
+ // three sizes of placeholders.
+ return getModifiers(symbols, currencySymbol, currencySymbol, currencySymbol, properties);
+ }
+
+ /**
+ * Generates modifiers using the three specified strings to replace the three lengths of currency
+ * placeholders: "¤", "¤¤", and "¤¤¤".
+ *
+ * @param symbols The symbols to interpolate for minus, plus, percent, and permille.
+ * @param curr1 The string to replace "¤".
+ * @param curr2 The string to replace "¤¤".
+ * @param curr3 The string to replace "¤¤¤".
+ * @param properties The bag of properties to convert.
+ * @return The positive and negative {@link Modifier}.
+ */
+ public Result getModifiers(
+ DecimalFormatSymbols symbols,
+ String curr1,
+ String curr2,
+ String curr3,
+ PositiveNegativeAffixFormat.IProperties properties) {
+
+ // Use a different code path for handling affixes with "always show plus sign"
+ if (properties.getPlusSignAlwaysShown()) {
+ return getModifiersWithPlusSign(symbols, curr1, curr2, curr3, properties);
+ }
+
+ CharSequence ppp = properties.getPositivePrefixPattern();
+ CharSequence psp = properties.getPositiveSuffixPattern();
+ CharSequence npp = properties.getNegativePrefixPattern();
+ CharSequence nsp = properties.getNegativeSuffixPattern();
+
+ // Set sb1/sb2 to the positive prefix/suffix.
+ sb1.clear();
+ sb2.clear();
+ AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1);
+ AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2);
+ setPositiveResult(sb1, sb2, properties);
+
+ // Set sb1/sb2 to the negative prefix/suffix.
+ if (npp == null && nsp == null) {
+ // Negative prefix defaults to positive prefix prepended with the minus sign.
+ // Negative suffix defaults to positive suffix.
+ sb1.insert(0, symbols.getMinusSignString(), Field.SIGN);
+ } else {
+ sb1.clear();
+ sb2.clear();
+ AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1);
+ AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2);
+ }
+ setNegativeResult(sb1, sb2, properties);
+
+ return resultInstance;
+ }
+
+ private Result getModifiersWithPlusSign(
+ DecimalFormatSymbols symbols,
+ String curr1,
+ String curr2,
+ String curr3,
+ IProperties properties) {
+
+ CharSequence ppp = properties.getPositivePrefixPattern();
+ CharSequence psp = properties.getPositiveSuffixPattern();
+ CharSequence npp = properties.getNegativePrefixPattern();
+ CharSequence nsp = properties.getNegativeSuffixPattern();
+
+ // There are three cases, listed below with their expected outcomes.
+ // TODO: Should we handle the cases when the positive subpattern has a '+' already?
+ //
+ // 1) No negative subpattern.
+ // Positive => Positive subpattern prepended with '+'
+ // Negative => Positive subpattern prepended with '-'
+ // 2) Negative subpattern does not have '-'.
+ // Positive => Positive subpattern prepended with '+'
+ // Negative => Negative subpattern
+ // 3) Negative subpattern has '-'.
+ // Positive => Negative subpattern with '+' substituted for '-'
+ // Negative => Negative subpattern
+
+ if (npp != null || nsp != null) {
+ // Case 2 or Case 3
+ sb1.clear();
+ sb2.clear();
+ sb3.clear();
+ sb4.clear();
+ AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1);
+ AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2);
+ AffixPatternUtils.unescape(
+ npp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb3);
+ AffixPatternUtils.unescape(
+ nsp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb4);
+ if (!charSequenceEquals(sb1, sb3) || !charSequenceEquals(sb2, sb4)) {
+ // Case 3. The plus sign substitution was successful.
+ setPositiveResult(sb3, sb4, properties);
+ setNegativeResult(sb1, sb2, properties);
+ return resultInstance;
+ } else {
+ // Case 2. There was no minus sign. Set the negative result and fall through.
+ setNegativeResult(sb1, sb2, properties);
+ }
+ }
+
+ // Case 1 or 2. Set sb1/sb2 to the positive prefix/suffix.
+ sb1.clear();
+ sb2.clear();
+ AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1);
+ AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2);
+
+ if (npp == null && nsp == null) {
+ // Case 1. Compute the negative result from the positive subpattern.
+ sb3.clear();
+ sb3.append(symbols.getMinusSignString(), Field.SIGN);
+ sb3.append(sb1);
+ setNegativeResult(sb3, sb2, properties);
+ }
+
+ // Case 1 or 2. Prepend a '+' sign to the positive prefix.
+ sb1.insert(0, symbols.getPlusSignString(), Field.SIGN);
+ setPositiveResult(sb1, sb2, properties);
+
+ return resultInstance;
+ }
+
+ private void setPositiveResult(
+ NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) {
+ if (properties.getPositivePrefix() != null || properties.getPositiveSuffix() != null) {
+ // Override with custom affixes
+ String _prefix = properties.getPositivePrefix();
+ String _suffix = properties.getPositiveSuffix();
+ if (_prefix == null) _prefix = "";
+ if (_suffix == null) _suffix = "";
+ if (_prefix.length() == 0 && _suffix.length() == 0) {
+ resultInstance.positive = ConstantAffixModifier.EMPTY;
+ return;
+ }
+ if (resultInstance.positive != null
+ && (resultInstance.positive instanceof ConstantAffixModifier)
+ && ((ConstantAffixModifier) resultInstance.positive).contentEquals(_prefix, _suffix)) {
+ // Use the cached modifier
+ return;
+ }
+ resultInstance.positive =
+ new ConstantAffixModifier(_prefix, _suffix, null, false);
+ } else {
+ // Use pattern affixes
+ if (prefix.length() == 0 && suffix.length() == 0) {
+ resultInstance.positive = ConstantAffixModifier.EMPTY;
+ return;
+ }
+ if (resultInstance.positive != null
+ && (resultInstance.positive instanceof ConstantMultiFieldModifier)
+ && ((ConstantMultiFieldModifier) resultInstance.positive).contentEquals(prefix, suffix)) {
+ // Use the cached modifier
+ return;
+ }
+ resultInstance.positive = new ConstantMultiFieldModifier(prefix, suffix, false);
+ }
+ }
+
+ private void setNegativeResult(
+ NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) {
+ if (properties.getNegativePrefix() != null || properties.getNegativeSuffix() != null) {
+ // Override with custom affixes
+ String _prefix = properties.getNegativePrefix();
+ String _suffix = properties.getNegativeSuffix();
+ if (_prefix == null) _prefix = "";
+ if (_suffix == null) _suffix = "";
+ if (_prefix.length() == 0 && _suffix.length() == 0) {
+ resultInstance.negative = ConstantAffixModifier.EMPTY;
+ return;
+ }
+ if (resultInstance.negative != null
+ && (resultInstance.negative instanceof ConstantAffixModifier)
+ && ((ConstantAffixModifier) resultInstance.negative).contentEquals(_prefix, _suffix)) {
+ // Use the cached modifier
+ return;
+ }
+ resultInstance.negative =
+ new ConstantAffixModifier(_prefix, _suffix, null, false);
+ } else {
+ // Use pattern affixes
+ if (prefix.length() == 0 && suffix.length() == 0) {
+ resultInstance.negative = ConstantAffixModifier.EMPTY;
+ return;
+ }
+ if (resultInstance.negative != null
+ && (resultInstance.negative instanceof ConstantMultiFieldModifier)
+ && ((ConstantMultiFieldModifier) resultInstance.negative).contentEquals(prefix, suffix)) {
+ // Use the cached modifier
+ return;
+ }
+ resultInstance.negative = new ConstantMultiFieldModifier(prefix, suffix, false);
+ }
+ }
+
+ /** A null-safe equals method for CharSequences. */
+ private static boolean charSequenceEquals(CharSequence a, CharSequence b) {
+ if (a == b) return true;
+ if (a == null || b == null) return false;
+ if (a.length() != b.length()) return false;
+ for (int i = 0; i < a.length(); i++) {
+ if (a.charAt(i) != b.charAt(i)) return false;
+ }
+ return true;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.TextTrieMap;
+import com.ibm.icu.impl.number.Parse.ParseMode;
+import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat;
+import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier;
+import com.ibm.icu.impl.number.formatters.PaddingFormat;
+import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import com.ibm.icu.lang.UCharacter;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.UnicodeSet;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyStringInfo;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ULocale;
+
+/**
+ * A parser designed to convert an arbitrary human-generated string to its best representation as a
+ * number: a long, a BigInteger, or a BigDecimal.
+ *
+ * <p>The parser may traverse multiple parse paths in the same strings if there is ambiguity. For
+ * example, the string "12,345.67" has two main interpretations: it could be "12.345" in a locale
+ * that uses '.' as the grouping separator, or it could be "12345.67" in a locale that uses ',' as
+ * the grouping separator. Since the second option has a longer parse path (consumes more of the
+ * input string), the parser will accept the second option.
+ */
+public class Parse {
+
+ /** Controls the set of rules for parsing a string. */
+ public static enum ParseMode {
+ /**
+ * Lenient mode should be used if you want to accept malformed user input. It will use
+ * heuristics to attempt to parse through typographical errors in the string.
+ */
+ LENIENT,
+
+ /**
+ * Strict mode should be used if you want to require that the input is well-formed. More
+ * specifically, it differs from lenient mode in the following ways:
+ *
+ * <ul>
+ * <li>Grouping widths must match the grouping settings. For example, "12,3,45" will fail if
+ * the grouping width is 3, as in the pattern "#,##0".
+ * <li>The string must contain a complete prefix and suffix. For example, if the pattern is
+ * "{#};(#)", then "{123}" or "(123)" would match, but "{123", "123}", and "123" would all
+ * fail. (The latter strings would be accepted in lenient mode.)
+ * <li>Whitespace may not appear at arbitrary places in the string. In lenient mode,
+ * whitespace is allowed to occur arbitrarily before and after prefixes and exponent
+ * separators.
+ * <li>Leading grouping separators are not allowed, as in ",123".
+ * <li>Minus and plus signs can only appear if specified in the pattern. In lenient mode, a
+ * plus or minus sign can always precede a number.
+ * <li>The set of characters that can be interpreted as a decimal or grouping separator is
+ * smaller.
+ * <li><strong>If currency parsing is enabled,</strong> currencies must only appear where
+ * specified in either the current pattern string or in a valid pattern string for the
+ * current locale. For example, if the pattern is "¤0.00", then "$1.23" would match, but
+ * "1.23$" would fail to match.
+ * </ul>
+ */
+ STRICT,
+
+ /**
+ * Fast mode should be used in applications that don't require prefixes and suffixes to match.
+ *
+ * <p>In addition to ignoring prefixes and suffixes, fast mode performs the following
+ * optimizations:
+ *
+ * <ul>
+ * <li>Ignores digit strings from {@link DecimalFormatSymbols} and only uses the code point's
+ * Unicode digit property. If you are not using custom digit strings, this should not
+ * cause a change in behavior.
+ * <li>Instead of traversing multiple possible parse paths, a "greedy" parsing strategy is
+ * used, which might mean that fast mode won't accept strings that lenient or strict mode
+ * would accept. Since prefix and suffix strings are ignored, this is not an issue unless
+ * you are using custom symbols.
+ * </ul>
+ */
+ FAST,
+ }
+
+ /** The set of properties required for {@link Parse}. Accepts a {@link Properties} object. */
+ public static interface IProperties
+ extends PositiveNegativeAffixFormat.IProperties,
+ PaddingFormat.IProperties,
+ CurrencyFormat.ICurrencyProperties,
+ BigDecimalMultiplier.IProperties,
+ MagnitudeMultiplier.IProperties,
+ PositiveDecimalFormat.IProperties {
+
+ boolean DEFAULT_PARSE_INTEGER_ONLY = false;
+
+ /** @see #setParseIntegerOnly */
+ public boolean getParseIntegerOnly();
+
+ /**
+ * Whether to ignore the fractional part of numbers. For example, parses "123.4" to "123"
+ * instead of "123.4".
+ *
+ * @param parseIntegerOnly true to parse integers only; false to parse integers with their
+ * fraction parts
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseIntegerOnly(boolean parseIntegerOnly);
+
+ boolean DEFAULT_PARSE_NO_EXPONENT = false;
+
+ /** @see #setParseNoExponent */
+ public boolean getParseNoExponent();
+
+ /**
+ * Whether to ignore the exponential part of numbers. For example, parses "123E4" to "123"
+ * instead of "1230000".
+ *
+ * @param parseIgnoreExponent true to ignore exponents; false to parse them.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseNoExponent(boolean parseIgnoreExponent);
+
+ boolean DEFAULT_DECIMAL_PATTERN_MATCH_REQUIRED = false;
+
+ /** @see #setDecimalPatternMatchRequired */
+ public boolean getDecimalPatternMatchRequired();
+
+ /**
+ * Whether to require that the presence of decimal point matches the pattern. If a decimal point
+ * is not present, but the pattern contained a decimal point, parse will not succeed: null will
+ * be returned from <code>parse()</code>, and an error index will be set in the {@link
+ * ParsePosition}.
+ *
+ * @param decimalPatternMatchRequired true to set an error if decimal is not present
+ * @return The property bag, for chaining.
+ */
+ public IProperties setDecimalPatternMatchRequired(boolean decimalPatternMatchRequired);
+
+ ParseMode DEFAULT_PARSE_MODE = null;
+
+ /** @see #setParseMode */
+ public ParseMode getParseMode();
+
+ /**
+ * Controls certain rules for how strict this parser is when reading strings. See {@link
+ * ParseMode#LENIENT} and {@link ParseMode#STRICT}.
+ *
+ * @param parseMode Either {@link ParseMode#LENIENT} or {@link ParseMode#STRICT}.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseMode(ParseMode parseMode);
+
+ boolean DEFAULT_PARSE_TO_BIG_DECIMAL = false;
+
+ /** @see #setParseToBigDecimal */
+ public boolean getParseToBigDecimal();
+
+ /**
+ * Whether to always return a BigDecimal from {@link Parse#parse} and all other parse methods.
+ * By default, a Long or a BigInteger are returned when possible.
+ *
+ * @param parseToBigDecimal true to always return a BigDecimal; false to return a Long or a
+ * BigInteger when possible.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseToBigDecimal(boolean parseToBigDecimal);
+
+ boolean DEFAULT_PARSE_CASE_SENSITIVE = false;
+
+ /** @see #setParseCaseSensitive */
+ public boolean getParseCaseSensitive();
+
+ /**
+ * Whether to require cases to match when parsing strings; default is true. Case sensitivity
+ * applies to prefixes, suffixes, the exponent separator, the symbol "NaN", and the infinity
+ * symbol. Grouping separators, decimal separators, and padding are always case-sensitive.
+ * Currencies are always case-insensitive.
+ *
+ * <p>This setting is ignored in fast mode. In fast mode, strings are always compared in a
+ * case-sensitive way.
+ *
+ * @param parseCaseSensitive true to be case-sensitive when parsing; false to allow any case.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setParseCaseSensitive(boolean parseCaseSensitive);
+ }
+
+ /**
+ * @see #parse(String, ParsePosition, ParseMode, boolean, boolean, IProperties,
+ * DecimalFormatSymbols)
+ */
+ private static enum StateName {
+ BEFORE_PREFIX,
+ AFTER_PREFIX,
+ AFTER_INTEGER_DIGIT,
+ AFTER_FRACTION_DIGIT,
+ AFTER_EXPONENT_SEPARATOR,
+ AFTER_EXPONENT_DIGIT,
+ BEFORE_SUFFIX,
+ BEFORE_SUFFIX_SEEN_EXPONENT,
+ AFTER_SUFFIX,
+ INSIDE_CURRENCY,
+ INSIDE_DIGIT,
+ INSIDE_STRING,
+ INSIDE_AFFIX_PATTERN;
+ }
+
+ // TODO: Does this set make sense for the whitespace characters?
+ private static final UnicodeSet UNISET_WHITESPACE =
+ new UnicodeSet("[[:whitespace:][\\u2000-\\u200D]]").freeze();
+
+ // BiDi characters are skipped over and ignored at any point in the string, even in strict mode.
+ private static final UnicodeSet UNISET_BIDI =
+ new UnicodeSet("[[\\u200E\\u200F\\u061C]]").freeze();
+
+ // TODO: Re-generate these sets from the database. They probably haven't been updated in a while.
+ private static final UnicodeSet UNISET_PERIOD_LIKE =
+ new UnicodeSet("[.\\u2024\\u3002\\uFE12\\uFE52\\uFF0E\\uFF61]").freeze();
+ private static final UnicodeSet UNISET_STRICT_PERIOD_LIKE =
+ new UnicodeSet("[.\\u2024\\uFE52\\uFF0E\\uFF61]").freeze();
+ private static final UnicodeSet UNISET_COMMA_LIKE =
+ new UnicodeSet("[,\\u060C\\u066B\\u3001\\uFE10\\uFE11\\uFE50\\uFE51\\uFF0C\\uFF64]").freeze();
+ private static final UnicodeSet UNISET_STRICT_COMMA_LIKE =
+ new UnicodeSet("[,\\u066B\\uFE10\\uFE50\\uFF0C]").freeze();
+ private static final UnicodeSet UNISET_OTHER_GROUPING_SEPARATORS =
+ new UnicodeSet(
+ "[\\ '\\u00A0\\u066C\\u2000-\\u200A\\u2018\\u2019\\u202F\\u205F\\u3000\\uFF07]")
+ .freeze();
+
+ private enum SeparatorType {
+ COMMA_LIKE,
+ PERIOD_LIKE,
+ OTHER_GROUPING,
+ UNKNOWN;
+
+ static SeparatorType fromCp(int cp, ParseMode mode) {
+ if (mode == ParseMode.FAST) {
+ return SeparatorType.UNKNOWN;
+ } else if (mode == ParseMode.STRICT) {
+ if (UNISET_STRICT_COMMA_LIKE.contains(cp)) return COMMA_LIKE;
+ if (UNISET_STRICT_PERIOD_LIKE.contains(cp)) return PERIOD_LIKE;
+ if (UNISET_OTHER_GROUPING_SEPARATORS.contains(cp)) return OTHER_GROUPING;
+ return UNKNOWN;
+ } else {
+ if (UNISET_COMMA_LIKE.contains(cp)) return COMMA_LIKE;
+ if (UNISET_PERIOD_LIKE.contains(cp)) return PERIOD_LIKE;
+ if (UNISET_OTHER_GROUPING_SEPARATORS.contains(cp)) return OTHER_GROUPING;
+ return UNKNOWN;
+ }
+ }
+ }
+
+ private static enum DigitType {
+ INTEGER,
+ FRACTION,
+ EXPONENT
+ }
+
+ /**
+ * Holds a snapshot in time of a single parse path. This includes the digits seen so far, the
+ * current state name, and other properties like the grouping separator used on this parse path,
+ * details about the exponent and negative signs, etc.
+ */
+ private static class StateItem {
+ // Parser state:
+ // The "trailingChars" is used to keep track of how many characters from the end of the string
+ // are ignorable and should be removed from the parse position should this item be accepted.
+ // The "score" is used to help rank two otherwise equivalent parse paths. Currently, the only
+ // function giving points to the score is prefix/suffix.
+ StateName name;
+ int trailingCount;
+ int score;
+
+ // Numerical value:
+ FormatQuantity4 fq = new FormatQuantity4();
+ int numDigits;
+ int trailingZeros;
+ int exponent;
+
+ // Other items that we've seen:
+ int groupingCp;
+ long groupingWidths;
+ String isoCode;
+ boolean sawNegative;
+ boolean sawNegativeExponent;
+ boolean sawCurrency;
+ boolean sawNaN;
+ boolean sawInfinity;
+ AffixHolder affix;
+ boolean sawPrefix;
+ boolean sawSuffix;
+ boolean sawDecimalPoint;
+
+ // Data for intermediate parsing steps:
+ StateName returnTo1;
+ StateName returnTo2;
+ // For string literals:
+ CharSequence currentString;
+ int currentOffset;
+ // For affix patterns:
+ CharSequence currentAffixPattern;
+ long currentStepwiseParserTag;
+ // For currency:
+ TextTrieMap<CurrencyStringInfo>.ParseState currentCurrencyTrieState;
+ // For multi-code-point digits:
+ TextTrieMap<Byte>.ParseState currentDigitTrieState;
+ DigitType currentDigitType;
+
+ /**
+ * Clears the instance so that it can be re-used.
+ *
+ * @return Myself, for chaining.
+ */
+ StateItem clear() {
+ // Parser state:
+ name = StateName.BEFORE_PREFIX;
+ trailingCount = 0;
+ score = 0;
+
+ // Numerical value:
+ fq.clear();
+ numDigits = 0;
+ trailingZeros = 0;
+ exponent = 0;
+
+ // Other items we've seen:
+ groupingCp = -1;
+ groupingWidths = 0L;
+ isoCode = null;
+ sawNegative = false;
+ sawNegativeExponent = false;
+ sawCurrency = false;
+ sawNaN = false;
+ sawInfinity = false;
+ affix = null;
+ sawPrefix = false;
+ sawSuffix = false;
+ sawDecimalPoint = false;
+
+ // Data for intermediate parsing steps:
+ returnTo1 = null;
+ returnTo2 = null;
+ currentString = null;
+ currentOffset = 0;
+ currentAffixPattern = null;
+ currentStepwiseParserTag = 0L;
+ currentCurrencyTrieState = null;
+ currentDigitTrieState = null;
+ currentDigitType = null;
+
+ return this;
+ }
+
+ /**
+ * Sets the internal value of this instance equal to another instance.
+ *
+ * <p>newName and cpOrN1 are required as parameters to this function because every time a code
+ * point is consumed and a state item is copied, both of the corresponding fields should be
+ * updated; it would be an error if they weren't updated.
+ *
+ * @param other The instance to copy from.
+ * @param newName The state name that the new copy should take on.
+ * @param trailing If positive, record this code point as trailing; if negative, reset the
+ * trailing count to zero.
+ * @return Myself, for chaining.
+ */
+ StateItem copyFrom(StateItem other, StateName newName, int trailing) {
+ // Parser state:
+ name = newName;
+ score = other.score;
+
+ // Either reset trailingCount or add the width of the current code point.
+ trailingCount = (trailing < 0) ? 0 : other.trailingCount + Character.charCount(trailing);
+
+ // Numerical value:
+ fq.copyFrom(other.fq);
+ numDigits = other.numDigits;
+ trailingZeros = other.trailingZeros;
+ exponent = other.exponent;
+
+ // Other items we've seen:
+ groupingCp = other.groupingCp;
+ groupingWidths = other.groupingWidths;
+ isoCode = other.isoCode;
+ sawNegative = other.sawNegative;
+ sawNegativeExponent = other.sawNegativeExponent;
+ sawCurrency = other.sawCurrency;
+ sawNaN = other.sawNaN;
+ sawInfinity = other.sawInfinity;
+ affix = other.affix;
+ sawPrefix = other.sawPrefix;
+ sawSuffix = other.sawSuffix;
+ sawDecimalPoint = other.sawDecimalPoint;
+
+ // Data for intermediate parsing steps:
+ returnTo1 = other.returnTo1;
+ returnTo2 = other.returnTo2;
+ currentString = other.currentString;
+ currentOffset = other.currentOffset;
+ currentAffixPattern = other.currentAffixPattern;
+ currentStepwiseParserTag = other.currentStepwiseParserTag;
+ currentCurrencyTrieState = other.currentCurrencyTrieState;
+ currentDigitTrieState = other.currentDigitTrieState;
+ currentDigitType = other.currentDigitType;
+
+ return this;
+ }
+
+ /**
+ * Adds a digit to the internal representation of this instance.
+ *
+ * @param digit The digit that was read from the string.
+ * @param type Whether the digit occured after the decimal point.
+ */
+ void appendDigit(byte digit, DigitType type) {
+ if (type == DigitType.EXPONENT) {
+ int newExponent = exponent * 10 + digit;
+ if (newExponent < exponent) {
+ // overflow
+ exponent = Integer.MAX_VALUE;
+ } else {
+ exponent = newExponent;
+ }
+ } else {
+ numDigits++;
+ if (type == DigitType.FRACTION && digit == 0) {
+ trailingZeros++;
+ } else if (type == DigitType.FRACTION) {
+ fq.appendDigit(digit, trailingZeros, false);
+ trailingZeros = 0;
+ } else {
+ fq.appendDigit(digit, 0, true);
+ }
+ }
+ }
+
+ /** @return Whether or not this item contains a valid number. */
+ public boolean hasNumber() {
+ return numDigits > 0 || sawNaN || sawInfinity;
+ }
+
+ /**
+ * Converts the internal digits from this instance into a Number, preferring a Long, then a
+ * BigInteger, then a BigDecimal. A Double is used for NaN, infinity, and -0.0.
+ *
+ * @return The Number. Never null.
+ */
+ Number toNumber(IProperties properties) {
+ // Check for NaN, infinity, and -0.0
+ if (sawNaN) {
+ return Double.NaN;
+ }
+ if (sawInfinity) {
+ if (sawNegative) {
+ return Double.NEGATIVE_INFINITY;
+ } else {
+ return Double.POSITIVE_INFINITY;
+ }
+ }
+ if (fq.isZero() && sawNegative) {
+ return -0.0;
+ }
+
+ // Check for exponent overflow
+ boolean forceBigDecimal = properties.getParseToBigDecimal();
+ if (exponent == Integer.MAX_VALUE) {
+ if (sawNegativeExponent && sawNegative) {
+ return -0.0;
+ } else if (sawNegativeExponent) {
+ return 0.0;
+ } else if (sawNegative) {
+ return Double.NEGATIVE_INFINITY;
+ } else {
+ return Double.POSITIVE_INFINITY;
+ }
+ } else if (exponent > 1000) {
+ // BigDecimals can handle huge values better than BigIntegers.
+ forceBigDecimal = true;
+ }
+
+ // Multipliers must be applied in reverse.
+ BigDecimal multiplier = properties.getMultiplier();
+ if (properties.getMagnitudeMultiplier() != 0) {
+ if (multiplier == null) multiplier = BigDecimal.ONE;
+ multiplier = multiplier.scaleByPowerOfTen(properties.getMagnitudeMultiplier());
+ }
+ int delta = (sawNegativeExponent ? -1 : 1) * exponent;
+
+ // We need to use a math context in order to prevent non-terminating decimal expansions.
+ // This is only used when dividing by the multiplier.
+ MathContext mc = RoundingUtils.getMathContextOr16Digits(properties);
+
+ // Construct the output number.
+ // This is the only step during fast-mode parsing that incurs object creations.
+ BigDecimal result = fq.toBigDecimal();
+ if (sawNegative) result = result.negate();
+ result = result.scaleByPowerOfTen(delta);
+ if (multiplier != null) {
+ result = result.divide(multiplier, mc);
+ }
+ result = result.stripTrailingZeros();
+ if (forceBigDecimal || result.scale() > 0) {
+ return result;
+ } else if (-result.scale() + result.precision() <= 18) {
+ return result.longValueExact();
+ } else {
+ return result.toBigIntegerExact();
+ }
+ }
+
+ /**
+ * Converts the internal digits to a number, and also associates the number with the parsed
+ * currency.
+ *
+ * @return The CurrencyAmount. Never null.
+ */
+ public CurrencyAmount toCurrencyAmount(IProperties properties) {
+ assert isoCode != null;
+ Number number = toNumber(properties);
+ Currency currency = Currency.getInstance(isoCode);
+ return new CurrencyAmount(number, currency);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<ParserStateItem ");
+ sb.append(name.name());
+ if (name == StateName.INSIDE_STRING) {
+ sb.append("{");
+ sb.append(currentString);
+ sb.append(":");
+ sb.append(currentOffset);
+ sb.append("}");
+ }
+ if (name == StateName.INSIDE_AFFIX_PATTERN) {
+ sb.append("{");
+ sb.append(currentAffixPattern);
+ sb.append(":");
+ sb.append(AffixPatternUtils.getOffset(currentStepwiseParserTag));
+ sb.append("}");
+ }
+ sb.append(" ");
+ sb.append(fq.toBigDecimal());
+ sb.append(" grouping:");
+ sb.append(groupingCp == -1 ? new char[] {'?'} : Character.toChars(groupingCp));
+ sb.append(" widths:");
+ sb.append(Long.toHexString(groupingWidths));
+ sb.append(" seen:");
+ sb.append(sawNegative ? 1 : 0);
+ sb.append(sawNegativeExponent ? 1 : 0);
+ sb.append(sawNaN ? 1 : 0);
+ sb.append(sawInfinity ? 1 : 0);
+ sb.append(sawPrefix ? 1 : 0);
+ sb.append(sawSuffix ? 1 : 0);
+ sb.append(sawDecimalPoint ? 1 : 0);
+ sb.append(" score:");
+ sb.append(score);
+ sb.append(" affix:");
+ sb.append(affix);
+ sb.append(" currency:");
+ sb.append(isoCode);
+ sb.append(">");
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Holds an ordered list of {@link StateItem} and other metadata about the string to be parsed.
+ * There are two internal arrays of {@link StateItem}, which are swapped back and forth in order
+ * to avoid object creations. The items in one array can be populated at the same time that items
+ * in the other array are being read from.
+ */
+ private static class ParserState {
+
+ // Basic ParserStateItem lists:
+ StateItem[] items = new StateItem[16];
+ StateItem[] prevItems = new StateItem[16];
+ int length;
+ int prevLength;
+
+ // Properties and Symbols memory:
+ IProperties properties;
+ DecimalFormatSymbols symbols;
+ ParseMode mode;
+ boolean caseSensitive;
+ boolean parseCurrency;
+
+ // Other pre-computed fields:
+ int decimalCp1;
+ int decimalCp2;
+ int groupingCp1;
+ int groupingCp2;
+ SeparatorType decimalType1;
+ SeparatorType decimalType2;
+ SeparatorType groupingType1;
+ SeparatorType groupingType2;
+ TextTrieMap<Byte> digitTrie;
+ Set<AffixHolder> affixHolders = new HashSet<AffixHolder>();
+
+ ParserState() {
+ for (int i = 0; i < items.length; i++) {
+ items[i] = new StateItem();
+ prevItems[i] = new StateItem();
+ }
+ }
+
+ /**
+ * Clears the internal state in order to prepare for parsing a new string.
+ *
+ * @return Myself, for chaining.
+ */
+ ParserState clear() {
+ length = 0;
+ prevLength = 0;
+ digitTrie = null;
+ affixHolders.clear();
+ return this;
+ }
+
+ /**
+ * Swaps the internal arrays of {@link StateItem}. Sets the length of the primary list to zero,
+ * so that it can be appended to.
+ */
+ void swap() {
+ StateItem[] temp = prevItems;
+ prevItems = items;
+ items = temp;
+ prevLength = length;
+ length = 0;
+ }
+
+ /**
+ * Swaps the internal arrays of {@link StateItem}. Sets the length of the primary list to the
+ * length of the previous list, so that it can be read from.
+ */
+ void swapBack() {
+ StateItem[] temp = prevItems;
+ prevItems = items;
+ items = temp;
+ length = prevLength;
+ prevLength = 0;
+ }
+
+ /**
+ * Gets the next available {@link StateItem} from the primary list for writing. This method
+ * should be thought of like a list append method, except that there are no object creations
+ * taking place.
+ *
+ * <p>It is the caller's responsibility to call either {@link StateItem#clear} or {@link
+ * StateItem#copyFrom} on the returned object.
+ *
+ * @return A dirty {@link StateItem}.
+ */
+ StateItem getNext() {
+ if (length >= items.length) {
+ // TODO: What to do here? Expand the array?
+ // This case is rare and would happen only with specially designed input.
+ // For now, just overwrite the last entry.
+ length = items.length - 1;
+ }
+ StateItem item = items[length];
+ length++;
+ return item;
+ }
+
+ /** @return The index of the last inserted StateItem via a call to {@link #getNext}. */
+ public int lastInsertedIndex() {
+ assert length > 0;
+ return length - 1;
+ }
+
+ /**
+ * Gets a {@link StateItem} from the primary list. Assumes that the item has already been added
+ * via a call to {@link #getNext}.
+ *
+ * @param i The index of the item to get.
+ * @return The item.
+ */
+ public StateItem getItem(int i) {
+ assert i >= 0 && i < length;
+ return items[i];
+ }
+ }
+
+ private static class AffixHolder {
+ final String p; // prefix
+ final String s; // suffix
+ final boolean strings;
+ final boolean negative;
+
+ static final AffixHolder EMPTY_POSITIVE = new AffixHolder("", "", true, false);
+ static final AffixHolder EMPTY_NEGATIVE = new AffixHolder("", "", true, true);
+ static final AffixHolder DEFAULT_POSITIVE = new AffixHolder("+", "", false, false);
+ static final AffixHolder DEFAULT_NEGATIVE = new AffixHolder("-", "", false, true);
+
+ static void addToState(ParserState state, IProperties properties) {
+ AffixHolder pp = fromPropertiesPositivePattern(properties);
+ AffixHolder np = fromPropertiesNegativePattern(properties);
+ AffixHolder ps = fromPropertiesPositiveString(properties);
+ AffixHolder ns = fromPropertiesNegativeString(properties);
+ if (pp == null && ps == null) {
+ if (properties.getPlusSignAlwaysShown()) {
+ state.affixHolders.add(DEFAULT_POSITIVE);
+ } else {
+ state.affixHolders.add(EMPTY_POSITIVE);
+ }
+ } else {
+ if (pp != null) state.affixHolders.add(pp);
+ if (ps != null) state.affixHolders.add(ps);
+ }
+ if (np == null && ns == null) {
+ state.affixHolders.add(DEFAULT_NEGATIVE);
+ } else {
+ if (np != null) state.affixHolders.add(np);
+ if (ns != null) state.affixHolders.add(ns);
+ }
+ }
+
+ static AffixHolder fromPropertiesPositivePattern(IProperties properties) {
+ String ppp = properties.getPositivePrefixPattern();
+ String psp = properties.getPositiveSuffixPattern();
+ return getInstance(ppp, psp, false, false);
+ }
+
+ static AffixHolder fromPropertiesNegativePattern(IProperties properties) {
+ String npp = properties.getNegativePrefixPattern();
+ String nsp = properties.getNegativeSuffixPattern();
+ return getInstance(npp, nsp, false, true);
+ }
+
+ static AffixHolder fromPropertiesPositiveString(IProperties properties) {
+ String pp = properties.getPositivePrefix();
+ String ps = properties.getPositiveSuffix();
+ return getInstance(pp, ps, true, false);
+ }
+
+ static AffixHolder fromPropertiesNegativeString(IProperties properties) {
+ String np = properties.getNegativePrefix();
+ String ns = properties.getNegativeSuffix();
+ return getInstance(np, ns, true, true);
+ }
+
+ static AffixHolder getInstance(String p, String s, boolean strings, boolean negative) {
+ if (p == null && s == null) return null;
+ if (p == null) p = "";
+ if (s == null) s = "";
+ if (p.length() == 0 && s.length() == 0) return negative ? EMPTY_NEGATIVE : EMPTY_POSITIVE;
+ return new AffixHolder(p, s, strings, negative);
+ }
+
+ AffixHolder(String pp, String sp, boolean strings, boolean negative) {
+ this.p = pp;
+ this.s = sp;
+ this.strings = strings;
+ this.negative = negative;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) return false;
+ if (this == other) return true;
+ if (!(other instanceof AffixHolder)) return false;
+ AffixHolder _other = (AffixHolder) other;
+ if (!p.equals(_other.p)) return false;
+ if (!s.equals(_other.s)) return false;
+ if (strings != _other.strings) return false;
+ if (negative != _other.negative) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return p.hashCode() ^ s.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ sb.append(p);
+ sb.append("|");
+ sb.append(s);
+ sb.append("|");
+ sb.append(strings ? 'S' : 'P');
+ sb.append("}");
+ return sb.toString();
+ }
+ }
+
+ /**
+ * A class that holds information about all currency affix patterns for the locale. This allows
+ * the parser to accept currencies in any format that are valid for the locale.
+ */
+ private static class CurrencyAffixPatterns {
+ private final Set<AffixHolder> set = new HashSet<AffixHolder>();
+
+ private static final ConcurrentHashMap<ULocale, CurrencyAffixPatterns> currencyAffixPatterns =
+ new ConcurrentHashMap<ULocale, CurrencyAffixPatterns>();
+
+ static void addToState(ULocale uloc, ParserState state) {
+ if (!currencyAffixPatterns.containsKey(uloc)) {
+ // There can be multiple threads computing the same CurrencyAffixPatterns simultaneously,
+ // but that scenario is harmless.
+ CurrencyAffixPatterns value = new CurrencyAffixPatterns(uloc);
+ currencyAffixPatterns.put(uloc, value);
+ }
+ CurrencyAffixPatterns instance = currencyAffixPatterns.get(uloc);
+ state.affixHolders.addAll(instance.set);
+ }
+
+ private CurrencyAffixPatterns(ULocale uloc) {
+ // Get the basic currency pattern.
+ String pattern = NumberFormat.getPattern(uloc, NumberFormat.CURRENCYSTYLE);
+ addPattern(pattern);
+
+ // Get the currency plural patterns.
+ // TODO: Update this after CurrencyPluralInfo is replaced.
+ CurrencyPluralInfo pluralInfo = CurrencyPluralInfo.getInstance(uloc);
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ pattern = pluralInfo.getCurrencyPluralPattern(plural.getKeyword());
+ addPattern(pattern);
+ }
+ }
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ private void addPattern(String pattern) {
+ Properties properties = threadLocalProperties.get();
+ try {
+ PatternString.parseToExistingProperties(pattern, properties);
+ } catch (IllegalArgumentException e) {
+ // This should only happen if there is a bug in CLDR data. Fail silently.
+ }
+ set.add(AffixHolder.fromPropertiesPositivePattern(properties));
+ set.add(AffixHolder.fromPropertiesNegativePattern(properties));
+ }
+ }
+
+ /**
+ * Makes a {@link TextTrieMap} for parsing digit strings. A trie is required only if the digit
+ * strings are longer than one code point. In order for this to be the case, the user would have
+ * needed to specify custom multi-character digits, like "(0)".
+ *
+ * @param digitStrings The list of digit strings from DecimalFormatSymbols.
+ * @return A trie, or null if a trie is not required.
+ */
+ static TextTrieMap<Byte> makeDigitTrie(String[] digitStrings) {
+ boolean requiresTrie = false;
+ for (int i = 0; i < 10; i++) {
+ String str = digitStrings[i];
+ if (Character.charCount(Character.codePointAt(str, 0)) != str.length()) {
+ requiresTrie = true;
+ break;
+ }
+ }
+ if (!requiresTrie) return null;
+
+ TextTrieMap<Byte> trieMap = new TextTrieMap<Byte>(false);
+ for (int i = 0; i < 10; i++) {
+ trieMap.put(digitStrings[i], (byte) i);
+ }
+ return trieMap;
+ }
+
+ protected static final ThreadLocal<ParserState> threadLocalParseState =
+ new ThreadLocal<ParserState>() {
+ @Override
+ protected ParserState initialValue() {
+ return new ParserState();
+ }
+ };
+
+ protected static final ThreadLocal<ParsePosition> threadLocalParsePosition =
+ new ThreadLocal<ParsePosition>() {
+ @Override
+ protected ParsePosition initialValue() {
+ return new ParsePosition(0);
+ }
+ };
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only. TODO: Remove this set from ScientificNumberFormat.
+ */
+ @Deprecated
+ public static final UnicodeSet UNISET_PLUS =
+ new UnicodeSet(
+ 0x002B, 0x002B, 0x207A, 0x207A, 0x208A, 0x208A, 0x2795, 0x2795, 0xFB29, 0xFB29,
+ 0xFE62, 0xFE62, 0xFF0B, 0xFF0B)
+ .freeze();
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only. TODO: Remove this set from ScientificNumberFormat.
+ */
+ @Deprecated
+ public static final UnicodeSet UNISET_MINUS =
+ new UnicodeSet(
+ 0x002D, 0x002D, 0x207B, 0x207B, 0x208B, 0x208B, 0x2212, 0x2212, 0x2796, 0x2796,
+ 0xFE63, 0xFE63, 0xFF0D, 0xFF0D)
+ .freeze();
+
+ public static Number parse(String input, IProperties properties, DecimalFormatSymbols symbols) {
+ ParsePosition ppos = threadLocalParsePosition.get();
+ ppos.setIndex(0);
+ return parse(input, ppos, properties, symbols);
+ }
+
+ // TODO: DELETE ME once debugging is finished
+ public static volatile boolean DEBUGGING = false;
+
+ /**
+ * Implements an iterative parser that maintains a lists of possible states at each code point in
+ * the string. At each code point in the string, the list of possible states is updated based on
+ * the states coming from the previous code point. The parser stops when it reaches the end of the
+ * string or when there are no possible parse paths remaining in the string.
+ *
+ * <p>TODO: This API is not fully flushed out. Right now this is internal-only.
+ *
+ * @param input The string to parse.
+ * @param ppos A {@link ParsePosition} to hold the index at which parsing stopped.
+ * @param properties A property bag, used only for determining the prefix/suffix strings and the
+ * padding character.
+ * @param symbols A {@link DecimalFormatSymbols} object, used for determining locale-specific
+ * symbols for grouping/decimal separators, digit strings, and prefix/suffix substitutions.
+ * @return A Number matching the parser's best interpretation of the string.
+ */
+ public static Number parse(
+ CharSequence input,
+ ParsePosition ppos,
+ IProperties properties,
+ DecimalFormatSymbols symbols) {
+ StateItem best = _parse(input, ppos, false, properties, symbols);
+ return (best == null) ? null : best.toNumber(properties);
+ }
+
+ public static CurrencyAmount parseCurrency(
+ String input, IProperties properties, DecimalFormatSymbols symbols) throws ParseException {
+ return parseCurrency(input, null, properties, symbols);
+ }
+
+ public static CurrencyAmount parseCurrency(
+ CharSequence input, ParsePosition ppos, IProperties properties, DecimalFormatSymbols symbols)
+ throws ParseException {
+ if (ppos == null) {
+ ppos = threadLocalParsePosition.get();
+ ppos.setIndex(0);
+ ppos.setErrorIndex(-1);
+ }
+ StateItem best = _parse(input, ppos, true, properties, symbols);
+ return (best == null) ? null : best.toCurrencyAmount(properties);
+ }
+
+ private static StateItem _parse(
+ CharSequence input,
+ ParsePosition ppos,
+ boolean parseCurrency,
+ IProperties properties,
+ DecimalFormatSymbols symbols) {
+
+ if (input == null || ppos == null || properties == null || symbols == null) {
+ throw new IllegalArgumentException("All arguments are required for parse.");
+ }
+
+ ParseMode mode = properties.getParseMode();
+ if (mode == null) mode = ParseMode.LENIENT;
+ boolean integerOnly = properties.getParseIntegerOnly();
+ boolean ignoreExponent = properties.getParseNoExponent();
+
+ // Set up the initial state
+ ParserState state = threadLocalParseState.get().clear();
+ state.properties = properties;
+ state.symbols = symbols;
+ state.mode = mode;
+ state.parseCurrency = parseCurrency;
+ state.caseSensitive = properties.getParseCaseSensitive();
+ state.decimalCp1 = Character.codePointAt(symbols.getDecimalSeparatorString(), 0);
+ state.decimalCp2 = Character.codePointAt(symbols.getMonetaryDecimalSeparatorString(), 0);
+ state.groupingCp1 = Character.codePointAt(symbols.getGroupingSeparatorString(), 0);
+ state.groupingCp2 = Character.codePointAt(symbols.getMonetaryGroupingSeparatorString(), 0);
+ state.decimalType1 = SeparatorType.fromCp(state.decimalCp1, mode);
+ state.decimalType2 = SeparatorType.fromCp(state.decimalCp2, mode);
+ state.groupingType1 = SeparatorType.fromCp(state.groupingCp1, mode);
+ state.groupingType2 = SeparatorType.fromCp(state.groupingCp2, mode);
+ StateItem initialStateItem = state.getNext().clear();
+ initialStateItem.name = StateName.BEFORE_PREFIX;
+
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ state.digitTrie = makeDigitTrie(symbols.getDigitStringsLocal());
+ AffixHolder.addToState(state, properties);
+ if (parseCurrency) {
+ CurrencyAffixPatterns.addToState(symbols.getULocale(), state);
+ }
+ }
+
+ if (DEBUGGING) {
+ System.out.println("Parsing: " + input);
+ System.out.println(properties);
+ System.out.println(state.affixHolders);
+ }
+
+ // Start walking through the string, one codepoint at a time. Backtracking is not allowed. This
+ // is to enforce linear runtime and prevent cases that could result in an infinite loop.
+ int offset = ppos.getIndex();
+ for (; offset < input.length(); ) {
+ int cp = Character.codePointAt(input, offset);
+ state.swap();
+ for (int i = 0; i < state.prevLength; i++) {
+ StateItem item = state.prevItems[i];
+ if (DEBUGGING) {
+ System.out.println(":" + offset + " " + item);
+ }
+
+ // In the switch statement below, if you see a line like:
+ // if (state.length > 0 && mode == ParseMode.FAST) break;
+ // it is used for accelerating the fast parse mode. The check is performed only in the
+ // states BEFORE_PREFIX, AFTER_INTEGER_DIGIT, and AFTER_FRACTION_DIGIT, which are the
+ // most common states.
+
+ switch (item.name) {
+ case BEFORE_PREFIX:
+ // Beginning of string
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptMinusOrPlusSign(cp, StateName.BEFORE_PREFIX, state, item, false);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptBidi(cp, StateName.BEFORE_PREFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptWhitespace(cp, StateName.BEFORE_PREFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptPadding(cp, StateName.BEFORE_PREFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptNan(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptInfinity(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (!integerOnly) {
+ acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptPrefix(cp, StateName.AFTER_PREFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_PREFIX, state, item);
+ }
+ }
+ break;
+
+ case AFTER_PREFIX:
+ // Prefix is consumed
+ acceptBidi(cp, StateName.AFTER_PREFIX, state, item);
+ acceptPadding(cp, StateName.AFTER_PREFIX, state, item);
+ acceptNan(cp, StateName.BEFORE_SUFFIX, state, item);
+ acceptInfinity(cp, StateName.BEFORE_SUFFIX, state, item);
+ acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (!integerOnly) {
+ acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.AFTER_PREFIX, state, item);
+ acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.AFTER_PREFIX, state, item);
+ }
+ }
+ break;
+
+ case AFTER_INTEGER_DIGIT:
+ // Previous character was an integer digit (or grouping/whitespace)
+ acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (!integerOnly) {
+ acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptBidi(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (!ignoreExponent) {
+ acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+ }
+ }
+ break;
+
+ case AFTER_FRACTION_DIGIT:
+ // We encountered a decimal point
+ acceptFractionDigit(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptBidi(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (!ignoreExponent) {
+ acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+ if (state.length > 0 && mode == ParseMode.FAST) break;
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+ }
+ }
+ break;
+
+ case AFTER_EXPONENT_SEPARATOR:
+ acceptBidi(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+ acceptMinusOrPlusSign(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item, true);
+ acceptExponentDigit(cp, StateName.AFTER_EXPONENT_DIGIT, state, item);
+ break;
+
+ case AFTER_EXPONENT_DIGIT:
+ acceptBidi(cp, StateName.AFTER_EXPONENT_DIGIT, state, item);
+ acceptPadding(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ acceptExponentDigit(cp, StateName.AFTER_EXPONENT_DIGIT, state, item);
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ }
+ }
+ break;
+
+ case BEFORE_SUFFIX:
+ // Accept whitespace, suffixes, and exponent separators
+ acceptBidi(cp, StateName.BEFORE_SUFFIX, state, item);
+ acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+ if (!ignoreExponent) {
+ acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+ acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+ }
+ }
+ break;
+
+ case BEFORE_SUFFIX_SEEN_EXPONENT:
+ // Accept whitespace and suffixes but not exponent separators
+ acceptBidi(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ acceptPadding(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+ acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+ acceptWhitespace(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item, false);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+ }
+ }
+ break;
+
+ case AFTER_SUFFIX:
+ if ((mode == ParseMode.LENIENT || mode == ParseMode.FAST) && parseCurrency) {
+ // Continue traversing in case there is a currency symbol to consume
+ acceptBidi(cp, StateName.AFTER_SUFFIX, state, item);
+ acceptPadding(cp, StateName.AFTER_SUFFIX, state, item);
+ acceptWhitespace(cp, StateName.AFTER_SUFFIX, state, item);
+ acceptMinusOrPlusSign(cp, StateName.AFTER_SUFFIX, state, item, false);
+ if (parseCurrency) {
+ acceptCurrency(cp, StateName.AFTER_SUFFIX, state, item);
+ }
+ }
+ // Otherwise, do not accept any more characters.
+ break;
+
+ case INSIDE_CURRENCY:
+ acceptCurrencyOffset(cp, state, item);
+ break;
+
+ case INSIDE_DIGIT:
+ acceptDigitTrieOffset(cp, state, item);
+ break;
+
+ case INSIDE_STRING:
+ acceptStringOffset(cp, state, item);
+ // Accept arbitrary bidi in the middle of strings.
+ if (state.length == 0 && UNISET_BIDI.contains(cp)) {
+ state.getNext().copyFrom(item, item.name, cp);
+ }
+ break;
+
+ case INSIDE_AFFIX_PATTERN:
+ acceptAffixPatternOffset(cp, state, item);
+ // Accept arbitrary bidi and whitespace (if lenient) in the middle of affixes.
+ if (state.length == 0 && isIgnorable(cp, state)) {
+ state.getNext().copyFrom(item, item.name, cp);
+ }
+ break;
+ }
+ }
+
+ if (state.length == 0) {
+ // No parse paths continue past this point. We have found the longest parsable string
+ // from the input. Restore previous state without the offset and break.
+ state.swapBack();
+ break;
+ }
+
+ offset += Character.charCount(cp);
+ }
+
+ // Post-processing
+ if (state.length == 0) {
+ if (DEBUGGING) {
+ System.out.println("No matches found");
+ System.out.println("- - - - - - - - - -");
+ }
+ return null;
+ } else {
+
+ // Loop through the candidates. "continue" skips a candidate as invalid.
+ StateItem best = null;
+ outer:
+ for (int i = 0; i < state.length; i++) {
+ StateItem item = state.items[i];
+
+ if (DEBUGGING) {
+ System.out.println(":end " + item);
+ }
+
+ // Check that at least one digit was read.
+ if (!item.hasNumber()) {
+ if (DEBUGGING) System.out.println("-> rejected due to no number value");
+ continue;
+ }
+
+ if (mode == ParseMode.STRICT) {
+ // Perform extra checks for strict mode.
+ // We require that the affixes match.
+ boolean sawPrefix = item.sawPrefix || (item.affix != null && item.affix.p.isEmpty());
+ boolean sawSuffix = item.sawSuffix || (item.affix != null && item.affix.s.isEmpty());
+ boolean hasEmptyAffix =
+ state.affixHolders.contains(AffixHolder.EMPTY_POSITIVE)
+ || state.affixHolders.contains(AffixHolder.EMPTY_NEGATIVE);
+ if (sawPrefix && sawSuffix) {
+ // OK
+ } else if (!sawPrefix && !sawSuffix && hasEmptyAffix) {
+ // OK
+ } else {
+ // Has a prefix or suffix that doesn't match
+ if (DEBUGGING) System.out.println("-> rejected due to mismatched prefix/suffix");
+ continue;
+ }
+
+ // Check that grouping sizes are valid.
+ int grouping1 = properties.getGroupingSize();
+ int grouping2 = properties.getSecondaryGroupingSize();
+ grouping1 = grouping1 > 0 ? grouping1 : grouping2;
+ grouping2 = grouping2 > 0 ? grouping2 : grouping1;
+ long groupingWidths = item.groupingWidths;
+ int numGroupingRegions = 16 - Long.numberOfLeadingZeros(groupingWidths) / 4;
+ // If the last grouping is zero, accept strings like "1," but reject string like "1,.23"
+ // Strip off multiple last-groupings to handle cases like "123,," or "123 "
+ while (numGroupingRegions > 1 && (groupingWidths & 0xf) == 0) {
+ if (item.sawDecimalPoint) {
+ if (DEBUGGING) System.out.println("-> rejected due to decimal point after grouping");
+ continue outer;
+ } else {
+ groupingWidths >>>= 4;
+ numGroupingRegions--;
+ }
+ }
+ if (grouping1 < 0) {
+ // OK (no grouping data available)
+ } else if (numGroupingRegions <= 1) {
+ // OK (no grouping digits)
+ } else if ((groupingWidths & 0xf) != grouping1) {
+ // First grouping size is invalid
+ if (DEBUGGING) System.out.println("-> rejected due to first grouping violation");
+ continue;
+ } else if (((groupingWidths >>> ((numGroupingRegions - 1) * 4)) & 0xf) > grouping2) {
+ // String like "1234,567" where the highest grouping is too large
+ if (DEBUGGING) System.out.println("-> rejected due to final grouping violation");
+ continue;
+ } else {
+ for (int j = 1; j < numGroupingRegions - 1; j++) {
+ if (((groupingWidths >>> (j * 4)) & 0xf) != grouping2) {
+ // A grouping size somewhere in the middle is invalid
+ if (DEBUGGING) System.out.println("-> rejected due to inner grouping violation");
+ continue outer;
+ }
+ }
+ }
+ }
+
+ // Optionally require that the presence of a decimal point matches the pattern.
+ if (properties.getDecimalPatternMatchRequired()
+ && item.sawDecimalPoint != PositiveDecimalFormat.allowsDecimalPoint(properties)) {
+ if (DEBUGGING) System.out.println("-> rejected due to decimal point violation");
+ continue;
+ }
+
+ // When parsing currencies, require that a currency symbol was found.
+ if (parseCurrency && !item.sawCurrency) {
+ if (DEBUGGING) System.out.println("-> rejected due to lack of currency");
+ continue;
+ }
+
+ // If we get here, then this candidate is acceptable.
+ // Use the earliest candidate in the list, or the one with the highest score.
+ if (best == null) {
+ best = item;
+ } else if (item.score > best.score) {
+ best = item;
+ }
+ }
+
+ if (DEBUGGING) {
+ System.out.println("- - - - - - - - - -");
+ }
+
+ if (best != null) {
+ ppos.setIndex(offset - best.trailingCount);
+ return best;
+ } else {
+ ppos.setErrorIndex(offset);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * If <code>cp</code> is whitespace (as determined by the unicode set {@link #UNISET_WHITESPACE}),
+ * copies <code>item</code> to the new list in <code>state</code> and sets its state name to
+ * <code>nextName</code>.
+ *
+ * @param cp The code point to check.
+ * @param nextName The new state name if the check passes.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptWhitespace(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ if (UNISET_WHITESPACE.contains(cp)) {
+ state.getNext().copyFrom(item, nextName, cp);
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a bidi control character (as determined by the unicode set {@link
+ * #UNISET_BIDI}), copies <code>item</code> to the new list in <code>state</code> and sets its
+ * state name to <code>nextName</code>.
+ *
+ * @param cp The code point to check.
+ * @param nextName The new state name if the check passes.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptBidi(int cp, StateName nextName, ParserState state, StateItem item) {
+ if (UNISET_BIDI.contains(cp)) {
+ state.getNext().copyFrom(item, nextName, cp);
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a padding character (as determined by {@link ParserState#paddingCp}),
+ * copies <code>item</code> to the new list in <code>state</code> and sets its state name to
+ * <code>nextName</code>.
+ *
+ * @param cp The code point to check.
+ * @param nextName The new state name if the check passes.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptPadding(int cp, StateName nextName, ParserState state, StateItem item) {
+ CharSequence padding = state.properties.getPadString();
+ if (padding == null || padding.length() == 0) return;
+ int referenceCp = Character.codePointAt(padding, 0);
+ if (cp == referenceCp) {
+ state.getNext().copyFrom(item, nextName, cp);
+ }
+ }
+
+ private static void acceptIntegerDigit(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ acceptDigitHelper(cp, nextName, state, item, DigitType.INTEGER);
+ }
+
+ private static void acceptFractionDigit(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ acceptDigitHelper(cp, nextName, state, item, DigitType.FRACTION);
+ }
+
+ private static void acceptExponentDigit(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ acceptDigitHelper(cp, nextName, state, item, DigitType.EXPONENT);
+ }
+
+ /**
+ * If <code>cp</code> is a digit character (as determined by either {@link UCharacter#digit} or
+ * {@link ParserState#digitCps}), copies <code>item</code> to the new list in <code>state</code>
+ * and sets its state name to one determined by <code>type</code>. Also copies the digit into a
+ * field in the new item determined by <code>type</code>.
+ *
+ * <p>This function guarantees that it will add no more than one {@link StateItem} to the {@link
+ * ParserState}. This means that {@link ParserState#lastInsertedIndex()} can be called to access
+ * the {@link StateItem} that was inserted.
+ *
+ * @param cp The code point to check.
+ * @param nextName The state to set if a digit is accepted.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ * @param type The digit type, which determines the next state and the field into which to insert
+ * the digit.
+ */
+ private static void acceptDigitHelper(
+ int cp, StateName nextName, ParserState state, StateItem item, DigitType type) {
+ // Check the Unicode digit character property
+ byte digit = (byte) UCharacter.digit(cp, 10);
+ StateItem next = null;
+
+ // Look for the digit:
+ if (digit >= 0) {
+ // Code point is a number
+ next = state.getNext().copyFrom(item, nextName, -1);
+ }
+
+ // Do not perform the expensive string manipulations in fast mode.
+ if (digit < 0 && (state.mode == ParseMode.LENIENT || state.mode == ParseMode.STRICT)) {
+ if (state.digitTrie == null) {
+ // Check custom digits, all of which are at most one code point
+ for (byte d = 0; d < 10; d++) {
+ int referenceCp = Character.codePointAt(state.symbols.getDigitStringsLocal()[d], 0);
+ if (cp == referenceCp) {
+ digit = d;
+ next = state.getNext().copyFrom(item, nextName, -1);
+ }
+ }
+ } else {
+ // Custom digits have more than one code point
+ acceptDigitTrie(cp, nextName, state, item, type);
+ }
+ }
+
+ // Save state:
+ if (next != null) {
+ next.appendDigit(digit, type);
+ if (type == DigitType.INTEGER && (next.groupingWidths & 0xf) < 15) {
+ next.groupingWidths++;
+ }
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a sign (as determined by the unicode sets {@link #UNISET_PLUS} and {@link
+ * #UNISET_MINUS}), copies <code>item</code> to the new list in <code>state</code>. Loops back to
+ * the same state name.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptMinusOrPlusSign(
+ int cp, StateName nextName, ParserState state, StateItem item, boolean exponent) {
+ acceptMinusOrPlusSign(cp, nextName, null, state, item, exponent);
+ }
+
+ private static void acceptMinusOrPlusSign(
+ int cp,
+ StateName returnTo1,
+ StateName returnTo2,
+ ParserState state,
+ StateItem item,
+ boolean exponent) {
+ if (UNISET_PLUS.contains(cp)) {
+ StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+ next.returnTo1 = returnTo2;
+ } else if (UNISET_MINUS.contains(cp)) {
+ StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+ next.returnTo1 = returnTo2;
+ if (exponent) {
+ next.sawNegativeExponent = true;
+ } else {
+ next.sawNegative = true;
+ }
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a grouping separator (as determined by the unicode set {@link
+ * #UNISET_GROUPING}), copies <code>item</code> to the new list in <code>state</code> and loops
+ * back to the same state. Also accepts if <code>cp</code> is the locale-specific grouping
+ * separator in {@link ParserState#groupingCp}, in which case the {@link
+ * StateItem#usesLocaleSymbols} flag is also set.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptGrouping(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ // Do not accept mixed grouping separators in the same string.
+ if (item.groupingCp == -1) {
+ // First time seeing a grouping separator.
+ SeparatorType cpType = SeparatorType.fromCp(cp, state.mode);
+
+ // Always accept if exactly the same as the locale symbol.
+ // Otherwise, reject if UNKNOWN or in the same class as the decimal separator.
+ if (cp != state.groupingCp1 && cp != state.groupingCp2) {
+ if (cpType == SeparatorType.UNKNOWN) {
+ return;
+ }
+ if (cpType == SeparatorType.COMMA_LIKE
+ && (state.decimalType1 == SeparatorType.COMMA_LIKE
+ || state.decimalType2 == SeparatorType.COMMA_LIKE)) {
+ return;
+ }
+ if (cpType == SeparatorType.PERIOD_LIKE
+ && (state.decimalType1 == SeparatorType.PERIOD_LIKE
+ || state.decimalType2 == SeparatorType.PERIOD_LIKE)) {
+ return;
+ }
+ }
+
+ // A match was found.
+ StateItem next = state.getNext().copyFrom(item, nextName, cp);
+ next.groupingCp = cp;
+ next.groupingWidths <<= 4;
+ } else {
+ // Have already seen a grouping separator.
+ if (cp == item.groupingCp) {
+ StateItem next = state.getNext().copyFrom(item, nextName, cp);
+ next.groupingWidths <<= 4;
+ }
+ }
+ }
+
+ /**
+ * If <code>cp</code> is a decimal (as determined by the unicode set {@link #UNISET_DECIMAL}),
+ * copies <code>item</code> to the new list in <code>state</code> and goes to {@link
+ * StateName#AFTER_FRACTION_DIGIT}. Also accepts if <code>cp</code> is the locale-specific decimal
+ * point in {@link ParserState#decimalCp}, in which case the {@link StateItem#usesLocaleSymbols}
+ * flag is also set.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptDecimalPoint(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ if (cp == item.groupingCp) {
+ // Don't accept a decimal point that is the same as the grouping separator
+ return;
+ }
+
+ SeparatorType cpType = SeparatorType.fromCp(cp, state.mode);
+
+ // We require that the decimal separator be in the same class as the locale.
+ if (cpType != state.decimalType1 && cpType != state.decimalType2) {
+ return;
+ }
+
+ // If in UNKNOWN or OTHER, require an exact match.
+ if (cpType == SeparatorType.OTHER_GROUPING || cpType == SeparatorType.UNKNOWN) {
+ if (cp != state.decimalCp1 && cp != state.decimalCp2) {
+ return;
+ }
+ }
+
+ // A match was found.
+ StateItem next = state.getNext().copyFrom(item, nextName, -1);
+ next.sawDecimalPoint = true;
+ }
+
+ private static void acceptNan(int cp, StateName nextName, ParserState state, StateItem item) {
+ CharSequence nan = state.symbols.getNaN();
+ long added = acceptString(cp, nextName, null, state, item, nan, 0);
+
+ // Set state in the items that were added by the function call
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ state.getItem(i).sawNaN = true;
+ }
+ }
+ }
+
+ private static void acceptInfinity(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ CharSequence inf = state.symbols.getInfinity();
+ long added = acceptString(cp, nextName, null, state, item, inf, 0);
+
+ // Set state in the items that were added by the function call
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ state.getItem(i).sawInfinity = true;
+ }
+ }
+ }
+
+ private static void acceptExponentSeparator(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ CharSequence exp = state.symbols.getExponentSeparator();
+ acceptString(cp, nextName, null, state, item, exp, 0);
+ }
+
+ private static void acceptPrefix(int cp, StateName nextName, ParserState state, StateItem item) {
+ for (AffixHolder holder : state.affixHolders) {
+ acceptAffixHolder(cp, nextName, state, item, holder, true);
+ }
+ }
+
+ private static void acceptSuffix(int cp, StateName nextName, ParserState state, StateItem item) {
+ if (item.affix != null) {
+ acceptAffixHolder(cp, nextName, state, item, item.affix, false);
+ } else {
+ for (AffixHolder holder : state.affixHolders) {
+ acceptAffixHolder(cp, nextName, state, item, holder, false);
+ }
+ }
+ }
+
+ private static void acceptAffixHolder(
+ int cp,
+ StateName nextName,
+ ParserState state,
+ StateItem item,
+ AffixHolder holder,
+ boolean prefix) {
+ if (holder == null) return;
+ String str = prefix ? holder.p : holder.s;
+ if (holder.strings) {
+ long added = acceptString(cp, nextName, null, state, item, str, 0);
+ // At most one item can be added upon consuming a string.
+ if (added != 0) {
+ int i = state.lastInsertedIndex();
+ // The following six lines are duplicated below; not enough for their own function.
+ state.getItem(i).affix = holder;
+ if (prefix) state.getItem(i).sawPrefix = true;
+ if (!prefix) state.getItem(i).sawSuffix = true;
+ if (holder.negative) state.getItem(i).sawNegative = true;
+ state.getItem(i).score++; // reward for consuming a prefix/suffix.
+ }
+ } else {
+ long added = acceptAffixPattern(cp, nextName, state, item, str, 0);
+ // Multiple items can be added upon consuming an affix pattern.
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ // The following six lines are duplicated above; not enough for their own function.
+ state.getItem(i).affix = holder;
+ if (prefix) state.getItem(i).sawPrefix = true;
+ if (!prefix) state.getItem(i).sawSuffix = true;
+ if (holder.negative) state.getItem(i).sawNegative = true;
+ state.getItem(i).score++; // reward for consuming a prefix/suffix.
+ }
+ }
+ }
+ }
+
+ private static void acceptStringOffset(int cp, ParserState state, StateItem item) {
+ acceptString(
+ cp, item.returnTo1, item.returnTo2, state, item, item.currentString, item.currentOffset);
+ }
+
+ /**
+ * Accepts a code point if the code point is compatible with the string at the given offset.
+ *
+ * <p>This method will add no more than one {@link StateItem} to the {@link ParserState}, which
+ * means that at most one bit will be set in the return value, corresponding to the return value
+ * of {@link ParserState#lastInsertedIndex()}.
+ *
+ * @param cp The current code point, which will be checked for a match to the string.
+ * @param returnTo1 The state to return to after reaching the end of the string.
+ * @param returnTo2 The state to save in <code>returnTo1</code> after reaching the end of the
+ * string. Set to null if returning to the main state loop.
+ * @param state The current {@link ParserState}
+ * @param item The current {@link StateItem}
+ * @param str The string against which to check for a match.
+ * @param offset The number of chars into the string. Initial value should be 0.
+ * @return A bitmask where the bits correspond to the items that were added. Set to 0L if no items
+ * were added.
+ */
+ private static long acceptString(
+ int cp,
+ StateName returnTo1,
+ StateName returnTo2,
+ ParserState state,
+ StateItem item,
+ CharSequence str,
+ int offset) {
+ if (str == null || str.length() == 0) return 0L;
+
+ // Fast path for fast mode
+ if (state.mode == ParseMode.FAST && Character.codePointAt(str, offset) != cp) return 0L;
+
+ // Skip over bidi code points at the beginning of the string.
+ // They will be accepted in the main loop.
+ int count = 0;
+ int referenceCp = -1;
+ boolean equals = false;
+ for (; offset < str.length(); offset += count) {
+ referenceCp = Character.codePointAt(str, offset);
+ count = Character.charCount(referenceCp);
+ equals = codePointEquals(cp, referenceCp, state);
+ if (!UNISET_BIDI.contains(cp)) break;
+ }
+
+ if (equals) {
+ // Matches first code point of the string
+ StateItem next = state.getNext().copyFrom(item, null, cp);
+
+ // Skip over ignorable code points in the middle of the string.
+ // They will be accepted in the main loop.
+ offset += count;
+ for (; offset < str.length(); offset += count) {
+ referenceCp = Character.codePointAt(str, offset);
+ count = Character.charCount(referenceCp);
+ if (!UNISET_BIDI.contains(cp)) break;
+ }
+
+ if (offset < str.length()) {
+ // String has more interesting code points.
+ next.name = StateName.INSIDE_STRING;
+ next.returnTo1 = returnTo1;
+ next.returnTo2 = returnTo2;
+ next.currentString = str;
+ next.currentOffset = offset;
+ } else {
+ // We've reached the end of the string.
+ next.name = returnTo1;
+ next.trailingCount = 0;
+ next.returnTo1 = returnTo2;
+ next.returnTo2 = null;
+ }
+ return 1L << state.lastInsertedIndex();
+ }
+ return 0L;
+ }
+
+ private static void acceptAffixPatternOffset(int cp, ParserState state, StateItem item) {
+ acceptAffixPattern(
+ cp, item.returnTo1, state, item, item.currentAffixPattern, item.currentStepwiseParserTag);
+ }
+
+ /**
+ * Accepts a code point if the code point is compatible with the affix pattern at the offset
+ * encoded in the tag argument.
+ *
+ * @param cp The current code point, which will be checked for a match to the string.
+ * @param returnTo The state to return to after reaching the end of the string.
+ * @param state The current {@link ParserState}
+ * @param item The current {@link StateItem}
+ * @param str The string containing the affix pattern.
+ * @param tag The current state of the stepwise parser. Initial value should be 0L.
+ * @return A bitmask where the bits correspond to the items that were added. Set to 0L if no items
+ * were added.
+ */
+ private static long acceptAffixPattern(
+ int cp, StateName returnTo, ParserState state, StateItem item, CharSequence str, long tag) {
+ if (str == null || str.length() == 0) return 0L;
+
+ // Skip over ignorable code points at the beginning of the affix pattern.
+ // They will be accepted in the main loop.
+ int typeOrCp = 0;
+ boolean hasNext = true;
+ while (hasNext) {
+ tag = AffixPatternUtils.nextToken(tag, str);
+ typeOrCp = AffixPatternUtils.getTypeOrCp(tag);
+ hasNext = AffixPatternUtils.hasNext(tag, str);
+ if (typeOrCp < 0 || !isIgnorable(typeOrCp, state)) break;
+ }
+
+ // Convert from the returned tag to a code point, string, or currency to check
+ int resolvedCp = -1;
+ CharSequence resolvedStr = null;
+ boolean resolvedMinusSign = false;
+ boolean resolvedPlusSign = false;
+ boolean resolvedCurrency = false;
+ if (typeOrCp < 0) {
+ // Symbol
+ switch (typeOrCp) {
+ case AffixPatternUtils.TYPE_MINUS_SIGN:
+ resolvedMinusSign = true;
+ break;
+ case AffixPatternUtils.TYPE_PLUS_SIGN:
+ resolvedPlusSign = true;
+ break;
+ case AffixPatternUtils.TYPE_PERCENT:
+ resolvedStr = state.symbols.getPercentString();
+ break;
+ case AffixPatternUtils.TYPE_PERMILLE:
+ resolvedStr = state.symbols.getPerMillString();
+ break;
+ case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
+ case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+ case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+ resolvedCurrency = true;
+ break;
+ default:
+ throw new AssertionError();
+ }
+ } else {
+ resolvedCp = typeOrCp;
+ }
+
+ // Skip over ignorable code points in the middle of the affix pattern.
+ // They will be accepted in the main loop.
+ while (hasNext) {
+ long futureTag = AffixPatternUtils.nextToken(tag, str);
+ int futureTypeOrCp = AffixPatternUtils.getTypeOrCp(futureTag);
+ if (futureTypeOrCp < 0 || !isIgnorable(futureTypeOrCp, state)) break;
+ tag = futureTag;
+ typeOrCp = futureTypeOrCp;
+ hasNext = AffixPatternUtils.hasNext(tag, str);
+ }
+
+ long added = 0L;
+ if (resolvedCp >= 0) {
+ // Code point
+ if (!codePointEquals(cp, resolvedCp, state)) return 0L;
+ StateItem next = state.getNext().copyFrom(item, null, cp);
+
+ if (hasNext) {
+ // Additional tokens in affix string.
+ next.name = StateName.INSIDE_AFFIX_PATTERN;
+ next.returnTo1 = returnTo;
+ } else {
+ // Reached last token in affix string.
+ next.name = returnTo;
+ next.trailingCount = 0;
+ next.returnTo1 = null;
+ }
+ added |= 1L << state.lastInsertedIndex();
+ }
+ if (resolvedMinusSign || resolvedPlusSign) {
+ // Sign
+ if (hasNext) {
+ acceptMinusOrPlusSign(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, false);
+ } else {
+ acceptMinusOrPlusSign(cp, returnTo, null, state, item, false);
+ }
+ // Decide whether to accept a custom string
+ if (resolvedMinusSign) {
+ String mss = state.symbols.getMinusSignString();
+ int mssCp = Character.codePointAt(mss, 0);
+ if (mss.length() != Character.charCount(mssCp) || !UNISET_MINUS.contains(mssCp)) {
+ resolvedStr = mss;
+ }
+ }
+ if (resolvedPlusSign) {
+ String pss = state.symbols.getPlusSignString();
+ int pssCp = Character.codePointAt(pss, 0);
+ if (pss.length() != Character.charCount(pssCp) || !UNISET_MINUS.contains(pssCp)) {
+ resolvedStr = pss;
+ }
+ }
+ }
+ if (resolvedStr != null) {
+ // String symbol
+ if (hasNext) {
+ added |=
+ acceptString(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, resolvedStr, 0);
+ } else {
+ added |= acceptString(cp, returnTo, null, state, item, resolvedStr, 0);
+ }
+ }
+ if (resolvedCurrency) {
+ // Currency symbol
+ if (hasNext) {
+ added |= acceptCurrency(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item);
+ } else {
+ added |= acceptCurrency(cp, returnTo, null, state, item);
+ }
+ }
+
+ // Set state in the items that were added by the function calls
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ state.getItem(i).currentAffixPattern = str;
+ state.getItem(i).currentStepwiseParserTag = tag;
+ }
+ }
+ return added;
+ }
+
+ /**
+ * This method can add up to four items to the new list in <code>state</code>.
+ *
+ * <p>If <code>cp</code> is equal to any known ISO code or long name, copies <code>item</code> to
+ * the new list in <code>state</code> and sets its ISO code to the corresponding currency.
+ *
+ * <p>If <code>cp</code> is the first code point of any ISO code or long name having more them one
+ * code point in length, copies <code>item</code> to the new list in <code>state</code> along with
+ * an instance of {@link TextTrieMap.ParseState} for tracking the following code points.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptCurrency(
+ int cp, StateName nextName, ParserState state, StateItem item) {
+ acceptCurrency(cp, nextName, null, state, item);
+ }
+
+ private static long acceptCurrency(
+ int cp, StateName returnTo1, StateName returnTo2, ParserState state, StateItem item) {
+ if (item.sawCurrency) return 0L;
+ long added = 0L;
+
+ // Accept from local currency information
+ String str1, str2;
+ Currency currency = state.properties.getCurrency();
+ if (currency != null) {
+ str1 = currency.getName(state.symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ str2 = currency.getCurrencyCode();
+ // TODO: Should we also accept long names? In currency mode, they are in the CLDR data.
+ } else {
+ currency = state.symbols.getCurrency();
+ str1 = state.symbols.getCurrencySymbol();
+ str2 = state.symbols.getInternationalCurrencySymbol();
+ }
+ added |= acceptString(cp, returnTo1, returnTo2, state, item, str1, 0);
+ added |= acceptString(cp, returnTo1, returnTo2, state, item, str2, 0);
+ for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+ if (((1L << i) & added) != 0) {
+ state.getItem(i).sawCurrency = true;
+ state.getItem(i).isoCode = str2;
+ }
+ }
+
+ // Accept from CLDR data
+ if (state.parseCurrency) {
+ ULocale uloc = state.symbols.getULocale();
+ TextTrieMap<Currency.CurrencyStringInfo>.ParseState trie1 =
+ Currency.openParseState(uloc, cp, Currency.LONG_NAME);
+ TextTrieMap<Currency.CurrencyStringInfo>.ParseState trie2 =
+ Currency.openParseState(uloc, cp, Currency.SYMBOL_NAME);
+ added |= acceptCurrencyHelper(cp, returnTo1, returnTo2, state, item, trie1);
+ added |= acceptCurrencyHelper(cp, returnTo1, returnTo2, state, item, trie2);
+ }
+
+ return added;
+ }
+
+ /**
+ * If <code>cp</code> is the next code point of any currency, copies <code>item</code> to the new
+ * list in <code>state</code> along with an instance of {@link TextTrieMap.ParseState} for
+ * tracking the following code points.
+ *
+ * <p>This method should only be called in a state following {@link #acceptCurrency}.
+ *
+ * @param cp The code point to check.
+ * @param state The state object to update.
+ * @param item The old state leading into the code point.
+ */
+ private static void acceptCurrencyOffset(int cp, ParserState state, StateItem item) {
+ acceptCurrencyHelper(
+ cp, item.returnTo1, item.returnTo2, state, item, item.currentCurrencyTrieState);
+ }
+
+ private static long acceptCurrencyHelper(
+ int cp,
+ StateName returnTo1,
+ StateName returnTo2,
+ ParserState state,
+ StateItem item,
+ TextTrieMap<Currency.CurrencyStringInfo>.ParseState trieState) {
+ if (trieState == null) return 0L;
+ trieState.accept(cp);
+ long added = 0L;
+ Iterator<Currency.CurrencyStringInfo> currentMatches = trieState.getCurrentMatches();
+ if (currentMatches != null) {
+ // Match on current code point
+ // TODO: What should happen with multiple currency matches?
+ StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+ next.returnTo1 = returnTo2;
+ next.returnTo2 = null;
+ next.sawCurrency = true;
+ next.isoCode = currentMatches.next().getISOCode();
+ added |= 1L << state.lastInsertedIndex();
+ }
+ if (!trieState.atEnd()) {
+ // Prepare for matches on future code points
+ StateItem next = state.getNext().copyFrom(item, StateName.INSIDE_CURRENCY, -1);
+ next.returnTo1 = returnTo1;
+ next.returnTo2 = returnTo2;
+ next.currentCurrencyTrieState = trieState;
+ added |= 1L << state.lastInsertedIndex();
+ }
+ return added;
+ }
+
+ private static long acceptDigitTrie(
+ int cp, StateName nextName, ParserState state, StateItem item, DigitType type) {
+ assert state.digitTrie != null;
+ TextTrieMap<Byte>.ParseState trieState = state.digitTrie.openParseState(cp);
+ if (trieState == null) return 0L;
+ return acceptDigitTrieHelper(cp, nextName, state, item, type, trieState);
+ }
+
+ private static void acceptDigitTrieOffset(int cp, ParserState state, StateItem item) {
+ acceptDigitTrieHelper(
+ cp, item.returnTo1, state, item, item.currentDigitType, item.currentDigitTrieState);
+ }
+
+ private static long acceptDigitTrieHelper(
+ int cp,
+ StateName returnTo1,
+ ParserState state,
+ StateItem item,
+ DigitType type,
+ TextTrieMap<Byte>.ParseState trieState) {
+ if (trieState == null) return 0L;
+ trieState.accept(cp);
+ long added = 0L;
+ Iterator<Byte> currentMatches = trieState.getCurrentMatches();
+ if (currentMatches != null) {
+ // Match on current code point
+ byte digit = currentMatches.next();
+ StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+ next.returnTo1 = null;
+ next.appendDigit(digit, type);
+ added |= 1L << state.lastInsertedIndex();
+ }
+ if (!trieState.atEnd()) {
+ // Prepare for matches on future code points
+ StateItem next = state.getNext().copyFrom(item, StateName.INSIDE_DIGIT, -1);
+ next.returnTo1 = returnTo1;
+ next.currentDigitTrieState = trieState;
+ next.currentDigitType = type;
+ added |= 1L << state.lastInsertedIndex();
+ }
+ return added;
+ }
+
+ /**
+ * Checks whether the two given code points are equal after applying case mapping as requested in
+ * the ParserState.
+ *
+ * @see #acceptString
+ * @see #acceptAffixPattern
+ */
+ private static boolean codePointEquals(int cp1, int cp2, ParserState state) {
+ if (!state.caseSensitive) {
+ cp1 = UCharacter.foldCase(cp1, true);
+ cp2 = UCharacter.foldCase(cp2, true);
+ }
+ return cp1 == cp2;
+ }
+
+ /**
+ * Checks whether the given code point is "ignorable" and should be skipped. BiDi characters are
+ * always ignorable, and whitespace is ignorable in lenient mode.
+ *
+ * @param cp The code point to test. Returns false if cp is negative.
+ * @param state The current {@link ParserState}, used for determining strict mode.
+ * @return true if cp is bidi or whitespace in lenient mode; false otherwise.
+ */
+ private static boolean isIgnorable(int cp, ParserState state) {
+ if (cp < 0) return false;
+ if (UNISET_BIDI.contains(cp)) return true;
+ return state.mode == ParseMode.LENIENT && UNISET_WHITESPACE.contains(cp);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.formatters.PaddingFormat;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.text.DecimalFormatSymbols;
+
+/**
+ * Handles parsing and creation of the compact pattern string representation of a decimal format.
+ */
+public class PatternString {
+
+ /**
+ * Parses a pattern string into a new property bag.
+ *
+ * @param pattern The pattern string, like "#,##0.00"
+ * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
+ * increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
+ * as CurrencyUsage, is to be used instead.
+ * @return A property bag object.
+ * @throws IllegalArgumentException If there is a syntax error in the pattern string.
+ */
+ public static Properties parseToProperties(String pattern, boolean ignoreRounding) {
+ Properties properties = new Properties();
+ LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
+ return properties;
+ }
+
+ public static Properties parseToProperties(String pattern) {
+ return parseToProperties(pattern, false);
+ }
+
+ /**
+ * Parses a pattern string into an existing property bag. All properties that can be encoded into
+ * a pattern string will be overwritten with either their default value or with the value coming
+ * from the pattern string. Properties that cannot be encoded into a pattern string, such as
+ * rounding mode, are not modified.
+ *
+ * @param pattern The pattern string, like "#,##0.00"
+ * @param properties The property bag object to overwrite.
+ * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
+ * increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
+ * as CurrencyUsage, is to be used instead.
+ * @throws IllegalArgumentException If there was a syntax error in the pattern string.
+ */
+ public static void parseToExistingProperties(
+ String pattern, Properties properties, boolean ignoreRounding) {
+ LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
+ }
+
+ public static void parseToExistingProperties(String pattern, Properties properties) {
+ parseToExistingProperties(pattern, properties, false);
+ }
+
+ /**
+ * Creates a pattern string from a property bag.
+ *
+ * <p>Since pattern strings support only a subset of the functionality available in a property
+ * bag, a new property bag created from the string returned by this function may not be the same
+ * as the original property bag.
+ *
+ * @param properties The property bag to serialize.
+ * @return A pattern string approximately serializing the property bag.
+ */
+ public static String propertiesToString(Properties properties) {
+ StringBuilder sb = new StringBuilder();
+
+ // Convenience references
+ // The Math.min() calls prevent DoS
+ int dosMax = 100;
+ int groupingSize = Math.min(properties.getSecondaryGroupingSize(), dosMax);
+ int firstGroupingSize = Math.min(properties.getGroupingSize(), dosMax);
+ int paddingWidth = Math.min(properties.getFormatWidth(), dosMax);
+ PadPosition paddingLocation = properties.getPadPosition();
+ String paddingString = properties.getPadString();
+ int minInt = Math.max(Math.min(properties.getMinimumIntegerDigits(), dosMax), 0);
+ int maxInt = Math.min(properties.getMaximumIntegerDigits(), dosMax);
+ int minFrac = Math.max(Math.min(properties.getMinimumFractionDigits(), dosMax), 0);
+ int maxFrac = Math.min(properties.getMaximumFractionDigits(), dosMax);
+ int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax);
+ int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax);
+ boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+ int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax);
+ boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown();
+ String pp = properties.getPositivePrefix();
+ String ppp = properties.getPositivePrefixPattern();
+ String ps = properties.getPositiveSuffix();
+ String psp = properties.getPositiveSuffixPattern();
+ String np = properties.getNegativePrefix();
+ String npp = properties.getNegativePrefixPattern();
+ String ns = properties.getNegativeSuffix();
+ String nsp = properties.getNegativeSuffixPattern();
+
+ // Prefixes
+ if (ppp != null) sb.append(ppp);
+ AffixPatternUtils.escape(pp, sb);
+ int afterPrefixPos = sb.length();
+
+ // Figure out the grouping sizes.
+ int grouping1, grouping2, grouping;
+ if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GROUPING_SIZE)
+ && firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZE)
+ && groupingSize != firstGroupingSize) {
+ grouping = groupingSize;
+ grouping1 = groupingSize;
+ grouping2 = firstGroupingSize;
+ } else if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GROUPING_SIZE)) {
+ grouping = groupingSize;
+ grouping1 = 0;
+ grouping2 = groupingSize;
+ } else if (firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZE)) {
+ grouping = groupingSize;
+ grouping1 = 0;
+ grouping2 = firstGroupingSize;
+ } else {
+ grouping = 0;
+ grouping1 = 0;
+ grouping2 = 0;
+ }
+ int groupingLength = grouping1 + grouping2 + 1;
+
+ // Figure out the digits we need to put in the pattern.
+ BigDecimal roundingInterval = properties.getRoundingIncrement();
+ StringBuilder digitsString = new StringBuilder();
+ int digitsStringScale = 0;
+ if (maxSig != Math.min(dosMax, Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS)) {
+ // Significant Digits.
+ while (digitsString.length() < minSig) {
+ digitsString.append('@');
+ }
+ while (digitsString.length() < maxSig) {
+ digitsString.append('#');
+ }
+ } else if (roundingInterval != Properties.DEFAULT_ROUNDING_INCREMENT) {
+ // Rounding Interval.
+ digitsStringScale = -roundingInterval.scale();
+ // TODO: Check for DoS here?
+ String str = roundingInterval.scaleByPowerOfTen(roundingInterval.scale()).toPlainString();
+ if (str.charAt(0) == '\'') {
+ // TODO: Unsupported operation exception or fail silently?
+ digitsString.append(str, 1, str.length());
+ } else {
+ digitsString.append(str);
+ }
+ }
+ while (digitsString.length() + digitsStringScale < minInt) {
+ digitsString.insert(0, '0');
+ }
+ while (-digitsStringScale < minFrac) {
+ digitsString.append('0');
+ digitsStringScale--;
+ }
+
+ // Write the digits to the string builder
+ int m0 = Math.max(groupingLength, digitsString.length() + digitsStringScale);
+ m0 = (maxInt != dosMax) ? Math.max(maxInt, m0) - 1 : m0 - 1;
+ int mN = (maxFrac != dosMax) ? Math.min(-maxFrac, digitsStringScale) : digitsStringScale;
+ for (int magnitude = m0; magnitude >= mN; magnitude--) {
+ int di = digitsString.length() + digitsStringScale - magnitude - 1;
+ if (di < 0 || di >= digitsString.length()) {
+ sb.append('#');
+ } else {
+ sb.append(digitsString.charAt(di));
+ }
+ if (magnitude > grouping2 && grouping > 0 && (magnitude - grouping2) % grouping == 0) {
+ sb.append(',');
+ } else if (magnitude > 0 && magnitude == grouping2) {
+ sb.append(',');
+ } else if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
+ sb.append('.');
+ }
+ }
+
+ // Exponential notation
+ if (exponentDigits != Math.min(dosMax, Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS)) {
+ sb.append('E');
+ if (exponentShowPlusSign) {
+ sb.append('+');
+ }
+ for (int i = 0; i < exponentDigits; i++) {
+ sb.append('0');
+ }
+ }
+
+ // Suffixes
+ int beforeSuffixPos = sb.length();
+ if (psp != null) sb.append(psp);
+ AffixPatternUtils.escape(ps, sb);
+
+ // Resolve Padding
+ if (paddingWidth != Properties.DEFAULT_FORMAT_WIDTH) {
+ while (paddingWidth - sb.length() > 0) {
+ sb.insert(afterPrefixPos, '#');
+ beforeSuffixPos++;
+ }
+ int addedLength;
+ switch (paddingLocation) {
+ case BEFORE_PREFIX:
+ addedLength = escapePaddingString(paddingString, sb, 0);
+ sb.insert(0, '*');
+ afterPrefixPos += addedLength + 1;
+ beforeSuffixPos += addedLength + 1;
+ break;
+ case AFTER_PREFIX:
+ addedLength = escapePaddingString(paddingString, sb, afterPrefixPos);
+ sb.insert(afterPrefixPos, '*');
+ afterPrefixPos += addedLength + 1;
+ beforeSuffixPos += addedLength + 1;
+ break;
+ case BEFORE_SUFFIX:
+ escapePaddingString(paddingString, sb, beforeSuffixPos);
+ sb.insert(beforeSuffixPos, '*');
+ break;
+ case AFTER_SUFFIX:
+ sb.append('*');
+ escapePaddingString(paddingString, sb, sb.length());
+ break;
+ }
+ }
+
+ // Negative affixes
+ // Ignore if the negative prefix pattern is "-" and the negative suffix is empty
+ if (np != null
+ || ns != null
+ || (npp == null && nsp != null)
+ || (npp != null && (npp.length() != 1 || npp.charAt(0) != '-' || nsp.length() != 0))) {
+ sb.append(';');
+ if (npp != null) sb.append(npp);
+ AffixPatternUtils.escape(np, sb);
+ // Copy the positive digit format into the negative.
+ // This is optional; the pattern is the same as if '#' were appended here instead.
+ sb.append(sb, afterPrefixPos, beforeSuffixPos);
+ if (nsp != null) sb.append(nsp);
+ AffixPatternUtils.escape(ns, sb);
+ }
+
+ return sb.toString();
+ }
+
+ /** @return The number of chars inserted. */
+ private static int escapePaddingString(CharSequence input, StringBuilder output, int startIndex) {
+ if (input == null || input.length() == 0) input = PaddingFormat.FALLBACK_PADDING_STRING;
+ int startLength = output.length();
+ if (input.length() == 1) {
+ if (input.equals("'")) {
+ output.insert(startIndex, "''");
+ } else {
+ output.insert(startIndex, input);
+ }
+ } else {
+ output.insert(startIndex, '\'');
+ int offset = 1;
+ for (int i = 0; i < input.length(); i++) {
+ // it's okay to deal in chars here because the quote mark is the only interesting thing.
+ char ch = input.charAt(i);
+ if (ch == '\'') {
+ output.insert(startIndex + offset, "''");
+ offset += 2;
+ } else {
+ output.insert(startIndex + offset, ch);
+ offset += 1;
+ }
+ }
+ output.insert(startIndex + offset, '\'');
+ }
+ return output.length() - startLength;
+ }
+
+ /**
+ * Converts a pattern between standard notation and localized notation. Localized notation means
+ * that instead of using generic placeholders in the pattern, you use the corresponding
+ * locale-specific characters instead. For example, in locale <em>fr-FR</em>, the period in the
+ * pattern "0.000" means "decimal" in standard notation (as it does in every other locale), but it
+ * means "grouping" in localized notation.
+ *
+ * @param input The pattern to convert.
+ * @param symbols The symbols corresponding to the localized pattern.
+ * @param toLocalized true to convert from standard to localized notation; false to convert from
+ * localized to standard notation.
+ * @return The pattern expressed in the other notation.
+ * @deprecated ICU 59 This method is provided for backwards compatibility and should not be used
+ * in any new code.
+ */
+ @Deprecated
+ public static String convertLocalized(
+ CharSequence input, DecimalFormatSymbols symbols, boolean toLocalized) {
+ if (input == null) return null;
+
+ /// This is not the prettiest function in the world, but it gets the job done. ///
+
+ // Construct a table of code points to be converted between localized and standard.
+ int[][] table = new int[6][2];
+ int standIdx = toLocalized ? 0 : 1;
+ int localIdx = toLocalized ? 1 : 0;
+ table[0][standIdx] = '%';
+ table[0][localIdx] = symbols.getPercent();
+ table[1][standIdx] = '‰';
+ table[1][localIdx] = symbols.getPerMill();
+ table[2][standIdx] = '.';
+ table[2][localIdx] = symbols.getDecimalSeparator();
+ table[3][standIdx] = ',';
+ table[3][localIdx] = symbols.getGroupingSeparator();
+ table[4][standIdx] = '-';
+ table[4][localIdx] = symbols.getMinusSign();
+ table[5][standIdx] = '+';
+ table[5][localIdx] = symbols.getPlusSign();
+
+ // Special case: localIdx characters are NOT allowed to be quotes, like in de_CH.
+ // Use '’' instead.
+ for (int i = 0; i < table.length; i++) {
+ if (table[i][localIdx] == '\'') {
+ table[i][localIdx] = '’';
+ }
+ }
+
+ // Iterate through the string and convert
+ int offset = 0;
+ int state = 0;
+ StringBuilder result = new StringBuilder();
+ for (; offset < input.length(); ) {
+ int cp = Character.codePointAt(input, offset);
+ int cpToAppend = cp;
+
+ if (state == 1 || state == 3 || state == 4) {
+ // Inside user-specified quote
+ if (cp == '\'') {
+ if (state == 1) {
+ state = 0;
+ } else if (state == 3) {
+ state = 2;
+ cpToAppend = -1;
+ } else {
+ state = 2;
+ }
+ }
+ } else {
+ // Base state or inside special character quote
+ if (cp == '\'') {
+ if (state == 2 && offset + 1 < input.length()) {
+ int nextCp = Character.codePointAt(input, offset + 1);
+ if (nextCp == '\'') {
+ // escaped quote
+ state = 4;
+ } else {
+ // begin user-specified quote sequence
+ // we are already in a quote sequence, so omit the opening quote
+ state = 3;
+ cpToAppend = -1;
+ }
+ } else {
+ state = 1;
+ }
+ } else {
+ boolean needsSpecialQuote = false;
+ for (int i = 0; i < table.length; i++) {
+ if (table[i][0] == cp) {
+ cpToAppend = table[i][1];
+ needsSpecialQuote = false; // in case an earlier translation triggered it
+ break;
+ } else if (table[i][1] == cp) {
+ needsSpecialQuote = true;
+ }
+ }
+ if (state == 0 && needsSpecialQuote) {
+ state = 2;
+ result.appendCodePoint('\'');
+ } else if (state == 2 && !needsSpecialQuote) {
+ state = 0;
+ result.appendCodePoint('\'');
+ }
+ }
+ }
+ if (cpToAppend != -1) {
+ result.appendCodePoint(cpToAppend);
+ }
+ offset += Character.charCount(cp);
+ }
+ if (state == 2) {
+ result.appendCodePoint('\'');
+ }
+ return result.toString();
+ }
+
+ /** Implements a recursive descent parser for decimal format patterns. */
+ static class LdmlDecimalPatternParser {
+
+ /**
+ * An internal, intermediate data structure used for storing parse results before they are
+ * finalized into a DecimalFormatPattern.Builder.
+ */
+ private static class PatternParseResult {
+ SubpatternParseResult positive = new SubpatternParseResult();
+ SubpatternParseResult negative = null;
+
+ /** Finalizes the temporary data stored in the PatternParseResult to the Builder. */
+ void saveToProperties(Properties properties, boolean ignoreRounding) {
+ // Translate from PatternState to Properties.
+ // Note that most data from "negative" is ignored per the specification of DecimalFormat.
+
+ // Grouping settings
+ if (positive.groupingSizes[1] != -1) {
+ properties.setGroupingSize(positive.groupingSizes[0]);
+ } else {
+ properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE);
+ }
+ if (positive.groupingSizes[2] != -1) {
+ properties.setSecondaryGroupingSize(positive.groupingSizes[1]);
+ } else {
+ properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE);
+ }
+
+ // For backwards compatibility, require that the pattern emit at least one min digit.
+ int minInt, minFrac;
+ if (positive.totalIntegerDigits == 0 && positive.maximumFractionDigits > 0) {
+ // patterns like ".##"
+ minInt = 0;
+ minFrac = Math.max(1, positive.minimumFractionDigits);
+ } else if (positive.minimumIntegerDigits == 0 && positive.minimumFractionDigits == 0) {
+ // patterns like "#.##"
+ minInt = 1;
+ minFrac = 0;
+ } else {
+ minInt = positive.minimumIntegerDigits;
+ minFrac = positive.minimumFractionDigits;
+ }
+
+ // Rounding settings
+ // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage
+ if (positive.minimumSignificantDigits > 0) {
+ properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+ properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+ properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+ properties.setMinimumSignificantDigits(positive.minimumSignificantDigits);
+ properties.setMaximumSignificantDigits(positive.maximumSignificantDigits);
+ } else if (!positive.rounding.isZero()) {
+ if (!ignoreRounding) {
+ properties.setMinimumFractionDigits(minFrac);
+ properties.setMaximumFractionDigits(positive.maximumFractionDigits);
+ properties.setRoundingIncrement(positive.rounding.toBigDecimal());
+ } else {
+ properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+ properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+ properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+ }
+ properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+ properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
+ } else {
+ if (!ignoreRounding) {
+ properties.setMinimumFractionDigits(minFrac);
+ properties.setMaximumFractionDigits(positive.maximumFractionDigits);
+ properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+ } else {
+ properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+ properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+ properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+ }
+ properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+ properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
+ }
+
+ // If the pattern ends with a '.' then force the decimal point.
+ if (positive.hasDecimal && positive.maximumFractionDigits == 0) {
+ properties.setDecimalSeparatorAlwaysShown(true);
+ } else {
+ properties.setDecimalSeparatorAlwaysShown(false);
+ }
+
+ // Scientific notation settings
+ if (positive.exponentDigits > 0) {
+ properties.setExponentSignAlwaysShown(positive.exponentShowPlusSign);
+ properties.setMinimumExponentDigits(positive.exponentDigits);
+ if (positive.minimumSignificantDigits == 0) {
+ // patterns without '@' can define max integer digits, used for engineering notation
+ properties.setMinimumIntegerDigits(positive.minimumIntegerDigits);
+ properties.setMaximumIntegerDigits(positive.totalIntegerDigits);
+ } else {
+ // patterns with '@' cannot define max integer digits
+ properties.setMinimumIntegerDigits(1);
+ properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
+ }
+ } else {
+ properties.setExponentSignAlwaysShown(Properties.DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN);
+ properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
+ properties.setMinimumIntegerDigits(minInt);
+ properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
+ }
+
+ // Padding settings
+ if (positive.padding.length() > 0) {
+ // The width of the positive prefix and suffix templates are included in the padding
+ int paddingWidth =
+ positive.paddingWidth
+ + AffixPatternUtils.unescapedLength(positive.prefix)
+ + AffixPatternUtils.unescapedLength(positive.suffix);
+ properties.setFormatWidth(paddingWidth);
+ if (positive.padding.length() == 1) {
+ properties.setPadString(positive.padding.toString());
+ } else if (positive.padding.length() == 2) {
+ if (positive.padding.charAt(0) == '\'') {
+ properties.setPadString("'");
+ } else {
+ properties.setPadString(positive.padding.toString());
+ }
+ } else {
+ properties.setPadString(
+ positive.padding.subSequence(1, positive.padding.length() - 1).toString());
+ }
+ assert positive.paddingLocation != null;
+ properties.setPadPosition(positive.paddingLocation);
+ } else {
+ properties.setFormatWidth(Properties.DEFAULT_FORMAT_WIDTH);
+ properties.setPadString(Properties.DEFAULT_PAD_STRING);
+ properties.setPadPosition(Properties.DEFAULT_PAD_POSITION);
+ }
+
+ // Set the affixes
+ // Always call the setter, even if the prefixes are empty, especially in the case of the
+ // negative prefix pattern, to prevent default values from overriding the pattern.
+ properties.setPositivePrefixPattern(positive.prefix.toString());
+ properties.setPositiveSuffixPattern(positive.suffix.toString());
+ if (negative != null) {
+ properties.setNegativePrefixPattern(negative.prefix.toString());
+ properties.setNegativeSuffixPattern(negative.suffix.toString());
+ } else {
+ properties.setNegativePrefixPattern(null);
+ properties.setNegativeSuffixPattern(null);
+ }
+
+ // Set the magnitude multiplier
+ if (positive.hasPercentSign) {
+ properties.setMagnitudeMultiplier(2);
+ } else if (positive.hasPerMilleSign) {
+ properties.setMagnitudeMultiplier(3);
+ } else {
+ properties.setMagnitudeMultiplier(Properties.DEFAULT_MAGNITUDE_MULTIPLIER);
+ }
+ }
+ }
+
+ private static class SubpatternParseResult {
+ int[] groupingSizes = new int[] {0, -1, -1};
+ int minimumIntegerDigits = 0;
+ int totalIntegerDigits = 0;
+ int minimumFractionDigits = 0;
+ int maximumFractionDigits = 0;
+ int minimumSignificantDigits = 0;
+ int maximumSignificantDigits = 0;
+ boolean hasDecimal = false;
+ int paddingWidth = 0;
+ PadPosition paddingLocation = null;
+ FormatQuantity4 rounding = new FormatQuantity4();
+ boolean exponentShowPlusSign = false;
+ int exponentDigits = 0;
+ boolean hasPercentSign = false;
+ boolean hasPerMilleSign = false;
+ boolean hasCurrencySign = false;
+
+ StringBuilder padding = new StringBuilder();
+ StringBuilder prefix = new StringBuilder();
+ StringBuilder suffix = new StringBuilder();
+ }
+
+ /** An internal class used for tracking the cursor during parsing of a pattern string. */
+ private static class ParserState {
+ final String pattern;
+ int offset;
+
+ ParserState(String pattern) {
+ this.pattern = pattern;
+ this.offset = 0;
+ }
+
+ int peek() {
+ if (offset == pattern.length()) {
+ return -1;
+ } else {
+ return pattern.codePointAt(offset);
+ }
+ }
+
+ int next() {
+ int codePoint = peek();
+ offset += Character.charCount(codePoint);
+ return codePoint;
+ }
+
+ IllegalArgumentException toParseException(String message) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Unexpected character in decimal format pattern: '");
+ sb.append(pattern);
+ sb.append("': ");
+ sb.append(message);
+ sb.append(": ");
+ if (peek() == -1) {
+ sb.append("EOL");
+ } else {
+ sb.append("'");
+ sb.append(Character.toChars(peek()));
+ sb.append("'");
+ }
+ return new IllegalArgumentException(sb.toString());
+ }
+ }
+
+ static void parse(String pattern, Properties properties, boolean ignoreRounding) {
+ if (pattern == null || pattern.length() == 0) {
+ // Backwards compatibility requires that we reset to the default values.
+ // TODO: Only overwrite the properties that "saveToProperties" normally touches?
+ properties.clear();
+ return;
+ }
+
+ // TODO: Use whitespace characters from PatternProps
+ // TODO: Use thread locals here.
+ ParserState state = new ParserState(pattern);
+ PatternParseResult result = new PatternParseResult();
+ consumePattern(state, result);
+ result.saveToProperties(properties, ignoreRounding);
+ }
+
+ private static void consumePattern(ParserState state, PatternParseResult result) {
+ // pattern := subpattern (';' subpattern)?
+ consumeSubpattern(state, result.positive);
+ if (state.peek() == ';') {
+ state.next(); // consume the ';'
+ result.negative = new SubpatternParseResult();
+ consumeSubpattern(state, result.negative);
+ }
+ if (state.peek() != -1) {
+ throw state.toParseException("pattern");
+ }
+ }
+
+ private static void consumeSubpattern(ParserState state, SubpatternParseResult result) {
+ // subpattern := literals? number exponent? literals?
+ consumePadding(state, result, PadPosition.BEFORE_PREFIX);
+ consumeAffix(state, result, result.prefix);
+ consumePadding(state, result, PadPosition.AFTER_PREFIX);
+ consumeFormat(state, result);
+ consumeExponent(state, result);
+ consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
+ consumeAffix(state, result, result.suffix);
+ consumePadding(state, result, PadPosition.AFTER_SUFFIX);
+ }
+
+ private static void consumePadding(
+ ParserState state, SubpatternParseResult result, PadPosition paddingLocation) {
+ if (state.peek() != '*') {
+ return;
+ }
+ result.paddingLocation = paddingLocation;
+ state.next(); // consume the '*'
+ consumeLiteral(state, result.padding);
+ }
+
+ private static void consumeAffix(
+ ParserState state, SubpatternParseResult result, StringBuilder destination) {
+ // literals := { literal }
+ while (true) {
+ switch (state.peek()) {
+ case '#':
+ case '@':
+ case ';':
+ case '*':
+ case '.':
+ case ',':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case -1:
+ // Characters that cannot appear unquoted in a literal
+ return;
+
+ case '%':
+ result.hasPercentSign = true;
+ break;
+
+ case '‰':
+ result.hasPerMilleSign = true;
+ break;
+
+ case '¤':
+ result.hasCurrencySign = true;
+ break;
+ }
+ consumeLiteral(state, destination);
+ }
+ }
+
+ private static void consumeLiteral(ParserState state, StringBuilder destination) {
+ if (state.peek() == -1) {
+ throw state.toParseException("expected unquoted literal but found end of string");
+ } else if (state.peek() == '\'') {
+ destination.appendCodePoint(state.next()); // consume the starting quote
+ while (state.peek() != '\'') {
+ if (state.peek() == -1) {
+ throw state.toParseException("expected quoted literal but found end of string");
+ } else {
+ destination.appendCodePoint(state.next()); // consume a quoted character
+ }
+ }
+ destination.appendCodePoint(state.next()); // consume the ending quote
+ } else {
+ // consume a non-quoted literal character
+ destination.appendCodePoint(state.next());
+ }
+ }
+
+ private static void consumeFormat(ParserState state, SubpatternParseResult result) {
+ consumeIntegerFormat(state, result);
+ if (state.peek() == '.') {
+ state.next(); // consume the decimal point
+ result.hasDecimal = true;
+ result.paddingWidth += 1;
+ consumeFractionFormat(state, result);
+ }
+ }
+
+ private static void consumeIntegerFormat(ParserState state, SubpatternParseResult result) {
+ boolean seenSignificantDigitMarker = false;
+ boolean seenDigit = false;
+
+ while (true) {
+ switch (state.peek()) {
+ case ',':
+ result.paddingWidth += 1;
+ result.groupingSizes[2] = result.groupingSizes[1];
+ result.groupingSizes[1] = result.groupingSizes[0];
+ result.groupingSizes[0] = 0;
+ break;
+
+ case '#':
+ if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
+ result.paddingWidth += 1;
+ result.groupingSizes[0] += 1;
+ result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
+ // no change to result.minimumIntegerDigits
+ // no change to result.minimumSignificantDigits
+ result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
+ result.rounding.appendDigit((byte) 0, 0, true);
+ break;
+
+ case '@':
+ seenSignificantDigitMarker = true;
+ if (seenDigit) throw state.toParseException("Can't mix @ and 0 in pattern");
+ result.paddingWidth += 1;
+ result.groupingSizes[0] += 1;
+ result.totalIntegerDigits += 1;
+ // no change to result.minimumIntegerDigits
+ result.minimumSignificantDigits += 1;
+ result.maximumSignificantDigits += 1;
+ result.rounding.appendDigit((byte) 0, 0, true);
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ seenDigit = true;
+ if (seenSignificantDigitMarker)
+ throw state.toParseException("Can't mix @ and 0 in pattern");
+ // TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
+ result.paddingWidth += 1;
+ result.groupingSizes[0] += 1;
+ result.totalIntegerDigits += 1;
+ result.minimumIntegerDigits += 1;
+ // no change to result.minimumSignificantDigits
+ result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
+ result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
+ break;
+
+ default:
+ return;
+ }
+ state.next(); // consume the symbol
+ }
+ }
+
+ private static void consumeFractionFormat(ParserState state, SubpatternParseResult result) {
+ int zeroCounter = 0;
+ boolean seenHash = false;
+ while (true) {
+ switch (state.peek()) {
+ case '#':
+ seenHash = true;
+ result.paddingWidth += 1;
+ // no change to result.minimumFractionDigits
+ result.maximumFractionDigits += 1;
+ zeroCounter++;
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ if (seenHash) throw state.toParseException("0 cannot follow # after decimal point");
+ result.paddingWidth += 1;
+ result.minimumFractionDigits += 1;
+ result.maximumFractionDigits += 1;
+ if (state.peek() == '0') {
+ zeroCounter++;
+ } else {
+ result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
+ zeroCounter = 0;
+ }
+ break;
+
+ default:
+ return;
+ }
+ state.next(); // consume the symbol
+ }
+ }
+
+ private static void consumeExponent(ParserState state, SubpatternParseResult result) {
+ if (state.peek() != 'E') {
+ return;
+ }
+ state.next(); // consume the E
+ result.paddingWidth++;
+ if (state.peek() == '+') {
+ state.next(); // consume the +
+ result.exponentShowPlusSign = true;
+ result.paddingWidth++;
+ }
+ while (state.peek() == '0') {
+ state.next(); // consume the 0
+ result.exponentDigits += 1;
+ result.paddingWidth++;
+ }
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+
+import com.ibm.icu.impl.number.Parse.ParseMode;
+import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier;
+import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle;
+import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier;
+import com.ibm.icu.impl.number.formatters.MeasureFormat;
+import com.ibm.icu.impl.number.formatters.PaddingFormat;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import com.ibm.icu.impl.number.formatters.ScientificFormat;
+import com.ibm.icu.impl.number.rounders.IncrementRounder;
+import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.MeasureUnit;
+
+public class Properties
+ implements Cloneable,
+ Serializable,
+ PositiveDecimalFormat.IProperties,
+ PositiveNegativeAffixFormat.IProperties,
+ MagnitudeMultiplier.IProperties,
+ ScientificFormat.IProperties,
+ MeasureFormat.IProperties,
+ CompactDecimalFormat.IProperties,
+ PaddingFormat.IProperties,
+ BigDecimalMultiplier.IProperties,
+ CurrencyFormat.IProperties,
+ Parse.IProperties,
+ IncrementRounder.IProperties,
+ MagnitudeRounder.IProperties,
+ SignificantDigitsRounder.IProperties {
+
+ private static final Properties DEFAULT = new Properties();
+
+ /** Auto-generated. */
+ private static final long serialVersionUID = 4095518955889349243L;
+
+ // The setters in this class should NOT have any side-effects or perform any validation. It is
+ // up to the consumer of the property bag to deal with property validation.
+
+ // The fields are all marked "transient" because custom serialization is being used.
+
+ /*--------------------------------------------------------------------------------------------+/
+ /| IMPORTANT! |/
+ /| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(), |/
+ /| and in #_hashCode(). |/
+ /| |/
+ /| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(), |/
+ /| or #equals(), but it will NOT catch if you forget to add it to #hashCode(). |/
+ /+--------------------------------------------------------------------------------------------*/
+
+ private transient CompactStyle compactStyle;
+ private transient Currency currency;
+ private transient CurrencyPluralInfo currencyPluralInfo;
+ private transient CurrencyStyle currencyStyle;
+ private transient CurrencyUsage currencyUsage;
+ private transient boolean decimalPatternMatchRequired;
+ private transient boolean decimalSeparatorAlwaysShown;
+ private transient boolean exponentSignAlwaysShown;
+ private transient int formatWidth;
+ private transient int groupingSize;
+ private transient int magnitudeMultiplier;
+ private transient MathContext mathContext;
+ private transient int maximumFractionDigits;
+ private transient int maximumIntegerDigits;
+ private transient int maximumSignificantDigits;
+ private transient FormatWidth measureFormatWidth;
+ private transient MeasureUnit measureUnit;
+ private transient int minimumExponentDigits;
+ private transient int minimumFractionDigits;
+ private transient int minimumGroupingDigits;
+ private transient int minimumIntegerDigits;
+ private transient int minimumSignificantDigits;
+ private transient BigDecimal multiplier;
+ private transient String negativePrefix;
+ private transient String negativePrefixPattern;
+ private transient String negativeSuffix;
+ private transient String negativeSuffixPattern;
+ private transient PadPosition padPosition;
+ private transient String padString;
+ private transient boolean parseCaseSensitive;
+ private transient boolean parseNoExponent;
+ private transient boolean parseIntegerOnly;
+ private transient ParseMode parseMode;
+ private transient boolean parseToBigDecimal;
+ private transient boolean plusSignAlwaysShown;
+ private transient String positivePrefix;
+ private transient String positivePrefixPattern;
+ private transient String positiveSuffix;
+ private transient String positiveSuffixPattern;
+ private transient BigDecimal roundingIncrement;
+ private transient RoundingMode roundingMode;
+ private transient int secondaryGroupingSize;
+ private transient SignificantDigitsMode significantDigitsMode;
+
+ /*--------------------------------------------------------------------------------------------+/
+ /| IMPORTANT! |/
+ /| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(), |/
+ /| and in #_hashCode(). |/
+ /| |/
+ /| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(), |/
+ /| or #equals(), but it will NOT catch if you forget to add it to #hashCode(). |/
+ /+--------------------------------------------------------------------------------------------*/
+
+ public Properties() {
+ clear();
+ }
+
+ private Properties _clear() {
+ compactStyle = DEFAULT_COMPACT_STYLE;
+ currency = DEFAULT_CURRENCY;
+ currencyPluralInfo = DEFAULT_CURRENCY_PLURAL_INFO;
+ currencyStyle = DEFAULT_CURRENCY_STYLE;
+ currencyUsage = DEFAULT_CURRENCY_USAGE;
+ decimalPatternMatchRequired = DEFAULT_DECIMAL_PATTERN_MATCH_REQUIRED;
+ decimalSeparatorAlwaysShown = DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN;
+ exponentSignAlwaysShown = DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN;
+ formatWidth = DEFAULT_FORMAT_WIDTH;
+ groupingSize = DEFAULT_GROUPING_SIZE;
+ magnitudeMultiplier = DEFAULT_MAGNITUDE_MULTIPLIER;
+ mathContext = DEFAULT_MATH_CONTEXT;
+ maximumFractionDigits = DEFAULT_MAXIMUM_FRACTION_DIGITS;
+ maximumIntegerDigits = DEFAULT_MAXIMUM_INTEGER_DIGITS;
+ maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS;
+ measureFormatWidth = DEFAULT_MEASURE_FORMAT_WIDTH;
+ measureUnit = DEFAULT_MEASURE_UNIT;
+ minimumExponentDigits = DEFAULT_MINIMUM_EXPONENT_DIGITS;
+ minimumFractionDigits = DEFAULT_MINIMUM_FRACTION_DIGITS;
+ minimumGroupingDigits = DEFAULT_MINIMUM_GROUPING_DIGITS;
+ minimumIntegerDigits = DEFAULT_MINIMUM_INTEGER_DIGITS;
+ minimumSignificantDigits = DEFAULT_MINIMUM_SIGNIFICANT_DIGITS;
+ multiplier = DEFAULT_MULTIPLIER;
+ negativePrefix = DEFAULT_NEGATIVE_PREFIX;
+ negativePrefixPattern = DEFAULT_NEGATIVE_PREFIX_PATTERN;
+ negativeSuffix = DEFAULT_NEGATIVE_SUFFIX;
+ negativeSuffixPattern = DEFAULT_NEGATIVE_SUFFIX_PATTERN;
+ padPosition = DEFAULT_PAD_POSITION;
+ padString = DEFAULT_PAD_STRING;
+ parseCaseSensitive = DEFAULT_PARSE_CASE_SENSITIVE;
+ parseIntegerOnly = DEFAULT_PARSE_INTEGER_ONLY;
+ parseMode = DEFAULT_PARSE_MODE;
+ parseNoExponent = DEFAULT_PARSE_NO_EXPONENT;
+ parseToBigDecimal = DEFAULT_PARSE_TO_BIG_DECIMAL;
+ plusSignAlwaysShown = DEFAULT_PLUS_SIGN_ALWAYS_SHOWN;
+ positivePrefix = DEFAULT_POSITIVE_PREFIX;
+ positivePrefixPattern = DEFAULT_POSITIVE_PREFIX_PATTERN;
+ positiveSuffix = DEFAULT_POSITIVE_SUFFIX;
+ positiveSuffixPattern = DEFAULT_POSITIVE_SUFFIX_PATTERN;
+ roundingIncrement = DEFAULT_ROUNDING_INCREMENT;
+ roundingMode = DEFAULT_ROUNDING_MODE;
+ secondaryGroupingSize = DEFAULT_SECONDARY_GROUPING_SIZE;
+ significantDigitsMode = DEFAULT_SIGNIFICANT_DIGITS_MODE;
+ return this;
+ }
+
+ private Properties _copyFrom(Properties other) {
+ compactStyle = other.compactStyle;
+ currency = other.currency;
+ currencyPluralInfo = other.currencyPluralInfo;
+ currencyStyle = other.currencyStyle;
+ currencyUsage = other.currencyUsage;
+ decimalPatternMatchRequired = other.decimalPatternMatchRequired;
+ decimalSeparatorAlwaysShown = other.decimalSeparatorAlwaysShown;
+ exponentSignAlwaysShown = other.exponentSignAlwaysShown;
+ formatWidth = other.formatWidth;
+ groupingSize = other.groupingSize;
+ magnitudeMultiplier = other.magnitudeMultiplier;
+ mathContext = other.mathContext;
+ maximumFractionDigits = other.maximumFractionDigits;
+ maximumIntegerDigits = other.maximumIntegerDigits;
+ maximumSignificantDigits = other.maximumSignificantDigits;
+ measureFormatWidth = other.measureFormatWidth;
+ measureUnit = other.measureUnit;
+ minimumExponentDigits = other.minimumExponentDigits;
+ minimumFractionDigits = other.minimumFractionDigits;
+ minimumGroupingDigits = other.minimumGroupingDigits;
+ minimumIntegerDigits = other.minimumIntegerDigits;
+ minimumSignificantDigits = other.minimumSignificantDigits;
+ multiplier = other.multiplier;
+ negativePrefix = other.negativePrefix;
+ negativePrefixPattern = other.negativePrefixPattern;
+ negativeSuffix = other.negativeSuffix;
+ negativeSuffixPattern = other.negativeSuffixPattern;
+ padPosition = other.padPosition;
+ padString = other.padString;
+ parseCaseSensitive = other.parseCaseSensitive;
+ parseIntegerOnly = other.parseIntegerOnly;
+ parseMode = other.parseMode;
+ parseNoExponent = other.parseNoExponent;
+ parseToBigDecimal = other.parseToBigDecimal;
+ plusSignAlwaysShown = other.plusSignAlwaysShown;
+ positivePrefix = other.positivePrefix;
+ positivePrefixPattern = other.positivePrefixPattern;
+ positiveSuffix = other.positiveSuffix;
+ positiveSuffixPattern = other.positiveSuffixPattern;
+ roundingIncrement = other.roundingIncrement;
+ roundingMode = other.roundingMode;
+ secondaryGroupingSize = other.secondaryGroupingSize;
+ significantDigitsMode = other.significantDigitsMode;
+ return this;
+ }
+
+ private boolean _equals(Properties other) {
+ boolean eq = true;
+ eq = eq && _equalsHelper(compactStyle, other.compactStyle);
+ eq = eq && _equalsHelper(currency, other.currency);
+ eq = eq && _equalsHelper(currencyPluralInfo, other.currencyPluralInfo);
+ eq = eq && _equalsHelper(currencyStyle, other.currencyStyle);
+ eq = eq && _equalsHelper(currencyUsage, other.currencyUsage);
+ eq = eq && _equalsHelper(decimalPatternMatchRequired, other.decimalPatternMatchRequired);
+ eq = eq && _equalsHelper(decimalSeparatorAlwaysShown, other.decimalSeparatorAlwaysShown);
+ eq = eq && _equalsHelper(exponentSignAlwaysShown, other.exponentSignAlwaysShown);
+ eq = eq && _equalsHelper(formatWidth, other.formatWidth);
+ eq = eq && _equalsHelper(groupingSize, other.groupingSize);
+ eq = eq && _equalsHelper(magnitudeMultiplier, other.magnitudeMultiplier);
+ eq = eq && _equalsHelper(mathContext, other.mathContext);
+ eq = eq && _equalsHelper(maximumFractionDigits, other.maximumFractionDigits);
+ eq = eq && _equalsHelper(maximumIntegerDigits, other.maximumIntegerDigits);
+ eq = eq && _equalsHelper(maximumSignificantDigits, other.maximumSignificantDigits);
+ eq = eq && _equalsHelper(measureFormatWidth, other.measureFormatWidth);
+ eq = eq && _equalsHelper(measureUnit, other.measureUnit);
+ eq = eq && _equalsHelper(minimumExponentDigits, other.minimumExponentDigits);
+ eq = eq && _equalsHelper(minimumFractionDigits, other.minimumFractionDigits);
+ eq = eq && _equalsHelper(minimumGroupingDigits, other.minimumGroupingDigits);
+ eq = eq && _equalsHelper(minimumIntegerDigits, other.minimumIntegerDigits);
+ eq = eq && _equalsHelper(minimumSignificantDigits, other.minimumSignificantDigits);
+ eq = eq && _equalsHelper(multiplier, other.multiplier);
+ eq = eq && _equalsHelper(negativePrefix, other.negativePrefix);
+ eq = eq && _equalsHelper(negativePrefixPattern, other.negativePrefixPattern);
+ eq = eq && _equalsHelper(negativeSuffix, other.negativeSuffix);
+ eq = eq && _equalsHelper(negativeSuffixPattern, other.negativeSuffixPattern);
+ eq = eq && _equalsHelper(padPosition, other.padPosition);
+ eq = eq && _equalsHelper(padString, other.padString);
+ eq = eq && _equalsHelper(parseCaseSensitive, other.parseCaseSensitive);
+ eq = eq && _equalsHelper(parseIntegerOnly, other.parseIntegerOnly);
+ eq = eq && _equalsHelper(parseMode, other.parseMode);
+ eq = eq && _equalsHelper(parseNoExponent, other.parseNoExponent);
+ eq = eq && _equalsHelper(parseToBigDecimal, other.parseToBigDecimal);
+ eq = eq && _equalsHelper(plusSignAlwaysShown, other.plusSignAlwaysShown);
+ eq = eq && _equalsHelper(positivePrefix, other.positivePrefix);
+ eq = eq && _equalsHelper(positivePrefixPattern, other.positivePrefixPattern);
+ eq = eq && _equalsHelper(positiveSuffix, other.positiveSuffix);
+ eq = eq && _equalsHelper(positiveSuffixPattern, other.positiveSuffixPattern);
+ eq = eq && _equalsHelper(roundingIncrement, other.roundingIncrement);
+ eq = eq && _equalsHelper(roundingMode, other.roundingMode);
+ eq = eq && _equalsHelper(secondaryGroupingSize, other.secondaryGroupingSize);
+ eq = eq && _equalsHelper(significantDigitsMode, other.significantDigitsMode);
+ return eq;
+ }
+
+ private boolean _equalsHelper(boolean mine, boolean theirs) {
+ return mine == theirs;
+ }
+
+ private boolean _equalsHelper(int mine, int theirs) {
+ return mine == theirs;
+ }
+
+ private boolean _equalsHelper(Object mine, Object theirs) {
+ if (mine == theirs) return true;
+ if (mine == null) return false;
+ return mine.equals(theirs);
+ }
+
+ private int _hashCode() {
+ int hashCode = 0;
+ hashCode ^= _hashCodeHelper(compactStyle);
+ hashCode ^= _hashCodeHelper(currency);
+ hashCode ^= _hashCodeHelper(currencyPluralInfo);
+ hashCode ^= _hashCodeHelper(currencyStyle);
+ hashCode ^= _hashCodeHelper(currencyUsage);
+ hashCode ^= _hashCodeHelper(decimalPatternMatchRequired);
+ hashCode ^= _hashCodeHelper(decimalSeparatorAlwaysShown);
+ hashCode ^= _hashCodeHelper(exponentSignAlwaysShown);
+ hashCode ^= _hashCodeHelper(formatWidth);
+ hashCode ^= _hashCodeHelper(groupingSize);
+ hashCode ^= _hashCodeHelper(magnitudeMultiplier);
+ hashCode ^= _hashCodeHelper(mathContext);
+ hashCode ^= _hashCodeHelper(maximumFractionDigits);
+ hashCode ^= _hashCodeHelper(maximumIntegerDigits);
+ hashCode ^= _hashCodeHelper(maximumSignificantDigits);
+ hashCode ^= _hashCodeHelper(measureFormatWidth);
+ hashCode ^= _hashCodeHelper(measureUnit);
+ hashCode ^= _hashCodeHelper(minimumExponentDigits);
+ hashCode ^= _hashCodeHelper(minimumFractionDigits);
+ hashCode ^= _hashCodeHelper(minimumGroupingDigits);
+ hashCode ^= _hashCodeHelper(minimumIntegerDigits);
+ hashCode ^= _hashCodeHelper(minimumSignificantDigits);
+ hashCode ^= _hashCodeHelper(multiplier);
+ hashCode ^= _hashCodeHelper(negativePrefix);
+ hashCode ^= _hashCodeHelper(negativePrefixPattern);
+ hashCode ^= _hashCodeHelper(negativeSuffix);
+ hashCode ^= _hashCodeHelper(negativeSuffixPattern);
+ hashCode ^= _hashCodeHelper(padPosition);
+ hashCode ^= _hashCodeHelper(padString);
+ hashCode ^= _hashCodeHelper(parseCaseSensitive);
+ hashCode ^= _hashCodeHelper(parseIntegerOnly);
+ hashCode ^= _hashCodeHelper(parseMode);
+ hashCode ^= _hashCodeHelper(parseNoExponent);
+ hashCode ^= _hashCodeHelper(parseToBigDecimal);
+ hashCode ^= _hashCodeHelper(plusSignAlwaysShown);
+ hashCode ^= _hashCodeHelper(positivePrefix);
+ hashCode ^= _hashCodeHelper(positivePrefixPattern);
+ hashCode ^= _hashCodeHelper(positiveSuffix);
+ hashCode ^= _hashCodeHelper(positiveSuffixPattern);
+ hashCode ^= _hashCodeHelper(roundingIncrement);
+ hashCode ^= _hashCodeHelper(roundingMode);
+ hashCode ^= _hashCodeHelper(secondaryGroupingSize);
+ hashCode ^= _hashCodeHelper(significantDigitsMode);
+ return hashCode;
+ }
+
+ private int _hashCodeHelper(boolean value) {
+ return value ? 1 : 0;
+ }
+
+ private int _hashCodeHelper(int value) {
+ return value * 13;
+ }
+
+ private int _hashCodeHelper(Object value) {
+ if (value == null) return 0;
+ return value.hashCode();
+ }
+
+ public Properties clear() {
+ return _clear();
+ }
+
+ /** Creates and returns a shallow copy of the property bag. */
+ @Override
+ public Properties clone() {
+ // super.clone() returns a shallow copy.
+ try {
+ return (Properties) super.clone();
+ } catch (CloneNotSupportedException e) {
+ // Should never happen since super is Object
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ /**
+ * Shallow-copies the properties from the given property bag into this property bag.
+ *
+ * @param other The property bag from which to copy and which will not be modified.
+ * @return The current property bag (the one modified by this operation), for chaining.
+ */
+ public Properties copyFrom(Properties other) {
+ return _copyFrom(other);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) return false;
+ if (this == other) return true;
+ if (!(other instanceof Properties)) return false;
+ return _equals((Properties) other);
+ }
+
+ @Override
+ public CompactStyle getCompactStyle() {
+ return compactStyle;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ /// BEGIN GETTERS/SETTERS ///
+
+ @Override
+ @Deprecated
+ public CurrencyPluralInfo getCurrencyPluralInfo() {
+ return currencyPluralInfo;
+ }
+
+ @Override
+ public CurrencyStyle getCurrencyStyle() {
+ return currencyStyle;
+ }
+
+ @Override
+ public CurrencyUsage getCurrencyUsage() {
+ return currencyUsage;
+ }
+
+ @Override
+ public boolean getDecimalPatternMatchRequired() {
+ return decimalPatternMatchRequired;
+ }
+
+ @Override
+ public boolean getDecimalSeparatorAlwaysShown() {
+ return decimalSeparatorAlwaysShown;
+ }
+
+ @Override
+ public boolean getExponentSignAlwaysShown() {
+ return exponentSignAlwaysShown;
+ }
+
+ @Override
+ public int getFormatWidth() {
+ return formatWidth;
+ }
+
+ @Override
+ public int getGroupingSize() {
+ return groupingSize;
+ }
+
+ @Override
+ public int getMagnitudeMultiplier() {
+ return magnitudeMultiplier;
+ }
+
+ @Override
+ public MathContext getMathContext() {
+ return mathContext;
+ }
+
+ @Override
+ public int getMaximumFractionDigits() {
+ return maximumFractionDigits;
+ }
+
+ @Override
+ public int getMaximumIntegerDigits() {
+ return maximumIntegerDigits;
+ }
+
+ @Override
+ public int getMaximumSignificantDigits() {
+ return maximumSignificantDigits;
+ }
+
+ @Override
+ public FormatWidth getMeasureFormatWidth() {
+ return measureFormatWidth;
+ }
+
+ @Override
+ public MeasureUnit getMeasureUnit() {
+ return measureUnit;
+ }
+
+ @Override
+ public int getMinimumExponentDigits() {
+ return minimumExponentDigits;
+ }
+
+ @Override
+ public int getMinimumFractionDigits() {
+ return minimumFractionDigits;
+ }
+
+ @Override
+ public int getMinimumGroupingDigits() {
+ return minimumGroupingDigits;
+ }
+
+ @Override
+ public int getMinimumIntegerDigits() {
+ return minimumIntegerDigits;
+ }
+
+ @Override
+ public int getMinimumSignificantDigits() {
+ return minimumSignificantDigits;
+ }
+
+ @Override
+ public BigDecimal getMultiplier() {
+ return multiplier;
+ }
+
+ @Override
+ public String getNegativePrefix() {
+ return negativePrefix;
+ }
+
+ @Override
+ public String getNegativePrefixPattern() {
+ return negativePrefixPattern;
+ }
+
+ @Override
+ public String getNegativeSuffix() {
+ return negativeSuffix;
+ }
+
+ @Override
+ public String getNegativeSuffixPattern() {
+ return negativeSuffixPattern;
+ }
+
+ @Override
+ public PadPosition getPadPosition() {
+ return padPosition;
+ }
+
+ @Override
+ public String getPadString() {
+ return padString;
+ }
+
+ @Override
+ public boolean getParseCaseSensitive() {
+ return parseCaseSensitive;
+ }
+
+ @Override
+ public boolean getParseIntegerOnly() {
+ return parseIntegerOnly;
+ }
+
+ @Override
+ public ParseMode getParseMode() {
+ return parseMode;
+ }
+
+ @Override
+ public boolean getParseNoExponent() {
+ return parseNoExponent;
+ }
+
+ @Override
+ public boolean getParseToBigDecimal() {
+ return parseToBigDecimal;
+ }
+
+ @Override
+ public boolean getPlusSignAlwaysShown() {
+ return plusSignAlwaysShown;
+ }
+
+ @Override
+ public String getPositivePrefix() {
+ return positivePrefix;
+ }
+
+ @Override
+ public String getPositivePrefixPattern() {
+ return positivePrefixPattern;
+ }
+
+ @Override
+ public String getPositiveSuffix() {
+ return positiveSuffix;
+ }
+
+ @Override
+ public String getPositiveSuffixPattern() {
+ return positiveSuffixPattern;
+ }
+
+ @Override
+ public BigDecimal getRoundingIncrement() {
+ return roundingIncrement;
+ }
+
+ @Override
+ public RoundingMode getRoundingMode() {
+ return roundingMode;
+ }
+
+ @Override
+ public int getSecondaryGroupingSize() {
+ return secondaryGroupingSize;
+ }
+
+ @Override
+ public SignificantDigitsMode getSignificantDigitsMode() {
+ return significantDigitsMode;
+ }
+
+ @Override
+ public int hashCode() {
+ return _hashCode();
+ }
+
+ /** Custom serialization: re-create object from serialized properties. */
+ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+ ois.defaultReadObject();
+
+ // Initialize to empty
+ clear();
+
+ // Extra int for possible future use
+ ois.readInt();
+
+ // 1) How many fields were serialized?
+ int count = ois.readInt();
+
+ // 2) Read each field by its name and value
+ for (int i=0; i<count; i++) {
+ String name = (String) ois.readObject();
+ Object value = ois.readObject();
+
+ // Get the field reference
+ Field field = null;
+ try {
+ field = Properties.class.getDeclaredField(name);
+ } catch (NoSuchFieldException e) {
+ // The field name does not exist! Possibly corrupted serialization. Ignore this entry.
+ continue;
+ } catch (SecurityException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ }
+
+ // NOTE: If the type of a field were changed in the future, this would be the place to check:
+ // If the variable `value` is the old type, perform any conversions necessary.
+
+ // Save value into the field
+ try {
+ field.set(this, value);
+ } catch (IllegalArgumentException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ } catch (IllegalAccessException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ @Override
+ public Properties setCompactStyle(CompactStyle compactStyle) {
+ this.compactStyle = compactStyle;
+ return this;
+ }
+
+ @Override
+ public Properties setCurrency(Currency currency) {
+ this.currency = currency;
+ return this;
+ }
+
+ @Override
+ @Deprecated
+ public Properties setCurrencyPluralInfo(CurrencyPluralInfo currencyPluralInfo) {
+ // TODO: In order to maintain immutability, we have to perform a clone here.
+ // It would be better to just retire CurrencyPluralInfo entirely.
+ if (currencyPluralInfo != null) {
+ currencyPluralInfo = (CurrencyPluralInfo) currencyPluralInfo.clone();
+ }
+ this.currencyPluralInfo = currencyPluralInfo;
+ return this;
+ }
+
+ @Override
+ public Properties setCurrencyStyle(CurrencyStyle currencyStyle) {
+ this.currencyStyle = currencyStyle;
+ return this;
+ }
+
+ @Override
+ public Properties setCurrencyUsage(CurrencyUsage currencyUsage) {
+ this.currencyUsage = currencyUsage;
+ return this;
+ }
+
+ @Override
+ public Properties setDecimalPatternMatchRequired(boolean decimalPatternMatchRequired) {
+ this.decimalPatternMatchRequired = decimalPatternMatchRequired;
+ return this;
+ }
+
+ @Override
+ public Properties setDecimalSeparatorAlwaysShown(boolean alwaysShowDecimal) {
+ this.decimalSeparatorAlwaysShown = alwaysShowDecimal;
+ return this;
+ }
+
+ @Override
+ public Properties setExponentSignAlwaysShown(boolean exponentSignAlwaysShown) {
+ this.exponentSignAlwaysShown = exponentSignAlwaysShown;
+ return this;
+ }
+
+ @Override
+ public Properties setFormatWidth(int paddingWidth) {
+ this.formatWidth = paddingWidth;
+ return this;
+ }
+
+ @Override
+ public Properties setGroupingSize(int groupingSize) {
+ this.groupingSize = groupingSize;
+ return this;
+ }
+
+ @Override
+ public Properties setMagnitudeMultiplier(int magnitudeMultiplier) {
+ this.magnitudeMultiplier = magnitudeMultiplier;
+ return this;
+ }
+
+ @Override
+ public Properties setMathContext(MathContext mathContext) {
+ this.mathContext = mathContext;
+ return this;
+ }
+
+ @Override
+ public Properties setMaximumFractionDigits(int maximumFractionDigits) {
+ this.maximumFractionDigits = maximumFractionDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMaximumIntegerDigits(int maximumIntegerDigits) {
+ this.maximumIntegerDigits = maximumIntegerDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMaximumSignificantDigits(int maximumSignificantDigits) {
+ this.maximumSignificantDigits = maximumSignificantDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMeasureFormatWidth(FormatWidth measureFormatWidth) {
+ this.measureFormatWidth = measureFormatWidth;
+ return this;
+ }
+
+ @Override
+ public Properties setMeasureUnit(MeasureUnit measureUnit) {
+ this.measureUnit = measureUnit;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumExponentDigits(int exponentDigits) {
+ this.minimumExponentDigits = exponentDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumFractionDigits(int minimumFractionDigits) {
+ this.minimumFractionDigits = minimumFractionDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumGroupingDigits(int minimumGroupingDigits) {
+ this.minimumGroupingDigits = minimumGroupingDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumIntegerDigits(int minimumIntegerDigits) {
+ this.minimumIntegerDigits = minimumIntegerDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMinimumSignificantDigits(int minimumSignificantDigits) {
+ this.minimumSignificantDigits = minimumSignificantDigits;
+ return this;
+ }
+
+ @Override
+ public Properties setMultiplier(BigDecimal multiplier) {
+ this.multiplier = multiplier;
+ return this;
+ }
+
+ @Override
+ public Properties setNegativePrefix(String negativePrefix) {
+ this.negativePrefix = negativePrefix;
+ return this;
+ }
+
+ @Override
+ public Properties setNegativePrefixPattern(String negativePrefixPattern) {
+ this.negativePrefixPattern = negativePrefixPattern;
+ return this;
+ }
+
+ @Override
+ public Properties setNegativeSuffix(String negativeSuffix) {
+ this.negativeSuffix = negativeSuffix;
+ return this;
+ }
+
+ @Override
+ public Properties setNegativeSuffixPattern(String negativeSuffixPattern) {
+ this.negativeSuffixPattern = negativeSuffixPattern;
+ return this;
+ }
+
+ @Override
+ public Properties setPadPosition(PadPosition paddingLocation) {
+ this.padPosition = paddingLocation;
+ return this;
+ }
+
+ @Override
+ public Properties setPadString(String paddingString) {
+ this.padString = paddingString;
+ return this;
+ }
+
+ @Override
+ public Properties setParseCaseSensitive(boolean parseCaseSensitive) {
+ this.parseCaseSensitive = parseCaseSensitive;
+ return this;
+ }
+
+ @Override
+ public Properties setParseIntegerOnly(boolean parseIntegerOnly) {
+ this.parseIntegerOnly = parseIntegerOnly;
+ return this;
+ }
+
+ @Override
+ public Properties setParseMode(ParseMode parseMode) {
+ this.parseMode = parseMode;
+ return this;
+ }
+
+ @Override
+ public Properties setParseNoExponent(boolean parseNoExponent) {
+ this.parseNoExponent = parseNoExponent;
+ return this;
+ }
+
+ @Override
+ public Properties setParseToBigDecimal(boolean parseToBigDecimal) {
+ this.parseToBigDecimal = parseToBigDecimal;
+ return this;
+ }
+
+ @Override
+ public Properties setPlusSignAlwaysShown(boolean plusSignAlwaysShown) {
+ this.plusSignAlwaysShown = plusSignAlwaysShown;
+ return this;
+ }
+
+ @Override
+ public Properties setPositivePrefix(String positivePrefix) {
+ this.positivePrefix = positivePrefix;
+ return this;
+ }
+
+ @Override
+ public Properties setPositivePrefixPattern(String positivePrefixPattern) {
+ this.positivePrefixPattern = positivePrefixPattern;
+ return this;
+ }
+
+ @Override
+ public Properties setPositiveSuffix(String positiveSuffix) {
+ this.positiveSuffix = positiveSuffix;
+ return this;
+ }
+
+ @Override
+ public Properties setPositiveSuffixPattern(String positiveSuffixPattern) {
+ this.positiveSuffixPattern = positiveSuffixPattern;
+ return this;
+ }
+
+ @Override
+ public Properties setRoundingIncrement(BigDecimal roundingIncrement) {
+ this.roundingIncrement = roundingIncrement;
+ return this;
+ }
+
+ @Override
+ public Properties setRoundingMode(RoundingMode roundingMode) {
+ this.roundingMode = roundingMode;
+ return this;
+ }
+
+ @Override
+ public Properties setSecondaryGroupingSize(int secondaryGroupingSize) {
+ this.secondaryGroupingSize = secondaryGroupingSize;
+ return this;
+ }
+
+ @Override
+ public Properties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode) {
+ this.significantDigitsMode = significantDigitsMode;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("<Properties");
+ Field[] fields = Properties.class.getDeclaredFields();
+ for (Field field : fields) {
+ Object myValue, defaultValue;
+ try {
+ myValue = field.get(this);
+ defaultValue = field.get(DEFAULT);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ continue;
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ continue;
+ }
+ if (myValue == null && defaultValue == null) {
+ continue;
+ } else if (myValue == null || defaultValue == null) {
+ result.append(" " + field.getName() + ":" + myValue);
+ } else if (!myValue.equals(defaultValue)) {
+ result.append(" " + field.getName() + ":" + myValue);
+ }
+ }
+ result.append(">");
+ return result.toString();
+ }
+
+ /**
+ * Custom serialization: save fields along with their name, so that fields can be easily added in
+ * the future in any order. Only save fields that differ from their default value.
+ */
+ private void writeObject(ObjectOutputStream oos) throws IOException {
+ oos.defaultWriteObject();
+
+ // Extra int for possible future use
+ oos.writeInt(0);
+
+ ArrayList<Field> fieldsToSerialize = new ArrayList<Field>();
+ ArrayList<Object> valuesToSerialize = new ArrayList<Object>();
+ Field[] fields = Properties.class.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+ try {
+ Object myValue = field.get(this);
+ if (myValue == null) {
+ // All *Object* values default to null; no need to serialize.
+ continue;
+ }
+ Object defaultValue = field.get(DEFAULT);
+ if (!myValue.equals(defaultValue)) {
+ fieldsToSerialize.add(field);
+ valuesToSerialize.add(myValue);
+ }
+ } catch (IllegalArgumentException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ } catch (IllegalAccessException e) {
+ // Should not happen
+ throw new AssertionError(e);
+ }
+ }
+
+ // 1) How many fields are to be serialized?
+ int count = fieldsToSerialize.size();
+ oos.writeInt(count);
+
+ // 2) Write each field with its name and value
+ for (int i = 0; i < count; i++) {
+ Field field = fieldsToSerialize.get(i);
+ Object value = valuesToSerialize.get(i);
+ oos.writeObject(field.getName());
+ oos.writeObject(value);
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
+import com.ibm.icu.impl.number.formatters.ScientificFormat;
+
+/**
+ * The base class for a Rounder used by ICU Decimal Format.
+ *
+ * <p>A Rounder must implement the method {@link #apply}. An implementation must:
+ *
+ * <ol>
+ * <li>Either have the code <code>applyDefaults(input);</code> in its apply function, or otherwise
+ * ensure that minFrac, maxFrac, minInt, and maxInt are obeyed, paying special attention to
+ * the case when the input is zero.
+ * <li>Call one of {@link FormatQuantity#roundToInterval}, {@link
+ * FormatQuantity#roundToMagnitude}, or {@link FormatQuantity#roundToInfinity} on the input.
+ * </ol>
+ *
+ * <p>In order to be used by {@link CompactDecimalFormat} and {@link ScientificFormat}, among
+ * others, your rounder must be stable upon <em>decreasing</em> the magnitude of the input number.
+ * For example, if your rounder converts "999" to "1000", it must also convert "99.9" to "100" and
+ * "0.999" to "1". (The opposite does not need to be the case: you can round "0.999" to "1" but keep
+ * "999" as "999".)
+ *
+ * @see com.ibm.icu.impl.number.rounders.MagnitudeRounder
+ * @see com.ibm.icu.impl.number.rounders.IncrementRounder
+ * @see com.ibm.icu.impl.number.rounders.SignificantDigitsRounder
+ * @see com.ibm.icu.impl.number.rounders.NoRounder
+ */
+public abstract class Rounder extends Format.BeforeFormat {
+
+ public static interface IBasicRoundingProperties {
+
+ static int DEFAULT_MINIMUM_INTEGER_DIGITS = -1;
+
+ /** @see #setMinimumIntegerDigits */
+ public int getMinimumIntegerDigits();
+
+ /**
+ * Sets the minimum number of digits to display before the decimal point. If the number has
+ * fewer than this number of digits, the number will be padded with zeros. The pattern "#00.0#",
+ * for example, corresponds to 2 minimum integer digits, and the number 5.3 would be formatted
+ * as "05.3" in locale <em>en-US</em>.
+ *
+ * @param minimumIntegerDigits The minimum number of integer digits to output.
+ * @return The property bag, for chaining.
+ */
+ public IBasicRoundingProperties setMinimumIntegerDigits(int minimumIntegerDigits);
+
+ static int DEFAULT_MAXIMUM_INTEGER_DIGITS = -1;
+
+ /** @see #setMaximumIntegerDigits */
+ public int getMaximumIntegerDigits();
+
+ /**
+ * Sets the maximum number of digits to display before the decimal point. If the number has more
+ * than this number of digits, the extra digits will be truncated. For example, if maximum
+ * integer digits is 2, and you attempt to format the number 1970, you will get "70" in locale
+ * <em>en-US</em>. It is not possible to specify the maximum integer digits using a pattern
+ * string, except in the special case of a scientific format pattern.
+ *
+ * @param maximumIntegerDigits The maximum number of integer digits to output.
+ * @return The property bag, for chaining.
+ */
+ public IBasicRoundingProperties setMaximumIntegerDigits(int maximumIntegerDigits);
+
+ static int DEFAULT_MINIMUM_FRACTION_DIGITS = -1;
+
+ /** @see #setMinimumFractionDigits */
+ public int getMinimumFractionDigits();
+
+ /**
+ * Sets the minimum number of digits to display after the decimal point. If the number has fewer
+ * than this number of digits, the number will be padded with zeros. The pattern "#00.0#", for
+ * example, corresponds to 1 minimum fraction digit, and the number 456 would be formatted as
+ * "456.0" in locale <em>en-US</em>.
+ *
+ * @param minimumFractionDigits The minimum number of fraction digits to output.
+ * @return The property bag, for chaining.
+ */
+ public IBasicRoundingProperties setMinimumFractionDigits(int minimumFractionDigits);
+
+ static int DEFAULT_MAXIMUM_FRACTION_DIGITS = -1;
+
+ /** @see #setMaximumFractionDigits */
+ public int getMaximumFractionDigits();
+
+ /**
+ * Sets the maximum number of digits to display after the decimal point. If the number has fewer
+ * than this number of digits, the number will be rounded off using the rounding mode specified
+ * by {@link #setRoundingMode(RoundingMode)}. The pattern "#00.0#", for example, corresponds to
+ * 2 maximum fraction digits, and the number 456.789 would be formatted as "456.79" in locale
+ * <em>en-US</em> with the default rounding mode. Note that the number 456.999 would be
+ * formatted as "457.0" given the same configurations.
+ *
+ * @param maximumFractionDigits The maximum number of fraction digits to output.
+ * @return The property bag, for chaining.
+ */
+ public IBasicRoundingProperties setMaximumFractionDigits(int maximumFractionDigits);
+
+ static RoundingMode DEFAULT_ROUNDING_MODE = null;
+
+ /** @see #setRoundingMode */
+ public RoundingMode getRoundingMode();
+
+ /**
+ * Sets the rounding mode, which determines under which conditions extra decimal places are
+ * rounded either up or down. See {@link RoundingMode} for details on the choices of rounding
+ * mode. The default if not set explicitly is {@link RoundingMode#HALF_EVEN}.
+ *
+ * <p>This setting is ignored if {@link #setMathContext} is used.
+ *
+ * @param roundingMode The rounding mode to use when rounding is required.
+ * @return The property bag, for chaining.
+ * @see RoundingMode
+ * @see #setMathContext
+ */
+ public IBasicRoundingProperties setRoundingMode(RoundingMode roundingMode);
+
+ static MathContext DEFAULT_MATH_CONTEXT = null;
+
+ /** @see #setMathContext */
+ public MathContext getMathContext();
+
+ /**
+ * Sets the {@link MathContext} to be used during math and rounding operations. A MathContext
+ * encapsulates a RoundingMode and the number of significant digits in the output.
+ *
+ * @param mathContext The math context to use when rounding is required.
+ * @return The property bag, for chaining.
+ * @see MathContext
+ * @see #setRoundingMode
+ */
+ public IBasicRoundingProperties setMathContext(MathContext mathContext);
+ }
+
+ public static interface MultiplierGenerator {
+ public int getMultiplier(int magnitude);
+ }
+
+ // Properties available to all rounding strategies
+ protected final MathContext mathContext;
+ protected final int minInt;
+ protected final int maxInt;
+ protected final int minFrac;
+ protected final int maxFrac;
+
+ /**
+ * Constructor that uses integer and fraction digit lengths from IBasicRoundingProperties.
+ *
+ * @param properties
+ */
+ protected Rounder(IBasicRoundingProperties properties) {
+ mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
+
+ int _maxInt = properties.getMaximumIntegerDigits();
+ int _minInt = properties.getMinimumIntegerDigits();
+ int _maxFrac = properties.getMaximumFractionDigits();
+ int _minFrac = properties.getMinimumFractionDigits();
+
+ // Validate min/max int/frac.
+ // For backwards compatibility, minimum overrides maximum if the two conflict.
+ // The following logic ensures that there is always a minimum of at least one digit.
+ if (_minInt == 0 && _maxFrac != 0) {
+ // Force a digit to the right of the decimal point.
+ minFrac = _minFrac <= 0 ? 1 : _minFrac;
+ maxFrac = _maxFrac < 0 ? Integer.MAX_VALUE : _maxFrac < minFrac ? minFrac : _maxFrac;
+ minInt = 0;
+ maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt;
+ } else {
+ // Force a digit to the left of the decimal point.
+ minFrac = _minFrac < 0 ? 0 : _minFrac;
+ maxFrac = _maxFrac < 0 ? Integer.MAX_VALUE : _maxFrac < minFrac ? minFrac : _maxFrac;
+ minInt = _minInt <= 0 ? 1 : _minInt;
+ maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt < minInt ? minInt : _maxInt;
+ }
+ }
+
+ /**
+ * Perform rounding and specification of integer and fraction digit lengths on the input quantity.
+ * Calling this method will change the state of the FormatQuantity.
+ *
+ * @param input The {@link FormatQuantity} to be modified and rounded.
+ */
+ public abstract void apply(FormatQuantity input);
+
+ /**
+ * Rounding can affect the magnitude. First we attempt to adjust according to the original
+ * magnitude, and if the magnitude changes, we adjust according to a magnitude one greater. Note
+ * that this algorithm assumes that increasing the multiplier never increases the number of digits
+ * that can be displayed.
+ *
+ * @param input The quantity to be rounded.
+ * @param mg The implementation that returns magnitude adjustment based on a given starting
+ * magnitude.
+ * @return The multiplier that was chosen to best fit the input.
+ */
+ public int chooseMultiplierAndApply(FormatQuantity input, MultiplierGenerator mg) {
+ FormatQuantity copy = input.clone();
+
+ int magnitude = input.getMagnitude();
+ int multiplier = mg.getMultiplier(magnitude);
+ input.adjustMagnitude(multiplier);
+ apply(input);
+ if (input.getMagnitude() == magnitude + multiplier + 1) {
+ magnitude += 1;
+ input.copyFrom(copy);
+ multiplier = mg.getMultiplier(magnitude);
+ input.adjustMagnitude(multiplier);
+ assert input.getMagnitude() == magnitude + multiplier - 1;
+ apply(input);
+ assert input.getMagnitude() == magnitude + multiplier;
+ }
+
+ return multiplier;
+ }
+
+ /**
+ * Implementations can call this method to perform default logic for min/max digits. This method
+ * performs logic for handling of a zero input.
+ *
+ * @param input The digits being formatted.
+ */
+ protected void applyDefaults(FormatQuantity input) {
+ input.setIntegerFractionLength(minInt, maxInt, minFrac, maxFrac);
+ }
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ /**
+ * Gets a thread-local property bag that can be used to deliver properties to a constructor.
+ * Rounders themselves are guaranteed to not internally use a copy of this property bag.
+ *
+ * @return A clean, thread-local property bag.
+ */
+ public static Properties getThreadLocalProperties() {
+ return threadLocalProperties.get().clear();
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ apply(input);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setMathContext(mathContext);
+ properties.setRoundingMode(mathContext.getRoundingMode());
+ properties.setMinimumFractionDigits(minFrac);
+ properties.setMinimumIntegerDigits(minInt);
+ properties.setMaximumFractionDigits(maxFrac);
+ properties.setMaximumIntegerDigits(maxInt);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.number.Rounder.IBasicRoundingProperties;
+
+/** @author sffc */
+public class RoundingUtils {
+
+ public static final int SECTION_LOWER = 1;
+ public static final int SECTION_MIDPOINT = 2;
+ public static final int SECTION_UPPER = 3;
+
+ /**
+ * Converts a rounding mode and metadata about the quantity being rounded to a boolean determining
+ * whether the value should be rounded toward infinity or toward zero.
+ *
+ * <p>The parameters are of type int because benchmarks on an x86-64 processor against OpenJDK
+ * showed that ints were demonstrably faster than enums in switch statements.
+ *
+ * @param isEven Whether the digit immediately before the rounding magnitude is even.
+ * @param isNegative Whether the quantity is negative.
+ * @param section Whether the part of the quantity to the right of the rounding magnitude is
+ * exactly halfway between two digits, whether it is in the lower part (closer to zero), or
+ * whether it is in the upper part (closer to infinity). See {@link #SECTION_LOWER}, {@link
+ * #SECTION_MIDPOINT}, and {@link #SECTION_UPPER}.
+ * @param roundingMode The integer version of the {@link RoundingMode}, which you can get via
+ * {@link RoundingMode#ordinal}.
+ * @param reference A reference object to be used when throwing an ArithmeticException.
+ * @return true if the number should be rounded toward zero; false if it should be rounded toward
+ * infinity.
+ */
+ public static boolean getRoundingDirection(
+ boolean isEven, boolean isNegative, int section, int roundingMode, Object reference) {
+ switch (roundingMode) {
+ case BigDecimal.ROUND_UP:
+ // round away from zero
+ return false;
+
+ case BigDecimal.ROUND_DOWN:
+ // round toward zero
+ return true;
+
+ case BigDecimal.ROUND_CEILING:
+ // round toward positive infinity
+ return isNegative;
+
+ case BigDecimal.ROUND_FLOOR:
+ // round toward negative infinity
+ return !isNegative;
+
+ case BigDecimal.ROUND_HALF_UP:
+ switch (section) {
+ case SECTION_MIDPOINT:
+ return false;
+ case SECTION_LOWER:
+ return true;
+ case SECTION_UPPER:
+ return false;
+ }
+ break;
+
+ case BigDecimal.ROUND_HALF_DOWN:
+ switch (section) {
+ case SECTION_MIDPOINT:
+ return true;
+ case SECTION_LOWER:
+ return true;
+ case SECTION_UPPER:
+ return false;
+ }
+ break;
+
+ case BigDecimal.ROUND_HALF_EVEN:
+ switch (section) {
+ case SECTION_MIDPOINT:
+ return isEven;
+ case SECTION_LOWER:
+ return true;
+ case SECTION_UPPER:
+ return false;
+ }
+ break;
+ }
+
+ // Rounding mode UNNECESSARY
+ throw new ArithmeticException("Rounding is required on " + reference.toString());
+ }
+
+ /**
+ * Gets whether the given rounding mode's rounding boundary is at the midpoint. The rounding
+ * boundary is the point at which a number switches from being rounded down to being rounded up.
+ * For example, with rounding mode HALF_EVEN, HALF_UP, or HALF_DOWN, the rounding boundary is at
+ * the midpoint, and this function would return true. However, for UP, DOWN, CEILING, and FLOOR,
+ * the rounding boundary is at the "edge", and this function would return false.
+ *
+ * @param roundingMode The integer version of the {@link RoundingMode}.
+ * @return true if rounding mode is HALF_EVEN, HALF_UP, or HALF_DOWN; false otherwise.
+ */
+ public static boolean roundsAtMidpoint(int roundingMode) {
+ switch (roundingMode) {
+ case BigDecimal.ROUND_UP:
+ case BigDecimal.ROUND_DOWN:
+ case BigDecimal.ROUND_CEILING:
+ case BigDecimal.ROUND_FLOOR:
+ return false;
+
+ default:
+ return true;
+ }
+ }
+
+ private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED =
+ new MathContext[RoundingMode.values().length];
+
+ private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS =
+ new MathContext[RoundingMode.values().length];
+
+ static {
+ for (int i = 0; i < MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS.length; i++) {
+ MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[i] = new MathContext(0, RoundingMode.valueOf(i));
+ MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS[i] = new MathContext(16);
+ }
+ }
+
+ /**
+ * Gets the user-specified math context out of the property bag. If there is none, falls back to a
+ * math context with unlimited precision and the user-specified rounding mode, which defaults to
+ * HALF_EVEN (the IEEE 754R default).
+ *
+ * @param properties The property bag.
+ * @return A {@link MathContext}. Never null.
+ */
+ public static MathContext getMathContextOrUnlimited(IBasicRoundingProperties properties) {
+ MathContext mathContext = properties.getMathContext();
+ if (mathContext == null) {
+ RoundingMode roundingMode = properties.getRoundingMode();
+ if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN;
+ mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[roundingMode.ordinal()];
+ }
+ return mathContext;
+ }
+
+ /**
+ * Gets the user-specified math context out of the property bag. If there is none, falls back to a
+ * math context with 16 digits of precision (the 64-bit IEEE 754R default) and the user-specified
+ * rounding mode, which defaults to HALF_EVEN (the IEEE 754R default).
+ *
+ * @param properties The property bag.
+ * @return A {@link MathContext}. Never null.
+ */
+ public static MathContext getMathContextOr16Digits(IBasicRoundingProperties properties) {
+ MathContext mathContext = properties.getMathContext();
+ if (mathContext == null) {
+ RoundingMode roundingMode = properties.getRoundingMode();
+ if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN;
+ mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS[roundingMode.ordinal()];
+ }
+ return mathContext;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.impl.number.formatters.RangeFormat;
+import com.ibm.icu.impl.number.modifiers.SimpleModifier;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.MeasureUnit;
+
+public class demo {
+
+ public static void main(String[] args) throws ParseException {
+ SimpleModifier.testFormatAsPrefixSuffix();
+
+ System.out.println(new FormatQuantity1(3.14159));
+ System.out.println(new FormatQuantity1(3.14159, true));
+ System.out.println(new FormatQuantity2(3.14159));
+
+ System.out.println(
+ PatternString.propertiesToString(PatternString.parseToProperties("+**##,##,#00.05#%")));
+
+ ParsePosition ppos = new ParsePosition(0);
+ System.out.println(
+ Parse.parse(
+ "dd123",
+ ppos,
+ new Properties().setPositivePrefix("dd").setNegativePrefix("ddd"),
+ DecimalFormatSymbols.getInstance()));
+ System.out.println(ppos);
+
+ List<Format> formats = new ArrayList<Format>();
+
+ Properties properties = new Properties();
+ Format ndf = Endpoint.fromBTA(properties);
+ formats.add(ndf);
+
+ properties =
+ new Properties()
+ .setMinimumSignificantDigits(3)
+ .setMaximumSignificantDigits(3)
+ .setCompactStyle(CompactStyle.LONG);
+ Format cdf = Endpoint.fromBTA(properties);
+ formats.add(cdf);
+
+ properties =
+ new Properties().setFormatWidth(10).setPadPosition(PadPosition.AFTER_PREFIX);
+ Format pdf = Endpoint.fromBTA(properties);
+ formats.add(pdf);
+
+ properties =
+ new Properties()
+ .setMinimumExponentDigits(1)
+ .setMaximumIntegerDigits(3)
+ .setMaximumFractionDigits(1);
+ Format exf = Endpoint.fromBTA(properties);
+ formats.add(exf);
+
+ properties = new Properties().setRoundingIncrement(new BigDecimal("0.5"));
+ Format rif = Endpoint.fromBTA(properties);
+ formats.add(rif);
+
+ properties = new Properties().setMeasureUnit(MeasureUnit.HECTARE);
+ Format muf = Endpoint.fromBTA(properties);
+ formats.add(muf);
+
+ properties =
+ new Properties().setMeasureUnit(MeasureUnit.HECTARE).setCompactStyle(CompactStyle.LONG);
+ Format cmf = Endpoint.fromBTA(properties);
+ formats.add(cmf);
+
+ properties = PatternString.parseToProperties("#,##0.00 \u00a4");
+ Format ptf = Endpoint.fromBTA(properties);
+ formats.add(ptf);
+
+ RangeFormat rf = new RangeFormat(cdf, cdf, " to ");
+ System.out.println(rf.format(new FormatQuantity2(1234), new FormatQuantity2(2345)));
+
+ String[] cases = {
+ "1.0",
+ "2.01",
+ "1234.56",
+ "3000.0",
+ // "512.0000000000017",
+ // "4096.000000000001",
+ // "4096.000000000004",
+ // "4096.000000000005",
+ // "4096.000000000006",
+ // "4096.000000000007",
+ "0.00026418",
+ "0.01789261",
+ "468160.0",
+ "999000.0",
+ "999900.0",
+ "999990.0",
+ "0.0",
+ "12345678901.0",
+ // "789000000000000000000000.0",
+ // "789123123567853156372158.0",
+ "-5193.48",
+ };
+
+ for (String str : cases) {
+ System.out.println("----------");
+ System.out.println(str);
+ System.out.println(" NDF: " + ndf.format(new FormatQuantity2(Double.parseDouble(str))));
+ System.out.println(" CDF: " + cdf.format(new FormatQuantity2(Double.parseDouble(str))));
+ System.out.println(" PWD: " + pdf.format(new FormatQuantity2(Double.parseDouble(str))));
+ System.out.println(" EXF: " + exf.format(new FormatQuantity2(Double.parseDouble(str))));
+ System.out.println(" RIF: " + rif.format(new FormatQuantity2(Double.parseDouble(str))));
+ System.out.println(" MUF: " + muf.format(new FormatQuantity2(Double.parseDouble(str))));
+ System.out.println(" CMF: " + cmf.format(new FormatQuantity2(Double.parseDouble(str))));
+ System.out.println(" PTF: " + ptf.format(new FormatQuantity2(Double.parseDouble(str))));
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.Format.BeforeFormat;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+
+public class BigDecimalMultiplier extends BeforeFormat {
+ public static interface IProperties {
+
+ static BigDecimal DEFAULT_MULTIPLIER = null;
+
+ /** @see #setMultiplier */
+ public BigDecimal getMultiplier();
+
+ /**
+ * Multiply all numbers by this amount before formatting.
+ *
+ * @param multiplier The amount to multiply by.
+ * @return The property bag, for chaining.
+ * @see MagnitudeMultiplier
+ */
+ public IProperties setMultiplier(BigDecimal multiplier);
+ }
+
+ public static boolean useMultiplier(IProperties properties) {
+ return properties.getMultiplier() != IProperties.DEFAULT_MULTIPLIER;
+ }
+
+ private final BigDecimal multiplier;
+
+ public static BigDecimalMultiplier getInstance(IProperties properties) {
+ if (properties.getMultiplier() == null) {
+ throw new IllegalArgumentException("The multiplier must be present for MultiplierFormat");
+ }
+ // TODO: Intelligently fall back to a MagnitudeMultiplier if the multiplier is a power of ten?
+ return new BigDecimalMultiplier(properties);
+ }
+
+ private BigDecimalMultiplier(IProperties properties) {
+ this.multiplier = properties.getMultiplier();
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ input.multiplyBy(multiplier);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setMultiplier(multiplier);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.ibm.icu.impl.ICUData;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.UResource;
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.Modifier.PositiveNegativeModifier;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.PNAffixGenerator;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+public class CompactDecimalFormat extends Format.BeforeFormat {
+ public static interface IProperties
+ extends RoundingFormat.IProperties, CurrencyFormat.ICurrencyProperties {
+
+ static CompactStyle DEFAULT_COMPACT_STYLE = null;
+
+ /** @see #setCompactStyle */
+ public CompactStyle getCompactStyle();
+
+ /**
+ * Use compact decimal formatting with the specified {@link CompactStyle}. CompactStyle.SHORT
+ * produces output like "10K" in locale <em>en-US</em>, whereas CompactStyle.LONG produces
+ * output like "10 thousand" in that locale.
+ *
+ * @param compactStyle The style of prefixes/suffixes to append.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setCompactStyle(CompactStyle compactStyle);
+ }
+
+ public static boolean useCompactDecimalFormat(IProperties properties) {
+ return properties.getCompactStyle() != IProperties.DEFAULT_COMPACT_STYLE;
+ }
+
+ static final int MAX_DIGITS = 15;
+
+ // Properties
+ private final CompactDecimalData data;
+ private final Rounder rounder;
+ private final PositiveNegativeModifier defaultMod;
+ private final CompactStyle style; // retained for exporting only
+
+ public static CompactDecimalFormat getInstance(
+ DecimalFormatSymbols symbols, IProperties properties) {
+ return new CompactDecimalFormat(symbols, properties);
+ }
+
+ private static Rounder getRounder(IProperties properties) {
+ // Use rounding settings if they were specified, or else use the default CDF rounder.
+ Rounder rounder = RoundingFormat.getDefaultOrNull(properties);
+ if (rounder == null) {
+ rounder =
+ SignificantDigitsRounder.getInstance(
+ SignificantDigitsRounder.getThreadLocalProperties()
+ .setMinimumSignificantDigits(1)
+ .setMaximumSignificantDigits(2)
+ .setSignificantDigitsMode(SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION));
+ }
+ return rounder;
+ }
+
+ protected static final ThreadLocal<Map<CompactDecimalFingerprint, CompactDecimalData>>
+ threadLocalDataCache =
+ new ThreadLocal<Map<CompactDecimalFingerprint, CompactDecimalData>>() {
+ @Override
+ protected Map<CompactDecimalFingerprint, CompactDecimalData> initialValue() {
+ return new HashMap<CompactDecimalFingerprint, CompactDecimalData>();
+ }
+ };
+
+ private static CompactDecimalData getData(
+ DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) {
+ // See if we already have a data object based on the fingerprint
+ CompactDecimalData data = threadLocalDataCache.get().get(fingerprint);
+ if (data != null) return data;
+
+ // Make data bundle object
+ data = new CompactDecimalData();
+ ULocale ulocale = symbols.getULocale();
+ CompactDecimalDataSink sink = new CompactDecimalDataSink(data, symbols, fingerprint);
+ String nsName = NumberingSystem.getInstance(ulocale).getName();
+ ICUResourceBundle r =
+ (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, ulocale);
+ r.getAllItemsWithFallback("NumberElements/" + nsName, sink);
+ if (data.isEmpty() && !nsName.equals("latn")) {
+ r.getAllItemsWithFallback("NumberElements/latn", sink);
+ }
+ if (sink.exception != null) {
+ throw sink.exception;
+ }
+ threadLocalDataCache.get().put(fingerprint, data);
+ return data;
+ }
+
+ private static PositiveNegativeModifier getDefaultMod(
+ DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) {
+ ULocale uloc = symbols.getULocale();
+ String pattern;
+ if (fingerprint.compactType == CompactType.CURRENCY) {
+ pattern = NumberFormat.getPattern(uloc, NumberFormat.CURRENCYSTYLE);
+ } else {
+ pattern = NumberFormat.getPattern(uloc, NumberFormat.NUMBERSTYLE);
+ }
+ // TODO: Clean this up; avoid the extra object creations.
+ // TODO: Currency may also need to override grouping settings, not just affixes.
+ Properties properties = PatternString.parseToProperties(pattern);
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ PNAffixGenerator.Result result =
+ pnag.getModifiers(symbols, fingerprint.currencySymbol, properties);
+ return new PositiveNegativeAffixModifier(result.positive, result.negative);
+ }
+
+ private CompactDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) {
+ CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties);
+ this.rounder = getRounder(properties);
+ this.data = getData(symbols, fingerprint);
+ this.defaultMod = getDefaultMod(symbols, fingerprint);
+ this.style = properties.getCompactStyle(); // for exporting only
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+ apply(input, mods, rules, rounder, data, defaultMod);
+ }
+
+ @Override
+ protected void before(FormatQuantity input, ModifierHolder mods) {
+ throw new UnsupportedOperationException();
+ }
+
+ public static void apply(
+ FormatQuantity input,
+ ModifierHolder mods,
+ PluralRules rules,
+ DecimalFormatSymbols symbols,
+ IProperties properties) {
+ CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties);
+ Rounder rounder = getRounder(properties);
+ CompactDecimalData data = getData(symbols, fingerprint);
+ PositiveNegativeModifier defaultMod = getDefaultMod(symbols, fingerprint);
+ apply(input, mods, rules, rounder, data, defaultMod);
+ }
+
+ private static void apply(
+ FormatQuantity input,
+ ModifierHolder mods,
+ PluralRules rules,
+ Rounder rounder,
+ CompactDecimalData data,
+ PositiveNegativeModifier defaultMod) {
+
+ // Treat zero as if it had magnitude 0
+ int magnitude;
+ if (input.isZero()) {
+ magnitude = 0;
+ rounder.apply(input);
+ } else {
+ int multiplier = rounder.chooseMultiplierAndApply(input, data);
+ magnitude = input.getMagnitude() - multiplier;
+ }
+
+ StandardPlural plural = input.getStandardPlural(rules);
+ boolean isNegative = input.isNegative();
+ Modifier mod = data.getModifier(magnitude, plural, isNegative);
+ if (mod == null) {
+ // Use the default (non-compact) modifier.
+ mod = defaultMod.getModifier(isNegative);
+ }
+ mods.add(mod);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setCompactStyle(style);
+ rounder.export(properties);
+ }
+
+ static class CompactDecimalData implements Rounder.MultiplierGenerator {
+
+ // A dummy object used when a "0" compact decimal entry is encountered. This is necessary
+ // in order to prevent falling back to root.
+ private static final Modifier USE_FALLBACK = new ConstantAffixModifier();
+
+ final Modifier[] mods;
+ final byte[] multipliers;
+ boolean isEmpty;
+ int largestMagnitude;
+
+ CompactDecimalData() {
+ mods = new Modifier[(MAX_DIGITS + 1) * StandardPlural.COUNT * 2];
+ multipliers = new byte[MAX_DIGITS + 1];
+ isEmpty = true;
+ largestMagnitude = -1;
+ }
+
+ boolean isEmpty() {
+ return isEmpty;
+ }
+
+ @Override
+ public int getMultiplier(int magnitude) {
+ if (magnitude < 0) {
+ return 0;
+ }
+ if (magnitude > largestMagnitude) {
+ magnitude = largestMagnitude;
+ }
+ return multipliers[magnitude];
+ }
+
+ int setOrGetMultiplier(int magnitude, byte multiplier) {
+ if (multipliers[magnitude] != 0) {
+ return multipliers[magnitude];
+ }
+ multipliers[magnitude] = multiplier;
+ isEmpty = false;
+ if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+ return multiplier;
+ }
+
+ Modifier getModifier(int magnitude, StandardPlural plural, boolean isNegative) {
+ if (magnitude < 0) {
+ return null;
+ }
+ if (magnitude > largestMagnitude) {
+ magnitude = largestMagnitude;
+ }
+ Modifier mod = mods[modIndex(magnitude, plural, isNegative)];
+ if (mod == null && plural != StandardPlural.OTHER) {
+ // Fall back to "other" plural variant
+ mod = mods[modIndex(magnitude, StandardPlural.OTHER, isNegative)];
+ }
+ if (mod == USE_FALLBACK) {
+ // Return null if USE_FALLBACK is present
+ mod = null;
+ }
+ return mod;
+ }
+
+ public boolean has(int magnitude, StandardPlural plural) {
+ // Return true if USE_FALLBACK is present
+ return mods[modIndex(magnitude, plural, false)] != null;
+ }
+
+ void setModifiers(Modifier positive, Modifier negative, int magnitude, StandardPlural plural) {
+ mods[modIndex(magnitude, plural, false)] = positive;
+ mods[modIndex(magnitude, plural, true)] = negative;
+ isEmpty = false;
+ if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+ }
+
+ void setNoFallback(int magnitude, StandardPlural plural) {
+ setModifiers(USE_FALLBACK, USE_FALLBACK, magnitude, plural);
+ }
+
+ private static final int modIndex(int magnitude, StandardPlural plural, boolean isNegative) {
+ return magnitude * StandardPlural.COUNT * 2 + plural.ordinal() * 2 + (isNegative ? 1 : 0);
+ }
+ }
+
+ // Should this be public or internal?
+ static enum CompactType {
+ DECIMAL,
+ CURRENCY
+ }
+
+ static class CompactDecimalFingerprint {
+ // TODO: Add more stuff to the fingerprint, like the symbols used by PNAffixGenerator
+ final CompactStyle compactStyle;
+ final CompactType compactType;
+ final ULocale uloc;
+ final String currencySymbol;
+
+ CompactDecimalFingerprint(DecimalFormatSymbols symbols, IProperties properties) {
+ // CompactDecimalFormat does not need to worry about the same constraints as non-compact
+ // currency formatting needs to consider, like the currency rounding mode and the currency
+ // long names with plural forms.
+ if (properties.getCurrency() != CurrencyFormat.ICurrencyProperties.DEFAULT_CURRENCY) {
+ compactType = CompactType.CURRENCY;
+ currencySymbol = CurrencyFormat.getCurrencySymbol(symbols, properties);
+ } else {
+ compactType = CompactType.DECIMAL;
+ currencySymbol = symbols.getCurrencySymbol(); // fallback; should remain unused
+ }
+ compactStyle = properties.getCompactStyle();
+ uloc = symbols.getULocale();
+ }
+
+ @Override
+ public boolean equals(Object _other) {
+ if (_other == null) return false;
+ if (this == _other) return true;
+ CompactDecimalFingerprint other = (CompactDecimalFingerprint) _other;
+ if (compactStyle != other.compactStyle) return false;
+ if (compactType != other.compactType) return false;
+ if (currencySymbol != other.currencySymbol) {
+ // String comparison with null handling
+ if (currencySymbol == null || other.currencySymbol == null) return false;
+ if (!currencySymbol.equals(other.currencySymbol)) return false;
+ }
+ if (!uloc.equals(other.uloc)) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 0;
+ if (compactStyle != null) hashCode ^= compactStyle.hashCode();
+ if (compactType != null) hashCode ^= compactType.hashCode();
+ if (uloc != null) hashCode ^= uloc.hashCode();
+ if (currencySymbol != null) hashCode ^= currencySymbol.hashCode();
+ return hashCode;
+ }
+ }
+
+ private static final class CompactDecimalDataSink extends UResource.Sink {
+
+ final CompactDecimalData data;
+ final DecimalFormatSymbols symbols;
+ final CompactStyle compactStyle;
+ final CompactType compactType;
+ final String currencySymbol;
+ final PNAffixGenerator pnag;
+ IllegalArgumentException exception;
+
+ /*
+ * NumberElements{ <-- top (numbering system table)
+ * latn{ <-- patternsTable (one per numbering system)
+ * patternsLong{ <-- formatsTable (one per pattern)
+ * decimalFormat{ <-- powersOfTenTable (one per format)
+ * 1000{ <-- pluralVariantsTable (one per power of ten)
+ * one{"0 thousand"} <-- plural variant and template
+ */
+
+ public CompactDecimalDataSink(
+ CompactDecimalData data,
+ DecimalFormatSymbols symbols,
+ CompactDecimalFingerprint fingerprint) {
+ this.data = data;
+ this.symbols = symbols;
+ compactType = fingerprint.compactType;
+ currencySymbol = fingerprint.currencySymbol;
+ compactStyle = fingerprint.compactStyle;
+ pnag = PNAffixGenerator.getThreadLocalInstance();
+ }
+
+ @Override
+ public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
+ UResource.Table patternsTable = value.getTable();
+ for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
+ if (key.contentEquals("patternsShort") && compactStyle == CompactStyle.SHORT) {
+ } else if (key.contentEquals("patternsLong") && compactStyle == CompactStyle.LONG) {
+ } else {
+ continue;
+ }
+
+ // traverse into the table of formats
+ UResource.Table formatsTable = value.getTable();
+ for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
+ if (key.contentEquals("decimalFormat") && compactType == CompactType.DECIMAL) {
+ } else if (key.contentEquals("currencyFormat") && compactType == CompactType.CURRENCY) {
+ } else {
+ continue;
+ }
+
+ // traverse into the table of powers of ten
+ UResource.Table powersOfTenTable = value.getTable();
+ for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
+ try {
+
+ // Assumes that the keys are always of the form "10000" where the magnitude is the
+ // length of the key minus one
+ byte magnitude = (byte) (key.length() - 1);
+
+ // Silently ignore divisors that are too big.
+ if (magnitude >= MAX_DIGITS) continue;
+
+ // Iterate over the plural variants ("one", "other", etc)
+ UResource.Table pluralVariantsTable = value.getTable();
+ for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
+
+ // Skip this magnitude/plural if we already have it from a child locale.
+ StandardPlural plural = StandardPlural.fromString(key.toString());
+ if (data.has(magnitude, plural)) {
+ continue;
+ }
+
+ // The value "0" means that we need to use the default pattern and not fall back
+ // to parent locales. Example locale where this is relevant: 'it'.
+ String patternString = value.toString();
+ if (patternString.equals("0")) {
+ data.setNoFallback(magnitude, plural);
+ continue;
+ }
+
+ // The magnitude multiplier is the difference between the magnitude and the number
+ // of zeros in the pattern, getMinimumIntegerDigits.
+ Properties properties = PatternString.parseToProperties(patternString);
+ byte _multiplier = (byte) -(magnitude - properties.getMinimumIntegerDigits() + 1);
+ if (_multiplier != data.setOrGetMultiplier(magnitude, _multiplier)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Different number of zeros for same power of ten in compact decimal format data for locale '%s', style '%s', type '%s'",
+ symbols.getULocale().toString(),
+ compactStyle.toString(),
+ compactType.toString()));
+ }
+
+ PNAffixGenerator.Result result =
+ pnag.getModifiers(symbols, currencySymbol, properties);
+ data.setModifiers(result.positive, result.negative, magnitude, plural);
+ }
+
+ } catch (IllegalArgumentException e) {
+ exception = e;
+ continue;
+ }
+ }
+
+ // We want only one table of compact decimal formats, so if we get here, stop consuming.
+ // The data.isEmpty() check will prevent further bundles from being traversed.
+ return;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.AffixPatternUtils;
+import com.ibm.icu.impl.number.PNAffixGenerator;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
+import com.ibm.icu.impl.number.rounders.IncrementRounder;
+import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+
+public class CurrencyFormat {
+
+ public enum CurrencyStyle {
+ SYMBOL,
+ ISO_CODE;
+ }
+
+ public static interface ICurrencyProperties {
+ static Currency DEFAULT_CURRENCY = null;
+
+ /** @see #setCurrency */
+ public Currency getCurrency();
+
+ /**
+ * Use the specified currency to substitute currency placeholders ('¤') in the pattern string.
+ *
+ * @param currency The currency.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setCurrency(Currency currency);
+
+ static CurrencyStyle DEFAULT_CURRENCY_STYLE = null;
+
+ /** @see #setCurrencyStyle */
+ public CurrencyStyle getCurrencyStyle();
+
+ /**
+ * Use the specified {@link CurrencyStyle} to replace currency placeholders ('¤').
+ * CurrencyStyle.SYMBOL will use the short currency symbol, like "$" or "€", whereas
+ * CurrencyStyle.ISO_CODE will use the ISO 4217 currency code, like "USD" or "EUR".
+ *
+ * <p>For long currency names, use {@link MeasureFormat.IProperties#setMeasureUnit}.
+ *
+ * @param currencyStyle The currency style. Defaults to CurrencyStyle.SYMBOL.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setCurrencyStyle(CurrencyStyle currencyStyle);
+
+ /**
+ * An old enum that specifies how currencies should be rounded. It contains a subset of the
+ * functionality supported by RoundingInterval.
+ */
+ static Currency.CurrencyUsage DEFAULT_CURRENCY_USAGE = null;
+
+ /** @see #setCurrencyUsage */
+ public Currency.CurrencyUsage getCurrencyUsage();
+
+ /**
+ * Use the specified {@link CurrencyUsage} instance, which provides default rounding rules for
+ * the currency in two styles, CurrencyUsage.CASH and CurrencyUsage.STANDARD.
+ *
+ * <p>The CurrencyUsage specified here will not be used unless there is a currency placeholder
+ * in the pattern.
+ *
+ * @param currencyUsage The currency usage. Defaults to CurrencyUsage.STANDARD.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setCurrencyUsage(Currency.CurrencyUsage currencyUsage);
+
+ static CurrencyPluralInfo DEFAULT_CURRENCY_PLURAL_INFO = null;
+
+ /** @see #setCurrencyPluralInfo */
+ @Deprecated
+ public CurrencyPluralInfo getCurrencyPluralInfo();
+
+ /**
+ * Use the specified {@link CurrencyPluralInfo} instance when formatting currency long names.
+ *
+ * @param currencyPluralInfo The currency plural info object.
+ * @return The property bag, for chaining.
+ * @deprecated Use {@link MeasureFormat.IProperties#setMeasureUnit} with a Currency instead.
+ */
+ @Deprecated
+ public IProperties setCurrencyPluralInfo(CurrencyPluralInfo currencyPluralInfo);
+
+ public IProperties clone();
+ }
+
+ public static interface IProperties
+ extends ICurrencyProperties,
+ RoundingFormat.IProperties,
+ PositiveNegativeAffixFormat.IProperties {}
+
+ /**
+ * Returns true if the currency is set in The property bag or if currency symbols are present in
+ * the prefix/suffix pattern.
+ */
+ public static boolean useCurrency(IProperties properties) {
+ return ((properties.getCurrency() != null)
+ || properties.getCurrencyPluralInfo() != null
+ || AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern())
+ || AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern())
+ || AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern())
+ || AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern()));
+ }
+
+ /**
+ * Returns the effective currency symbol based on the input. If {@link
+ * ICurrencyProperties#setCurrencyStyle} was set to {@link CurrencyStyle#ISO_CODE}, the ISO Code
+ * will be returned; otherwise, the currency symbol, like "$", will be returned.
+ *
+ * @param symbols The current {@link DecimalFormatSymbols} instance
+ * @param properties The current property bag
+ * @return The currency symbol string, e.g., to substitute '¤' in a decimal pattern string.
+ */
+ public static String getCurrencySymbol(
+ DecimalFormatSymbols symbols, ICurrencyProperties properties) {
+ // If the user asked for ISO Code, return the ISO Code instead of the symbol
+ CurrencyStyle style = properties.getCurrencyStyle();
+ if (style == CurrencyStyle.ISO_CODE) {
+ return getCurrencyIsoCode(symbols, properties);
+ }
+
+ // Get the currency symbol
+ Currency currency = properties.getCurrency();
+ if (currency == null) {
+ return symbols.getCurrencySymbol();
+ } else if (currency.equals(symbols.getCurrency())) {
+ // The user may have set a custom currency symbol in DecimalFormatSymbols.
+ return symbols.getCurrencySymbol();
+ } else {
+ // Use the canonical symbol.
+ return currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ }
+ }
+
+ /**
+ * Returns the currency ISO code based on the input, like "USD".
+ *
+ * @param symbols The current {@link DecimalFormatSymbols} instance
+ * @param properties The current property bag
+ * @return The currency ISO code string, e.g., to substitute '¤¤' in a decimal pattern string.
+ */
+ public static String getCurrencyIsoCode(
+ DecimalFormatSymbols symbols, ICurrencyProperties properties) {
+ Currency currency = properties.getCurrency();
+ if (currency == null) {
+ // If a currency object was not provided, use the string from symbols
+ // Note: symbols.getCurrency().getCurrencyCode() won't work here because
+ // DecimalFormatSymbols#setInternationalCurrencySymbol() does not update the
+ // immutable internal currency instance.
+ return symbols.getInternationalCurrencySymbol();
+ } else if (currency.equals(symbols.getCurrency())) {
+ // The user may have set a custom currency symbol in DecimalFormatSymbols.
+ return symbols.getInternationalCurrencySymbol();
+ } else {
+ // Use the canonical currency code.
+ return currency.getCurrencyCode();
+ }
+ }
+
+ /**
+ * Returns the currency long name on the input, like "US dollars".
+ *
+ * @param symbols The current {@link DecimalFormatSymbols} instance
+ * @param properties The current property bag
+ * @param plural The plural form
+ * @return The currency long name string, e.g., to substitute '¤¤¤' in a decimal pattern string.
+ */
+ public static String getCurrencyLongName(
+ DecimalFormatSymbols symbols, ICurrencyProperties properties, StandardPlural plural) {
+ // Attempt to get a currency object first from properties then from symbols
+ Currency currency = properties.getCurrency();
+ if (currency == null) {
+ currency = symbols.getCurrency();
+ }
+
+ // If no currency object is available, fall back to the currency symbol
+ if (currency == null) {
+ return getCurrencySymbol(symbols, properties);
+ }
+
+ // Get the long name
+ return currency.getName(
+ symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
+ }
+
+ public static GeneralPluralModifier getCurrencyModifier(
+ DecimalFormatSymbols symbols, IProperties properties) {
+
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ String sym = getCurrencySymbol(symbols, properties);
+ String iso = getCurrencyIsoCode(symbols, properties);
+
+ // Previously, the user was also able to specify '¤¤' and '¤¤¤' directly into the prefix or
+ // suffix, which is how the user specified whether they wanted the ISO code or long name.
+ // For backwards compatibility support, that feature is implemented here.
+
+ CurrencyPluralInfo info = properties.getCurrencyPluralInfo();
+ GeneralPluralModifier mod = new GeneralPluralModifier();
+ Properties temp = new Properties();
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ String longName = getCurrencyLongName(symbols, properties, plural);
+
+ PNAffixGenerator.Result result;
+ if (info == null) {
+ // CurrencyPluralInfo is not available.
+ result = pnag.getModifiers(symbols, sym, iso, longName, properties);
+ } else {
+ // CurrencyPluralInfo is available. Use it to generate affixes for long name support.
+ String pluralPattern = info.getCurrencyPluralPattern(plural.getKeyword());
+ PatternString.parseToExistingProperties(pluralPattern, temp, true);
+ result = pnag.getModifiers(symbols, sym, iso, longName, temp);
+ }
+ mod.put(plural, result.positive, result.negative);
+ }
+ return mod;
+ }
+
+ private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
+
+ public static void populateCurrencyRounderProperties(
+ Properties destination, DecimalFormatSymbols symbols, IProperties properties) {
+
+ Currency currency = properties.getCurrency();
+ if (currency == null) {
+ // Fall back to the DecimalFormatSymbols currency instance.
+ currency = symbols.getCurrency();
+ }
+ if (currency == null) {
+ // There is a currency symbol in the pattern, but we have no currency available to use.
+ // Use the default currency instead so that we can still apply currency usage rules.
+ currency = DEFAULT_CURRENCY;
+ }
+
+ Currency.CurrencyUsage currencyUsage = properties.getCurrencyUsage();
+ if (currencyUsage == null) {
+ currencyUsage = CurrencyUsage.STANDARD;
+ }
+
+ double incrementDouble = currency.getRoundingIncrement(currencyUsage);
+ int fractionDigits = currency.getDefaultFractionDigits(currencyUsage);
+
+ destination.setRoundingMode(properties.getRoundingMode());
+ destination.setMinimumIntegerDigits(properties.getMinimumIntegerDigits());
+ destination.setMaximumIntegerDigits(properties.getMaximumIntegerDigits());
+
+ int _minFrac = properties.getMinimumFractionDigits();
+ int _maxFrac = properties.getMaximumFractionDigits();
+ if (_minFrac >= 0 || _maxFrac >= 0) {
+ // User override
+ destination.setMinimumFractionDigits(_minFrac);
+ destination.setMaximumFractionDigits(_maxFrac);
+ } else {
+ destination.setMinimumFractionDigits(fractionDigits);
+ destination.setMaximumFractionDigits(fractionDigits);
+ }
+
+ if (incrementDouble > 0.0) {
+ BigDecimal incrementBigDecimal;
+ BigDecimal _roundingIncrement = properties.getRoundingIncrement();
+ if (_roundingIncrement != null) {
+ incrementBigDecimal = _roundingIncrement;
+ } else {
+ incrementBigDecimal = BigDecimal.valueOf(incrementDouble);
+ }
+ destination.setRoundingIncrement(incrementBigDecimal);
+ } else {
+ }
+ }
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ public static Rounder getCurrencyRounder(DecimalFormatSymbols symbols, IProperties properties) {
+ Properties cprops = threadLocalProperties.get().clear();
+ populateCurrencyRounderProperties(cprops, symbols, properties);
+ if (cprops.getRoundingIncrement() != null) {
+ return IncrementRounder.getInstance(cprops);
+ } else {
+ return MagnitudeRounder.getInstance(cprops);
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.Format.BeforeFormat;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+
+public class MagnitudeMultiplier extends Format.BeforeFormat {
+ private static final MagnitudeMultiplier DEFAULT = new MagnitudeMultiplier(0);
+
+ public static interface IProperties {
+
+ static int DEFAULT_MAGNITUDE_MULTIPLIER = 0;
+
+ /** @see #setMagnitudeMultiplier */
+ public int getMagnitudeMultiplier();
+
+ /**
+ * Multiply all numbers by this power of ten before formatting. Negative multipliers reduce the
+ * magnitude and make numbers smaller (closer to zero).
+ *
+ * @param magnitudeMultiplier The number of powers of ten to scale.
+ * @return The property bag, for chaining.
+ * @see BigDecimalMultiplier
+ */
+ public IProperties setMagnitudeMultiplier(int magnitudeMultiplier);
+ }
+
+ public static boolean useMagnitudeMultiplier(IProperties properties) {
+ return properties.getMagnitudeMultiplier() != IProperties.DEFAULT_MAGNITUDE_MULTIPLIER;
+ }
+
+ // Properties
+ final int delta;
+
+ public static BeforeFormat getInstance(Properties properties) {
+ if (properties.getMagnitudeMultiplier() == 0) {
+ return DEFAULT;
+ }
+ return new MagnitudeMultiplier(properties.getMagnitudeMultiplier());
+ }
+
+ private MagnitudeMultiplier(int delta) {
+ this.delta = delta;
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ input.adjustMagnitude(delta);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setMagnitudeMultiplier(delta);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
+import com.ibm.icu.impl.number.modifiers.SimpleModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+public class MeasureFormat {
+
+ public static interface IProperties {
+
+ static MeasureUnit DEFAULT_MEASURE_UNIT = null;
+
+ /** @see #setMeasureUnit */
+ public MeasureUnit getMeasureUnit();
+
+ /**
+ * Apply prefixes and suffixes for the specified {@link MeasureUnit} to the formatted number.
+ *
+ * @param measureUnit The measure unit.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMeasureUnit(MeasureUnit measureUnit);
+
+ static FormatWidth DEFAULT_MEASURE_FORMAT_WIDTH = null;
+
+ /** @see #setMeasureFormatWidth */
+ public FormatWidth getMeasureFormatWidth();
+
+ /**
+ * Use the specified {@link FormatWidth} when choosing the style of measure unit prefix/suffix.
+ *
+ * <p>Must be used in conjunction with {@link #setMeasureUnit}.
+ *
+ * @param measureFormatWidth The width style. Defaults to FormatWidth.WIDE.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMeasureFormatWidth(FormatWidth measureFormatWidth);
+ }
+
+ public static boolean useMeasureFormat(IProperties properties) {
+ return properties.getMeasureUnit() != IProperties.DEFAULT_MEASURE_UNIT;
+ }
+
+ public static GeneralPluralModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+ ULocale uloc = symbols.getULocale();
+ MeasureUnit unit = properties.getMeasureUnit();
+ FormatWidth width = properties.getMeasureFormatWidth();
+
+ if (unit == null) {
+ throw new IllegalArgumentException("A measure unit is required for MeasureFormat");
+ }
+ if (width == null) {
+ width = FormatWidth.WIDE;
+ }
+
+ // Temporarily, create a MeasureFormat instance for its data loading capability
+ // TODO: Move data loading directly into this class file
+ com.ibm.icu.text.MeasureFormat mf = com.ibm.icu.text.MeasureFormat.getInstance(uloc, width);
+ GeneralPluralModifier mod = new GeneralPluralModifier();
+ for (StandardPlural plural : StandardPlural.VALUES) {
+ String formatString = null;
+ mf.getPluralFormatter(unit, width, plural.ordinal());
+ mod.put(plural, new SimpleModifier(formatString, null, false));
+ }
+ return mod;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Format.AfterFormat;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+
+public class PaddingFormat implements AfterFormat {
+ public enum PadPosition {
+ BEFORE_PREFIX,
+ AFTER_PREFIX,
+ BEFORE_SUFFIX,
+ AFTER_SUFFIX;
+
+ public static PadPosition fromOld(int old) {
+ switch (old) {
+ case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX:
+ return PadPosition.BEFORE_PREFIX;
+ case com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX:
+ return PadPosition.AFTER_PREFIX;
+ case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX:
+ return PadPosition.BEFORE_SUFFIX;
+ case com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX:
+ return PadPosition.AFTER_SUFFIX;
+ default:
+ throw new IllegalArgumentException("Don't know how to map " + old);
+ }
+ }
+
+ public int toOld() {
+ switch (this) {
+ case BEFORE_PREFIX:
+ return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX;
+ case AFTER_PREFIX:
+ return com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX;
+ case BEFORE_SUFFIX:
+ return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX;
+ case AFTER_SUFFIX:
+ return com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX;
+ default:
+ return -1; // silence compiler errors
+ }
+ }
+ }
+
+ public static interface IProperties {
+
+ static int DEFAULT_FORMAT_WIDTH = 0;
+
+ /** @see #setFormatWidth */
+ public int getFormatWidth();
+
+ /**
+ * Sets the minimum width of the string output by the formatting pipeline. For example, if
+ * padding is enabled and paddingWidth is set to 6, formatting the number "3.14159" with the
+ * pattern "0.00" will result in "··3.14" if '·' is your padding string.
+ *
+ * <p>If the number is longer than your padding width, the number will display as if no padding
+ * width had been specified, which may result in strings longer than the padding width.
+ *
+ * <p>Width is counted in UTF-16 code units.
+ *
+ * @param formatWidth The output width.
+ * @return The property bag, for chaining.
+ * @see #setPadPosition
+ * @see #setPadString
+ */
+ public IProperties setFormatWidth(int formatWidth);
+
+ static String DEFAULT_PAD_STRING = null;
+
+ /** @see #setPadString */
+ public String getPadString();
+
+ /**
+ * Sets the string used for padding. The string should contain a single character or grapheme
+ * cluster.
+ *
+ * <p>Must be used in conjunction with {@link #setFormatWidth}.
+ *
+ * @param paddingString The padding string. Defaults to an ASCII space (U+0020).
+ * @return The property bag, for chaining.
+ * @see #setFormatWidth
+ */
+ public IProperties setPadString(String paddingString);
+
+ static PadPosition DEFAULT_PAD_POSITION = null;
+
+ /** @see #setPadPosition */
+ public PadPosition getPadPosition();
+
+ /**
+ * Sets the location where the padding string is to be inserted to maintain the padding width:
+ * one of BEFORE_PREFIX, AFTER_PREFIX, BEFORE_SUFFIX, or AFTER_SUFFIX.
+ *
+ * <p>Must be used in conjunction with {@link #setFormatWidth}.
+ *
+ * @param padPosition The output width.
+ * @return The property bag, for chaining.
+ * @see #setFormatWidth
+ */
+ public IProperties setPadPosition(PadPosition padPosition);
+ }
+
+ public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space
+
+ public static boolean usePadding(IProperties properties) {
+ return properties.getFormatWidth() != IProperties.DEFAULT_FORMAT_WIDTH;
+ }
+
+ public static AfterFormat getInstance(IProperties properties) {
+ return new PaddingFormat(
+ properties.getFormatWidth(),
+ properties.getPadString(),
+ properties.getPadPosition());
+ }
+
+ // Properties
+ private final int paddingWidth;
+ private final String paddingString;
+ private final PadPosition paddingLocation;
+
+ private PaddingFormat(
+ int paddingWidth, String paddingString, PadPosition paddingLocation) {
+ this.paddingWidth = paddingWidth > 0 ? paddingWidth : 10; // TODO: Is this a sensible default?
+ this.paddingString = paddingString != null ? paddingString : FALLBACK_PADDING_STRING;
+ this.paddingLocation =
+ paddingLocation != null ? paddingLocation : PadPosition.BEFORE_PREFIX;
+ }
+
+ @Override
+ public int after(ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) {
+
+ // TODO: Count code points instead of code units?
+ int requiredPadding = paddingWidth - (rightIndex - leftIndex) - mods.totalLength();
+
+ if (requiredPadding <= 0) {
+ // Skip padding, but still apply modifiers to be consistent
+ return mods.applyAll(string, leftIndex, rightIndex);
+ }
+
+ int length = 0;
+ if (paddingLocation == PadPosition.AFTER_PREFIX) {
+ length += addPadding(requiredPadding, string, leftIndex);
+ } else if (paddingLocation == PadPosition.BEFORE_SUFFIX) {
+ length += addPadding(requiredPadding, string, rightIndex);
+ }
+ length += mods.applyAll(string, leftIndex, rightIndex + length);
+ if (paddingLocation == PadPosition.BEFORE_PREFIX) {
+ length += addPadding(requiredPadding, string, leftIndex);
+ } else if (paddingLocation == PadPosition.AFTER_SUFFIX) {
+ length += addPadding(requiredPadding, string, rightIndex + length);
+ }
+
+ return length;
+ }
+
+ private int addPadding(int requiredPadding, NumberStringBuilder string, int index) {
+ for (int i = 0; i < requiredPadding; i++) {
+ string.insert(index, paddingString, null);
+ }
+ return paddingString.length() * requiredPadding;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setFormatWidth(paddingWidth);
+ properties.setPadString(paddingString);
+ properties.setPadPosition(paddingLocation);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.NumberFormat.Field;
+
+public class PositiveDecimalFormat implements Format.TargetFormat {
+
+ public static interface IProperties extends CurrencyFormat.IProperties {
+
+ static int DEFAULT_GROUPING_SIZE = -1;
+
+ /** @see #setGroupingSize */
+ public int getGroupingSize();
+
+ /**
+ * Sets the number of digits between grouping separators. For example, the <em>en-US</em> locale
+ * uses a grouping size of 3, so the number 1234567 would be formatted as "1,234,567". For
+ * locales whose grouping sizes vary with magnitude, see {@link #setSecondaryGroupingSize(int)}.
+ *
+ * @param groupingSize The primary grouping size.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setGroupingSize(int groupingSize);
+
+ static int DEFAULT_SECONDARY_GROUPING_SIZE = -1;
+
+ /** @see #setSecondaryGroupingSize */
+ public int getSecondaryGroupingSize();
+
+ /**
+ * Sets the number of digits between grouping separators higher than the least-significant
+ * grouping separator. For example, the locale <em>hi</em> uses a primary grouping size of 3 and
+ * a secondary grouping size of 2, so the number 1234567 would be formatted as "12,34,567".
+ *
+ * <p>The two levels of grouping separators can be specified in the pattern string. For example,
+ * the <em>hi</em> locale's default decimal format pattern is "#,##,##0.###".
+ *
+ * @param secondaryGroupingSize The secondary grouping size.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setSecondaryGroupingSize(int secondaryGroupingSize);
+
+ static boolean DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN = false;
+
+ /** @see #setDecimalSeparatorAlwaysShown */
+ public boolean getDecimalSeparatorAlwaysShown();
+
+ /**
+ * Sets whether to always show the decimal point, even if the number doesn't require one. For
+ * example, if always show decimal is true, the number 123 would be formatted as "123." in
+ * locale <em>en-US</em>.
+ *
+ * @param decimalSeparatorAlwaysShown Whether to show the decimal point when it is optional.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setDecimalSeparatorAlwaysShown(boolean decimalSeparatorAlwaysShown);
+
+ static int DEFAULT_MINIMUM_GROUPING_DIGITS = 1;
+
+ /** @see #setMinimumGroupingDigits */
+ public int getMinimumGroupingDigits();
+
+ /**
+ * Sets the minimum number of digits required to be beyond the first grouping separator in order
+ * to enable grouping. For example, if the minimum grouping digits is 2, then 1234 would be
+ * formatted as "1234" but 12345 would be formatted as "12,345" in <em>en-US</em>. Note that
+ * 1234567 would still be formatted as "1,234,567", not "1234,567".
+ *
+ * @param minimumGroupingDigits How many digits must appear before a grouping separator before
+ * enabling grouping.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMinimumGroupingDigits(int minimumGroupingDigits);
+ }
+
+ public static boolean useGrouping(IProperties properties) {
+ return properties.getGroupingSize() != IProperties.DEFAULT_GROUPING_SIZE
+ || properties.getSecondaryGroupingSize() != IProperties.DEFAULT_SECONDARY_GROUPING_SIZE;
+ }
+
+ public static boolean allowsDecimalPoint(IProperties properties) {
+ return properties.getDecimalSeparatorAlwaysShown() || properties.getMaximumFractionDigits() != 0;
+ }
+
+ // Properties
+ private final boolean alwaysShowDecimal;
+ private final int groupingSize;
+ private final int secondaryGroupingSize;
+ private final int minimumGroupingDigits;
+
+ // Symbols
+ private final String infinityString;
+ private final String nanString;
+ private final String groupingSeparator;
+ private final String decimalSeparator;
+ private final String[] digitStrings;
+ private final int codePointZero;
+
+ public PositiveDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) {
+ groupingSize =
+ (properties.getGroupingSize() < 0)
+ ? properties.getSecondaryGroupingSize()
+ : properties.getGroupingSize();
+ secondaryGroupingSize =
+ (properties.getSecondaryGroupingSize() < 0)
+ ? properties.getGroupingSize()
+ : properties.getSecondaryGroupingSize();
+
+ minimumGroupingDigits = properties.getMinimumGroupingDigits();
+ alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+ infinityString = symbols.getInfinity();
+ nanString = symbols.getNaN();
+
+ if (CurrencyFormat.useCurrency(properties)) {
+ groupingSeparator = symbols.getMonetaryGroupingSeparatorString();
+ decimalSeparator = symbols.getMonetaryDecimalSeparatorString();
+ } else {
+ groupingSeparator = symbols.getGroupingSeparatorString();
+ decimalSeparator = symbols.getDecimalSeparatorString();
+ }
+
+ // Check to see if we can use code points instead of strings (~15% format performance boost)
+ int _codePointZero = -1;
+ String[] _digitStrings = symbols.getDigitStringsLocal();
+ for (int i = 0; i < _digitStrings.length; i++) {
+ int cp = Character.codePointAt(_digitStrings[i], 0);
+ int cc = Character.charCount(cp);
+ if (cc != _digitStrings[i].length()) {
+ _codePointZero = -1;
+ break;
+ } else if (i == 0) {
+ _codePointZero = cp;
+ } else if (cp != _codePointZero + i) {
+ _codePointZero = -1;
+ break;
+ }
+ }
+ if (_codePointZero != -1) {
+ digitStrings = null;
+ codePointZero = _codePointZero;
+ } else {
+ digitStrings = symbols.getDigitStrings(); // makes a copy
+ codePointZero = -1;
+ }
+ }
+
+ @Override
+ public int target(FormatQuantity input, NumberStringBuilder string, int startIndex) {
+ int length = 0;
+
+ if (input.isInfinite()) {
+ length += string.insert(startIndex, infinityString, NumberFormat.Field.INTEGER);
+
+ } else if (input.isNaN()) {
+ length += string.insert(startIndex, nanString, NumberFormat.Field.INTEGER);
+
+ } else {
+ // Add the integer digits
+ length += addIntegerDigits(input, string, startIndex);
+
+ // Add the decimal point
+ if (input.getLowerDisplayMagnitude() < 0 || alwaysShowDecimal) {
+ length += string.insert(startIndex + length, decimalSeparator, NumberFormat.Field.DECIMAL_SEPARATOR);
+ }
+
+ // Add the fraction digits
+ length += addFractionDigits(input, string, startIndex + length);
+ }
+
+ return length;
+ }
+
+ private int addIntegerDigits(FormatQuantity input, NumberStringBuilder string, int startIndex) {
+ int length = 0;
+ int integerCount = input.getUpperDisplayMagnitude() + 1;
+ for (int i = 0; i < integerCount; i++) {
+ // Add grouping separator
+ if (groupingSize > 0 && i == groupingSize && integerCount - i >= minimumGroupingDigits) {
+ length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR);
+ } else if (secondaryGroupingSize > 0
+ && i > groupingSize
+ && (i - groupingSize) % secondaryGroupingSize == 0) {
+ length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR);
+ }
+
+ // Get and append the next digit value
+ byte nextDigit = input.getDigit(i);
+ length += addDigit(nextDigit, string, startIndex, NumberFormat.Field.INTEGER);
+ }
+
+ return length;
+ }
+
+ private int addFractionDigits(FormatQuantity input, NumberStringBuilder string, int index) {
+ int length = 0;
+ int fractionCount = -input.getLowerDisplayMagnitude();
+ for (int i = 0; i < fractionCount; i++) {
+ // Get and append the next digit value
+ byte nextDigit = input.getDigit(-i - 1);
+ length += addDigit(nextDigit, string, index + length, NumberFormat.Field.FRACTION);
+ }
+ return length;
+ }
+
+ private int addDigit(byte digit, NumberStringBuilder outputString, int index, Field field) {
+ if (codePointZero != -1) {
+ return outputString.insertCodePoint(index, codePointZero + digit, field);
+ } else {
+ return outputString.insert(index, digitStrings[digit], field);
+ }
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setDecimalSeparatorAlwaysShown(alwaysShowDecimal);
+ properties.setGroupingSize(groupingSize);
+ properties.setSecondaryGroupingSize(secondaryGroupingSize);
+ properties.setMinimumGroupingDigits(minimumGroupingDigits);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.PNAffixGenerator;
+import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+
+/**
+ * The implementation of this class is a thin wrapper around {@link PNAffixGenerator}, a utility
+ * used by this and other classes, including {@link CompactDecimalFormat} and {@link Parse}, to
+ * efficiently convert from the abstract properties in the property bag to actual prefix and suffix
+ * strings.
+ */
+
+/**
+ * This class is responsible for adding the positive/negative prefixes and suffixes from the decimal
+ * format pattern. Properties are set using the following methods:
+ *
+ * <ul>
+ * <li>{@link IProperties#setPositivePrefix(String)}
+ * <li>{@link IProperties#setPositiveSuffix(String)}
+ * <li>{@link IProperties#setNegativePrefix(String)}
+ * <li>{@link IProperties#setNegativeSuffix(String)}
+ * <li>{@link IProperties#setPositivePrefixPattern(String)}
+ * <li>{@link IProperties#setPositiveSuffixPattern(String)}
+ * <li>{@link IProperties#setNegativePrefixPattern(String)}
+ * <li>{@link IProperties#setNegativeSuffixPattern(String)}
+ * </ul>
+ *
+ * If one of the first four methods is used (those of the form <code>setXxxYyy</code>), the value
+ * will be interpreted literally. If one of the second four methods is used (those of the form
+ * <code>setXxxYyyPattern</code>), locale-specific symbols for the plus sign, minus sign, percent
+ * sign, permille sign, and currency sign will be substituted into the string, according to Unicode
+ * Technical Standard #35 (LDML) section 3.2.
+ *
+ * <p>Literal characters can be used in the <code>setXxxYyyPattern</code> methods by using quotes;
+ * for example, to display a literal "%" sign, you can set the pattern <code>'%'</code>. To display
+ * a literal quote, use two quotes in a row, like <code>''</code>.
+ *
+ * <p>If a value is set in both a <code>setXxxYyy</code> method and in the corresponding <code>
+ * setXxxYyyPattern</code> method, the one set in <code>setXxxYyy</code> takes precedence.
+ *
+ * <p>For more information on formatting currencies, see {@link CurrencyFormat}.
+ *
+ * <p>The parameter is taken by reference by these methods into the property bag, meaning that if a
+ * mutable object like StringBuilder is passed, changes to the StringBuilder will be reflected in
+ * the property bag. However, upon creation of a finalized formatter object, all prefixes and
+ * suffixes will be converted to strings and will stop reflecting changes in the property bag.
+ */
+public class PositiveNegativeAffixFormat {
+
+ public static interface IProperties {
+
+ static String DEFAULT_POSITIVE_PREFIX = null;
+
+ /** @see #setPositivePrefix */
+ public String getPositivePrefix();
+
+ /**
+ * Sets the prefix to prepend to positive numbers. The prefix will be interpreted literally. For
+ * example, if you set a positive prefix of <code>p</code>, then the number 123 will be
+ * formatted as "p123" in the locale <em>en-US</em>.
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param positivePrefix The CharSequence to prepend to positive numbers.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setPositivePrefixPattern
+ */
+ public IProperties setPositivePrefix(String positivePrefix);
+
+ static String DEFAULT_POSITIVE_SUFFIX = null;
+
+ /** @see #setPositiveSuffix */
+ public String getPositiveSuffix();
+
+ /**
+ * Sets the suffix to append to positive numbers. The suffix will be interpreted literally. For
+ * example, if you set a positive suffix of <code>p</code>, then the number 123 will be
+ * formatted as "123p" in the locale <em>en-US</em>.
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param positiveSuffix The CharSequence to append to positive numbers.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setPositiveSuffixPattern
+ */
+ public IProperties setPositiveSuffix(String positiveSuffix);
+
+ static String DEFAULT_NEGATIVE_PREFIX = null;
+
+ /** @see #setNegativePrefix */
+ public String getNegativePrefix();
+
+ /**
+ * Sets the prefix to prepend to negative numbers. The prefix will be interpreted literally. For
+ * example, if you set a negative prefix of <code>n</code>, then the number -123 will be
+ * formatted as "n123" in the locale <em>en-US</em>. Note that if the negative prefix is left unset,
+ * the locale's minus sign is used.
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param negativePrefix The CharSequence to prepend to negative numbers.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setNegativePrefixPattern
+ */
+ public IProperties setNegativePrefix(String negativePrefix);
+
+ static String DEFAULT_NEGATIVE_SUFFIX = null;
+
+ /** @see #setNegativeSuffix */
+ public String getNegativeSuffix();
+
+ /**
+ * Sets the suffix to append to negative numbers. The suffix will be interpreted literally. For
+ * example, if you set a suffix prefix of <code>n</code>, then the number -123 will be formatted
+ * as "-123n" in the locale <em>en-US</em>. Note that the minus sign is prepended by default unless
+ * otherwise specified in either the pattern string or in one of the {@link #setNegativePrefix}
+ * methods.
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param negativeSuffix The CharSequence to append to negative numbers.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setNegativeSuffixPattern
+ */
+ public IProperties setNegativeSuffix(String negativeSuffix);
+
+ static String DEFAULT_POSITIVE_PREFIX_PATTERN = null;
+
+ /** @see #setPositivePrefixPattern */
+ public String getPositivePrefixPattern();
+
+ /**
+ * Sets the prefix to prepend to positive numbers. Locale-specific symbols will be substituted
+ * into the string according to Unicode Technical Standard #35 (LDML).
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param positivePrefixPattern The CharSequence to prepend to positive numbers after locale
+ * symbol substitutions take place.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setPositivePrefix
+ */
+ public IProperties setPositivePrefixPattern(String positivePrefixPattern);
+
+ static String DEFAULT_POSITIVE_SUFFIX_PATTERN = null;
+
+ /** @see #setPositiveSuffixPattern */
+ public String getPositiveSuffixPattern();
+
+ /**
+ * Sets the suffix to append to positive numbers. Locale-specific symbols will be substituted
+ * into the string according to Unicode Technical Standard #35 (LDML).
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param positiveSuffixPattern The CharSequence to append to positive numbers after locale
+ * symbol substitutions take place.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setPositiveSuffix
+ */
+ public IProperties setPositiveSuffixPattern(String positiveSuffixPattern);
+
+ static String DEFAULT_NEGATIVE_PREFIX_PATTERN = null;
+
+ /** @see #setNegativePrefixPattern */
+ public String getNegativePrefixPattern();
+
+ /**
+ * Sets the prefix to prepend to negative numbers. Locale-specific symbols will be substituted
+ * into the string according to Unicode Technical Standard #35 (LDML).
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param negativePrefixPattern The CharSequence to prepend to negative numbers after locale
+ * symbol substitutions take place.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setNegativePrefix
+ */
+ public IProperties setNegativePrefixPattern(String negativePrefixPattern);
+
+ static String DEFAULT_NEGATIVE_SUFFIX_PATTERN = null;
+
+ /** @see #setNegativeSuffixPattern */
+ public String getNegativeSuffixPattern();
+
+ /**
+ * Sets the suffix to append to negative numbers. Locale-specific symbols will be substituted
+ * into the string according to Unicode Technical Standard #35 (LDML).
+ *
+ * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+ *
+ * @param negativeSuffixPattern The CharSequence to append to negative numbers after locale
+ * symbol substitutions take place.
+ * @return The property bag, for chaining.
+ * @see PositiveNegativeAffixFormat
+ * @see #setNegativeSuffix
+ */
+ public IProperties setNegativeSuffixPattern(String negativeSuffixPattern);
+
+ static boolean DEFAULT_PLUS_SIGN_ALWAYS_SHOWN = false;
+
+ /** @see #setPlusSignAlwaysShown */
+ public boolean getPlusSignAlwaysShown();
+
+ /**
+ * Sets whether to always display of a plus sign on positive numbers.
+ *
+ * <p>If the location of the negative sign is specified by the decimal format pattern (or by the
+ * negative prefix/suffix pattern methods), a plus sign is substituted into that location, in
+ * accordance with Unicode Technical Standard #35 (LDML) section 3.2.1. Otherwise, the plus sign
+ * is prepended to the number. For example, if the decimal format pattern <code>#;#-</code> is
+ * used, then formatting 123 would result in "123+" in the locale <em>en-US</em>.
+ *
+ * <p>This method should be used <em>instead of</em> setting the positive prefix/suffix. The
+ * behavior is undefined if alwaysShowPlusSign is set but the positive prefix/suffix already
+ * contains a plus sign.
+ *
+ * @param plusSignAlwaysShown Whether positive numbers should display a plus sign.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setPlusSignAlwaysShown(boolean plusSignAlwaysShown);
+ }
+
+ public static PositiveNegativeAffixModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties);
+ return new PositiveNegativeAffixModifier(result.positive, result.negative);
+ }
+
+ // TODO: Investigate static interface methods (Java 8 only?)
+ public static void apply(
+ FormatQuantity input,
+ ModifierHolder mods,
+ DecimalFormatSymbols symbols,
+ IProperties properties) {
+ PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+ PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties);
+ if (input.isNegative()) {
+ mods.add(result.negative);
+ } else {
+ mods.add(result.positive);
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+// THIS CLASS IS A PROOF OF CONCEPT ONLY.
+// IT REQUIRES ADDITIONAL DISCUSION ABOUT ITS DESIGN AND IMPLEMENTATION.
+
+package com.ibm.icu.impl.number.formatters;
+
+import java.util.Deque;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+
+public class RangeFormat extends Format {
+ // Primary settings
+ private final String separator;
+
+ // Child formatters
+ private final Format left;
+ private final Format right;
+
+ public RangeFormat(Format left, Format right, String separator) {
+ this.separator = separator; // TODO: This would be loaded from locale data.
+ this.left = left;
+ this.right = right;
+
+ if (left == null || right == null) {
+ throw new IllegalArgumentException("Both child formatters are required for RangeFormat");
+ }
+ }
+
+ @Override
+ public int process(
+ Deque<FormatQuantity> inputs,
+ ModifierHolder mods,
+ NumberStringBuilder string,
+ int startIndex) {
+ ModifierHolder lMods = new ModifierHolder();
+ ModifierHolder rMods = new ModifierHolder();
+ int lLen = left.process(inputs, lMods, string, startIndex);
+ int rLen = right.process(inputs, rMods, string, startIndex + lLen);
+
+ // Bubble up any modifiers that are shared between the two sides
+ while (lMods.peekLast() != null && lMods.peekLast() == rMods.peekLast()) {
+ mods.add(lMods.removeLast());
+ rMods.removeLast();
+ }
+
+ // Apply the remaining modifiers
+ lLen += lMods.applyAll(string, startIndex, startIndex + lLen);
+ rLen += rMods.applyAll(string, startIndex + lLen, startIndex + lLen + rLen);
+
+ int sLen = string.insert(startIndex + lLen, separator, null);
+
+ return lLen + sLen + rLen;
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Rounder;
+import com.ibm.icu.impl.number.Rounder.IBasicRoundingProperties;
+import com.ibm.icu.impl.number.rounders.IncrementRounder;
+import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
+import com.ibm.icu.impl.number.rounders.NoRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+
+// TODO: Figure out a better place to put these methods.
+
+public class RoundingFormat {
+
+ public static interface IProperties
+ extends IBasicRoundingProperties,
+ IncrementRounder.IProperties,
+ MagnitudeRounder.IProperties,
+ SignificantDigitsRounder.IProperties {}
+
+ public static Rounder getDefaultOrNoRounder(IProperties properties) {
+ Rounder candidate = getDefaultOrNull(properties);
+ if (candidate == null) {
+ candidate = NoRounder.getInstance(properties);
+ }
+ return candidate;
+ }
+
+ public static Rounder getDefaultOrNull(IProperties properties) {
+ if (SignificantDigitsRounder.useSignificantDigits(properties)) {
+ return SignificantDigitsRounder.getInstance(properties);
+ } else if (IncrementRounder.useRoundingIncrement(properties)) {
+ return IncrementRounder.getInstance(properties);
+ } else if (MagnitudeRounder.useFractionFormat(properties)) {
+ return MagnitudeRounder.getInstance(properties);
+ } else {
+ return null;
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantitySelector;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import com.ibm.icu.impl.number.rounders.IncrementRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+
+public class ScientificFormat extends Format.BeforeFormat implements Rounder.MultiplierGenerator {
+
+ public static interface IProperties
+ extends RoundingFormat.IProperties, CurrencyFormat.IProperties {
+
+ static boolean DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN = false;
+
+ /** @see #setExponentSignAlwaysShown */
+ public boolean getExponentSignAlwaysShown();
+
+ /**
+ * Sets whether to show the plus sign in the exponent part of numbers with a zero or positive
+ * exponent. For example, the number "1200" with the pattern "0.0E0" would be formatted as
+ * "1.2E+3" instead of "1.2E3" in <em>en-US</em>.
+ *
+ * @param exponentSignAlwaysShown Whether to show the plus sign in positive exponents.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setExponentSignAlwaysShown(boolean exponentSignAlwaysShown);
+
+ static int DEFAULT_MINIMUM_EXPONENT_DIGITS = -1;
+
+ /** @see #setMinimumExponentDigits */
+ public int getMinimumExponentDigits();
+
+ /**
+ * Sets the minimum number of digits to display in the exponent. For example, the number "1200"
+ * with the pattern "0.0E00", which has 2 exponent digits, would be formatted as "1.2E03" in
+ * <em>en-US</em>.
+ *
+ * @param minimumExponentDigits The minimum number of digits to display in the exponent field.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMinimumExponentDigits(int minimumExponentDigits);
+
+ @Override
+ public IProperties clone();
+ }
+
+ public static boolean useScientificNotation(IProperties properties) {
+ return properties.getMinimumExponentDigits() != IProperties.DEFAULT_MINIMUM_EXPONENT_DIGITS;
+ }
+
+ private static final ThreadLocal<Properties> threadLocalProperties =
+ new ThreadLocal<Properties>() {
+ @Override
+ protected Properties initialValue() {
+ return new Properties();
+ }
+ };
+
+ public static ScientificFormat getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+ // If significant digits or rounding interval are specified through normal means, we use those.
+ // Otherwise, we use the special significant digit rules for scientific notation.
+ Rounder rounder;
+ if (IncrementRounder.useRoundingIncrement(properties)) {
+ rounder = IncrementRounder.getInstance(properties);
+ } else if (SignificantDigitsRounder.useSignificantDigits(properties)) {
+ rounder = SignificantDigitsRounder.getInstance(properties);
+ } else {
+ Properties rprops = threadLocalProperties.get().clear();
+
+ int minInt = properties.getMinimumIntegerDigits();
+ int maxInt = properties.getMaximumIntegerDigits();
+ int minFrac = properties.getMinimumFractionDigits();
+ int maxFrac = properties.getMaximumFractionDigits();
+
+ // If currency is in use, pull information from CurrencyUsage.
+ if (CurrencyFormat.useCurrency(properties)) {
+ // Use rprops as the vehicle (it is still clean)
+ CurrencyFormat.populateCurrencyRounderProperties(rprops, symbols, properties);
+ minFrac = rprops.getMinimumFractionDigits();
+ maxFrac = rprops.getMaximumFractionDigits();
+ rprops.clear();
+ }
+
+ // TODO: Mark/Andy, take a look at this logic and see if it makes sense to you.
+ // I fiddled with the settings and fallbacks to make the unit tests pass, but I
+ // don't feel that it's the "right way" to do things.
+
+ if (minInt < 0) minInt = 0;
+ if (maxInt < minInt) maxInt = minInt;
+ if (minFrac < 0) minFrac = 0;
+ if (maxFrac < minFrac) maxFrac = minFrac;
+
+ rprops.setRoundingMode(properties.getRoundingMode());
+
+ if (minInt == 0 && maxFrac == 0) {
+ // Special case for the pattern "#E0" with no significant digits specified.
+ rprops.setMinimumSignificantDigits(1);
+ rprops.setMaximumSignificantDigits(Integer.MAX_VALUE);
+ } else if (minInt == 0 && minFrac == 0) {
+ // Special case for patterns like "#.##E0" with no significant digits specified.
+ rprops.setMinimumSignificantDigits(1);
+ rprops.setMaximumSignificantDigits(1 + maxFrac);
+ } else {
+ rprops.setMinimumSignificantDigits(minInt + minFrac);
+ rprops.setMaximumSignificantDigits(minInt + maxFrac);
+ }
+ rprops.setMinimumIntegerDigits(maxInt == 0 ? 0 : Math.max(1, minInt + minFrac - maxFrac));
+ rprops.setMaximumIntegerDigits(maxInt);
+ rprops.setMinimumFractionDigits(Math.max(0, minFrac + minInt - maxInt));
+ rprops.setMaximumFractionDigits(maxFrac);
+ rounder = SignificantDigitsRounder.getInstance(rprops);
+ }
+
+ return new ScientificFormat(symbols, properties, rounder);
+ }
+
+ public static ScientificFormat getInstance(
+ DecimalFormatSymbols symbols, IProperties properties, Rounder rounder) {
+ return new ScientificFormat(symbols, properties, rounder);
+ }
+
+ // Properties
+ private final boolean exponentShowPlusSign;
+ private final int exponentDigits;
+ private final int minInt;
+ private final int maxInt;
+ private final int interval;
+ private final Rounder rounder;
+ private final ConstantAffixModifier separatorMod;
+ private final PositiveNegativeAffixModifier signMod;
+
+ // Symbols
+ private final String[] digitStrings;
+
+ private ScientificFormat(DecimalFormatSymbols symbols, IProperties properties, Rounder rounder) {
+ exponentShowPlusSign = properties.getExponentSignAlwaysShown();
+ exponentDigits = Math.max(1, properties.getMinimumExponentDigits());
+ int _maxInt = properties.getMaximumIntegerDigits();
+ int _minInt = properties.getMinimumIntegerDigits();
+ // Special behavior:
+ if (_maxInt > 8) {
+ _maxInt = _minInt;
+ }
+ maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt;
+ minInt = _minInt < 0 ? 0 : _minInt < maxInt ? _minInt : maxInt;
+ interval = Math.max(1, maxInt);
+ this.rounder = rounder;
+ digitStrings = symbols.getDigitStrings(); // makes a copy
+
+ separatorMod =
+ new ConstantAffixModifier(
+ "", symbols.getExponentSeparator(), NumberFormat.Field.EXPONENT_SYMBOL, true);
+ signMod =
+ new PositiveNegativeAffixModifier(
+ new ConstantAffixModifier(
+ "",
+ exponentShowPlusSign ? symbols.getPlusSignString() : "",
+ NumberFormat.Field.EXPONENT_SIGN,
+ true),
+ new ConstantAffixModifier(
+ "", symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN, true));
+ }
+
+ private static final ThreadLocal<StringBuilder> threadLocalStringBuilder =
+ new ThreadLocal<StringBuilder>() {
+ @Override
+ protected StringBuilder initialValue() {
+ return new StringBuilder();
+ }
+ };
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+
+ // Treat zero as if it had magnitude 0
+ int exponent;
+ if (input.isZero()) {
+ rounder.apply(input);
+ exponent = 0;
+ } else {
+ exponent = -rounder.chooseMultiplierAndApply(input, this);
+ }
+
+ // Format the exponent part of the scientific format.
+ // Insert digits starting from the left so that append can be used.
+ // TODO: Use thread locals here.
+ FormatQuantity exponentQ = FormatQuantitySelector.from(exponent);
+ StringBuilder exponentSB = threadLocalStringBuilder.get();
+ exponentSB.setLength(0);
+ exponentQ.setIntegerFractionLength(exponentDigits, Integer.MAX_VALUE, 0, 0);
+ for (int i = exponentQ.getUpperDisplayMagnitude(); i >= 0; i--) {
+ exponentSB.append(digitStrings[exponentQ.getDigit(i)]);
+ }
+
+ // Add modifiers from the outside in.
+ mods.add(
+ new ConstantAffixModifier("", exponentSB.toString(), NumberFormat.Field.EXPONENT, true));
+ mods.add(signMod.getModifier(exponent < 0));
+ mods.add(separatorMod);
+ }
+
+ @Override
+ public int getMultiplier(int magnitude) {
+ int digitsShown = ((magnitude % interval + interval) % interval) + 1;
+ if (digitsShown < minInt) {
+ digitsShown = minInt;
+ } else if (digitsShown > maxInt) {
+ digitsShown = maxInt;
+ }
+ int retval = digitsShown - magnitude - 1;
+ return retval;
+ }
+
+ @Override
+ public void export(Properties properties) {
+ properties.setMinimumExponentDigits(exponentDigits);
+ properties.setExponentSignAlwaysShown(exponentShowPlusSign);
+
+ // Set the transformed object into the property bag. This may result in a pattern string that
+ // uses different syntax from the original, but it will be functionally equivalent.
+ rounder.export(properties);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import java.util.Deque;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+
+// TODO: This class isn't currently being used anywhere. Consider removing it.
+
+/** Attaches all prefixes and suffixes at this point in the render tree without bubbling up. */
+public class StrongAffixFormat extends Format implements Format.AfterFormat {
+ private final Format child;
+
+ public StrongAffixFormat(Format child) {
+ this.child = child;
+
+ if (child == null) {
+ throw new IllegalArgumentException("A child formatter is required for StrongAffixFormat");
+ }
+ }
+
+ @Override
+ public int process(
+ Deque<FormatQuantity> inputs,
+ ModifierHolder mods,
+ NumberStringBuilder string,
+ int startIndex) {
+ int length = child.process(inputs, mods, string, startIndex);
+ length += mods.applyAll(string, startIndex, startIndex + length);
+ return length;
+ }
+
+ @Override
+ public int after(
+ ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) {
+ return mods.applyAll(string, leftIndex, rightIndex);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ // Nothing to do.
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.Modifier.AffixModifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/** The canonical implementation of {@link Modifier}, containing a prefix and suffix string. */
+public class ConstantAffixModifier extends Modifier.BaseModifier implements AffixModifier {
+
+ // TODO: Avoid making a new instance by default if prefix and suffix are empty
+ public static final AffixModifier EMPTY = new ConstantAffixModifier();
+
+ private final String prefix;
+ private final String suffix;
+ private final Field field;
+ private final boolean strong;
+
+ /**
+ * Constructs an instance with the given strings.
+ *
+ * <p>The arguments need to be Strings, not CharSequences, because Strings are immutable but
+ * CharSequences are not.
+ *
+ * @param prefix The prefix string.
+ * @param suffix The suffix string.
+ * @param field The field type to be associated with this modifier. Can be null.
+ * @param strong Whether this modifier should be strongly applied.
+ * @see Field
+ */
+ public ConstantAffixModifier(String prefix, String suffix, Field field, boolean strong) {
+ // Use an empty string instead of null if we are given null
+ // TODO: Consider returning a null modifier if both prefix and suffix are empty.
+ this.prefix = (prefix == null ? "" : prefix);
+ this.suffix = (suffix == null ? "" : suffix);
+ this.field = field;
+ this.strong = strong;
+ }
+
+ /**
+ * Constructs a new instance with an empty prefix, suffix, and field.
+ */
+ public ConstantAffixModifier() {
+ prefix = "";
+ suffix = "";
+ field = null;
+ strong = false;
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ // Insert the suffix first since inserting the prefix will change the rightIndex
+ int length = output.insert(rightIndex, suffix, field);
+ length += output.insert(leftIndex, prefix, field);
+ return length;
+ }
+
+ @Override
+ public int length() {
+ return prefix.length() + suffix.length();
+ }
+
+ @Override
+ public boolean isStrong() {
+ return strong;
+ }
+
+ @Override
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @Override
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public boolean contentEquals(CharSequence _prefix, CharSequence _suffix) {
+ if (_prefix == null && !prefix.isEmpty()) return false;
+ if (_suffix == null && !suffix.isEmpty()) return false;
+ if (prefix.length() != _prefix.length()) return false;
+ if (suffix.length() != _suffix.length()) return false;
+ for (int i = 0; i < prefix.length(); i++) {
+ if (prefix.charAt(i) != _prefix.charAt(i)) return false;
+ }
+ for (int i = 0; i < suffix.length(); i++) {
+ if (suffix.charAt(i) != _suffix.charAt(i)) return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "<ConstantAffixModifier(%d) prefix:'%s' suffix:'%s'>", length(), prefix, suffix);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ throw new UnsupportedOperationException();
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.Modifier.AffixModifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/**
+ * An implementation of {@link Modifier} that allows for multiple types of fields in the same
+ * modifier. Constructed based on the contents of two {@link NumberStringBuilder} instances (one for
+ * the prefix, one for the suffix).
+ */
+public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements AffixModifier {
+
+ // TODO: Avoid making a new instance by default if prefix and suffix are empty
+ public static final ConstantMultiFieldModifier EMPTY = new ConstantMultiFieldModifier();
+
+ private final char[] prefixChars;
+ private final char[] suffixChars;
+ private final Field[] prefixFields;
+ private final Field[] suffixFields;
+ private final String prefix;
+ private final String suffix;
+ private final boolean strong;
+
+ public ConstantMultiFieldModifier(
+ NumberStringBuilder prefix, NumberStringBuilder suffix, boolean strong) {
+ prefixChars = prefix.toCharArray();
+ suffixChars = suffix.toCharArray();
+ prefixFields = prefix.toFieldArray();
+ suffixFields = suffix.toFieldArray();
+ this.prefix = new String(prefixChars);
+ this.suffix = new String(suffixChars);
+ this.strong = strong;
+ }
+
+ private ConstantMultiFieldModifier() {
+ prefixChars = new char[0];
+ suffixChars = new char[0];
+ prefixFields = new Field[0];
+ suffixFields = new Field[0];
+ prefix = "";
+ suffix = "";
+ strong = false;
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ // Insert the suffix first since inserting the prefix will change the rightIndex
+ int length = output.insert(rightIndex, suffixChars, suffixFields);
+ length += output.insert(leftIndex, prefixChars, prefixFields);
+ return length;
+ }
+
+ @Override
+ public int length() {
+ return prefixChars.length + suffixChars.length;
+ }
+
+ @Override
+ public boolean isStrong() {
+ return strong;
+ }
+
+ @Override
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @Override
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public boolean contentEquals(NumberStringBuilder prefix, NumberStringBuilder suffix) {
+ return prefix.contentEquals(prefixChars, prefixFields)
+ && suffix.contentEquals(suffixChars, suffixFields);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "<ConstantMultiFieldModifier(%d) prefix:'%s' suffix:'%s'>", length(), prefix, suffix);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ throw new UnsupportedOperationException();
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.PluralRules;
+
+// TODO: Is it okay that this class is not completely immutable? Right now it is internal-only.
+// Freezable or Builder could be used if necessary.
+
+/**
+ * A basic implementation of {@link com.ibm.icu.impl.number.Modifier.PositiveNegativePluralModifier}
+ * that is built on the fly using its <code>put</code> methods.
+ */
+public class GeneralPluralModifier extends Format.BeforeFormat
+ implements Modifier.PositiveNegativePluralModifier {
+ /**
+ * A single array for modifiers. Even elements are positive; odd elements are negative. The
+ * elements 2i and 2i+1 belong to the StandardPlural with ordinal i.
+ */
+ private final Modifier[] mods;
+
+ public GeneralPluralModifier() {
+ this.mods = new Modifier[StandardPlural.COUNT * 2];
+ }
+
+ /** Adds a positive/negative-agnostic modifier for the specified plural form. */
+ public void put(StandardPlural plural, Modifier modifier) {
+ put(plural, modifier, modifier);
+ }
+
+ /** Adds a positive and a negative modifier for the specified plural form. */
+ public void put(StandardPlural plural, Modifier positive, Modifier negative) {
+ assert mods[plural.ordinal() * 2] == null;
+ assert mods[plural.ordinal() * 2 + 1] == null;
+ assert positive != null;
+ assert negative != null;
+ mods[plural.ordinal() * 2] = positive;
+ mods[plural.ordinal() * 2 + 1] = negative;
+ }
+
+ @Override
+ public Modifier getModifier(StandardPlural plural, boolean isNegative) {
+ Modifier mod = mods[plural.ordinal() * 2 + (isNegative ? 1 : 0)];
+ if (mod == null) {
+ mod = mods[StandardPlural.OTHER.ordinal()*2 + (isNegative ? 1 : 0)];
+ }
+ if (mod == null) {
+ throw new UnsupportedOperationException();
+ }
+ return mod;
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+ mods.add(getModifier(input.getStandardPlural(rules), input.isNegative()));
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void export(Properties properties) {
+ // Since we can export only one affix pair, do the one for "other".
+ Modifier positive = getModifier(StandardPlural.OTHER, false);
+ Modifier negative = getModifier(StandardPlural.OTHER, true);
+ PositiveNegativeAffixModifier.exportPositiveNegative(properties, positive, negative);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.Modifier.AffixModifier;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+
+/** A class containing a positive form and a negative form of {@link ConstantAffixModifier}. */
+public class PositiveNegativeAffixModifier extends Format.BeforeFormat
+ implements Modifier.PositiveNegativeModifier {
+ private final AffixModifier positive;
+ private final AffixModifier negative;
+
+ /**
+ * Constructs an instance using the two {@link ConstantMultiFieldModifier} classes for positive
+ * and negative.
+ *
+ * @param positive The positive-form Modifier.
+ * @param negative The negative-form Modifier.
+ */
+ public PositiveNegativeAffixModifier(AffixModifier positive, AffixModifier negative) {
+ this.positive = positive;
+ this.negative = negative;
+ }
+
+ @Override
+ public Modifier getModifier(boolean isNegative) {
+ return isNegative ? negative : positive;
+ }
+
+ @Override
+ public void before(FormatQuantity input, ModifierHolder mods) {
+ Modifier mod = getModifier(input.isNegative());
+ mods.add(mod);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ exportPositiveNegative(properties, positive, negative);
+ }
+
+ /** Internal method used to export a positive and negative modifier to a property bag. */
+ static void exportPositiveNegative(Properties properties, Modifier positive, Modifier negative) {
+ properties.setPositivePrefix(positive.getPrefix().isEmpty() ? null : positive.getPrefix());
+ properties.setPositiveSuffix(positive.getSuffix().isEmpty() ? null : positive.getSuffix());
+ properties.setNegativePrefix(negative.getPrefix().isEmpty() ? null : negative.getPrefix());
+ properties.setNegativeSuffix(negative.getSuffix().isEmpty() ? null : negative.getSuffix());
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.SimpleFormatterImpl;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/**
+ * The second primary implementation of {@link Modifier}, this one consuming a {@link
+ * com.ibm.icu.text.SimpleFormatter} pattern.
+ */
+public class SimpleModifier extends Modifier.BaseModifier {
+ private final String compiledPattern;
+ private final Field field;
+ private final boolean strong;
+
+ /** Creates a modifier that uses the SimpleFormatter string formats. */
+ public SimpleModifier(String compiledPattern, Field field, boolean strong) {
+ this.compiledPattern = (compiledPattern == null) ? "\u0001\u0000" : compiledPattern;
+ this.field = field;
+ this.strong = strong;
+ }
+
+ @Override
+ public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+ return formatAsPrefixSuffix(compiledPattern, output, leftIndex, rightIndex, field);
+ }
+
+ @Override
+ public int length() {
+ // TODO: Make a separate method for computing the length only?
+ return formatAsPrefixSuffix(compiledPattern, null, -1, -1, field);
+ }
+
+ @Override
+ public boolean isStrong() {
+ return strong;
+ }
+
+ @Override
+ public String getPrefix() {
+ // TODO: Implement this when MeasureFormat is ready.
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getSuffix() {
+ // TODO: Implement this when MeasureFormat is ready.
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is
+ * because DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it
+ * should not depend on it.
+ *
+ * <p>Formats a value that is already stored inside the StringBuilder <code>result</code> between
+ * the indices <code>startIndex</code> and <code>endIndex</code> by inserting characters before
+ * the start index and after the end index.
+ *
+ * <p>This is well-defined only for patterns with exactly one argument.
+ *
+ * @param compiledPattern Compiled form of a pattern string.
+ * @param result The StringBuilder containing the value argument.
+ * @param startIndex The left index of the value within the string builder.
+ * @param endIndex The right index of the value within the string builder.
+ * @return The number of characters (UTF-16 code points) that were added to the StringBuilder.
+ */
+ public static int formatAsPrefixSuffix(
+ String compiledPattern,
+ NumberStringBuilder result,
+ int startIndex,
+ int endIndex,
+ Field field) {
+ assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 1;
+ int ARG_NUM_LIMIT = 0x100;
+ int length = 0, offset = 2;
+ if (compiledPattern.charAt(1) != '\u0000') {
+ int prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT;
+ if (result != null) {
+ result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field);
+ }
+ length += prefixLength;
+ offset = 3 + prefixLength;
+ }
+ if (offset < compiledPattern.length()) {
+ int suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT;
+ if (result != null) {
+ result.insert(
+ endIndex + length, compiledPattern, offset + 1, offset + suffixLength + 1, field);
+ }
+ length += suffixLength;
+ }
+ return length;
+ }
+
+ /** TODO: Move this to a test file somewhere, once we figure out what to do with the method. */
+ public static void testFormatAsPrefixSuffix() {
+ String[] patterns = {"{0}", "X{0}Y", "XX{0}YYY", "{0}YY", "XXXX{0}"};
+ Object[][] outputs = {{"", 0, 0}, {"abcde", 0, 0}, {"abcde", 2, 2}, {"abcde", 1, 3}};
+ String[][] expecteds = {
+ {"", "XY", "XXYYY", "YY", "XXXX"},
+ {"abcde", "XYabcde", "XXYYYabcde", "YYabcde", "XXXXabcde"},
+ {"abcde", "abXYcde", "abXXYYYcde", "abYYcde", "abXXXXcde"},
+ {"abcde", "aXbcYde", "aXXbcYYYde", "abcYYde", "aXXXXbcde"}
+ };
+ for (int i = 0; i < patterns.length; i++) {
+ for (int j = 0; j < outputs.length; j++) {
+ String pattern = patterns[i];
+ String compiledPattern =
+ SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, new StringBuilder(), 1, 1);
+ NumberStringBuilder output = new NumberStringBuilder();
+ output.append((String) outputs[j][0], null);
+ formatAsPrefixSuffix(
+ compiledPattern, output, (Integer) outputs[j][1], (Integer) outputs[j][2], null);
+ String expected = expecteds[j][i];
+ String actual = output.toString();
+ assert expected.equals(actual);
+ }
+ }
+ }
+
+ @Override
+ public void export(Properties properties) {
+ throw new UnsupportedOperationException();
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.rounders;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+
+public class IncrementRounder extends Rounder {
+
+ public static interface IProperties extends IBasicRoundingProperties {
+
+ static BigDecimal DEFAULT_ROUNDING_INCREMENT = null;
+
+ /** @see #setRoundingIncrement */
+ public BigDecimal getRoundingIncrement();
+
+ /**
+ * Sets the increment to which to round numbers. For example, with a rounding interval of 0.05,
+ * the number 11.17 would be formatted as "11.15" in locale <em>en-US</em> with the default
+ * rounding mode.
+ *
+ * <p>You can use either a rounding increment or significant digits, but not both at the same
+ * time.
+ *
+ * <p>The rounding increment can be specified in a pattern string. For example, the pattern
+ * "#,##0.05" corresponds to a rounding interval of 0.05 with 1 minimum integer digit and a
+ * grouping size of 3.
+ *
+ * @param roundingIncrement The interval to which to round.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setRoundingIncrement(BigDecimal roundingIncrement);
+ }
+
+ public static boolean useRoundingIncrement(IProperties properties) {
+ return properties.getRoundingIncrement() != IProperties.DEFAULT_ROUNDING_INCREMENT;
+ }
+
+ private final BigDecimal roundingIncrement;
+
+ public static IncrementRounder getInstance(IProperties properties) {
+ return new IncrementRounder(properties);
+ }
+
+ private IncrementRounder(IProperties properties) {
+ super(properties);
+ if (properties.getRoundingIncrement().compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Rounding interval must be greater than zero");
+ }
+ roundingIncrement = properties.getRoundingIncrement();
+ }
+
+ @Override
+ public void apply(FormatQuantity input) {
+ input.roundToIncrement(roundingIncrement, mathContext);
+ applyDefaults(input);
+ }
+
+ @Override
+ public void export(Properties properties) {
+ super.export(properties);
+ properties.setRoundingIncrement(roundingIncrement);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.rounders;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Rounder;
+
+public class MagnitudeRounder extends Rounder {
+
+ public static interface IProperties extends IBasicRoundingProperties {}
+
+ public static boolean useFractionFormat(IProperties properties) {
+ return properties.getMinimumFractionDigits() != IProperties.DEFAULT_MINIMUM_FRACTION_DIGITS
+ || properties.getMaximumFractionDigits() != IProperties.DEFAULT_MAXIMUM_FRACTION_DIGITS;
+ }
+
+ public static MagnitudeRounder getInstance(IBasicRoundingProperties properties) {
+ return new MagnitudeRounder(properties);
+ }
+
+ private MagnitudeRounder(IBasicRoundingProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ public void apply(FormatQuantity input) {
+ input.roundToMagnitude(-maxFrac, mathContext);
+ applyDefaults(input);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.rounders;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Rounder;
+
+/** Sets the integer and fraction length based on the properties, but does not perform rounding. */
+public final class NoRounder extends Rounder {
+
+ public static NoRounder getInstance(IBasicRoundingProperties properties) {
+ return new NoRounder(properties);
+ }
+
+ private NoRounder(IBasicRoundingProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ public void apply(FormatQuantity input) {
+ applyDefaults(input);
+ input.roundToInfinity();
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.rounders;
+
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+
+public class SignificantDigitsRounder extends Rounder {
+
+ /**
+ * Sets whether the minimum significant digits should override the maximum integer and fraction
+ * digits. This affects both display and rounding. Default is true.
+ *
+ * <p>For example, if this option is enabled, formatting the number 4.567 with 3 min/max
+ * significant digits against the pattern "0.0" (1 min/max fraction digits) will result in "4.57"
+ * in locale <em>en-US</em> with the default rounding mode. If this option is disabled, the max
+ * fraction digits take priority instead, and the output will be "4.6".
+ *
+ * @param significantDigitsOverride true to ensure that the minimum significant digits are always
+ * shown; false to ensure that the maximum integer and fraction digits are obeyed.
+ * @return The property bag, for chaining.
+ */
+ public static enum SignificantDigitsMode {
+ OVERRIDE_MAXIMUM_FRACTION,
+ RESPECT_MAXIMUM_FRACTION,
+ ENSURE_MINIMUM_SIGNIFICANT
+ };
+
+ public static interface IProperties extends IBasicRoundingProperties {
+
+ static int DEFAULT_MINIMUM_SIGNIFICANT_DIGITS = -1;
+
+ /** @see #setMinimumSignificantDigits */
+ public int getMinimumSignificantDigits();
+
+ /**
+ * Sets the minimum number of significant digits to display. If, after rounding to the number of
+ * significant digits specified by {@link #setMaximumSignificantDigits}, the number of remaining
+ * significant digits is less than the minimum, the number will be padded with zeros. For
+ * example, if minimum significant digits is 3, the number 5.8 will be formatted as "5.80" in
+ * locale <em>en-US</em>. Note that minimum significant digits is relevant only when numbers
+ * have digits after the decimal point.
+ *
+ * <p>If both minimum significant digits and minimum integer/fraction digits are set at the same
+ * time, both values will be respected, and the one that results in the greater number of
+ * padding zeros will be used. For example, formatting the number 73 with 3 minimum significant
+ * digits and 2 minimum fraction digits will produce "73.00".
+ *
+ * <p>The number of significant digits can be specified in a pattern string using the '@'
+ * character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3
+ * significant digits.
+ *
+ * @param minimumSignificantDigits The minimum number of significant digits to display.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMinimumSignificantDigits(int minimumSignificantDigits);
+
+ static int DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = -1;
+
+ /** @see #setMaximumSignificantDigits */
+ public int getMaximumSignificantDigits();
+
+ /**
+ * Sets the maximum number of significant digits to display. The number of significant digits is
+ * equal to the number of digits counted from the leftmost nonzero digit through the rightmost
+ * nonzero digit; for example, the number "2010" has 3 significant digits. If the number has
+ * more significant digits than specified here, the extra significant digits will be rounded off
+ * using the rounding mode specified by {@link #setRoundingMode(RoundingMode)}. For example, if
+ * maximum significant digits is 3, the number 1234.56 will be formatted as "1230" in locale
+ * <em>en-US</em> with the default rounding mode.
+ *
+ * <p>If both maximum significant digits and maximum integer/fraction digits are set at the same
+ * time, the behavior is undefined.
+ *
+ * <p>The number of significant digits can be specified in a pattern string using the '@'
+ * character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3
+ * significant digits.
+ *
+ * @param maximumSignificantDigits The maximum number of significant digits to display.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setMaximumSignificantDigits(int maximumSignificantDigits);
+
+ static SignificantDigitsMode DEFAULT_SIGNIFICANT_DIGITS_MODE = null;
+
+ /** @see #setSignificantDigitsMode */
+ public SignificantDigitsMode getSignificantDigitsMode();
+
+ /**
+ * Sets the strategy used when reconciling significant digits versus integer and fraction
+ * lengths.
+ *
+ * @param significantDigitsMode One of the options from {@link SignificantDigitsMode}.
+ * @return The property bag, for chaining.
+ */
+ public IProperties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode);
+ }
+
+ public static boolean useSignificantDigits(IProperties properties) {
+ return properties.getMinimumSignificantDigits()
+ != IProperties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS
+ || properties.getMaximumSignificantDigits()
+ != IProperties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS
+ || properties.getSignificantDigitsMode() != IProperties.DEFAULT_SIGNIFICANT_DIGITS_MODE;
+ }
+
+ public static SignificantDigitsRounder getInstance(IProperties properties) {
+ return new SignificantDigitsRounder(properties);
+ }
+
+ private final int minSig;
+ private final int maxSig;
+ private final SignificantDigitsMode mode;
+
+ private SignificantDigitsRounder(IProperties properties) {
+ super(properties);
+ int _minSig = properties.getMinimumSignificantDigits();
+ int _maxSig = properties.getMaximumSignificantDigits();
+ minSig = _minSig < 1 ? 1 : _minSig > 1000 ? 1000 : _minSig;
+ maxSig = _maxSig < 0 ? 1000 : _maxSig < minSig ? minSig : _maxSig > 1000 ? 1000 : _maxSig;
+ SignificantDigitsMode _mode = properties.getSignificantDigitsMode();
+ mode = _mode == null ? SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION : _mode;
+ }
+
+ @Override
+ public void apply(FormatQuantity input) {
+
+ int magnitude, effectiveMag, magMinSig, magMaxSig;
+
+ if (input.isZero()) {
+ // Treat zero as if magnitude corresponded to the minimum number of zeros
+ magnitude = minInt - 1;
+ } else {
+ magnitude = input.getMagnitude();
+ }
+ effectiveMag = Math.min(magnitude + 1, maxInt);
+ magMinSig = effectiveMag - minSig;
+ magMaxSig = effectiveMag - maxSig;
+
+ // Step 1: pick the rounding magnitude and apply.
+ int roundingMagnitude;
+ switch (mode) {
+ case OVERRIDE_MAXIMUM_FRACTION:
+ // Always round to maxSig.
+ // Of the six possible orders:
+ // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+ // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+ // Case 3: minSig, minFrac, maxFrac, maxSig -- maxSig wins
+ // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+ // Case 5: minFrac, minSig, maxFrac, maxSig -- maxSig wins
+ // Case 6: minFrac, maxFrac, minSig, maxSig -- maxSig wins
+ roundingMagnitude = magMaxSig;
+ break;
+ case RESPECT_MAXIMUM_FRACTION:
+ // Round to the strongest of maxFrac, maxInt, and maxSig.
+ // Of the six possible orders:
+ // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+ // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+ // Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default
+ // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+ // Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default
+ // Case 6: minFrac, maxFrac, minSig, maxSig -- maxFrac wins --> differs from default
+ //
+ // Math.max() picks the rounding magnitude farthest to the left (most significant).
+ // Math.min() picks the rounding magnitude farthest to the right (least significant).
+ roundingMagnitude = Math.max(-maxFrac, magMaxSig);
+ break;
+ case ENSURE_MINIMUM_SIGNIFICANT:
+ // Round to the strongest of maxFrac and maxSig, and always ensure minSig.
+ // Of the six possible orders:
+ // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+ // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+ // Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default
+ // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+ // Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default
+ // Case 6: minFrac, maxFrac, minSig, maxSig -- minSig wins --> differs from default
+ roundingMagnitude = Math.min(magMinSig, Math.max(-maxFrac, magMaxSig));
+ break;
+ default:
+ throw new AssertionError();
+ }
+ input.roundToMagnitude(roundingMagnitude, mathContext);
+
+ // In case magnitude changed:
+ if (input.isZero()) {
+ magnitude = minInt - 1;
+ } else {
+ magnitude = input.getMagnitude();
+ }
+ effectiveMag = Math.min(magnitude + 1, maxInt);
+ magMinSig = effectiveMag - minSig;
+ magMaxSig = effectiveMag - maxSig;
+
+ // Step 2: pick the number of visible digits.
+ switch (mode) {
+ case OVERRIDE_MAXIMUM_FRACTION:
+ // Ensure minSig is always displayed.
+ input.setIntegerFractionLength(
+ minInt, maxInt, Math.max(minFrac, -magMinSig), Integer.MAX_VALUE);
+ break;
+ case RESPECT_MAXIMUM_FRACTION:
+ // Ensure minSig is displayed, unless doing so is in violation of maxFrac.
+ input.setIntegerFractionLength(
+ minInt, maxInt, Math.min(maxFrac, Math.max(minFrac, -magMinSig)), maxFrac);
+ break;
+ case ENSURE_MINIMUM_SIGNIFICANT:
+ // Follow minInt/minFrac, but ensure all digits are allowed to be visible.
+ input.setIntegerFractionLength(minInt, maxInt, minFrac, Integer.MAX_VALUE);
+ break;
+ }
+ }
+
+ @Override
+ public void export(Properties properties) {
+ super.export(properties);
+ properties.setMinimumSignificantDigits(minSig);
+ properties.setMaximumSignificantDigits(maxSig);
+ properties.setSignificantDigitsMode(mode);
+ }
+}
+++ /dev/null
-// © 2016 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-/*
- *******************************************************************************
- * Copyright (C) 2012-2016, International Business Machines Corporation and
- * others. All Rights Reserved.
- *******************************************************************************
- */
-package com.ibm.icu.text;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.MissingResourceException;
-
-import com.ibm.icu.impl.ICUCache;
-import com.ibm.icu.impl.ICUData;
-import com.ibm.icu.impl.ICUResourceBundle;
-import com.ibm.icu.impl.SimpleCache;
-import com.ibm.icu.impl.UResource;
-import com.ibm.icu.text.DecimalFormat.Unit;
-import com.ibm.icu.util.ULocale;
-import com.ibm.icu.util.UResourceBundle;
-
-/**
- * A cache containing data by locale for {@link CompactDecimalFormat}
- *
- * @author Travis Keep
- */
-class CompactDecimalDataCache {
-
- private static final String SHORT_STYLE = "short";
- private static final String LONG_STYLE = "long";
- private static final String SHORT_CURRENCY_STYLE = "shortCurrency";
- private static final String NUMBER_ELEMENTS = "NumberElements";
- private static final String PATTERNS_LONG = "patternsLong";
- private static final String PATTERNS_SHORT = "patternsShort";
- private static final String DECIMAL_FORMAT = "decimalFormat";
- private static final String CURRENCY_FORMAT = "currencyFormat";
- private static final String LATIN_NUMBERING_SYSTEM = "latn";
-
- private static enum PatternsTableKey { PATTERNS_LONG, PATTERNS_SHORT };
- private static enum FormatsTableKey { DECIMAL_FORMAT, CURRENCY_FORMAT };
-
- public static final String OTHER = "other";
-
- /**
- * We can specify prefixes or suffixes for values with up to 15 digits,
- * less than 10^15.
- */
- static final int MAX_DIGITS = 15;
-
- private final ICUCache<ULocale, DataBundle> cache =
- new SimpleCache<ULocale, DataBundle>();
-
- /**
- * Data contains the compact decimal data for a particular locale. Data consists
- * of one array and two hashmaps. The index of the divisors array as well
- * as the arrays stored in the values of the two hashmaps correspond
- * to log10 of the number being formatted, so when formatting 12,345, the 4th
- * index of the arrays should be used. Divisors contain the number to divide
- * by before doing formatting. In the case of english, <code>divisors[4]</code>
- * is 1000. So to format 12,345, divide by 1000 to get 12. Then use
- * PluralRules with the current locale to figure out which of the 6 plural variants
- * 12 matches: "zero", "one", "two", "few", "many", or "other." Prefixes and
- * suffixes are maps whose key is the plural variant and whose values are
- * arrays of strings with indexes corresponding to log10 of the original number.
- * these arrays contain the prefix or suffix to use.
- *
- * Each array in data is 15 in length, and every index is filled.
- *
- * @author Travis Keep
- *
- */
- static class Data {
- long[] divisors;
- Map<String, DecimalFormat.Unit[]> units;
- boolean fromFallback;
-
- Data(long[] divisors, Map<String, DecimalFormat.Unit[]> units)
- {
- this.divisors = divisors;
- this.units = units;
- }
-
- public boolean isEmpty() {
- return units == null || units.isEmpty();
- }
- }
-
- /**
- * DataBundle contains compact decimal data for all the styles in a particular
- * locale. Currently available styles are short and long for decimals, and
- * short only for currencies.
- *
- * @author Travis Keep
- */
- static class DataBundle {
- Data shortData;
- Data longData;
- Data shortCurrencyData;
-
- private DataBundle(Data shortData, Data longData, Data shortCurrencyData) {
- this.shortData = shortData;
- this.longData = longData;
- this.shortCurrencyData = shortCurrencyData;
- }
-
- private static DataBundle createEmpty() {
- return new DataBundle(
- new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
- new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
- new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>())
- );
- }
- }
-
- /**
- * Sink for enumerating all of the compact decimal format patterns.
- *
- * More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
- * Only store a value if it is still missing, that is, it has not been overridden.
- */
- private static final class CompactDecimalDataSink extends UResource.Sink {
-
- private DataBundle dataBundle; // Where to save values when they are read
- private ULocale locale; // The locale we are traversing (for exception messages)
- private boolean isLatin; // Whether or not we are traversing the Latin table
- private boolean isFallback; // Whether or not we are traversing the Latin table as fallback
-
- /*
- * NumberElements{ <-- top (numbering system table)
- * latn{ <-- patternsTable (one per numbering system)
- * patternsLong{ <-- formatsTable (one per pattern)
- * decimalFormat{ <-- powersOfTenTable (one per format)
- * 1000{ <-- pluralVariantsTable (one per power of ten)
- * one{"0 thousand"} <-- plural variant and template
- */
-
- public CompactDecimalDataSink(DataBundle dataBundle, ULocale locale) {
- this.dataBundle = dataBundle;
- this.locale = locale;
- }
-
- @Override
- public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
- // SPECIAL CASE: Don't consume root in the non-Latin numbering system
- if (isRoot && !isLatin) { return; }
-
- UResource.Table patternsTable = value.getTable();
- for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
-
- // patterns table: check for patternsShort or patternsLong
- PatternsTableKey patternsTableKey;
- if (key.contentEquals(PATTERNS_SHORT)) {
- patternsTableKey = PatternsTableKey.PATTERNS_SHORT;
- } else if (key.contentEquals(PATTERNS_LONG)) {
- patternsTableKey = PatternsTableKey.PATTERNS_LONG;
- } else {
- continue;
- }
-
- // traverse into the table of formats
- UResource.Table formatsTable = value.getTable();
- for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
-
- // formats table: check for decimalFormat or currencyFormat
- FormatsTableKey formatsTableKey;
- if (key.contentEquals(DECIMAL_FORMAT)) {
- formatsTableKey = FormatsTableKey.DECIMAL_FORMAT;
- } else if (key.contentEquals(CURRENCY_FORMAT)) {
- formatsTableKey = FormatsTableKey.CURRENCY_FORMAT;
- } else {
- continue;
- }
-
- // Set the current style and destination based on the lvl1 and lvl2 keys
- String style = null;
- Data destination = null;
- if (patternsTableKey == PatternsTableKey.PATTERNS_LONG
- && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
- style = LONG_STYLE;
- destination = dataBundle.longData;
- } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
- && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
- style = SHORT_STYLE;
- destination = dataBundle.shortData;
- } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
- && formatsTableKey == FormatsTableKey.CURRENCY_FORMAT) {
- style = SHORT_CURRENCY_STYLE;
- destination = dataBundle.shortCurrencyData;
- } else {
- // Silently ignore this case
- continue;
- }
-
- // SPECIAL CASE: RULES FOR WHETHER OR NOT TO CONSUME THIS TABLE:
- // 1) Don't consume longData if shortData was consumed from the non-Latin
- // locale numbering system
- // 2) Don't consume longData for the first time if this is the root bundle and
- // shortData is already populated from a more specific locale. Note that if
- // both longData and shortData are both only in root, longData will be
- // consumed since it is alphabetically before shortData in the bundle.
- if (isFallback
- && style == LONG_STYLE
- && !dataBundle.shortData.isEmpty()
- && !dataBundle.shortData.fromFallback) {
- continue;
- }
- if (isRoot
- && style == LONG_STYLE
- && dataBundle.longData.isEmpty()
- && !dataBundle.shortData.isEmpty()) {
- continue;
- }
-
- // Set the "fromFallback" flag on the data object
- destination.fromFallback = isFallback;
-
- // traverse into the table of powers of ten
- UResource.Table powersOfTenTable = value.getTable();
- for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
-
- // This value will always be some even power of 10. e.g 10000.
- long power10 = Long.parseLong(key.toString());
- int log10Value = (int) Math.log10(power10);
-
- // Silently ignore divisors that are too big.
- if (log10Value >= MAX_DIGITS) continue;
-
- // Iterate over the plural variants ("one", "other", etc)
- UResource.Table pluralVariantsTable = value.getTable();
- for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
- // TODO: Use StandardPlural rather than String.
- String pluralVariant = key.toString();
- String template = value.toString();
-
- // Copy the data into the in-memory data bundle (do not overwrite
- // existing values)
- int numZeros = populatePrefixSuffix(
- pluralVariant, log10Value, template, locale, style, destination, false);
-
- // If populatePrefixSuffix returns -1, it means that this key has been
- // encountered already.
- if (numZeros < 0) {
- continue;
- }
-
- // Set the divisor, which is based on the number of zeros in the template
- // string. If the divisor from here is different from the one previously
- // stored, it means that the number of zeros in different plural variants
- // differs; throw an exception.
- long divisor = calculateDivisor(power10, numZeros);
- if (destination.divisors[log10Value] != 0L
- && destination.divisors[log10Value] != divisor) {
- throw new IllegalArgumentException("Plural variant '" + pluralVariant
- + "' template '" + template
- + "' for 10^" + log10Value
- + " has wrong number of zeros in " + localeAndStyle(locale, style));
- }
- destination.divisors[log10Value] = divisor;
- }
- }
- }
- }
- }
- }
-
- /**
- * Fetch data for a particular locale. Clients must not modify any part of the returned data. Portions of returned
- * data may be shared so modifying it will have unpredictable results.
- */
- DataBundle get(ULocale locale) {
- DataBundle result = cache.get(locale);
- if (result == null) {
- result = load(locale);
- cache.put(locale, result);
- }
- return result;
- }
-
- private static DataBundle load(ULocale ulocale) throws MissingResourceException {
- DataBundle dataBundle = DataBundle.createEmpty();
- String nsName = NumberingSystem.getInstance(ulocale).getName();
- ICUResourceBundle r = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
- ulocale);
- CompactDecimalDataSink sink = new CompactDecimalDataSink(dataBundle, ulocale);
- sink.isFallback = false;
-
- // First load the number elements data from nsName if nsName is not Latin.
- if (!nsName.equals(LATIN_NUMBERING_SYSTEM)) {
- sink.isLatin = false;
-
- try {
- r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + nsName, sink);
- } catch (MissingResourceException e) {
- // Silently ignore and use Latin
- }
-
- // Set the "isFallback" flag for when we read Latin
- sink.isFallback = true;
- }
-
- // Now load Latin, which will fill in things that were left out from above.
- sink.isLatin = true;
- r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + LATIN_NUMBERING_SYSTEM, sink);
-
- // If longData is empty, default it to be equal to shortData
- if (dataBundle.longData.isEmpty()) {
- dataBundle.longData = dataBundle.shortData;
- }
-
- // Check for "other" variants in each of the three data classes
- checkForOtherVariants(dataBundle.longData, ulocale, LONG_STYLE);
- checkForOtherVariants(dataBundle.shortData, ulocale, SHORT_STYLE);
- checkForOtherVariants(dataBundle.shortCurrencyData, ulocale, SHORT_CURRENCY_STYLE);
-
- // Resolve missing elements
- fillInMissing(dataBundle.longData);
- fillInMissing(dataBundle.shortData);
- fillInMissing(dataBundle.shortCurrencyData);
-
- // Return the data bundle
- return dataBundle;
- }
-
-
- /**
- * Populates prefix and suffix information for a particular plural variant
- * and index (log10 value).
- * @param pluralVariant e.g "one", "other"
- * @param idx the index (log10 value of the number) 0 <= idx < MAX_DIGITS
- * @param template e.g "00K"
- * @param locale the locale
- * @param style the style
- * @param destination Extracted prefix and suffix stored here.
- * @return number of zeros found before any decimal point in template, or -1 if it was not saved.
- */
- private static int populatePrefixSuffix(
- String pluralVariant, int idx, String template, ULocale locale, String style,
- Data destination, boolean overwrite) {
- int firstIdx = template.indexOf("0");
- int lastIdx = template.lastIndexOf("0");
- if (firstIdx == -1) {
- throw new IllegalArgumentException(
- "Expect at least one zero in template '" + template +
- "' for variant '" +pluralVariant + "' for 10^" + idx +
- " in " + localeAndStyle(locale, style));
- }
- String prefix = template.substring(0, firstIdx);
- String suffix = template.substring(lastIdx + 1);
-
- // Save the unit, and return -1 if it was not saved
- boolean saved = saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, destination.units, overwrite);
- if (!saved) {
- return -1;
- }
-
- // If there is effectively no prefix or suffix, ignore the actual
- // number of 0's and act as if the number of 0's matches the size
- // of the number
- if (prefix.trim().length() == 0 && suffix.trim().length() == 0) {
- return idx + 1;
- }
-
- // Calculate number of zeros before decimal point.
- int i = firstIdx + 1;
- while (i <= lastIdx && template.charAt(i) == '0') {
- i++;
- }
- return i - firstIdx;
- }
-
- /**
- * Calculate a divisor based on the magnitude and number of zeros in the
- * template string.
- * @param power10
- * @param numZeros
- * @return
- */
- private static long calculateDivisor(long power10, int numZeros) {
- // We craft our divisor such that when we divide by it, we get a
- // number with the same number of digits as zeros found in the
- // plural variant templates. If our magnitude is 10000 and we have
- // two 0's in our plural variants, then we want a divisor of 1000.
- // Note that if we have 43560 which is of same magnitude as 10000.
- // When we divide by 1000 we a quotient which rounds to 44 (2 digits)
- long divisor = power10;
- for (int i = 1; i < numZeros; i++) {
- divisor /= 10;
- }
- return divisor;
- }
-
-
- /**
- * Returns locale and style. Used to form useful messages in thrown exceptions.
- *
- * Note: This is not covered by unit tests since no exceptions are thrown on the default CLDR data. It is too
- * cumbersome to cover via reflection.
- *
- * @param locale the locale
- * @param style the style
- */
- private static String localeAndStyle(ULocale locale, String style) {
- return "locale '" + locale + "' style '" + style + "'";
- }
-
- /**
- * Checks to make sure that an "other" variant is present in all powers of 10.
- * @param data
- */
- private static void checkForOtherVariants(Data data, ULocale locale, String style) {
- DecimalFormat.Unit[] otherByBase = data.units.get(OTHER);
-
- if (otherByBase == null) {
- throw new IllegalArgumentException("No 'other' plural variants defined in "
- + localeAndStyle(locale, style));
- }
-
- // Check all other plural variants, and make sure that if any of them are populated, then
- // other is also populated
- for (Map.Entry<String, Unit[]> entry : data.units.entrySet()) {
- if (entry.getKey() == OTHER) continue;
- DecimalFormat.Unit[] variantByBase = entry.getValue();
- for (int log10Value = 0; log10Value < MAX_DIGITS; log10Value++) {
- if (variantByBase[log10Value] != null && otherByBase[log10Value] == null) {
- throw new IllegalArgumentException(
- "No 'other' plural variant defined for 10^" + log10Value
- + " but a '" + entry.getKey() + "' variant is defined"
- + " in " +localeAndStyle(locale, style));
- }
- }
- }
- }
-
- /**
- * After reading information from resource bundle into a Data object, there
- * is guarantee that it is complete.
- *
- * This method fixes any incomplete data it finds within <code>result</code>.
- * It looks at each log10 value applying the two rules.
- * <p>
- * If no prefix is defined for the "other" variant, use the divisor, prefixes and
- * suffixes for all defined variants from the previous log10. For log10 = 0,
- * use all empty prefixes and suffixes and a divisor of 1.
- * </p><p>
- * Otherwise, examine each plural variant defined for the given log10 value.
- * If it has no prefix and suffix for a particular variant, use the one from the
- * "other" variant.
- * </p>
- *
- * @param result this instance is fixed in-place.
- */
- private static void fillInMissing(Data result) {
- // Initially we assume that previous divisor is 1 with no prefix or suffix.
- long lastDivisor = 1L;
- for (int i = 0; i < result.divisors.length; i++) {
- if (result.units.get(OTHER)[i] == null) {
- result.divisors[i] = lastDivisor;
- copyFromPreviousIndex(i, result.units);
- } else {
- lastDivisor = result.divisors[i];
- propagateOtherToMissing(i, result.units);
- }
- }
- }
-
- private static void propagateOtherToMissing(
- int idx, Map<String, DecimalFormat.Unit[]> units) {
- DecimalFormat.Unit otherVariantValue = units.get(OTHER)[idx];
- for (DecimalFormat.Unit[] byBase : units.values()) {
- if (byBase[idx] == null) {
- byBase[idx] = otherVariantValue;
- }
- }
- }
-
- private static void copyFromPreviousIndex(int idx, Map<String, DecimalFormat.Unit[]> units) {
- for (DecimalFormat.Unit[] byBase : units.values()) {
- if (idx == 0) {
- byBase[idx] = DecimalFormat.NULL_UNIT;
- } else {
- byBase[idx] = byBase[idx - 1];
- }
- }
- }
-
- private static boolean saveUnit(
- DecimalFormat.Unit unit, String pluralVariant, int idx,
- Map<String, DecimalFormat.Unit[]> units,
- boolean overwrite) {
- DecimalFormat.Unit[] byBase = units.get(pluralVariant);
- if (byBase == null) {
- byBase = new DecimalFormat.Unit[MAX_DIGITS];
- units.put(pluralVariant, byBase);
- }
-
- // Don't overwrite a pre-existing value unless the "overwrite" flag is true.
- if (!overwrite && byBase[idx] != null) {
- return false;
- }
-
- // Save the value and return
- byBase[idx] = unit;
- return true;
- }
-
- /**
- * Fetches a prefix or suffix given a plural variant and log10 value. If it
- * can't find the given variant, it falls back to "other".
- * @param prefixOrSuffix the prefix or suffix map
- * @param variant the plural variant
- * @param base log10 value. 0 <= base < MAX_DIGITS.
- * @return the prefix or suffix.
- */
- static DecimalFormat.Unit getUnit(
- Map<String, DecimalFormat.Unit[]> units, String variant, int base) {
- DecimalFormat.Unit[] byBase = units.get(variant);
- if (byBase == null) {
- byBase = units.get(CompactDecimalDataCache.OTHER);
- }
- return byBase[base];
- }
-}
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.ParsePosition;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.regex.Pattern;
-import com.ibm.icu.text.CompactDecimalDataCache.Data;
-import com.ibm.icu.text.PluralRules.FixedDecimal;
-import com.ibm.icu.util.Currency;
-import com.ibm.icu.util.CurrencyAmount;
-import com.ibm.icu.util.Output;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.util.ULocale;
/**
- * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will limited real estate.
- * For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will be appropriate for the given language,
- * such as "1,2 Mrd." for German.
- * <p>
- * For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be short for supported
- * languages. However, the result may sometimes exceed 7 characters, such as when there are combining marks or thin
- * characters. In such cases, the visual width in fonts should still be short.
- * <p>
- * By default, there are 2 significant digits. After creation, if more than three significant digits are set (with
- * setMaximumSignificantDigits), or if a fixed number of digits are set (with setMaximumIntegerDigits or
- * setMaximumFractionDigits), then result may be wider.
- * <p>
- * The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of "$1,200,000.00" (English) or
- * "5,3 Mio. €" instead of "5.300.000,00 €" (German). Localized data concerning longer formats is not available yet in
- * the Unicode CLDR. Because of this, attempting to format a currency amount using the "long" style will produce
- * an UnsupportedOperationException.
+ * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will
+ * limited real estate. For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will
+ * be appropriate for the given language, such as "1,2 Mrd." for German.
*
- * At this time, negative numbers and parsing are not supported, and will produce an UnsupportedOperationException.
- * Resetting the pattern prefixes or suffixes is not supported; the method calls are ignored.
- * <p>
- * Note that important methods, like setting the number of decimals, will be moved up from DecimalFormat to
- * NumberFormat.
+ * <p>For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be
+ * short for supported languages. However, the result may sometimes exceed 7 characters, such as
+ * when there are combining marks or thin characters. In such cases, the visual width in fonts
+ * should still be short.
+ *
+ * <p>By default, there are 2 significant digits. After creation, if more than three significant
+ * digits are set (with setMaximumSignificantDigits), or if a fixed number of digits are set (with
+ * setMaximumIntegerDigits or setMaximumFractionDigits), then result may be wider.
+ *
+ * <p>The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of
+ * "$1,200,000.00" (English) or "5,3 Mio. €" instead of "5.300.000,00 €" (German). Localized data
+ * concerning longer formats is not available yet in the Unicode CLDR. Because of this, attempting
+ * to format a currency amount using the "long" style will produce an UnsupportedOperationException.
+ *
+ * <p>At this time, negative numbers and parsing are not supported, and will produce an
+ * UnsupportedOperationException. Resetting the pattern prefixes or suffixes is not supported; the
+ * method calls are ignored.
+ *
+ * <p>Note that important methods, like setting the number of decimals, will be moved up from
+ * DecimalFormat to NumberFormat.
*
* @author markdavis
* @stable ICU 49
*/
public class CompactDecimalFormat extends DecimalFormat {
- private static final long serialVersionUID = 4716293295276629682L;
-
-// private static final int POSITIVE_PREFIX = 0, POSITIVE_SUFFIX = 1, AFFIX_SIZE = 2;
- private static final CompactDecimalDataCache cache = new CompactDecimalDataCache();
-
- private final Map<String, DecimalFormat.Unit[]> units;
- private final Map<String, DecimalFormat.Unit[]> currencyUnits;
- private final long[] divisor;
- private final long[] currencyDivisor;
- private final Map<String, Unit> pluralToCurrencyAffixes;
- private CompactStyle style;
-
- // null if created internally using explicit prefixes and suffixes.
- private final PluralRules pluralRules;
-
- /**
- * Style parameter for CompactDecimalFormat.
- * @stable ICU 50
- */
- public enum CompactStyle {
- /**
- * Short version, like "1.2T"
- * @stable ICU 50
- */
- SHORT,
- /**
- * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not.
- * @stable ICU 50
- */
- LONG
- }
-
- /**
- * Create a CompactDecimalFormat appropriate for a locale. The result may
- * be affected by the number system in the locale, such as ar-u-nu-latn.
- *
- * @param locale the desired locale
- * @param style the compact style
- * @stable ICU 50
- */
- public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) {
- return new CompactDecimalFormat(locale, style);
- }
-
+ /**
+ * Style parameter for CompactDecimalFormat.
+ *
+ * @stable ICU 50
+ */
+ public enum CompactStyle {
/**
- * Create a CompactDecimalFormat appropriate for a locale. The result may
- * be affected by the number system in the locale, such as ar-u-nu-latn.
+ * Short version, like "1.2T"
*
- * @param locale the desired locale
- * @param style the compact style
* @stable ICU 50
*/
- public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) {
- return new CompactDecimalFormat(ULocale.forLocale(locale), style);
- }
-
+ SHORT,
/**
- * The public mechanism is CompactDecimalFormat.getInstance().
+ * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not.
*
- * @param locale
- * the desired locale
- * @param style
- * the compact style
- */
- CompactDecimalFormat(ULocale locale, CompactStyle style) {
- this.pluralRules = PluralRules.forLocale(locale);
- DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(locale);
- CompactDecimalDataCache.Data data = getData(locale, style);
- CompactDecimalDataCache.Data currencyData = getCurrencyData(locale);
- this.units = data.units;
- this.divisor = data.divisors;
- this.currencyUnits = currencyData.units;
- this.currencyDivisor = currencyData.divisors;
- this.style = style;
- pluralToCurrencyAffixes = null;
-
-// DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
-// // TODO fix to use plural-dependent affixes
-// Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix());
-// pluralToCurrencyAffixes = new HashMap<String,Unit>();
-// for (String key : pluralRules.getKeywords()) {
-// pluralToCurrencyAffixes.put(key, currency);
-// }
-// // TODO fix to get right symbol for the count
-
- finishInit(style, format.toPattern(), format.getDecimalFormatSymbols());
- }
-
- /**
- * Create a short number "from scratch". Intended for internal use. The prefix, suffix, and divisor arrays are
- * parallel, and provide the information for each power of 10. When formatting a value, the correct power of 10 is
- * found, then the value is divided by the divisor, and the prefix and suffix are set (using
- * setPositivePrefix/Suffix).
- *
- * @param pattern
- * A number format pattern. Note that the prefix and suffix are discarded, and the decimals are
- * overridden by default.
- * @param formatSymbols
- * Decimal format symbols, typically from a locale.
- * @param style
- * compact style.
- * @param divisor
- * An array of prefix values, one for each power of 10 from 0 to 14
- * @param pluralAffixes
- * A map from plural categories to affixes.
- * @param currencyAffixes
- * A map from plural categories to currency affixes.
- * @param debugCreationErrors
- * A collection of strings for debugging. If null on input, then any errors found will be added to that
- * collection instead of throwing exceptions.
- * @internal
- * @deprecated This API is ICU internal only.
- */
- @Deprecated
- public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols,
- CompactStyle style, PluralRules pluralRules,
- long[] divisor, Map<String,String[][]> pluralAffixes, Map<String, String[]> currencyAffixes,
- Collection<String> debugCreationErrors) {
-
- this.pluralRules = pluralRules;
- this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
- this.currencyUnits = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
- if (!pluralRules.getKeywords().equals(this.units.keySet())) {
- debugCreationErrors.add("Missmatch in pluralCategories, should be: " + pluralRules.getKeywords() + ", was actually " + this.units.keySet());
- }
- this.divisor = divisor.clone();
- this.currencyDivisor = divisor.clone();
- if (currencyAffixes == null) {
- pluralToCurrencyAffixes = null;
- } else {
- pluralToCurrencyAffixes = new HashMap<String,Unit>();
- for (Entry<String, String[]> s : currencyAffixes.entrySet()) {
- String[] pair = s.getValue();
- pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1]));
- }
- }
- finishInit(style, pattern, formatSymbols);
- }
-
- private void finishInit(CompactStyle style, String pattern, DecimalFormatSymbols formatSymbols) {
- applyPattern(pattern);
- setDecimalFormatSymbols(formatSymbols);
- setMaximumSignificantDigits(2); // default significant digits
- setSignificantDigitsUsed(true);
- if (style == CompactStyle.SHORT) {
- setGroupingUsed(false);
- }
- setCurrency(null);
- }
-
- /**
- * {@inheritDoc}
- * @stable ICU 49
- */
- @Override
- public boolean equals(Object obj) {
- if (obj == null)
- return false;
- if (!super.equals(obj))
- return false; // super does class check
- CompactDecimalFormat other = (CompactDecimalFormat) obj;
- return mapsAreEqual(units, other.units)
- && Arrays.equals(divisor, other.divisor)
- && (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes
- || pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes))
- && pluralRules.equals(other.pluralRules);
- }
-
- private boolean mapsAreEqual(
- Map<String, DecimalFormat.Unit[]> lhs, Map<String, DecimalFormat.Unit[]> rhs) {
- if (lhs.size() != rhs.size()) {
- return false;
- }
- // For each MapEntry in lhs, see if there is a matching one in rhs.
- for (Map.Entry<String, DecimalFormat.Unit[]> entry : lhs.entrySet()) {
- DecimalFormat.Unit[] value = rhs.get(entry.getKey());
- if (value == null || !Arrays.equals(entry.getValue(), value)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * {@inheritDoc}
- * @stable ICU 49
- */
- @Override
- public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
- return format(number, null, toAppendTo, pos);
- }
-
- /**
- * {@inheritDoc}
* @stable ICU 50
*/
- @Override
- public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
- if (!(obj instanceof Number)) {
- throw new IllegalArgumentException();
- }
- Number number = (Number) obj;
- Amount amount = toAmount(number.doubleValue(), null, null);
- return super.formatToCharacterIterator(amount.getQty(), amount.getUnit());
- }
-
- /**
- * {@inheritDoc}
- * @stable ICU 49
- */
- @Override
- public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
- return format((double) number, toAppendTo, pos);
- }
-
- /**
- * {@inheritDoc}
- * @stable ICU 49
- */
- @Override
- public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
- return format(number.doubleValue(), toAppendTo, pos);
- }
-
- /**
- * {@inheritDoc}
- * @stable ICU 49
- */
- @Override
- public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
- return format(number.doubleValue(), toAppendTo, pos);
- }
-
- /**
- * {@inheritDoc}
- * @stable ICU 49
- */
- @Override
- public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
- return format(number.doubleValue(), toAppendTo, pos);
- }
- /**
- * {@inheritDoc}
- * @internal ICU 57 technology preview
- * @deprecated This API might change or be removed in a future release.
- */
- @Override
- @Deprecated
- public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
- return format(currAmt.getNumber().doubleValue(), currAmt.getCurrency(), toAppendTo, pos);
- }
-
- /**
- * Parsing is currently unsupported, and throws an UnsupportedOperationException.
- * @stable ICU 49
- */
- @Override
- public Number parse(String text, ParsePosition parsePosition) {
- throw new UnsupportedOperationException();
- }
-
- // DISALLOW Serialization, at least while draft
-
- private void writeObject(ObjectOutputStream out) throws IOException {
- throw new NotSerializableException();
- }
-
- private void readObject(ObjectInputStream in) throws IOException {
- throw new NotSerializableException();
- }
-
- /* INTERNALS */
- private StringBuffer format(double number, Currency curr, StringBuffer toAppendTo, FieldPosition pos) {
- if (curr != null && style == CompactStyle.LONG) {
- throw new UnsupportedOperationException("CompactDecimalFormat does not support LONG style for currency.");
- }
-
- // Compute the scaled amount, prefix, and suffix appropriate for the number's magnitude.
- Output<Unit> currencyUnit = new Output<Unit>();
- Amount amount = toAmount(number, curr, currencyUnit);
- Unit unit = amount.getUnit();
-
- // Note that currencyUnit is a remnant. In almost all cases, it will be null.
- StringBuffer prefix = new StringBuffer();
- StringBuffer suffix = new StringBuffer();
- if (currencyUnit.value != null) {
- currencyUnit.value.writePrefix(prefix);
- }
- unit.writePrefix(prefix);
- unit.writeSuffix(suffix);
- if (currencyUnit.value != null) {
- currencyUnit.value.writeSuffix(suffix);
- }
-
- if (curr == null) {
- // Prevent locking when not formatting a currency number.
- toAppendTo.append(escape(prefix.toString()));
- super.format(amount.getQty(), toAppendTo, pos);
- toAppendTo.append(escape(suffix.toString()));
-
- } else {
- // To perform the formatting, we set this DecimalFormat's pattern to have the correct prefix, suffix,
- // and currency, and then reset it back to what it was before.
- // This has to be synchronized since this information is held in the state of the DecimalFormat object.
- synchronized(this) {
-
- String originalPattern = this.toPattern();
- Currency originalCurrency = this.getCurrency();
- StringBuffer newPattern = new StringBuffer();
-
- // Write prefixes and suffixes to the pattern. Note that we have to apply it to both halves of a
- // positive/negative format (separated by ';')
- int semicolonPos = originalPattern.indexOf(';');
- newPattern.append(prefix);
- if (semicolonPos != -1) {
- newPattern.append(originalPattern, 0, semicolonPos);
- newPattern.append(suffix);
- newPattern.append(';');
- newPattern.append(prefix);
- }
- newPattern.append(originalPattern, semicolonPos + 1, originalPattern.length());
- newPattern.append(suffix);
-
- // Overwrite the pattern and currency.
- setCurrency(curr);
- applyPattern(newPattern.toString());
-
- // Actually perform the formatting.
- super.format(amount.getQty(), toAppendTo, pos);
-
- // Reset the pattern and currency.
- setCurrency(originalCurrency);
- applyPattern(originalPattern);
- }
- }
- return toAppendTo;
- }
-
- private static final Pattern UNESCAPE_QUOTE = Pattern.compile("((?<!'))'");
-
- private static String escape(String string) {
- if (string.indexOf('\'') >= 0) {
- return UNESCAPE_QUOTE.matcher(string).replaceAll("$1");
- }
- return string;
- }
-
- private Amount toAmount(double number, Currency curr, Output<Unit> currencyUnit) {
- // We do this here so that the prefix or suffix we choose is always consistent
- // with the rounding we do. This way, 999999 -> 1M instead of 1000K.
- boolean negative = isNumberNegative(number);
- number = adjustNumberAsInFormatting(number);
- int base = number <= 1.0d ? 0 : (int) Math.log10(number);
- if (base >= CompactDecimalDataCache.MAX_DIGITS) {
- base = CompactDecimalDataCache.MAX_DIGITS - 1;
- }
- if (curr != null) {
- number /= currencyDivisor[base];
- } else {
- number /= divisor[base];
- }
- String pluralVariant = getPluralForm(getFixedDecimal(number, toDigitList(number)));
- if (pluralToCurrencyAffixes != null && currencyUnit != null) {
- currencyUnit.value = pluralToCurrencyAffixes.get(pluralVariant);
- }
- if (negative) {
- number = -number;
- }
- if ( curr != null ) {
- return new Amount(number, CompactDecimalDataCache.getUnit(currencyUnits, pluralVariant, base));
- } else {
- return new Amount(number, CompactDecimalDataCache.getUnit(units, pluralVariant, base));
- }
- }
-
- private void recordError(Collection<String> creationErrors, String errorMessage) {
- if (creationErrors == null) {
- throw new IllegalArgumentException(errorMessage);
- }
- creationErrors.add(errorMessage);
- }
-
- /**
- * Manufacture the unit list from arrays
- */
- private Map<String, DecimalFormat.Unit[]> otherPluralVariant(Map<String, String[][]> pluralCategoryToPower10ToAffix,
- long[] divisor, Collection<String> debugCreationErrors) {
-
- // check for bad divisors
- if (divisor.length < CompactDecimalDataCache.MAX_DIGITS) {
- recordError(debugCreationErrors, "Must have at least " + CompactDecimalDataCache.MAX_DIGITS + " prefix items.");
- }
- long oldDivisor = 0;
- for (int i = 0; i < divisor.length; ++i) {
-
- // divisor must be a power of 10, and must be less than or equal to 10^i
- int log = (int) Math.log10(divisor[i]);
- if (log > i) {
- recordError(debugCreationErrors, "Divisor[" + i + "] must be less than or equal to 10^" + i
- + ", but is: " + divisor[i]);
- }
- long roundTrip = (long) Math.pow(10.0d, log);
- if (roundTrip != divisor[i]) {
- recordError(debugCreationErrors, "Divisor[" + i + "] must be a power of 10, but is: " + divisor[i]);
- }
-
- if (divisor[i] < oldDivisor) {
- recordError(debugCreationErrors, "Bad divisor, the divisor for 10E" + i + "(" + divisor[i]
- + ") is less than the divisor for the divisor for 10E" + (i - 1) + "(" + oldDivisor + ")");
- }
- oldDivisor = divisor[i];
- }
-
- Map<String, DecimalFormat.Unit[]> result = new HashMap<String, DecimalFormat.Unit[]>();
- Map<String,Integer> seen = new HashMap<String,Integer>();
-
- String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other");
-
- for (Entry<String, String[][]> pluralCategoryAndPower10ToAffix : pluralCategoryToPower10ToAffix.entrySet()) {
- String pluralCategory = pluralCategoryAndPower10ToAffix.getKey();
- String[][] power10ToAffix = pluralCategoryAndPower10ToAffix.getValue();
-
- // we can't have one of the arrays be of different length
- if (power10ToAffix.length != divisor.length) {
- recordError(debugCreationErrors, "Prefixes & suffixes must be present for all divisors " + pluralCategory);
- }
- DecimalFormat.Unit[] units = new DecimalFormat.Unit[power10ToAffix.length];
- for (int i = 0; i < power10ToAffix.length; i++) {
- String[] pair = power10ToAffix[i];
- if (pair == null) {
- pair = defaultPower10ToAffix[i];
- }
-
- // we can't have bad pair
- if (pair.length != 2 || pair[0] == null || pair[1] == null) {
- recordError(debugCreationErrors, "Prefix or suffix is null for " + pluralCategory + ", " + i + ", " + Arrays.asList(pair));
- continue;
- }
-
- // we can't have two different indexes with the same display
- int log = (int) Math.log10(divisor[i]);
- String key = pair[0] + "\uFFFF" + pair[1] + "\uFFFF" + (i - log);
- Integer old = seen.get(key);
- if (old == null) {
- seen.put(key, i);
- } else if (old != i) {
- recordError(debugCreationErrors, "Collision between values for " + i + " and " + old
- + " for [prefix/suffix/index-log(divisor)" + key.replace('\uFFFF', ';'));
- }
-
- units[i] = new Unit(pair[0], pair[1]);
- }
- result.put(pluralCategory, units);
- }
- return result;
- }
-
- private String getPluralForm(FixedDecimal fixedDecimal) {
- if (pluralRules == null) {
- return CompactDecimalDataCache.OTHER;
- }
- return pluralRules.select(fixedDecimal);
- }
-
- /**
- * Gets the data for a particular locale and style. If style is unrecognized,
- * we just return data for CompactStyle.SHORT.
- * @param locale The locale.
- * @param style The style.
- * @return The data which must not be modified.
- */
- private Data getData(ULocale locale, CompactStyle style) {
- CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
- switch (style) {
- case SHORT:
- return bundle.shortData;
- case LONG:
- return bundle.longData;
- default:
- return bundle.shortData;
- }
- }
- /**
- * Gets the currency data for a particular locale.
- * Currently only short currency format is supported, since that is
- * the only form in CLDR.
- * @param locale The locale.
- * @return The data which must not be modified.
- */
- private Data getCurrencyData(ULocale locale) {
- CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
- return bundle.shortCurrencyData;
- }
-
- private static class Amount {
- private final double qty;
- private final Unit unit;
-
- public Amount(double qty, Unit unit) {
- this.qty = qty;
- this.unit = unit;
- }
-
- public double getQty() {
- return qty;
- }
-
- public Unit getUnit() {
- return unit;
- }
- }
+ LONG
+ }
+
+ /**
+ * Create a CompactDecimalFormat appropriate for a locale. The result may be affected by the
+ * number system in the locale, such as ar-u-nu-latn.
+ *
+ * @param locale the desired locale
+ * @param style the compact style
+ * @stable ICU 50
+ */
+ public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) {
+ return new CompactDecimalFormat(locale, style);
+ }
+
+ /**
+ * Create a CompactDecimalFormat appropriate for a locale. The result may be affected by the
+ * number system in the locale, such as ar-u-nu-latn.
+ *
+ * @param locale the desired locale
+ * @param style the compact style
+ * @stable ICU 50
+ */
+ public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) {
+ return new CompactDecimalFormat(ULocale.forLocale(locale), style);
+ }
+
+ /**
+ * The public mechanism is CompactDecimalFormat.getInstance().
+ *
+ * @param locale the desired locale
+ * @param style the compact style
+ */
+ CompactDecimalFormat(ULocale locale, CompactStyle style) {
+ // Use the locale's default pattern
+ String pattern = getPattern(locale, 0);
+ symbols = DecimalFormatSymbols.getInstance(locale);
+ properties = new Properties();
+ properties.setCompactStyle(style);
+ exportedProperties = new Properties();
+ setPropertiesFromPattern(pattern, true);
+ if (style == CompactStyle.SHORT) {
+ // TODO: This was setGroupingUsed(false) in ICU 58. Is it okay that I changed it for ICU 59?
+ properties.setMinimumGroupingDigits(2);
+ }
+ refreshFormatter();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @stable ICU 49
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return super.equals(obj);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @stable ICU 49
+ */
+ @Override
+ public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq, toAppendTo, pos);
+ fq.populateUFieldPosition(pos);
+ return toAppendTo;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @stable ICU 50
+ */
+ @Override
+ public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
+ if (!(obj instanceof Number)) throw new IllegalArgumentException();
+ Number number = (Number) obj;
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ AttributedCharacterIterator result = formatter.formatToCharacterIterator(fq);
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @stable ICU 49
+ */
+ @Override
+ public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq, toAppendTo, pos);
+ fq.populateUFieldPosition(pos);
+ return toAppendTo;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @stable ICU 49
+ */
+ @Override
+ public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq, toAppendTo, pos);
+ fq.populateUFieldPosition(pos);
+ return toAppendTo;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @stable ICU 49
+ */
+ @Override
+ public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
+ FormatQuantity4 fq = new FormatQuantity4(number);
+ formatter.format(fq, toAppendTo, pos);
+ fq.populateUFieldPosition(pos);
+ return toAppendTo;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @stable ICU 49
+ */
+ @Override
+ public StringBuffer format(
+ com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
+ FormatQuantity4 fq = new FormatQuantity4(number.toBigDecimal());
+ formatter.format(fq, toAppendTo, pos);
+ fq.populateUFieldPosition(pos);
+ return toAppendTo;
+ }
+
+// /**
+// * {@inheritDoc}
+// *
+// * @internal ICU 57 technology preview
+// * @deprecated This API might change or be removed in a future release.
+// */
+// @Override
+// @Deprecated
+// public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
+// // TODO(sffc)
+// throw new UnsupportedOperationException();
+// }
+
+ /**
+ * Parsing is currently unsupported, and throws an UnsupportedOperationException.
+ *
+ * @stable ICU 49
+ */
+ @Override
+ public Number parse(String text, ParsePosition parsePosition) {
+ throw new UnsupportedOperationException();
+ }
+
+ // DISALLOW Serialization, at least while draft
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ throw new NotSerializableException();
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException {
+ throw new NotSerializableException();
+ }
}
}
/**
- * Set plural rules. These are initially set in the constructor based on the locale,
+ * Set plural rules. These are initially set in the constructor based on the locale,
* and usually do not need to be changed.
*
* @param ruleDescription new plural rule description
* Set currency plural patterns. These are initially set in the constructor based on the
* locale, and usually do not need to be changed.
*
+ * The decimal digits part of the pattern cannot be specified via this method. All plural
+ * forms will use the same decimal pattern as set in the constructor of DecimalFormat. For
+ * example, you can't set "0.0" for plural "few" but "0.00" for plural "many".
+ *
* @param pluralCount the plural count for which the currency pattern will
* be overridden.
* @param pattern the new currency plural pattern
*
* @stable ICU 4.2
*/
+ @Override
public Object clone() {
try {
CurrencyPluralInfo other = (CurrencyPluralInfo) super.clone();
*
* @stable ICU 4.2
*/
+ @Override
public boolean equals(Object a) {
if (a instanceof CurrencyPluralInfo) {
CurrencyPluralInfo other = (CurrencyPluralInfo)a;
}
return false;
}
-
+
/**
- * Mock implementation of hashCode(). This implementation always returns a constant
- * value. When Java assertion is enabled, this method triggers an assertion failure.
+ * Override hashCode
+ *
* @internal
* @deprecated This API is ICU internal only.
*/
+ @Override
@Deprecated
public int hashCode() {
- assert false : "hashCode not designed";
- return 42;
+ return pluralCountToCurrencyUnitPattern.hashCode()
+ ^ pluralRules.hashCode()
+ ^ ulocale.hashCode();
}
/**
private void setupCurrencyPluralPattern(ULocale uloc) {
pluralCountToCurrencyUnitPattern = new HashMap<String, String>();
-
+
String numberStylePattern = NumberFormat.getPattern(uloc, NumberFormat.NUMBERSTYLE);
// Split the number style pattern into pos and neg if applicable
int separatorIndex = numberStylePattern.indexOf(";");
for (Map.Entry<String, String> e : map.entrySet()) {
String pluralCount = e.getKey();
String pattern = e.getValue();
-
+
// replace {0} with numberStylePattern
// and {1} with triple currency sign
String patternWithNumber = pattern.replace("{0}", numberStylePattern);
* Returns the array of strings used as digits, in order from 0 through 9
* Package private method - doesn't create a defensively copy.
* @return the array of digit strings
+ * @internal
+ * @deprecated This API is ICU internal only.
*/
- String[] getDigitStringsLocal() {
+ @Deprecated
+ public String[] getDigitStringsLocal() {
return digitStrings;
}
setMonetaryGroupingSeparatorString(numberElements[11]);
setExponentMultiplicationSign(numberElements[12]);
- digit = DecimalFormat.PATTERN_DIGIT; // Localized pattern character no longer in CLDR
- padEscape = DecimalFormat.PATTERN_PAD_ESCAPE;
- sigDigit = DecimalFormat.PATTERN_SIGNIFICANT_DIGIT;
+ digit = '#'; // Localized pattern character no longer in CLDR
+ padEscape = '*';
+ sigDigit = '@';
CurrencyDisplayInfo info = CurrencyData.provider.getInstance(locale, true);
exponential = 'E';
}
if (serialVersionOnStream < 2) {
- padEscape = DecimalFormat.PATTERN_PAD_ESCAPE;
- plusSign = DecimalFormat.PATTERN_PLUS_SIGN;
+ padEscape = '*';
+ plusSign = '+';
exponentSeparator = String.valueOf(exponential);
// Although we read the exponential field on stream to create the
// exponentSeparator, we don't do the reverse, since scientific
groupingSeparatorString = String.valueOf(groupingSeparator);
}
if (percentString == null) {
- percentString = String.valueOf(percentString);
+ percentString = String.valueOf(percent);
}
if (perMillString == null) {
perMillString = String.valueOf(perMill);
--- /dev/null
+// © 2016 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+/*
+ *******************************************************************************
+ * Copyright (C) 1996-2016, International Business Machines Corporation and
+ * others. All Rights Reserved.
+ *******************************************************************************
+ */
+package com.ibm.icu.text;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.math.BigInteger;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.text.ChoiceFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.ibm.icu.impl.ICUConfig;
+import com.ibm.icu.impl.PatternProps;
+import com.ibm.icu.impl.Utility;
+import com.ibm.icu.lang.UCharacter;
+import com.ibm.icu.math.BigDecimal;
+import com.ibm.icu.math.MathContext;
+import com.ibm.icu.text.PluralRules.FixedDecimal;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.ULocale.Category;
+
+/**
+ * {@icuenhanced java.text.DecimalFormat}.{@icu _usage_}
+ *
+ * <code>DecimalFormat</code> is a concrete subclass of {@link NumberFormat} that formats
+ * decimal numbers. It has a variety of features designed to make it possible to parse and
+ * format numbers in any locale, including support for Western, Arabic, or Indic digits.
+ * It also supports different flavors of numbers, including integers ("123"), fixed-point
+ * numbers ("123.4"), scientific notation ("1.23E4"), percentages ("12%"), and currency
+ * amounts ("$123.00", "USD123.00", "123.00 US dollars"). All of these flavors can be
+ * easily localized.
+ *
+ * <p>To obtain a {@link NumberFormat} for a specific locale (including the default
+ * locale) call one of <code>NumberFormat</code>'s factory methods such as {@link
+ * NumberFormat#getInstance}. Do not call the <code>DecimalFormat</code> constructors
+ * directly, unless you know what you are doing, since the {@link NumberFormat} factory
+ * methods may return subclasses other than <code>DecimalFormat</code>. If you need to
+ * customize the format object, do something like this:
+ *
+ * <blockquote><pre>
+ * NumberFormat f = NumberFormat.getInstance(loc);
+ * if (f instanceof DecimalFormat) {
+ * ((DecimalFormat) f).setDecimalSeparatorAlwaysShown(true);
+ * }</pre></blockquote>
+ *
+ * <p><strong>Example Usage</strong>
+ *
+ * Print out a number using the localized number, currency, and percent
+ * format for each locale.
+ *
+ * <blockquote><pre>
+ * Locale[] locales = NumberFormat.getAvailableLocales();
+ * double myNumber = -1234.56;
+ * NumberFormat format;
+ * for (int j=0; j<3; ++j) {
+ * System.out.println("FORMAT");
+ * for (int i = 0; i < locales.length; ++i) {
+ * if (locales[i].getCountry().length() == 0) {
+ * // Skip language-only locales
+ * continue;
+ * }
+ * System.out.print(locales[i].getDisplayName());
+ * switch (j) {
+ * case 0:
+ * format = NumberFormat.getInstance(locales[i]); break;
+ * case 1:
+ * format = NumberFormat.getCurrencyInstance(locales[i]); break;
+ * default:
+ * format = NumberFormat.getPercentInstance(locales[i]); break;
+ * }
+ * try {
+ * // Assume format is a DecimalFormat
+ * System.out.print(": " + ((DecimalFormat) format).toPattern()
+ * + " -> " + form.format(myNumber));
+ * } catch (Exception e) {}
+ * try {
+ * System.out.println(" -> " + format.parse(form.format(myNumber)));
+ * } catch (ParseException e) {}
+ * }
+ * }</pre></blockquote>
+ *
+ * <p>Another example use getInstance(style).<br>
+ * Print out a number using the localized number, currency, percent,
+ * scientific, integer, iso currency, and plural currency format for each locale.
+ *
+ * <blockquote><pre>
+ * ULocale locale = new ULocale("en_US");
+ * double myNumber = 1234.56;
+ * for (int j=NumberFormat.NUMBERSTYLE; j<=NumberFormat.PLURALCURRENCYSTYLE; ++j) {
+ * NumberFormat format = NumberFormat.getInstance(locale, j);
+ * try {
+ * // Assume format is a DecimalFormat
+ * System.out.print(": " + ((DecimalFormat) format).toPattern()
+ * + " -> " + form.format(myNumber));
+ * } catch (Exception e) {}
+ * try {
+ * System.out.println(" -> " + format.parse(form.format(myNumber)));
+ * } catch (ParseException e) {}
+ * }</pre></blockquote>
+ *
+ * <h3>Patterns</h3>
+ *
+ * <p>A <code>DecimalFormat</code> consists of a <em>pattern</em> and a set of
+ * <em>symbols</em>. The pattern may be set directly using {@link #applyPattern}, or
+ * indirectly using other API methods which manipulate aspects of the pattern, such as the
+ * minimum number of integer digits. The symbols are stored in a {@link
+ * DecimalFormatSymbols} object. When using the {@link NumberFormat} factory methods, the
+ * pattern and symbols are read from ICU's locale data.
+ *
+ * <h4>Special Pattern Characters</h4>
+ *
+ * <p>Many characters in a pattern are taken literally; they are matched during parsing
+ * and output unchanged during formatting. Special characters, on the other hand, stand
+ * for other characters, strings, or classes of characters. For example, the '#'
+ * character is replaced by a localized digit. Often the replacement character is the
+ * same as the pattern character; in the U.S. locale, the ',' grouping character is
+ * replaced by ','. However, the replacement is still happening, and if the symbols are
+ * modified, the grouping character changes. Some special characters affect the behavior
+ * of the formatter by their presence; for example, if the percent character is seen, then
+ * the value is multiplied by 100 before being displayed.
+ *
+ * <p>To insert a special character in a pattern as a literal, that is, without any
+ * special meaning, the character must be quoted. There are some exceptions to this which
+ * are noted below.
+ *
+ * <p>The characters listed here are used in non-localized patterns. Localized patterns
+ * use the corresponding characters taken from this formatter's {@link
+ * DecimalFormatSymbols} object instead, and these characters lose their special status.
+ * Two exceptions are the currency sign and quote, which are not localized.
+ *
+ * <blockquote>
+ * <table border=0 cellspacing=3 cellpadding=0 summary="Chart showing symbol,
+ * location, localized, and meaning.">
+ * <tr style="background-color: #ccccff">
+ * <th align=left>Symbol
+ * <th align=left>Location
+ * <th align=left>Localized?
+ * <th align=left>Meaning
+ * <tr style="vertical-align: top;">
+ * <td><code>0</code>
+ * <td>Number
+ * <td>Yes
+ * <td>Digit
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>1-9</code>
+ * <td>Number
+ * <td>Yes
+ * <td>'1' through '9' indicate rounding.
+ * <tr style="vertical-align: top;">
+ * <td><code>@</code>
+ * <td>Number
+ * <td>No
+ * <td>Significant digit
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>#</code>
+ * <td>Number
+ * <td>Yes
+ * <td>Digit, zero shows as absent
+ * <tr style="vertical-align: top;">
+ * <td><code>.</code>
+ * <td>Number
+ * <td>Yes
+ * <td>Decimal separator or monetary decimal separator
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>-</code>
+ * <td>Number
+ * <td>Yes
+ * <td>Minus sign
+ * <tr style="vertical-align: top;">
+ * <td><code>,</code>
+ * <td>Number
+ * <td>Yes
+ * <td>Grouping separator
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>E</code>
+ * <td>Number
+ * <td>Yes
+ * <td>Separates mantissa and exponent in scientific notation.
+ * <em>Need not be quoted in prefix or suffix.</em>
+ * <tr style="vertical-align: top;">
+ * <td><code>+</code>
+ * <td>Exponent
+ * <td>Yes
+ * <td>Prefix positive exponents with localized plus sign.
+ * <em>Need not be quoted in prefix or suffix.</em>
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>;</code>
+ * <td>Subpattern boundary
+ * <td>Yes
+ * <td>Separates positive and negative subpatterns
+ * <tr style="vertical-align: top;">
+ * <td><code>%</code>
+ * <td>Prefix or suffix
+ * <td>Yes
+ * <td>Multiply by 100 and show as percentage
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>\u2030</code>
+ * <td>Prefix or suffix
+ * <td>Yes
+ * <td>Multiply by 1000 and show as per mille
+ * <tr style="vertical-align: top;">
+ * <td><code>¤</code> (<code>\u00A4</code>)
+ * <td>Prefix or suffix
+ * <td>No
+ * <td>Currency sign, replaced by currency symbol. If
+ * doubled, replaced by international currency symbol.
+ * If tripled, replaced by currency plural names, for example,
+ * "US dollar" or "US dollars" for America.
+ * If present in a pattern, the monetary decimal separator
+ * is used instead of the decimal separator.
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>'</code>
+ * <td>Prefix or suffix
+ * <td>No
+ * <td>Used to quote special characters in a prefix or suffix,
+ * for example, <code>"'#'#"</code> formats 123 to
+ * <code>"#123"</code>. To create a single quote
+ * itself, use two in a row: <code>"# o''clock"</code>.
+ * <tr style="vertical-align: top;">
+ * <td><code>*</code>
+ * <td>Prefix or suffix boundary
+ * <td>Yes
+ * <td>Pad escape, precedes pad character
+ * </table>
+ * </blockquote>
+ *
+ * <p>A <code>DecimalFormat</code> pattern contains a postive and negative subpattern, for
+ * example, "#,##0.00;(#,##0.00)". Each subpattern has a prefix, a numeric part, and a
+ * suffix. If there is no explicit negative subpattern, the negative subpattern is the
+ * localized minus sign prefixed to the positive subpattern. That is, "0.00" alone is
+ * equivalent to "0.00;-0.00". If there is an explicit negative subpattern, it serves
+ * only to specify the negative prefix and suffix; the number of digits, minimal digits,
+ * and other characteristics are ignored in the negative subpattern. That means that
+ * "#,##0.0#;(#)" has precisely the same result as "#,##0.0#;(#,##0.0#)".
+ *
+ * <p>The prefixes, suffixes, and various symbols used for infinity, digits, thousands
+ * separators, decimal separators, etc. may be set to arbitrary values, and they will
+ * appear properly during formatting. However, care must be taken that the symbols and
+ * strings do not conflict, or parsing will be unreliable. For example, either the
+ * positive and negative prefixes or the suffixes must be distinct for {@link #parse} to
+ * be able to distinguish positive from negative values. Another example is that the
+ * decimal separator and thousands separator should be distinct characters, or parsing
+ * will be impossible.
+ *
+ * <p>The <em>grouping separator</em> is a character that separates clusters of integer
+ * digits to make large numbers more legible. It commonly used for thousands, but in some
+ * locales it separates ten-thousands. The <em>grouping size</em> is the number of digits
+ * between the grouping separators, such as 3 for "100,000,000" or 4 for "1 0000
+ * 0000". There are actually two different grouping sizes: One used for the least
+ * significant integer digits, the <em>primary grouping size</em>, and one used for all
+ * others, the <em>secondary grouping size</em>. In most locales these are the same, but
+ * sometimes they are different. For example, if the primary grouping interval is 3, and
+ * the secondary is 2, then this corresponds to the pattern "#,##,##0", and the number
+ * 123456789 is formatted as "12,34,56,789". If a pattern contains multiple grouping
+ * separators, the interval between the last one and the end of the integer defines the
+ * primary grouping size, and the interval between the last two defines the secondary
+ * grouping size. All others are ignored, so "#,##,###,####" == "###,###,####" ==
+ * "##,#,###,####".
+ *
+ * <p>Illegal patterns, such as "#.#.#" or "#.###,###", will cause
+ * <code>DecimalFormat</code> to throw an {@link IllegalArgumentException} with a message
+ * that describes the problem.
+ *
+ * <h4>Pattern BNF</h4>
+ *
+ * <pre>
+ * pattern := subpattern (';' subpattern)?
+ * subpattern := prefix? number exponent? suffix?
+ * number := (integer ('.' fraction)?) | sigDigits
+ * prefix := '\u0000'..'\uFFFD' - specialCharacters
+ * suffix := '\u0000'..'\uFFFD' - specialCharacters
+ * integer := '#'* '0'* '0'
+ * fraction := '0'* '#'*
+ * sigDigits := '#'* '@' '@'* '#'*
+ * exponent := 'E' '+'? '0'* '0'
+ * padSpec := '*' padChar
+ * padChar := '\u0000'..'\uFFFD' - quote
+ *  
+ * Notation:
+ * X* 0 or more instances of X
+ * X? 0 or 1 instances of X
+ * X|Y either X or Y
+ * C..D any character from C up to D, inclusive
+ * S-T characters in S, except those in T
+ * </pre>
+ * The first subpattern is for positive numbers. The second (optional)
+ * subpattern is for negative numbers.
+ *
+ * <p>Not indicated in the BNF syntax above:
+ *
+ * <ul>
+ *
+ * <li>The grouping separator ',' can occur inside the integer and sigDigits
+ * elements, between any two pattern characters of that element, as long as the integer or
+ * sigDigits element is not followed by the exponent element.
+ *
+ * <li>Two grouping intervals are recognized: That between the decimal point and the first
+ * grouping symbol, and that between the first and second grouping symbols. These
+ * intervals are identical in most locales, but in some locales they differ. For example,
+ * the pattern "#,##,###" formats the number 123456789 as
+ * "12,34,56,789".
+ *
+ * <li>The pad specifier <code>padSpec</code> may appear before the prefix, after the
+ * prefix, before the suffix, after the suffix, or not at all.
+ *
+ * <li>In place of '0', the digits '1' through '9' may be used to indicate a rounding
+ * increment.
+ *
+ * </ul>
+ *
+ * <h4>Parsing</h4>
+ *
+ * <p><code>DecimalFormat</code> parses all Unicode characters that represent decimal
+ * digits, as defined by {@link UCharacter#digit}. In addition,
+ * <code>DecimalFormat</code> also recognizes as digits the ten consecutive characters
+ * starting with the localized zero digit defined in the {@link DecimalFormatSymbols}
+ * object. During formatting, the {@link DecimalFormatSymbols}-based digits are output.
+ *
+ * <p>During parsing, grouping separators are ignored.
+ *
+ * <p>For currency parsing, the formatter is able to parse every currency style formats no
+ * matter which style the formatter is constructed with. For example, a formatter
+ * instance gotten from NumberFormat.getInstance(ULocale, NumberFormat.CURRENCYSTYLE) can
+ * parse formats such as "USD1.00" and "3.00 US dollars".
+ *
+ * <p>If {@link #parse(String, ParsePosition)} fails to parse a string, it returns
+ * <code>null</code> and leaves the parse position unchanged. The convenience method
+ * {@link #parse(String)} indicates parse failure by throwing a {@link
+ * java.text.ParseException}.
+ *
+ * <p>Parsing an extremely large or small absolute value (such as 1.0E10000 or 1.0E-10000)
+ * requires huge memory allocation for representing the parsed number. Such input may expose
+ * a risk of DoS attacks. To prevent huge memory allocation triggered by such inputs,
+ * <code>DecimalFormat</code> internally limits of maximum decimal digits to be 1000. Thus,
+ * an input string resulting more than 1000 digits in plain decimal representation (non-exponent)
+ * will be treated as either overflow (positive/negative infinite) or underflow (+0.0/-0.0).
+ *
+ * <h4>Formatting</h4>
+ *
+ * <p>Formatting is guided by several parameters, all of which can be specified either
+ * using a pattern or using the API. The following description applies to formats that do
+ * not use <a href="#sci">scientific notation</a> or <a href="#sigdig">significant
+ * digits</a>.
+ *
+ * <ul><li>If the number of actual integer digits exceeds the <em>maximum integer
+ * digits</em>, then only the least significant digits are shown. For example, 1997 is
+ * formatted as "97" if the maximum integer digits is set to 2.
+ *
+ * <li>If the number of actual integer digits is less than the <em>minimum integer
+ * digits</em>, then leading zeros are added. For example, 1997 is formatted as "01997"
+ * if the minimum integer digits is set to 5.
+ *
+ * <li>If the number of actual fraction digits exceeds the <em>maximum fraction
+ * digits</em>, then half-even rounding it performed to the maximum fraction digits. For
+ * example, 0.125 is formatted as "0.12" if the maximum fraction digits is 2. This
+ * behavior can be changed by specifying a rounding increment and a rounding mode.
+ *
+ * <li>If the number of actual fraction digits is less than the <em>minimum fraction
+ * digits</em>, then trailing zeros are added. For example, 0.125 is formatted as
+ * "0.1250" if the mimimum fraction digits is set to 4.
+ *
+ * <li>Trailing fractional zeros are not displayed if they occur <em>j</em> positions
+ * after the decimal, where <em>j</em> is less than the maximum fraction digits. For
+ * example, 0.10004 is formatted as "0.1" if the maximum fraction digits is four or less.
+ * </ul>
+ *
+ * <p><strong>Special Values</strong>
+ *
+ * <p><code>NaN</code> is represented as a single character, typically
+ * <code>\uFFFD</code>. This character is determined by the {@link
+ * DecimalFormatSymbols} object. This is the only value for which the prefixes and
+ * suffixes are not used.
+ *
+ * <p>Infinity is represented as a single character, typically <code>\u221E</code>,
+ * with the positive or negative prefixes and suffixes applied. The infinity character is
+ * determined by the {@link DecimalFormatSymbols} object.
+ *
+ * <h4><a name="sci">Scientific Notation</a></h4>
+ *
+ * <p>Numbers in scientific notation are expressed as the product of a mantissa and a
+ * power of ten, for example, 1234 can be expressed as 1.234 x 10<sup>3</sup>. The
+ * mantissa is typically in the half-open interval [1.0, 10.0) or sometimes [0.0, 1.0),
+ * but it need not be. <code>DecimalFormat</code> supports arbitrary mantissas.
+ * <code>DecimalFormat</code> can be instructed to use scientific notation through the API
+ * or through the pattern. In a pattern, the exponent character immediately followed by
+ * one or more digit characters indicates scientific notation. Example: "0.###E0" formats
+ * the number 1234 as "1.234E3".
+ *
+ * <ul>
+ *
+ * <li>The number of digit characters after the exponent character gives the minimum
+ * exponent digit count. There is no maximum. Negative exponents are formatted using the
+ * localized minus sign, <em>not</em> the prefix and suffix from the pattern. This allows
+ * patterns such as "0.###E0 m/s". To prefix positive exponents with a localized plus
+ * sign, specify '+' between the exponent and the digits: "0.###E+0" will produce formats
+ * "1E+1", "1E+0", "1E-1", etc. (In localized patterns, use the localized plus sign
+ * rather than '+'.)
+ *
+ * <li>The minimum number of integer digits is achieved by adjusting the exponent.
+ * Example: 0.00123 formatted with "00.###E0" yields "12.3E-4". This only happens if
+ * there is no maximum number of integer digits. If there is a maximum, then the minimum
+ * number of integer digits is fixed at one.
+ *
+ * <li>The maximum number of integer digits, if present, specifies the exponent grouping.
+ * The most common use of this is to generate <em>engineering notation</em>, in which the
+ * exponent is a multiple of three, e.g., "##0.###E0". The number 12345 is formatted
+ * using "##0.####E0" as "12.345E3".
+ *
+ * <li>When using scientific notation, the formatter controls the digit counts using
+ * significant digits logic. The maximum number of significant digits limits the total
+ * number of integer and fraction digits that will be shown in the mantissa; it does not
+ * affect parsing. For example, 12345 formatted with "##0.##E0" is "12.3E3". See the
+ * section on significant digits for more details.
+ *
+ * <li>The number of significant digits shown is determined as follows: If
+ * areSignificantDigitsUsed() returns false, then the minimum number of significant digits
+ * shown is one, and the maximum number of significant digits shown is the sum of the
+ * <em>minimum integer</em> and <em>maximum fraction</em> digits, and is unaffected by the
+ * maximum integer digits. If this sum is zero, then all significant digits are shown.
+ * If areSignificantDigitsUsed() returns true, then the significant digit counts are
+ * specified by getMinimumSignificantDigits() and getMaximumSignificantDigits(). In this
+ * case, the number of integer digits is fixed at one, and there is no exponent grouping.
+ *
+ * <li>Exponential patterns may not contain grouping separators.
+ *
+ * </ul>
+ *
+ * <h4><a name="sigdig">Significant Digits</a></h4>
+ *
+ * <code>DecimalFormat</code> has two ways of controlling how many digits are shows: (a)
+ * significant digits counts, or (b) integer and fraction digit counts. Integer and
+ * fraction digit counts are described above. When a formatter is using significant
+ * digits counts, the number of integer and fraction digits is not specified directly, and
+ * the formatter settings for these counts are ignored. Instead, the formatter uses
+ * however many integer and fraction digits are required to display the specified number
+ * of significant digits. Examples:
+ *
+ * <blockquote>
+ * <table border=0 cellspacing=3 cellpadding=0>
+ * <tr style="background-color: #ccccff">
+ * <th align=left>Pattern
+ * <th align=left>Minimum significant digits
+ * <th align=left>Maximum significant digits
+ * <th align=left>Number
+ * <th align=left>Output of format()
+ * <tr style="vertical-align: top;">
+ * <td><code>@@@</code>
+ * <td>3
+ * <td>3
+ * <td>12345
+ * <td><code>12300</code>
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>@@@</code>
+ * <td>3
+ * <td>3
+ * <td>0.12345
+ * <td><code>0.123</code>
+ * <tr style="vertical-align: top;">
+ * <td><code>@@##</code>
+ * <td>2
+ * <td>4
+ * <td>3.14159
+ * <td><code>3.142</code>
+ * <tr style="vertical-align: top; background-color: #eeeeff;">
+ * <td><code>@@##</code>
+ * <td>2
+ * <td>4
+ * <td>1.23004
+ * <td><code>1.23</code>
+ * </table>
+ * </blockquote>
+ *
+ * <ul>
+ *
+ * <li>Significant digit counts may be expressed using patterns that specify a minimum and
+ * maximum number of significant digits. These are indicated by the <code>'@'</code> and
+ * <code>'#'</code> characters. The minimum number of significant digits is the number of
+ * <code>'@'</code> characters. The maximum number of significant digits is the number of
+ * <code>'@'</code> characters plus the number of <code>'#'</code> characters following on
+ * the right. For example, the pattern <code>"@@@"</code> indicates exactly 3 significant
+ * digits. The pattern <code>"@##"</code> indicates from 1 to 3 significant digits.
+ * Trailing zero digits to the right of the decimal separator are suppressed after the
+ * minimum number of significant digits have been shown. For example, the pattern
+ * <code>"@##"</code> formats the number 0.1203 as <code>"0.12"</code>.
+ *
+ * <li>If a pattern uses significant digits, it may not contain a decimal separator, nor
+ * the <code>'0'</code> pattern character. Patterns such as <code>"@00"</code> or
+ * <code>"@.###"</code> are disallowed.
+ *
+ * <li>Any number of <code>'#'</code> characters may be prepended to the left of the
+ * leftmost <code>'@'</code> character. These have no effect on the minimum and maximum
+ * significant digits counts, but may be used to position grouping separators. For
+ * example, <code>"#,#@#"</code> indicates a minimum of one significant digits, a maximum
+ * of two significant digits, and a grouping size of three.
+ *
+ * <li>In order to enable significant digits formatting, use a pattern containing the
+ * <code>'@'</code> pattern character. Alternatively, call {@link
+ * #setSignificantDigitsUsed setSignificantDigitsUsed(true)}.
+ *
+ * <li>In order to disable significant digits formatting, use a pattern that does not
+ * contain the <code>'@'</code> pattern character. Alternatively, call {@link
+ * #setSignificantDigitsUsed setSignificantDigitsUsed(false)}.
+ *
+ * <li>The number of significant digits has no effect on parsing.
+ *
+ * <li>Significant digits may be used together with exponential notation. Such patterns
+ * are equivalent to a normal exponential pattern with a minimum and maximum integer digit
+ * count of one, a minimum fraction digit count of <code>getMinimumSignificantDigits() -
+ * 1</code>, and a maximum fraction digit count of <code>getMaximumSignificantDigits() -
+ * 1</code>. For example, the pattern <code>"@@###E0"</code> is equivalent to
+ * <code>"0.0###E0"</code>.
+ *
+ * <li>If signficant digits are in use, then the integer and fraction digit counts, as set
+ * via the API, are ignored. If significant digits are not in use, then the signficant
+ * digit counts, as set via the API, are ignored.
+ *
+ * </ul>
+ *
+ * <h4>Padding</h4>
+ *
+ * <p><code>DecimalFormat</code> supports padding the result of {@link #format} to a
+ * specific width. Padding may be specified either through the API or through the pattern
+ * syntax. In a pattern the pad escape character, followed by a single pad character,
+ * causes padding to be parsed and formatted. The pad escape character is '*' in
+ * unlocalized patterns, and can be localized using {@link
+ * DecimalFormatSymbols#setPadEscape}. For example, <code>"$*x#,##0.00"</code> formats
+ * 123 to <code>"$xx123.00"</code>, and 1234 to <code>"$1,234.00"</code>.
+ *
+ * <ul>
+ *
+ * <li>When padding is in effect, the width of the positive subpattern, including prefix
+ * and suffix, determines the format width. For example, in the pattern <code>"* #0
+ * o''clock"</code>, the format width is 10.
+ *
+ * <li>The width is counted in 16-bit code units (Java <code>char</code>s).
+ *
+ * <li>Some parameters which usually do not matter have meaning when padding is used,
+ * because the pattern width is significant with padding. In the pattern "*
+ * ##,##,#,##0.##", the format width is 14. The initial characters "##,##," do not affect
+ * the grouping size or maximum integer digits, but they do affect the format width.
+ *
+ * <li>Padding may be inserted at one of four locations: before the prefix, after the
+ * prefix, before the suffix, or after the suffix. If padding is specified in any other
+ * location, {@link #applyPattern} throws an {@link IllegalArgumentException}. If there
+ * is no prefix, before the prefix and after the prefix are equivalent, likewise for the
+ * suffix.
+ *
+ * <li>When specified in a pattern, the 16-bit <code>char</code> immediately following the
+ * pad escape is the pad character. This may be any character, including a special pattern
+ * character. That is, the pad escape <em>escapes</em> the following character. If there
+ * is no character after the pad escape, then the pattern is illegal.
+ *
+ * </ul>
+ *
+ * <p>
+ * <strong>Rounding</strong>
+ *
+ * <p><code>DecimalFormat</code> supports rounding to a specific increment. For example,
+ * 1230 rounded to the nearest 50 is 1250. 1.234 rounded to the nearest 0.65 is 1.3. The
+ * rounding increment may be specified through the API or in a pattern. To specify a
+ * rounding increment in a pattern, include the increment in the pattern itself. "#,#50"
+ * specifies a rounding increment of 50. "#,##0.05" specifies a rounding increment of
+ * 0.05.
+ *
+ * <ul>
+ *
+ * <li>Rounding only affects the string produced by formatting. It does not affect
+ * parsing or change any numerical values.
+ *
+ * <li>A <em>rounding mode</em> determines how values are rounded; see the {@link
+ * com.ibm.icu.math.BigDecimal} documentation for a description of the modes. Rounding
+ * increments specified in patterns use the default mode, {@link
+ * com.ibm.icu.math.BigDecimal#ROUND_HALF_EVEN}.
+ *
+ * <li>Some locales use rounding in their currency formats to reflect the smallest
+ * currency denomination.
+ *
+ * <li>In a pattern, digits '1' through '9' specify rounding, but otherwise behave
+ * identically to digit '0'.
+ *
+ * </ul>
+ *
+ * <h4>Synchronization</h4>
+ *
+ * <p><code>DecimalFormat</code> objects are not synchronized. Multiple threads should
+ * not access one formatter concurrently.
+ *
+ * @see java.text.Format
+ * @see NumberFormat
+ * @author Mark Davis
+ * @author Alan Liu
+ * @deprecated DecimalFormat was overhauled in ICU 59. This is the old implementation, provided
+ * temporarily to ease the transition. This class will be removed from ICU 60.
+ */
+@Deprecated
+public class DecimalFormat_ICU58 extends NumberFormat {
+
+ /**
+ * Creates a DecimalFormat using the default pattern and symbols for the default
+ * <code>FORMAT</code> locale. This is a convenient way to obtain a DecimalFormat when
+ * internationalization is not the main concern.
+ *
+ * <p>To obtain standard formats for a given locale, use the factory methods on
+ * NumberFormat such as getNumberInstance. These factories will return the most
+ * appropriate sub-class of NumberFormat for a given locale.
+ *
+ * @see NumberFormat#getInstance
+ * @see NumberFormat#getNumberInstance
+ * @see NumberFormat#getCurrencyInstance
+ * @see NumberFormat#getPercentInstance
+ * @see Category#FORMAT
+ * @stable ICU 2.0
+ */
+ public DecimalFormat_ICU58() {
+ ULocale def = ULocale.getDefault(Category.FORMAT);
+ String pattern = getPattern(def, 0);
+ // Always applyPattern after the symbols are set
+ this.symbols = new DecimalFormatSymbols(def);
+ setCurrency(Currency.getInstance(def));
+ applyPatternWithoutExpandAffix(pattern, false);
+ if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+ currencyPluralInfo = new CurrencyPluralInfo(def);
+ // the exact pattern is not known until the plural count is known.
+ // so, no need to expand affix now.
+ } else {
+ expandAffixAdjustWidth(null);
+ }
+ }
+
+ /**
+ * Creates a DecimalFormat from the given pattern and the symbols for the default
+ * <code>FORMAT</code> locale. This is a convenient way to obtain a DecimalFormat when
+ * internationalization is not the main concern.
+ *
+ * <p>To obtain standard formats for a given locale, use the factory methods on
+ * NumberFormat such as getNumberInstance. These factories will return the most
+ * appropriate sub-class of NumberFormat for a given locale.
+ *
+ * @param pattern A non-localized pattern string.
+ * @throws IllegalArgumentException if the given pattern is invalid.
+ * @see NumberFormat#getInstance
+ * @see NumberFormat#getNumberInstance
+ * @see NumberFormat#getCurrencyInstance
+ * @see NumberFormat#getPercentInstance
+ * @see Category#FORMAT
+ * @stable ICU 2.0
+ */
+ public DecimalFormat_ICU58(String pattern) {
+ // Always applyPattern after the symbols are set
+ ULocale def = ULocale.getDefault(Category.FORMAT);
+ this.symbols = new DecimalFormatSymbols(def);
+ setCurrency(Currency.getInstance(def));
+ applyPatternWithoutExpandAffix(pattern, false);
+ if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+ currencyPluralInfo = new CurrencyPluralInfo(def);
+ } else {
+ expandAffixAdjustWidth(null);
+ }
+ }
+
+ /**
+ * Creates a DecimalFormat from the given pattern and symbols. Use this constructor
+ * when you need to completely customize the behavior of the format.
+ *
+ * <p>To obtain standard formats for a given locale, use the factory methods on
+ * NumberFormat such as getInstance or getCurrencyInstance. If you need only minor
+ * adjustments to a standard format, you can modify the format returned by a
+ * NumberFormat factory method.
+ *
+ * @param pattern a non-localized pattern string
+ * @param symbols the set of symbols to be used
+ * @exception IllegalArgumentException if the given pattern is invalid
+ * @see NumberFormat#getInstance
+ * @see NumberFormat#getNumberInstance
+ * @see NumberFormat#getCurrencyInstance
+ * @see NumberFormat#getPercentInstance
+ * @see DecimalFormatSymbols
+ * @stable ICU 2.0
+ */
+ public DecimalFormat_ICU58(String pattern, DecimalFormatSymbols symbols) {
+ createFromPatternAndSymbols(pattern, symbols);
+ }
+
+ private void createFromPatternAndSymbols(String pattern, DecimalFormatSymbols inputSymbols) {
+ // Always applyPattern after the symbols are set
+ symbols = (DecimalFormatSymbols) inputSymbols.clone();
+ if (pattern.indexOf(CURRENCY_SIGN) >= 0) {
+ // Only spend time with currency symbols when we're going to display it.
+ // Also set some defaults before the apply pattern.
+ setCurrencyForSymbols();
+ }
+ applyPatternWithoutExpandAffix(pattern, false);
+ if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+ currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
+ } else {
+ expandAffixAdjustWidth(null);
+ }
+ }
+
+ /**
+ * Creates a DecimalFormat from the given pattern, symbols, information used for
+ * currency plural format, and format style. Use this constructor when you need to
+ * completely customize the behavior of the format.
+ *
+ * <p>To obtain standard formats for a given locale, use the factory methods on
+ * NumberFormat such as getInstance or getCurrencyInstance.
+ *
+ * <p>If you need only minor adjustments to a standard format, you can modify the
+ * format returned by a NumberFormat factory method using the setters.
+ *
+ * <p>If you want to completely customize a decimal format, using your own
+ * DecimalFormatSymbols (such as group separators) and your own information for
+ * currency plural formatting (such as plural rule and currency plural patterns), you
+ * can use this constructor.
+ *
+ * @param pattern a non-localized pattern string
+ * @param symbols the set of symbols to be used
+ * @param infoInput the information used for currency plural format, including
+ * currency plural patterns and plural rules.
+ * @param style the decimal formatting style, it is one of the following values:
+ * NumberFormat.NUMBERSTYLE; NumberFormat.CURRENCYSTYLE; NumberFormat.PERCENTSTYLE;
+ * NumberFormat.SCIENTIFICSTYLE; NumberFormat.INTEGERSTYLE;
+ * NumberFormat.ISOCURRENCYSTYLE; NumberFormat.PLURALCURRENCYSTYLE;
+ * @stable ICU 4.2
+ */
+ public DecimalFormat_ICU58(String pattern, DecimalFormatSymbols symbols, CurrencyPluralInfo infoInput,
+ int style) {
+ CurrencyPluralInfo info = infoInput;
+ if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+ info = (CurrencyPluralInfo) infoInput.clone();
+ }
+ create(pattern, symbols, info, style);
+ }
+
+ private void create(String pattern, DecimalFormatSymbols inputSymbols, CurrencyPluralInfo info,
+ int inputStyle) {
+ if (inputStyle != NumberFormat.PLURALCURRENCYSTYLE) {
+ createFromPatternAndSymbols(pattern, inputSymbols);
+ } else {
+ // Always applyPattern after the symbols are set
+ symbols = (DecimalFormatSymbols) inputSymbols.clone();
+ currencyPluralInfo = info;
+ // the pattern used in format is not fixed until formatting, in which, the
+ // number is known and will be used to pick the right pattern based on plural
+ // count. Here, set the pattern as the pattern of plural count == "other".
+ // For most locale, the patterns are probably the same for all plural
+ // count. If not, the right pattern need to be re-applied during format.
+ String currencyPluralPatternForOther =
+ currencyPluralInfo.getCurrencyPluralPattern("other");
+ applyPatternWithoutExpandAffix(currencyPluralPatternForOther, false);
+ setCurrencyForSymbols();
+ }
+ style = inputStyle;
+ }
+
+ /**
+ * Creates a DecimalFormat for currency plural format from the given pattern, symbols,
+ * and style.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public DecimalFormat_ICU58(String pattern, DecimalFormatSymbols inputSymbols, int style) {
+ CurrencyPluralInfo info = null;
+ if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+ info = new CurrencyPluralInfo(inputSymbols.getULocale());
+ }
+ create(pattern, inputSymbols, info, style);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 2.0
+ */
+ @Override
+ public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
+ return format(number, result, fieldPosition, false);
+ }
+
+ // See if number is negative.
+ // usage: isNegative(multiply(numberToBeFormatted));
+ private boolean isNegative(double number) {
+ // Detecting whether a double is negative is easy with the exception of the value
+ // -0.0. This is a double which has a zero mantissa (and exponent), but a negative
+ // sign bit. It is semantically distinct from a zero with a positive sign bit, and
+ // this distinction is important to certain kinds of computations. However, it's a
+ // little tricky to detect, since (-0.0 == 0.0) and !(-0.0 < 0.0). How then, you
+ // may ask, does it behave distinctly from +0.0? Well, 1/(-0.0) ==
+ // -Infinity. Proper detection of -0.0 is needed to deal with the issues raised by
+ // bugs 4106658, 4106667, and 4147706. Liu 7/6/98.
+ return (number < 0.0) || (number == 0.0 && 1 / number < 0.0);
+ }
+
+ // Rounds the number and strips of the negative sign.
+ // usage: round(multiply(numberToBeFormatted))
+ private double round(double number) {
+ boolean isNegative = isNegative(number);
+ if (isNegative)
+ number = -number;
+
+ // Apply rounding after multiplier
+ if (roundingDouble > 0.0) {
+ // number = roundingDouble
+ // * round(number / roundingDouble, roundingMode, isNegative);
+ return round(
+ number, roundingDouble, roundingDoubleReciprocal, roundingMode,
+ isNegative);
+ }
+ return number;
+ }
+
+ // Multiplies given number by multipler (if there is one) returning the new
+ // number. If there is no multiplier, returns the number passed in unchanged.
+ private double multiply(double number) {
+ if (multiplier != 1) {
+ return number * multiplier;
+ }
+ return number;
+ }
+
+ // [Spark/CDL] The actual method to format number. If boolean value
+ // parseAttr == true, then attribute information will be recorded.
+ private StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition,
+ boolean parseAttr) {
+ fieldPosition.setBeginIndex(0);
+ fieldPosition.setEndIndex(0);
+
+ if (Double.isNaN(number)) {
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+ fieldPosition.setBeginIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+
+ result.append(symbols.getNaN());
+ // TODO: Combine setting a single FieldPosition or adding to an AttributedCharacterIterator
+ // into a function like recordAttribute(FieldAttribute, begin, end).
+
+ // [Spark/CDL] Add attribute for NaN here.
+ // result.append(symbols.getNaN());
+ if (parseAttr) {
+ addAttribute(Field.INTEGER, result.length() - symbols.getNaN().length(),
+ result.length());
+ }
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+ fieldPosition.setEndIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ fieldPosition.setEndIndex(result.length());
+ }
+
+ addPadding(result, fieldPosition, 0, 0);
+ return result;
+ }
+
+ // Do this BEFORE checking to see if value is negative or infinite and
+ // before rounding.
+ number = multiply(number);
+ boolean isNegative = isNegative(number);
+ number = round(number);
+
+ if (Double.isInfinite(number)) {
+ int prefixLen = appendAffix(result, isNegative, true, fieldPosition, parseAttr);
+
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+ fieldPosition.setBeginIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+
+ // [Spark/CDL] Add attribute for infinity here.
+ result.append(symbols.getInfinity());
+ if (parseAttr) {
+ addAttribute(Field.INTEGER, result.length() - symbols.getInfinity().length(),
+ result.length());
+ }
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+ fieldPosition.setEndIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ fieldPosition.setEndIndex(result.length());
+ }
+
+ int suffixLen = appendAffix(result, isNegative, false, fieldPosition, parseAttr);
+
+ addPadding(result, fieldPosition, prefixLen, suffixLen);
+ return result;
+ }
+
+ int precision = precision(false);
+
+ // This is to fix rounding for scientific notation. See ticket:10542.
+ // This code should go away when a permanent fix is done for ticket:9931.
+ //
+ // This block of code only executes for scientific notation so it will not interfere with the
+ // previous fix in {@link #resetActualRounding} for fixed decimal numbers.
+ // Moreover this code only runs when there is rounding to be done (precision > 0) and when the
+ // rounding mode is something other than ROUND_HALF_EVEN.
+ // This block of code does the correct rounding of number in advance so that it will fit into
+ // the number of digits indicated by precision. In this way, we avoid using the default
+ // ROUND_HALF_EVEN behavior of DigitList. For example, if number = 0.003016 and roundingMode =
+ // ROUND_DOWN and precision = 3 then after this code executes, number = 0.00301 (3 significant digits)
+ if (useExponentialNotation && precision > 0 && number != 0.0 && roundingMode != BigDecimal.ROUND_HALF_EVEN) {
+ int log10RoundingIncr = 1 - precision + (int) Math.floor(Math.log10(Math.abs(number)));
+ double roundingIncReciprocal = 0.0;
+ double roundingInc = 0.0;
+ if (log10RoundingIncr < 0) {
+ roundingIncReciprocal =
+ BigDecimal.ONE.movePointRight(-log10RoundingIncr).doubleValue();
+ } else {
+ roundingInc =
+ BigDecimal.ONE.movePointRight(log10RoundingIncr).doubleValue();
+ }
+ number = DecimalFormat_ICU58.round(number, roundingInc, roundingIncReciprocal, roundingMode, isNegative);
+ }
+ // End fix for ticket:10542
+
+ // At this point we are guaranteed a nonnegative finite
+ // number.
+ synchronized (digitList) {
+ digitList.set(number, precision, !useExponentialNotation &&
+ !areSignificantDigitsUsed());
+ return subformat(number, result, fieldPosition, isNegative, false, parseAttr);
+ }
+ }
+
+ /**
+ * This is a special function used by the CompactDecimalFormat subclass.
+ * It completes only the rounding portion of the formatting and returns
+ * the resulting double. CompactDecimalFormat uses the result to compute
+ * the plural form to use.
+ *
+ * @param number The number to format.
+ * @return The number rounded to the correct number of significant digits
+ * with negative sign stripped off.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ double adjustNumberAsInFormatting(double number) {
+ if (Double.isNaN(number)) {
+ return number;
+ }
+ number = round(multiply(number));
+ if (Double.isInfinite(number)) {
+ return number;
+ }
+ return toDigitList(number).getDouble();
+ }
+
+ @Deprecated
+ DigitList toDigitList(double number) {
+ DigitList result = new DigitList();
+ result.set(number, precision(false), false);
+ return result;
+ }
+
+ /**
+ * This is a special function used by the CompactDecimalFormat subclass
+ * to determine if the number to be formatted is negative.
+ *
+ * @param number The number to format.
+ * @return True if number is negative.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ boolean isNumberNegative(double number) {
+ if (Double.isNaN(number)) {
+ return false;
+ }
+ return isNegative(multiply(number));
+ }
+
+ /**
+ * Round a double value to the nearest multiple of the given rounding increment,
+ * according to the given mode. This is equivalent to rounding value/roundingInc to
+ * the nearest integer, according to the given mode, and returning that integer *
+ * roundingInc. Note this is changed from the version in 2.4, since division of
+ * doubles have inaccuracies. jitterbug 1871.
+ *
+ * @param number
+ * the absolute value of the number to be rounded
+ * @param roundingInc
+ * the rounding increment
+ * @param roundingIncReciprocal
+ * if non-zero, is the reciprocal of rounding inc.
+ * @param mode
+ * a BigDecimal rounding mode
+ * @param isNegative
+ * true if the number to be rounded is negative
+ * @return the absolute value of the rounded result
+ */
+ private static double round(double number, double roundingInc, double roundingIncReciprocal,
+ int mode, boolean isNegative) {
+
+ double div = roundingIncReciprocal == 0.0 ? number / roundingInc : number *
+ roundingIncReciprocal;
+
+ // do the absolute cases first
+
+ switch (mode) {
+ case BigDecimal.ROUND_CEILING:
+ div = (isNegative ? Math.floor(div + epsilon) : Math.ceil(div - epsilon));
+ break;
+ case BigDecimal.ROUND_FLOOR:
+ div = (isNegative ? Math.ceil(div - epsilon) : Math.floor(div + epsilon));
+ break;
+ case BigDecimal.ROUND_DOWN:
+ div = (Math.floor(div + epsilon));
+ break;
+ case BigDecimal.ROUND_UP:
+ div = (Math.ceil(div - epsilon));
+ break;
+ case BigDecimal.ROUND_UNNECESSARY:
+ if (div != Math.floor(div)) {
+ throw new ArithmeticException("Rounding necessary");
+ }
+ return number;
+ default:
+
+ // Handle complex cases, where the choice depends on the closer value.
+
+ // We figure out the distances to the two possible values, ceiling and floor.
+ // We then go for the diff that is smaller. Only if they are equal does the
+ // mode matter.
+
+ double ceil = Math.ceil(div);
+ double ceildiff = ceil - div; // (ceil * roundingInc) - number;
+ double floor = Math.floor(div);
+ double floordiff = div - floor; // number - (floor * roundingInc);
+
+ // Note that the diff values were those mapped back to the "normal" space by
+ // using the roundingInc. I don't have access to the original author of the
+ // code but suspect that that was to produce better result in edge cases
+ // because of machine precision, rather than simply using the difference
+ // between, say, ceil and div. However, it didn't work in all cases. Am
+ // trying instead using an epsilon value.
+
+ switch (mode) {
+ case BigDecimal.ROUND_HALF_EVEN:
+ // We should be able to just return Math.rint(a), but this
+ // doesn't work in some VMs.
+ // if one is smaller than the other, take the corresponding side
+ if (floordiff + epsilon < ceildiff) {
+ div = floor;
+ } else if (ceildiff + epsilon < floordiff) {
+ div = ceil;
+ } else { // they are equal, so we want to round to whichever is even
+ double testFloor = floor / 2;
+ div = (testFloor == Math.floor(testFloor)) ? floor : ceil;
+ }
+ break;
+ case BigDecimal.ROUND_HALF_DOWN:
+ div = ((floordiff <= ceildiff + epsilon) ? floor : ceil);
+ break;
+ case BigDecimal.ROUND_HALF_UP:
+ div = ((ceildiff <= floordiff + epsilon) ? ceil : floor);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid rounding mode: " + mode);
+ }
+ }
+ number = roundingIncReciprocal == 0.0 ? div * roundingInc : div / roundingIncReciprocal;
+ return number;
+ }
+
+ private static double epsilon = 0.00000000001;
+
+ /**
+ * @stable ICU 2.0
+ */
+ // [Spark/CDL] Delegate to format_long_StringBuffer_FieldPosition_boolean
+ @Override
+ public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) {
+ return format(number, result, fieldPosition, false);
+ }
+
+ private StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition,
+ boolean parseAttr) {
+ fieldPosition.setBeginIndex(0);
+ fieldPosition.setEndIndex(0);
+
+ // If we are to do rounding, we need to move into the BigDecimal
+ // domain in order to do divide/multiply correctly.
+ if (actualRoundingIncrementICU != null) {
+ return format(BigDecimal.valueOf(number), result, fieldPosition);
+ }
+
+ boolean isNegative = (number < 0);
+ if (isNegative)
+ number = -number;
+
+ // In general, long values always represent real finite numbers, so we don't have
+ // to check for +/- Infinity or NaN. However, there is one case we have to be
+ // careful of: The multiplier can push a number near MIN_VALUE or MAX_VALUE
+ // outside the legal range. We check for this before multiplying, and if it
+ // happens we use BigInteger instead.
+ if (multiplier != 1) {
+ boolean tooBig = false;
+ if (number < 0) { // This can only happen if number == Long.MIN_VALUE
+ long cutoff = Long.MIN_VALUE / multiplier;
+ tooBig = (number <= cutoff); // number == cutoff can only happen if multiplier == -1
+ } else {
+ long cutoff = Long.MAX_VALUE / multiplier;
+ tooBig = (number > cutoff);
+ }
+ if (tooBig) {
+ // [Spark/CDL] Use
+ // format_BigInteger_StringBuffer_FieldPosition_boolean instead
+ // parseAttr is used to judge whether to synthesize attributes.
+ return format(BigInteger.valueOf(isNegative ? -number : number), result,
+ fieldPosition, parseAttr);
+ }
+ }
+
+ number *= multiplier;
+ synchronized (digitList) {
+ digitList.set(number, precision(true));
+ // Issue 11808
+ if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
+ throw new ArithmeticException("Rounding necessary");
+ }
+ return subformat(number, result, fieldPosition, isNegative, true, parseAttr);
+ }
+ }
+
+ /**
+ * Formats a BigInteger number.
+ *
+ * @stable ICU 2.0
+ */
+ @Override
+ public StringBuffer format(BigInteger number, StringBuffer result,
+ FieldPosition fieldPosition) {
+ return format(number, result, fieldPosition, false);
+ }
+
+ private StringBuffer format(BigInteger number, StringBuffer result, FieldPosition fieldPosition,
+ boolean parseAttr) {
+ // If we are to do rounding, we need to move into the BigDecimal
+ // domain in order to do divide/multiply correctly.
+ if (actualRoundingIncrementICU != null) {
+ return format(new BigDecimal(number), result, fieldPosition);
+ }
+
+ if (multiplier != 1) {
+ number = number.multiply(BigInteger.valueOf(multiplier));
+ }
+
+ // At this point we are guaranteed a nonnegative finite
+ // number.
+ synchronized (digitList) {
+ digitList.set(number, precision(true));
+ // For issue 11808.
+ if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
+ throw new ArithmeticException("Rounding necessary");
+ }
+ return subformat(number.intValue(), result, fieldPosition, number.signum() < 0, true,
+ parseAttr);
+ }
+ }
+
+ /**
+ * Formats a BigDecimal number.
+ *
+ * @stable ICU 2.0
+ */
+ @Override
+ public StringBuffer format(java.math.BigDecimal number, StringBuffer result,
+ FieldPosition fieldPosition) {
+ return format(number, result, fieldPosition, false);
+ }
+
+ private StringBuffer format(java.math.BigDecimal number, StringBuffer result,
+ FieldPosition fieldPosition,
+ boolean parseAttr) {
+ if (multiplier != 1) {
+ number = number.multiply(java.math.BigDecimal.valueOf(multiplier));
+ }
+
+ if (actualRoundingIncrement != null) {
+ number = number.divide(actualRoundingIncrement, 0, roundingMode).multiply(actualRoundingIncrement);
+ }
+
+ synchronized (digitList) {
+ digitList.set(number, precision(false), !useExponentialNotation &&
+ !areSignificantDigitsUsed());
+ // For issue 11808.
+ if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
+ throw new ArithmeticException("Rounding necessary");
+ }
+ return subformat(number.doubleValue(), result, fieldPosition, number.signum() < 0,
+ false, parseAttr);
+ }
+ }
+
+ /**
+ * Formats a BigDecimal number.
+ *
+ * @stable ICU 2.0
+ */
+ @Override
+ public StringBuffer format(BigDecimal number, StringBuffer result,
+ FieldPosition fieldPosition) {
+ // This method is just a copy of the corresponding java.math.BigDecimal method
+ // for now. It isn't very efficient since it must create a conversion object to
+ // do math on the rounding increment. In the future we may try to clean this up,
+ // or even better, limit our support to just one flavor of BigDecimal.
+ if (multiplier != 1) {
+ number = number.multiply(BigDecimal.valueOf(multiplier), mathContext);
+ }
+
+ if (actualRoundingIncrementICU != null) {
+ number = number.divide(actualRoundingIncrementICU, 0, roundingMode)
+ .multiply(actualRoundingIncrementICU, mathContext);
+ }
+
+ synchronized (digitList) {
+ digitList.set(number, precision(false), !useExponentialNotation &&
+ !areSignificantDigitsUsed());
+ // For issue 11808.
+ if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
+ throw new ArithmeticException("Rounding necessary");
+ }
+ return subformat(number.doubleValue(), result, fieldPosition, number.signum() < 0,
+ false, false);
+ }
+ }
+
+ /**
+ * Returns true if a grouping separator belongs at the given position, based on whether
+ * grouping is in use and the values of the primary and secondary grouping interval.
+ *
+ * @param pos the number of integer digits to the right of the current position. Zero
+ * indicates the position after the rightmost integer digit.
+ * @return true if a grouping character belongs at the current position.
+ */
+ private boolean isGroupingPosition(int pos) {
+ boolean result = false;
+ if (isGroupingUsed() && (pos > 0) && (groupingSize > 0)) {
+ if ((groupingSize2 > 0) && (pos > groupingSize)) {
+ result = ((pos - groupingSize) % groupingSize2) == 0;
+ } else {
+ result = pos % groupingSize == 0;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Return the number of fraction digits to display, or the total
+ * number of digits for significant digit formats and exponential
+ * formats.
+ */
+ private int precision(boolean isIntegral) {
+ if (areSignificantDigitsUsed()) {
+ return getMaximumSignificantDigits();
+ } else if (useExponentialNotation) {
+ return getMinimumIntegerDigits() + getMaximumFractionDigits();
+ } else {
+ return isIntegral ? 0 : getMaximumFractionDigits();
+ }
+ }
+
+ private StringBuffer subformat(int number, StringBuffer result, FieldPosition fieldPosition,
+ boolean isNegative, boolean isInteger, boolean parseAttr) {
+ if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+ // compute the plural category from the digitList plus other settings
+ return subformat(currencyPluralInfo.select(getFixedDecimal(number)),
+ result, fieldPosition, isNegative,
+ isInteger, parseAttr);
+ } else {
+ return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
+ }
+ }
+
+ /**
+ * This is ugly, but don't see a better way to do it without major restructuring of the code.
+ */
+ /*package*/ FixedDecimal getFixedDecimal(double number) {
+ // get the visible fractions and the number of fraction digits.
+ return getFixedDecimal(number, digitList);
+ }
+
+ FixedDecimal getFixedDecimal(double number, DigitList dl) {
+ int fractionalDigitsInDigitList = dl.count - dl.decimalAt;
+ int v;
+ long f;
+ int maxFractionalDigits;
+ int minFractionalDigits;
+ if (useSignificantDigits) {
+ maxFractionalDigits = maxSignificantDigits - dl.decimalAt;
+ minFractionalDigits = minSignificantDigits - dl.decimalAt;
+ if (minFractionalDigits < 0) {
+ minFractionalDigits = 0;
+ }
+ if (maxFractionalDigits < 0) {
+ maxFractionalDigits = 0;
+ }
+ } else {
+ maxFractionalDigits = getMaximumFractionDigits();
+ minFractionalDigits = getMinimumFractionDigits();
+ }
+ v = fractionalDigitsInDigitList;
+ if (v < minFractionalDigits) {
+ v = minFractionalDigits;
+ } else if (v > maxFractionalDigits) {
+ v = maxFractionalDigits;
+ }
+ f = 0;
+ if (v > 0) {
+ for (int i = Math.max(0, dl.decimalAt); i < dl.count; ++i) {
+ f *= 10;
+ f += (dl.digits[i] - '0');
+ }
+ for (int i = v; i < fractionalDigitsInDigitList; ++i) {
+ f *= 10;
+ }
+ }
+ return new FixedDecimal(number, v, f);
+ }
+
+ private StringBuffer subformat(double number, StringBuffer result, FieldPosition fieldPosition,
+ boolean isNegative,
+ boolean isInteger, boolean parseAttr) {
+ if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+ // compute the plural category from the digitList plus other settings
+ return subformat(currencyPluralInfo.select(getFixedDecimal(number)),
+ result, fieldPosition, isNegative,
+ isInteger, parseAttr);
+ } else {
+ return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
+ }
+ }
+
+ private StringBuffer subformat(String pluralCount, StringBuffer result, FieldPosition fieldPosition,
+ boolean isNegative, boolean isInteger, boolean parseAttr) {
+ // There are 2 ways to activate currency plural format: by applying a pattern with
+ // 3 currency sign directly, or by instantiate a decimal formatter using
+ // PLURALCURRENCYSTYLE. For both cases, the number of currency sign in the
+ // pattern is 3. Even if the number of currency sign in the pattern is 3, it does
+ // not mean we need to reset the pattern. For 1st case, we do not need to reset
+ // pattern. For 2nd case, we might need to reset pattern, if the default pattern
+ // (corresponding to plural count 'other') we use is different from the pattern
+ // based on 'pluralCount'.
+ //
+ // style is only valid when decimal formatter is constructed through
+ // DecimalFormat(pattern, symbol, style)
+ if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+ // May need to reset pattern if the style is PLURALCURRENCYSTYLE.
+ String currencyPluralPattern = currencyPluralInfo.getCurrencyPluralPattern(pluralCount);
+ if (formatPattern.equals(currencyPluralPattern) == false) {
+ applyPatternWithoutExpandAffix(currencyPluralPattern, false);
+ }
+ }
+ // Expand the affix to the right name according to the plural rule. This is only
+ // used for currency plural formatting. Currency plural name is not a fixed
+ // static one, it is a dynamic name based on the currency plural count. So, the
+ // affixes need to be expanded here. For other cases, the affix is a static one
+ // based on pattern alone, and it is already expanded during applying pattern, or
+ // setDecimalFormatSymbols, or setCurrency.
+ expandAffixAdjustWidth(pluralCount);
+ return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
+ }
+
+ /**
+ * Complete the formatting of a finite number. On entry, the
+ * digitList must be filled in with the correct digits.
+ */
+ private StringBuffer subformat(StringBuffer result, FieldPosition fieldPosition,
+ boolean isNegative, boolean isInteger, boolean parseAttr) {
+ // NOTE: This isn't required anymore because DigitList takes care of this.
+ //
+ // // The negative of the exponent represents the number of leading // zeros
+ // between the decimal and the first non-zero digit, for // a value < 0.1 (e.g.,
+ // for 0.00123, -fExponent == 2). If this // is more than the maximum fraction
+ // digits, then we have an underflow // for the printed representation. We
+ // recognize this here and set // the DigitList representation to zero in this
+ // situation.
+ //
+ // if (-digitList.decimalAt >= getMaximumFractionDigits())
+ // {
+ // digitList.count = 0;
+ // }
+
+
+
+ // Per bug 4147706, DecimalFormat must respect the sign of numbers which format as
+ // zero. This allows sensible computations and preserves relations such as
+ // signum(1/x) = signum(x), where x is +Infinity or -Infinity. Prior to this fix,
+ // we always formatted zero values as if they were positive. Liu 7/6/98.
+ if (digitList.isZero()) {
+ digitList.decimalAt = 0; // Normalize
+ }
+
+ int prefixLen = appendAffix(result, isNegative, true, fieldPosition, parseAttr);
+
+ if (useExponentialNotation) {
+ subformatExponential(result, fieldPosition, parseAttr);
+ } else {
+ subformatFixed(result, fieldPosition, isInteger, parseAttr);
+ }
+
+ int suffixLen = appendAffix(result, isNegative, false, fieldPosition, parseAttr);
+ addPadding(result, fieldPosition, prefixLen, suffixLen);
+ return result;
+ }
+
+ private void subformatFixed(StringBuffer result,
+ FieldPosition fieldPosition,
+ boolean isInteger,
+ boolean parseAttr) {
+ String[] digits = symbols.getDigitStrings();
+
+ String grouping = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
+ symbols.getGroupingSeparatorString(): symbols.getMonetaryGroupingSeparatorString();
+ String decimal = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
+ symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
+ boolean useSigDig = areSignificantDigitsUsed();
+ int maxIntDig = getMaximumIntegerDigits();
+ int minIntDig = getMinimumIntegerDigits();
+ int i;
+ // [Spark/CDL] Record the integer start index.
+ int intBegin = result.length();
+ // Record field information for caller.
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD ||
+ fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ fieldPosition.setBeginIndex(intBegin);
+ }
+ long fractionalDigits = 0;
+ int fractionalDigitsCount = 0;
+ boolean recordFractionDigits = false;
+
+ int sigCount = 0;
+ int minSigDig = getMinimumSignificantDigits();
+ int maxSigDig = getMaximumSignificantDigits();
+ if (!useSigDig) {
+ minSigDig = 0;
+ maxSigDig = Integer.MAX_VALUE;
+ }
+
+ // Output the integer portion. Here 'count' is the total number of integer
+ // digits we will display, including both leading zeros required to satisfy
+ // getMinimumIntegerDigits, and actual digits present in the number.
+ int count = useSigDig ? Math.max(1, digitList.decimalAt) : minIntDig;
+ if (digitList.decimalAt > 0 && count < digitList.decimalAt) {
+ count = digitList.decimalAt;
+ }
+
+ // Handle the case where getMaximumIntegerDigits() is smaller than the real
+ // number of integer digits. If this is so, we output the least significant
+ // max integer digits. For example, the value 1997 printed with 2 max integer
+ // digits is just "97".
+
+ int digitIndex = 0; // Index into digitList.fDigits[]
+ if (count > maxIntDig && maxIntDig >= 0) {
+ count = maxIntDig;
+ digitIndex = digitList.decimalAt - count;
+ }
+
+ int sizeBeforeIntegerPart = result.length();
+ for (i = count - 1; i >= 0; --i) {
+ if (i < digitList.decimalAt && digitIndex < digitList.count
+ && sigCount < maxSigDig) {
+ // Output a real digit
+ result.append(digits[digitList.getDigitValue(digitIndex++)]);
+ ++sigCount;
+ } else {
+ // Output a zero (leading or trailing)
+ result.append(digits[0]);
+ if (sigCount > 0) {
+ ++sigCount;
+ }
+ }
+
+ // Output grouping separator if necessary.
+ if (isGroupingPosition(i)) {
+ result.append(grouping);
+ // [Spark/CDL] Add grouping separator attribute here.
+ // Set only for the first instance.
+ // Length of grouping separator is 1.
+ if (fieldPosition.getFieldAttribute() == Field.GROUPING_SEPARATOR &&
+ fieldPosition.getBeginIndex() == 0 && fieldPosition.getEndIndex() == 0) {
+ fieldPosition.setBeginIndex(result.length()-1);
+ fieldPosition.setEndIndex(result.length());
+ }
+ if (parseAttr) {
+ addAttribute(Field.GROUPING_SEPARATOR, result.length() - 1, result.length());
+ }
+ }
+ }
+
+ // Record field information for caller.
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD ||
+ fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ fieldPosition.setEndIndex(result.length());
+ }
+
+ // This handles the special case of formatting 0. For zero only, we count the
+ // zero to the left of the decimal point as one signficant digit. Ordinarily we
+ // do not count any leading 0's as significant. If the number we are formatting
+ // is not zero, then either sigCount or digits.getCount() will be non-zero.
+ if (sigCount == 0 && digitList.count == 0) {
+ sigCount = 1;
+ }
+
+ // Determine whether or not there are any printable fractional digits. If
+ // we've used up the digits we know there aren't.
+ boolean fractionPresent = (!isInteger && digitIndex < digitList.count)
+ || (useSigDig ? (sigCount < minSigDig) : (getMinimumFractionDigits() > 0));
+
+ // If there is no fraction present, and we haven't printed any integer digits,
+ // then print a zero. Otherwise we won't print _any_ digits, and we won't be
+ // able to parse this string.
+ if (!fractionPresent && result.length() == sizeBeforeIntegerPart)
+ result.append(digits[0]);
+ // [Spark/CDL] Add attribute for integer part.
+ if (parseAttr) {
+ addAttribute(Field.INTEGER, intBegin, result.length());
+ }
+ // Output the decimal separator if we always do so.
+ if (decimalSeparatorAlwaysShown || fractionPresent) {
+ if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+ result.append(decimal);
+ if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ // [Spark/CDL] Add attribute for decimal separator
+ if (parseAttr) {
+ addAttribute(Field.DECIMAL_SEPARATOR, result.length() - 1, result.length());
+ }
+ }
+
+ // Record field information for caller.
+ if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+ fieldPosition.setBeginIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+
+ // [Spark/CDL] Record the begin index of fraction part.
+ int fracBegin = result.length();
+ recordFractionDigits = fieldPosition instanceof UFieldPosition;
+
+ count = useSigDig ? Integer.MAX_VALUE : getMaximumFractionDigits();
+ if (useSigDig && (sigCount == maxSigDig ||
+ (sigCount >= minSigDig && digitIndex == digitList.count))) {
+ count = 0;
+ }
+ for (i = 0; i < count; ++i) {
+ // Here is where we escape from the loop. We escape if we've output the
+ // maximum fraction digits (specified in the for expression above). We
+ // also stop when we've output the minimum digits and either: we have an
+ // integer, so there is no fractional stuff to display, or we're out of
+ // significant digits.
+ if (!useSigDig && i >= getMinimumFractionDigits() &&
+ (isInteger || digitIndex >= digitList.count)) {
+ break;
+ }
+
+ // Output leading fractional zeros. These are zeros that come after the
+ // decimal but before any significant digits. These are only output if
+ // abs(number being formatted) < 1.0.
+ if (-1 - i > (digitList.decimalAt - 1)) {
+ result.append(digits[0]);
+ if (recordFractionDigits) {
+ ++fractionalDigitsCount;
+ fractionalDigits *= 10;
+ }
+ continue;
+ }
+
+ // Output a digit, if we have any precision left, or a zero if we
+ // don't. We don't want to output noise digits.
+ if (!isInteger && digitIndex < digitList.count) {
+ byte digit = digitList.getDigitValue(digitIndex++);
+ result.append(digits[digit]);
+ if (recordFractionDigits) {
+ ++fractionalDigitsCount;
+ fractionalDigits *= 10;
+ fractionalDigits += digit;
+ }
+ } else {
+ result.append(digits[0]);
+ if (recordFractionDigits) {
+ ++fractionalDigitsCount;
+ fractionalDigits *= 10;
+ }
+ }
+
+ // If we reach the maximum number of significant digits, or if we output
+ // all the real digits and reach the minimum, then we are done.
+ ++sigCount;
+ if (useSigDig && (sigCount == maxSigDig ||
+ (digitIndex == digitList.count && sigCount >= minSigDig))) {
+ break;
+ }
+ }
+
+ // Record field information for caller.
+ if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+ fieldPosition.setEndIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ if (recordFractionDigits) {
+ ((UFieldPosition) fieldPosition).setFractionDigits(fractionalDigitsCount, fractionalDigits);
+ }
+
+ // [Spark/CDL] Add attribute information if necessary.
+ if (parseAttr && (decimalSeparatorAlwaysShown || fractionPresent)) {
+ addAttribute(Field.FRACTION, fracBegin, result.length());
+ }
+ }
+
+ private void subformatExponential(StringBuffer result,
+ FieldPosition fieldPosition,
+ boolean parseAttr) {
+ String[] digits = symbols.getDigitStringsLocal();
+ String decimal = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
+ symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
+ boolean useSigDig = areSignificantDigitsUsed();
+ int maxIntDig = getMaximumIntegerDigits();
+ int minIntDig = getMinimumIntegerDigits();
+ int i;
+ // Record field information for caller.
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+ fieldPosition.setBeginIndex(result.length());
+ fieldPosition.setEndIndex(-1);
+ } else if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+ fieldPosition.setBeginIndex(-1);
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ fieldPosition.setBeginIndex(result.length());
+ fieldPosition.setEndIndex(-1);
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+ fieldPosition.setBeginIndex(-1);
+ }
+
+ // [Spark/CDL]
+ // the begin index of integer part
+ // the end index of integer part
+ // the begin index of fractional part
+ int intBegin = result.length();
+ int intEnd = -1;
+ int fracBegin = -1;
+ int minFracDig = 0;
+ if (useSigDig) {
+ maxIntDig = minIntDig = 1;
+ minFracDig = getMinimumSignificantDigits() - 1;
+ } else {
+ minFracDig = getMinimumFractionDigits();
+ if (maxIntDig > MAX_SCIENTIFIC_INTEGER_DIGITS) {
+ maxIntDig = 1;
+ if (maxIntDig < minIntDig) {
+ maxIntDig = minIntDig;
+ }
+ }
+ if (maxIntDig > minIntDig) {
+ minIntDig = 1;
+ }
+ }
+ long fractionalDigits = 0;
+ int fractionalDigitsCount = 0;
+ boolean recordFractionDigits = false;
+
+ // Minimum integer digits are handled in exponential format by adjusting the
+ // exponent. For example, 0.01234 with 3 minimum integer digits is "123.4E-4".
+
+ // Maximum integer digits are interpreted as indicating the repeating
+ // range. This is useful for engineering notation, in which the exponent is
+ // restricted to a multiple of 3. For example, 0.01234 with 3 maximum integer
+ // digits is "12.34e-3". If maximum integer digits are defined and are larger
+ // than minimum integer digits, then minimum integer digits are ignored.
+
+ int exponent = digitList.decimalAt;
+ if (maxIntDig > 1 && maxIntDig != minIntDig) {
+ // A exponent increment is defined; adjust to it.
+ exponent = (exponent > 0) ? (exponent - 1) / maxIntDig : (exponent / maxIntDig) - 1;
+ exponent *= maxIntDig;
+ } else {
+ // No exponent increment is defined; use minimum integer digits.
+ // If none is specified, as in "#E0", generate 1 integer digit.
+ exponent -= (minIntDig > 0 || minFracDig > 0) ? minIntDig : 1;
+ }
+
+ // We now output a minimum number of digits, and more if there are more
+ // digits, up to the maximum number of digits. We place the decimal point
+ // after the "integer" digits, which are the first (decimalAt - exponent)
+ // digits.
+ int minimumDigits = minIntDig + minFracDig;
+ // The number of integer digits is handled specially if the number
+ // is zero, since then there may be no digits.
+ int integerDigits = digitList.isZero() ? minIntDig : digitList.decimalAt - exponent;
+ int totalDigits = digitList.count;
+ if (minimumDigits > totalDigits)
+ totalDigits = minimumDigits;
+ if (integerDigits > totalDigits)
+ totalDigits = integerDigits;
+
+ for (i = 0; i < totalDigits; ++i) {
+ if (i == integerDigits) {
+ // Record field information for caller.
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+ fieldPosition.setEndIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ fieldPosition.setEndIndex(result.length());
+ }
+
+ // [Spark/CDL] Add attribute for integer part
+ if (parseAttr) {
+ intEnd = result.length();
+ addAttribute(Field.INTEGER, intBegin, result.length());
+ }
+ if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+ result.append(decimal);
+ if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ // [Spark/CDL] Add attribute for decimal separator
+ fracBegin = result.length();
+ if (parseAttr) {
+ // Length of decimal separator is 1.
+ int decimalSeparatorBegin = result.length() - 1;
+ addAttribute(Field.DECIMAL_SEPARATOR, decimalSeparatorBegin,
+ result.length());
+ }
+ // Record field information for caller.
+ if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+ fieldPosition.setBeginIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+ recordFractionDigits = fieldPosition instanceof UFieldPosition;
+
+ }
+ byte digit = (i < digitList.count) ? digitList.getDigitValue(i) : (byte)0;
+ result.append(digits[digit]);
+ if (recordFractionDigits) {
+ ++fractionalDigitsCount;
+ fractionalDigits *= 10;
+ fractionalDigits += digit;
+ }
+ }
+
+ // For ICU compatibility and format 0 to 0E0 with pattern "#E0" [Richard/GCL]
+ if (digitList.isZero() && (totalDigits == 0)) {
+ result.append(digits[0]);
+ }
+
+ // add the decimal separator if it is to be always shown AND there are no decimal digits
+ if ((fracBegin == -1) && this.decimalSeparatorAlwaysShown) {
+ if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+ result.append(decimal);
+ if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ if (parseAttr) {
+ // Length of decimal separator is 1.
+ int decimalSeparatorBegin = result.length() - 1;
+ addAttribute(Field.DECIMAL_SEPARATOR, decimalSeparatorBegin, result.length());
+ }
+ }
+
+ // Record field information
+ if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+ if (fieldPosition.getEndIndex() < 0) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ } else if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+ if (fieldPosition.getBeginIndex() < 0) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+ fieldPosition.setEndIndex(result.length());
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+ if (fieldPosition.getEndIndex() < 0) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+ if (fieldPosition.getBeginIndex() < 0) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+ fieldPosition.setEndIndex(result.length());
+ }
+ if (recordFractionDigits) {
+ ((UFieldPosition) fieldPosition).setFractionDigits(fractionalDigitsCount, fractionalDigits);
+ }
+
+ // [Spark/CDL] Calculate the end index of integer part and fractional
+ // part if they are not properly processed yet.
+ if (parseAttr) {
+ if (intEnd < 0) {
+ addAttribute(Field.INTEGER, intBegin, result.length());
+ }
+ if (fracBegin > 0) {
+ addAttribute(Field.FRACTION, fracBegin, result.length());
+ }
+ }
+
+ // The exponent is output using the pattern-specified minimum exponent
+ // digits. There is no maximum limit to the exponent digits, since truncating
+ // the exponent would result in an unacceptable inaccuracy.
+ if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SYMBOL) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+
+ result.append(symbols.getExponentSeparator());
+ if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SYMBOL) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ // [Spark/CDL] For exponent symbol, add an attribute.
+ if (parseAttr) {
+ addAttribute(Field.EXPONENT_SYMBOL, result.length() -
+ symbols.getExponentSeparator().length(), result.length());
+ }
+ // For zero values, we force the exponent to zero. We must do this here, and
+ // not earlier, because the value is used to determine integer digit count
+ // above.
+ if (digitList.isZero())
+ exponent = 0;
+
+ boolean negativeExponent = exponent < 0;
+ if (negativeExponent) {
+ exponent = -exponent;
+ if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+ result.append(symbols.getMinusSignString());
+ if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ // [Spark/CDL] If exponent has sign, then add an exponent sign
+ // attribute.
+ if (parseAttr) {
+ // Length of exponent sign is 1.
+ addAttribute(Field.EXPONENT_SIGN, result.length() - 1, result.length());
+ }
+ } else if (exponentSignAlwaysShown) {
+ if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
+ fieldPosition.setBeginIndex(result.length());
+ }
+ result.append(symbols.getPlusSignString());
+ if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
+ fieldPosition.setEndIndex(result.length());
+ }
+ // [Spark/CDL] Add an plus sign attribute.
+ if (parseAttr) {
+ // Length of exponent sign is 1.
+ int expSignBegin = result.length() - 1;
+ addAttribute(Field.EXPONENT_SIGN, expSignBegin, result.length());
+ }
+ }
+ int expBegin = result.length();
+ digitList.set(exponent);
+ {
+ int expDig = minExponentDigits;
+ if (useExponentialNotation && expDig < 1) {
+ expDig = 1;
+ }
+ for (i = digitList.decimalAt; i < expDig; ++i)
+ result.append(digits[0]);
+ }
+ for (i = 0; i < digitList.decimalAt; ++i) {
+ result.append((i < digitList.count) ? digits[digitList.getDigitValue(i)]
+ : digits[0]);
+ }
+ // [Spark/CDL] Add attribute for exponent part.
+ if (fieldPosition.getFieldAttribute() == Field.EXPONENT) {
+ fieldPosition.setBeginIndex(expBegin);
+ fieldPosition.setEndIndex(result.length());
+ }
+ if (parseAttr) {
+ addAttribute(Field.EXPONENT, expBegin, result.length());
+ }
+ }
+
+ private final void addPadding(StringBuffer result, FieldPosition fieldPosition, int prefixLen,
+ int suffixLen) {
+ if (formatWidth > 0) {
+ int len = formatWidth - result.length();
+ if (len > 0) {
+ char[] padding = new char[len];
+ for (int i = 0; i < len; ++i) {
+ padding[i] = pad;
+ }
+ switch (padPosition) {
+ case PAD_AFTER_PREFIX:
+ result.insert(prefixLen, padding);
+ break;
+ case PAD_BEFORE_PREFIX:
+ result.insert(0, padding);
+ break;
+ case PAD_BEFORE_SUFFIX:
+ result.insert(result.length() - suffixLen, padding);
+ break;
+ case PAD_AFTER_SUFFIX:
+ result.append(padding);
+ break;
+ }
+ if (padPosition == PAD_BEFORE_PREFIX || padPosition == PAD_AFTER_PREFIX) {
+ fieldPosition.setBeginIndex(fieldPosition.getBeginIndex() + len);
+ fieldPosition.setEndIndex(fieldPosition.getEndIndex() + len);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses the given string, returning a <code>Number</code> object to represent the
+ * parsed value. <code>Double</code> objects are returned to represent non-integral
+ * values which cannot be stored in a <code>BigDecimal</code>. These are
+ * <code>NaN</code>, infinity, -infinity, and -0.0. If {@link #isParseBigDecimal()} is
+ * false (the default), all other values are returned as <code>Long</code>,
+ * <code>BigInteger</code>, or <code>BigDecimal</code> values, in that order of
+ * preference. If {@link #isParseBigDecimal()} is true, all other values are returned
+ * as <code>BigDecimal</code> valuse. If the parse fails, null is returned.
+ *
+ * @param text the string to be parsed
+ * @param parsePosition defines the position where parsing is to begin, and upon
+ * return, the position where parsing left off. If the position has not changed upon
+ * return, then parsing failed.
+ * @return a <code>Number</code> object with the parsed value or
+ * <code>null</code> if the parse failed
+ * @stable ICU 2.0
+ */
+ @Override
+ public Number parse(String text, ParsePosition parsePosition) {
+ return (Number) parse(text, parsePosition, null);
+ }
+
+ /**
+ * Parses text from the given string as a CurrencyAmount. Unlike the parse() method,
+ * this method will attempt to parse a generic currency name, searching for a match of
+ * this object's locale's currency display names, or for a 3-letter ISO currency
+ * code. This method will fail if this format is not a currency format, that is, if it
+ * does not contain the currency pattern symbol (U+00A4) in its prefix or suffix.
+ *
+ * @param text the text to parse
+ * @param pos input-output position; on input, the position within text to match; must
+ * have 0 <= pos.getIndex() < text.length(); on output, the position after the last
+ * matched character. If the parse fails, the position in unchanged upon output.
+ * @return a CurrencyAmount, or null upon failure
+ * @stable ICU 49
+ */
+ @Override
+ public CurrencyAmount parseCurrency(CharSequence text, ParsePosition pos) {
+ Currency[] currency = new Currency[1];
+ return (CurrencyAmount) parse(text.toString(), pos, currency);
+ }
+
+ /**
+ * Parses the given text as either a Number or a CurrencyAmount.
+ *
+ * @param text the string to parse
+ * @param parsePosition input-output position; on input, the position within text to
+ * match; must have 0 <= pos.getIndex() < text.length(); on output, the position after
+ * the last matched character. If the parse fails, the position in unchanged upon
+ * output.
+ * @param currency if non-null, a CurrencyAmount is parsed and returned; otherwise a
+ * Number is parsed and returned
+ * @return a Number or CurrencyAmount or null
+ */
+ private Object parse(String text, ParsePosition parsePosition, Currency[] currency) {
+ int backup;
+ int i = backup = parsePosition.getIndex();
+
+ // Handle NaN as a special case:
+
+ // Skip padding characters, if around prefix
+ if (formatWidth > 0 &&
+ (padPosition == PAD_BEFORE_PREFIX || padPosition == PAD_AFTER_PREFIX)) {
+ i = skipPadding(text, i);
+ }
+ if (text.regionMatches(i, symbols.getNaN(), 0, symbols.getNaN().length())) {
+ i += symbols.getNaN().length();
+ // Skip padding characters, if around suffix
+ if (formatWidth > 0 && (padPosition == PAD_BEFORE_SUFFIX ||
+ padPosition == PAD_AFTER_SUFFIX)) {
+ i = skipPadding(text, i);
+ }
+ parsePosition.setIndex(i);
+ return new Double(Double.NaN);
+ }
+
+ // NaN parse failed; start over
+ i = backup;
+
+ boolean[] status = new boolean[STATUS_LENGTH];
+ if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
+ if (!parseForCurrency(text, parsePosition, currency, status)) {
+ return null;
+ }
+ } else if (currency != null) {
+ return null;
+ } else {
+ if (!subparse(text, parsePosition, digitList, status, currency, negPrefixPattern,
+ negSuffixPattern, posPrefixPattern, posSuffixPattern,
+ false, Currency.SYMBOL_NAME)) {
+ parsePosition.setIndex(backup);
+ return null;
+ }
+ }
+
+ Number n = null;
+
+ // Handle infinity
+ if (status[STATUS_INFINITE]) {
+ n = new Double(status[STATUS_POSITIVE] ? Double.POSITIVE_INFINITY :
+ Double.NEGATIVE_INFINITY);
+ }
+
+ // Handle underflow
+ else if (status[STATUS_UNDERFLOW]) {
+ n = status[STATUS_POSITIVE] ? new Double("0.0") : new Double("-0.0");
+ }
+
+ // Handle -0.0
+ else if (!status[STATUS_POSITIVE] && digitList.isZero()) {
+ n = new Double("-0.0");
+ }
+
+ else {
+ // Do as much of the multiplier conversion as possible without
+ // losing accuracy.
+ int mult = multiplier; // Don't modify this.multiplier
+ while (mult % 10 == 0) {
+ --digitList.decimalAt;
+ mult /= 10;
+ }
+
+ // Handle integral values
+ if (!parseBigDecimal && mult == 1 && digitList.isIntegral()) {
+ // hack quick long
+ if (digitList.decimalAt < 12) { // quick check for long
+ long l = 0;
+ if (digitList.count > 0) {
+ int nx = 0;
+ while (nx < digitList.count) {
+ l = l * 10 + (char) digitList.digits[nx++] - '0';
+ }
+ while (nx++ < digitList.decimalAt) {
+ l *= 10;
+ }
+ if (!status[STATUS_POSITIVE]) {
+ l = -l;
+ }
+ }
+ n = Long.valueOf(l);
+ } else {
+ BigInteger big = digitList.getBigInteger(status[STATUS_POSITIVE]);
+ n = (big.bitLength() < 64) ? (Number) Long.valueOf(big.longValue()) : (Number) big;
+ }
+ }
+ // Handle non-integral values or the case where parseBigDecimal is set
+ else {
+ BigDecimal big = digitList.getBigDecimalICU(status[STATUS_POSITIVE]);
+ n = big;
+ if (mult != 1) {
+ n = big.divide(BigDecimal.valueOf(mult), mathContext);
+ }
+ }
+ }
+
+ // Assemble into CurrencyAmount if necessary
+ return (currency != null) ? (Object) new CurrencyAmount(n, currency[0]) : (Object) n;
+ }
+
+ private boolean parseForCurrency(String text, ParsePosition parsePosition,
+ Currency[] currency, boolean[] status) {
+ int origPos = parsePosition.getIndex();
+ if (!isReadyForParsing) {
+ int savedCurrencySignCount = currencySignCount;
+ setupCurrencyAffixForAllPatterns();
+ // reset pattern back
+ if (savedCurrencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+ applyPatternWithoutExpandAffix(formatPattern, false);
+ } else {
+ applyPattern(formatPattern, false);
+ }
+ isReadyForParsing = true;
+ }
+ int maxPosIndex = origPos;
+ int maxErrorPos = -1;
+ boolean[] savedStatus = null;
+ // First, parse against current pattern.
+ // Since current pattern could be set by applyPattern(),
+ // it could be an arbitrary pattern, and it may not be the one
+ // defined in current locale.
+ boolean[] tmpStatus = new boolean[STATUS_LENGTH];
+ ParsePosition tmpPos = new ParsePosition(origPos);
+ DigitList tmpDigitList = new DigitList();
+ boolean found;
+ if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+ found = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
+ negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
+ true, Currency.LONG_NAME);
+ } else {
+ found = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
+ negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
+ true, Currency.SYMBOL_NAME);
+ }
+ if (found) {
+ if (tmpPos.getIndex() > maxPosIndex) {
+ maxPosIndex = tmpPos.getIndex();
+ savedStatus = tmpStatus;
+ digitList = tmpDigitList;
+ }
+ } else {
+ maxErrorPos = tmpPos.getErrorIndex();
+ }
+ // Then, parse against affix patterns. Those are currency patterns and currency
+ // plural patterns defined in the locale.
+ for (AffixForCurrency affix : affixPatternsForCurrency) {
+ tmpStatus = new boolean[STATUS_LENGTH];
+ tmpPos = new ParsePosition(origPos);
+ tmpDigitList = new DigitList();
+ boolean result = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
+ affix.getNegPrefix(), affix.getNegSuffix(),
+ affix.getPosPrefix(), affix.getPosSuffix(),
+ true, affix.getPatternType());
+ if (result) {
+ found = true;
+ if (tmpPos.getIndex() > maxPosIndex) {
+ maxPosIndex = tmpPos.getIndex();
+ savedStatus = tmpStatus;
+ digitList = tmpDigitList;
+ }
+ } else {
+ maxErrorPos = (tmpPos.getErrorIndex() > maxErrorPos) ? tmpPos.getErrorIndex()
+ : maxErrorPos;
+ }
+ }
+ // Finally, parse against simple affix to find the match. For example, in
+ // TestMonster suite, if the to-be-parsed text is "-\u00A40,00".
+ // complexAffixCompare will not find match, since there is no ISO code matches
+ // "\u00A4", and the parse stops at "\u00A4". We will just use simple affix
+ // comparison (look for exact match) to pass it.
+ //
+ // TODO: We should parse against simple affix first when
+ // output currency is not requested. After the complex currency
+ // parsing implementation was introduced, the default currency
+ // instance parsing slowed down because of the new code flow.
+ // I filed #10312 - Yoshito
+ tmpStatus = new boolean[STATUS_LENGTH];
+ tmpPos = new ParsePosition(origPos);
+ tmpDigitList = new DigitList();
+
+ // Disable complex currency parsing and try it again.
+ boolean result = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
+ negativePrefix, negativeSuffix, positivePrefix, positiveSuffix,
+ false /* disable complex currency parsing */, Currency.SYMBOL_NAME);
+ if (result) {
+ if (tmpPos.getIndex() > maxPosIndex) {
+ maxPosIndex = tmpPos.getIndex();
+ savedStatus = tmpStatus;
+ digitList = tmpDigitList;
+ }
+ found = true;
+ } else {
+ maxErrorPos = (tmpPos.getErrorIndex() > maxErrorPos) ? tmpPos.getErrorIndex() :
+ maxErrorPos;
+ }
+
+ if (!found) {
+ // parsePosition.setIndex(origPos);
+ parsePosition.setErrorIndex(maxErrorPos);
+ } else {
+ parsePosition.setIndex(maxPosIndex);
+ parsePosition.setErrorIndex(-1);
+ for (int index = 0; index < STATUS_LENGTH; ++index) {
+ status[index] = savedStatus[index];
+ }
+ }
+ return found;
+ }
+
+ // Get affix patterns used in locale's currency pattern (NumberPatterns[1]) and
+ // currency plural pattern (CurrencyUnitPatterns).
+ private void setupCurrencyAffixForAllPatterns() {
+ if (currencyPluralInfo == null) {
+ currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
+ }
+ affixPatternsForCurrency = new HashSet<AffixForCurrency>();
+
+ // save the current pattern, since it will be changed by
+ // applyPatternWithoutExpandAffix
+ String savedFormatPattern = formatPattern;
+
+ // CURRENCYSTYLE and ISOCURRENCYSTYLE should have the same prefix and suffix, so,
+ // only need to save one of them. Here, chose onlyApplyPatternWithoutExpandAffix
+ // without saving the actualy pattern in 'pattern' data member. TODO: is it uloc?
+ applyPatternWithoutExpandAffix(getPattern(symbols.getULocale(), NumberFormat.CURRENCYSTYLE),
+ false);
+ AffixForCurrency affixes = new AffixForCurrency(
+ negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
+ Currency.SYMBOL_NAME);
+ affixPatternsForCurrency.add(affixes);
+
+ // add plural pattern
+ Iterator<String> iter = currencyPluralInfo.pluralPatternIterator();
+ Set<String> currencyUnitPatternSet = new HashSet<String>();
+ while (iter.hasNext()) {
+ String pluralCount = iter.next();
+ String currencyPattern = currencyPluralInfo.getCurrencyPluralPattern(pluralCount);
+ if (currencyPattern != null &&
+ currencyUnitPatternSet.contains(currencyPattern) == false) {
+ currencyUnitPatternSet.add(currencyPattern);
+ applyPatternWithoutExpandAffix(currencyPattern, false);
+ affixes = new AffixForCurrency(negPrefixPattern, negSuffixPattern, posPrefixPattern,
+ posSuffixPattern, Currency.LONG_NAME);
+ affixPatternsForCurrency.add(affixes);
+ }
+ }
+ // reset pattern back
+ formatPattern = savedFormatPattern;
+ }
+
+ // currency formatting style options
+ private static final int CURRENCY_SIGN_COUNT_ZERO = 0;
+ private static final int CURRENCY_SIGN_COUNT_IN_SYMBOL_FORMAT = 1;
+ private static final int CURRENCY_SIGN_COUNT_IN_ISO_FORMAT = 2;
+ private static final int CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT = 3;
+
+ private static final int STATUS_INFINITE = 0;
+ private static final int STATUS_POSITIVE = 1;
+ private static final int STATUS_UNDERFLOW = 2;
+ private static final int STATUS_LENGTH = 3;
+
+ private static final UnicodeSet dotEquivalents = new UnicodeSet(
+ //"[.\u2024\u3002\uFE12\uFE52\uFF0E\uFF61]"
+ 0x002E, 0x002E,
+ 0x2024, 0x2024,
+ 0x3002, 0x3002,
+ 0xFE12, 0xFE12,
+ 0xFE52, 0xFE52,
+ 0xFF0E, 0xFF0E,
+ 0xFF61, 0xFF61).freeze();
+
+ private static final UnicodeSet commaEquivalents = new UnicodeSet(
+ //"[,\u060C\u066B\u3001\uFE10\uFE11\uFE50\uFE51\uFF0C\uFF64]"
+ 0x002C, 0x002C,
+ 0x060C, 0x060C,
+ 0x066B, 0x066B,
+ 0x3001, 0x3001,
+ 0xFE10, 0xFE11,
+ 0xFE50, 0xFE51,
+ 0xFF0C, 0xFF0C,
+ 0xFF64, 0xFF64).freeze();
+
+// private static final UnicodeSet otherGroupingSeparators = new UnicodeSet(
+// //"[\\ '\u00A0\u066C\u2000-\u200A\u2018\u2019\u202F\u205F\u3000\uFF07]"
+// 0x0020, 0x0020,
+// 0x0027, 0x0027,
+// 0x00A0, 0x00A0,
+// 0x066C, 0x066C,
+// 0x2000, 0x200A,
+// 0x2018, 0x2019,
+// 0x202F, 0x202F,
+// 0x205F, 0x205F,
+// 0x3000, 0x3000,
+// 0xFF07, 0xFF07).freeze();
+
+ private static final UnicodeSet strictDotEquivalents = new UnicodeSet(
+ //"[.\u2024\uFE52\uFF0E\uFF61]"
+ 0x002E, 0x002E,
+ 0x2024, 0x2024,
+ 0xFE52, 0xFE52,
+ 0xFF0E, 0xFF0E,
+ 0xFF61, 0xFF61).freeze();
+
+ private static final UnicodeSet strictCommaEquivalents = new UnicodeSet(
+ //"[,\u066B\uFE10\uFE50\uFF0C]"
+ 0x002C, 0x002C,
+ 0x066B, 0x066B,
+ 0xFE10, 0xFE10,
+ 0xFE50, 0xFE50,
+ 0xFF0C, 0xFF0C).freeze();
+
+// private static final UnicodeSet strictOtherGroupingSeparators = new UnicodeSet(
+// //"[\\ '\u00A0\u066C\u2000-\u200A\u2018\u2019\u202F\u205F\u3000\uFF07]"
+// 0x0020, 0x0020,
+// 0x0027, 0x0027,
+// 0x00A0, 0x00A0,
+// 0x066C, 0x066C,
+// 0x2000, 0x200A,
+// 0x2018, 0x2019,
+// 0x202F, 0x202F,
+// 0x205F, 0x205F,
+// 0x3000, 0x3000,
+// 0xFF07, 0xFF07).freeze();
+
+ private static final UnicodeSet defaultGroupingSeparators =
+ // new UnicodeSet(dotEquivalents).addAll(commaEquivalents)
+ // .addAll(otherGroupingSeparators).freeze();
+ new UnicodeSet(
+ 0x0020, 0x0020,
+ 0x0027, 0x0027,
+ 0x002C, 0x002C,
+ 0x002E, 0x002E,
+ 0x00A0, 0x00A0,
+ 0x060C, 0x060C,
+ 0x066B, 0x066C,
+ 0x2000, 0x200A,
+ 0x2018, 0x2019,
+ 0x2024, 0x2024,
+ 0x202F, 0x202F,
+ 0x205F, 0x205F,
+ 0x3000, 0x3002,
+ 0xFE10, 0xFE12,
+ 0xFE50, 0xFE52,
+ 0xFF07, 0xFF07,
+ 0xFF0C, 0xFF0C,
+ 0xFF0E, 0xFF0E,
+ 0xFF61, 0xFF61,
+ 0xFF64, 0xFF64).freeze();
+
+ private static final UnicodeSet strictDefaultGroupingSeparators =
+ // new UnicodeSet(strictDotEquivalents).addAll(strictCommaEquivalents)
+ // .addAll(strictOtherGroupingSeparators).freeze();
+ new UnicodeSet(
+ 0x0020, 0x0020,
+ 0x0027, 0x0027,
+ 0x002C, 0x002C,
+ 0x002E, 0x002E,
+ 0x00A0, 0x00A0,
+ 0x066B, 0x066C,
+ 0x2000, 0x200A,
+ 0x2018, 0x2019,
+ 0x2024, 0x2024,
+ 0x202F, 0x202F,
+ 0x205F, 0x205F,
+ 0x3000, 0x3000,
+ 0xFE10, 0xFE10,
+ 0xFE50, 0xFE50,
+ 0xFE52, 0xFE52,
+ 0xFF07, 0xFF07,
+ 0xFF0C, 0xFF0C,
+ 0xFF0E, 0xFF0E,
+ 0xFF61, 0xFF61).freeze();
+
+ static final UnicodeSet minusSigns =
+ new UnicodeSet(
+ 0x002D, 0x002D,
+ 0x207B, 0x207B,
+ 0x208B, 0x208B,
+ 0x2212, 0x2212,
+ 0x2796, 0x2796,
+ 0xFE63, 0xFE63,
+ 0xFF0D, 0xFF0D).freeze();
+
+ static final UnicodeSet plusSigns =
+ new UnicodeSet(
+ 0x002B, 0x002B,
+ 0x207A, 0x207A,
+ 0x208A, 0x208A,
+ 0x2795, 0x2795,
+ 0xFB29, 0xFB29,
+ 0xFE62, 0xFE62,
+ 0xFF0B, 0xFF0B).freeze();
+
+ // equivalent grouping and decimal support
+ static final boolean skipExtendedSeparatorParsing = ICUConfig.get(
+ "com.ibm.icu.text.DecimalFormat.SkipExtendedSeparatorParsing", "false")
+ .equals("true");
+
+ // allow control of requiring a matching decimal point when parsing
+ boolean parseRequireDecimalPoint = false;
+
+ // When parsing a number with big exponential value, it requires to transform the
+ // value into a string representation to construct BigInteger instance. We want to
+ // set the maximum size because it can easily trigger OutOfMemoryException.
+ // PARSE_MAX_EXPONENT is currently set to 1000 (See getParseMaxDigits()),
+ // which is much bigger than MAX_VALUE of Double ( See the problem reported by ticket#5698
+ private int PARSE_MAX_EXPONENT = 1000;
+
+ /**
+ * Parses the given text into a number. The text is parsed beginning at parsePosition,
+ * until an unparseable character is seen.
+ *
+ * @param text the string to parse.
+ * @param parsePosition the position at which to being parsing. Upon return, the first
+ * unparseable character.
+ * @param digits the DigitList to set to the parsed value.
+ * @param status Upon return contains boolean status flags indicating whether the
+ * value was infinite and whether it was positive.
+ * @param currency return value for parsed currency, for generic currency parsing
+ * mode, or null for normal parsing. In generic currency parsing mode, any currency is
+ * parsed, not just the currency that this formatter is set to.
+ * @param negPrefix negative prefix pattern
+ * @param negSuffix negative suffix pattern
+ * @param posPrefix positive prefix pattern
+ * @param negSuffix negative suffix pattern
+ * @param parseComplexCurrency whether it is complex currency parsing or not.
+ * @param type type of currency to parse against, LONG_NAME only or not.
+ */
+ private final boolean subparse(
+ String text, ParsePosition parsePosition, DigitList digits,
+ boolean status[], Currency currency[], String negPrefix, String negSuffix, String posPrefix,
+ String posSuffix, boolean parseComplexCurrency, int type) {
+
+ int position = parsePosition.getIndex();
+ int oldStart = parsePosition.getIndex();
+
+ // Match padding before prefix
+ if (formatWidth > 0 && padPosition == PAD_BEFORE_PREFIX) {
+ position = skipPadding(text, position);
+ }
+
+ // Match positive and negative prefixes; prefer longest match.
+ int posMatch = compareAffix(text, position, false, true, posPrefix, parseComplexCurrency, type, currency);
+ int negMatch = compareAffix(text, position, true, true, negPrefix, parseComplexCurrency, type, currency);
+ if (posMatch >= 0 && negMatch >= 0) {
+ if (posMatch > negMatch) {
+ negMatch = -1;
+ } else if (negMatch > posMatch) {
+ posMatch = -1;
+ }
+ }
+ if (posMatch >= 0) {
+ position += posMatch;
+ } else if (negMatch >= 0) {
+ position += negMatch;
+ } else {
+ parsePosition.setErrorIndex(position);
+ return false;
+ }
+
+ // Match padding after prefix
+ if (formatWidth > 0 && padPosition == PAD_AFTER_PREFIX) {
+ position = skipPadding(text, position);
+ }
+
+ // process digits or Inf, find decimal position
+ status[STATUS_INFINITE] = false;
+ if (text.regionMatches(position, symbols.getInfinity(), 0,
+ symbols.getInfinity().length())) {
+ position += symbols.getInfinity().length();
+ status[STATUS_INFINITE] = true;
+ } else {
+ // We now have a string of digits, possibly with grouping symbols, and decimal
+ // points. We want to process these into a DigitList. We don't want to put a
+ // bunch of leading zeros into the DigitList though, so we keep track of the
+ // location of the decimal point, put only significant digits into the
+ // DigitList, and adjust the exponent as needed.
+
+ digits.decimalAt = digits.count = 0;
+ String decimal = (currencySignCount == CURRENCY_SIGN_COUNT_ZERO) ?
+ symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
+ String grouping = (currencySignCount == CURRENCY_SIGN_COUNT_ZERO) ?
+ symbols.getGroupingSeparatorString() : symbols.getMonetaryGroupingSeparatorString();
+
+ String exponentSep = symbols.getExponentSeparator();
+ boolean sawDecimal = false;
+ boolean sawGrouping = false;
+ boolean sawDigit = false;
+ long exponent = 0; // Set to the exponent value, if any
+
+ // strict parsing
+ boolean strictParse = isParseStrict();
+ boolean strictFail = false; // did we exit with a strict parse failure?
+ int lastGroup = -1; // where did we last see a grouping separator?
+ int groupedDigitCount = 0; // tracking count of digits delimited by grouping separator
+ int gs2 = groupingSize2 == 0 ? groupingSize : groupingSize2;
+
+ UnicodeSet decimalEquiv = skipExtendedSeparatorParsing ? UnicodeSet.EMPTY :
+ getEquivalentDecimals(decimal, strictParse);
+ UnicodeSet groupEquiv = skipExtendedSeparatorParsing ? UnicodeSet.EMPTY :
+ (strictParse ? strictDefaultGroupingSeparators : defaultGroupingSeparators);
+
+ // We have to track digitCount ourselves, because digits.count will pin when
+ // the maximum allowable digits is reached.
+ int digitCount = 0;
+
+ int backup = -1; // used for preserving the last confirmed position
+ int[] parsedDigit = {-1}; // allocates int[1] for parsing a single digit
+
+ while (position < text.length()) {
+ // Check if the sequence at the current position matches a decimal digit
+ int matchLen = matchesDigit(text, position, parsedDigit);
+ if (matchLen > 0) {
+ // matched a digit
+ // Cancel out backup setting (see grouping handler below)
+ if (backup != -1) {
+ if (strictParse) {
+ // comma followed by digit, so group before comma is a secondary
+ // group. If there was a group separator before that, the group
+ // must == the secondary group length, else it can be <= the the
+ // secondary group length.
+ if ((lastGroup != -1 && groupedDigitCount != gs2)
+ || (lastGroup == -1 && groupedDigitCount > gs2)) {
+ strictFail = true;
+ break;
+ }
+ }
+ lastGroup = backup;
+ groupedDigitCount = 0;
+ }
+
+ groupedDigitCount++;
+ position += matchLen;
+ backup = -1;
+ sawDigit = true;
+ if (parsedDigit[0] == 0 && digits.count == 0) {
+ // Handle leading zeros
+ if (!sawDecimal) {
+ // Ignore leading zeros in integer part of number.
+ continue;
+ }
+ // If we have seen the decimal, but no significant digits yet,
+ // then we account for leading zeros by decrementing the
+ // digits.decimalAt into negative values.
+ --digits.decimalAt;
+ } else {
+ ++digitCount;
+ digits.append((char) (parsedDigit[0] + '0'));
+ }
+ continue;
+ }
+
+ // Check if the sequence at the current position matches locale's decimal separator
+ int decimalStrLen = decimal.length();
+ if (text.regionMatches(position, decimal, 0, decimalStrLen)) {
+ // matched a decimal separator
+ if (strictParse) {
+ if (backup != -1 ||
+ (lastGroup != -1 && groupedDigitCount != groupingSize)) {
+ strictFail = true;
+ break;
+ }
+ }
+
+ // If we're only parsing integers, or if we ALREADY saw the decimal,
+ // then don't parse this one.
+ if (isParseIntegerOnly() || sawDecimal) {
+ break;
+ }
+
+ digits.decimalAt = digitCount; // Not digits.count!
+ sawDecimal = true;
+ position += decimalStrLen;
+ continue;
+ }
+
+ if (isGroupingUsed()) {
+ // Check if the sequence at the current position matches locale's grouping separator
+ int groupingStrLen = grouping.length();
+ if (text.regionMatches(position, grouping, 0, groupingStrLen)) {
+ if (sawDecimal) {
+ break;
+ }
+
+ if (strictParse) {
+ if ((!sawDigit || backup != -1)) {
+ // leading group, or two group separators in a row
+ strictFail = true;
+ break;
+ }
+ }
+
+ // Ignore grouping characters, if we are using them, but require that
+ // they be followed by a digit. Otherwise we backup and reprocess
+ // them.
+ backup = position;
+ position += groupingStrLen;
+ sawGrouping = true;
+ continue;
+ }
+ }
+
+ // Check if the code point at the current position matches one of decimal/grouping equivalent group chars
+ int cp = text.codePointAt(position);
+ if (!sawDecimal && decimalEquiv.contains(cp)) {
+ // matched a decimal separator
+ if (strictParse) {
+ if (backup != -1 ||
+ (lastGroup != -1 && groupedDigitCount != groupingSize)) {
+ strictFail = true;
+ break;
+ }
+ }
+
+ // If we're only parsing integers, or if we ALREADY saw the decimal,
+ // then don't parse this one.
+ if (isParseIntegerOnly()) {
+ break;
+ }
+
+ digits.decimalAt = digitCount; // Not digits.count!
+
+ // Once we see a decimal separator character, we only accept that
+ // decimal separator character from then on.
+ decimal = String.valueOf(Character.toChars(cp));
+
+ sawDecimal = true;
+ position += Character.charCount(cp);
+ continue;
+ }
+
+ if (isGroupingUsed() && !sawGrouping && groupEquiv.contains(cp)) {
+ // matched a grouping separator
+ if (sawDecimal) {
+ break;
+ }
+
+ if (strictParse) {
+ if ((!sawDigit || backup != -1)) {
+ // leading group, or two group separators in a row
+ strictFail = true;
+ break;
+ }
+ }
+
+ // Once we see a grouping character, we only accept that grouping
+ // character from then on.
+ grouping = String.valueOf(Character.toChars(cp));
+
+ // Ignore grouping characters, if we are using them, but require that
+ // they be followed by a digit. Otherwise we backup and reprocess
+ // them.
+ backup = position;
+ position += Character.charCount(cp);
+ sawGrouping = true;
+ continue;
+ }
+
+ // Check if the sequence at the current position matches locale's exponent separator
+ int exponentSepStrLen = exponentSep.length();
+ if (text.regionMatches(true, position, exponentSep, 0, exponentSepStrLen)) {
+ // parse sign, if present
+ boolean negExp = false;
+ int pos = position + exponentSep.length();
+ if (pos < text.length()) {
+ String plusSign = symbols.getPlusSignString();
+ String minusSign = symbols.getMinusSignString();
+ if (text.regionMatches(pos, plusSign, 0, plusSign.length())) {
+ pos += plusSign.length();
+ } else if (text.regionMatches(pos, minusSign, 0, minusSign.length())) {
+ pos += minusSign.length();
+ negExp = true;
+ }
+ }
+
+ DigitList exponentDigits = new DigitList();
+ exponentDigits.count = 0;
+ while (pos < text.length()) {
+ int digitMatchLen = matchesDigit(text, pos, parsedDigit);
+ if (digitMatchLen > 0) {
+ exponentDigits.append((char) (parsedDigit[0] + '0'));
+ pos += digitMatchLen;
+ } else {
+ break;
+ }
+ }
+
+ if (exponentDigits.count > 0) {
+ // defer strict parse until we know we have a bona-fide exponent
+ if (strictParse && sawGrouping) {
+ strictFail = true;
+ break;
+ }
+
+ // Quick overflow check for exponential part. Actual limit check
+ // will be done later in this code.
+ if (exponentDigits.count > 10 /* maximum decimal digits for int */) {
+ if (negExp) {
+ // set underflow flag
+ status[STATUS_UNDERFLOW] = true;
+ } else {
+ // set infinite flag
+ status[STATUS_INFINITE] = true;
+ }
+ } else {
+ exponentDigits.decimalAt = exponentDigits.count;
+ exponent = exponentDigits.getLong();
+ if (negExp) {
+ exponent = -exponent;
+ }
+ }
+ position = pos; // Advance past the exponent
+ }
+
+ break; // Whether we fail or succeed, we exit this loop
+ }
+
+ // All other cases, stop parsing
+ break;
+ }
+
+ if (digits.decimalAt == 0 && isDecimalPatternMatchRequired()) {
+ if (this.formatPattern.indexOf(decimal) != -1) {
+ parsePosition.setIndex(oldStart);
+ parsePosition.setErrorIndex(position);
+ return false;
+ }
+ }
+
+ if (backup != -1)
+ position = backup;
+
+ // If there was no decimal point we have an integer
+ if (!sawDecimal) {
+ digits.decimalAt = digitCount; // Not digits.count!
+ }
+
+ // check for strict parse errors
+ if (strictParse && !sawDecimal) {
+ if (lastGroup != -1 && groupedDigitCount != groupingSize) {
+ strictFail = true;
+ }
+ }
+ if (strictFail) {
+ // only set with strictParse and a leading zero error leading zeros are an
+ // error with strict parsing except immediately before nondigit (except
+ // group separator followed by digit), or end of text.
+
+ parsePosition.setIndex(oldStart);
+ parsePosition.setErrorIndex(position);
+ return false;
+ }
+
+ // Adjust for exponent, if any
+ exponent += digits.decimalAt;
+ if (exponent < -getParseMaxDigits()) {
+ status[STATUS_UNDERFLOW] = true;
+ } else if (exponent > getParseMaxDigits()) {
+ status[STATUS_INFINITE] = true;
+ } else {
+ digits.decimalAt = (int) exponent;
+ }
+
+ // If none of the text string was recognized. For example, parse "x" with
+ // pattern "#0.00" (return index and error index both 0) parse "$" with
+ // pattern "$#0.00". (return index 0 and error index 1).
+ if (!sawDigit && digitCount == 0) {
+ parsePosition.setIndex(oldStart);
+ parsePosition.setErrorIndex(oldStart);
+ return false;
+ }
+ }
+
+ // Match padding before suffix
+ if (formatWidth > 0 && padPosition == PAD_BEFORE_SUFFIX) {
+ position = skipPadding(text, position);
+ }
+
+ // Match positive and negative suffixes; prefer longest match.
+ if (posMatch >= 0) {
+ posMatch = compareAffix(text, position, false, false, posSuffix, parseComplexCurrency, type, currency);
+ }
+ if (negMatch >= 0) {
+ negMatch = compareAffix(text, position, true, false, negSuffix, parseComplexCurrency, type, currency);
+ }
+ if (posMatch >= 0 && negMatch >= 0) {
+ if (posMatch > negMatch) {
+ negMatch = -1;
+ } else if (negMatch > posMatch) {
+ posMatch = -1;
+ }
+ }
+
+ // Fail if neither or both
+ if ((posMatch >= 0) == (negMatch >= 0)) {
+ parsePosition.setErrorIndex(position);
+ return false;
+ }
+
+ position += (posMatch >= 0 ? posMatch : negMatch);
+
+ // Match padding after suffix
+ if (formatWidth > 0 && padPosition == PAD_AFTER_SUFFIX) {
+ position = skipPadding(text, position);
+ }
+
+ parsePosition.setIndex(position);
+
+ status[STATUS_POSITIVE] = (posMatch >= 0);
+
+ if (parsePosition.getIndex() == oldStart) {
+ parsePosition.setErrorIndex(position);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check if the substring at the specified position matches a decimal digit.
+ * If matched, this method sets the decimal value to <code>decVal</code> and
+ * returns matched length.
+ *
+ * @param str The input string
+ * @param start The start index
+ * @param decVal Receives decimal value
+ * @return Length of match, or 0 if the sequence at the position is not
+ * a decimal digit.
+ */
+ private int matchesDigit(String str, int start, int[] decVal) {
+ String[] localeDigits = symbols.getDigitStringsLocal();
+
+ // Check if the sequence at the current position matches locale digits.
+ for (int i = 0; i < 10; i++) {
+ int digitStrLen = localeDigits[i].length();
+ if (str.regionMatches(start, localeDigits[i], 0, digitStrLen)) {
+ decVal[0] = i;
+ return digitStrLen;
+ }
+ }
+
+ // If no locale digit match, then check if this is a Unicode digit
+ int cp = str.codePointAt(start);
+ decVal[0] = UCharacter.digit(cp, 10);
+ if (decVal[0] >= 0) {
+ return Character.charCount(cp);
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns a set of characters equivalent to the given desimal separator used for
+ * parsing number. This method may return an empty set.
+ */
+ private UnicodeSet getEquivalentDecimals(String decimal, boolean strictParse) {
+ UnicodeSet equivSet = UnicodeSet.EMPTY;
+ if (strictParse) {
+ if (strictDotEquivalents.contains(decimal)) {
+ equivSet = strictDotEquivalents;
+ } else if (strictCommaEquivalents.contains(decimal)) {
+ equivSet = strictCommaEquivalents;
+ }
+ } else {
+ if (dotEquivalents.contains(decimal)) {
+ equivSet = dotEquivalents;
+ } else if (commaEquivalents.contains(decimal)) {
+ equivSet = commaEquivalents;
+ }
+ }
+ return equivSet;
+ }
+
+ /**
+ * Starting at position, advance past a run of pad characters, if any. Return the
+ * index of the first character after position that is not a pad character. Result is
+ * >= position.
+ */
+ private final int skipPadding(String text, int position) {
+ while (position < text.length() && text.charAt(position) == pad) {
+ ++position;
+ }
+ return position;
+ }
+
+ /**
+ * Returns the length matched by the given affix, or -1 if none. Runs of white space
+ * in the affix, match runs of white space in the input. Pattern white space and input
+ * white space are determined differently; see code.
+ *
+ * @param text input text
+ * @param pos offset into input at which to begin matching
+ * @param isNegative
+ * @param isPrefix
+ * @param affixPat affix pattern used for currency affix comparison
+ * @param complexCurrencyParsing whether it is currency parsing or not
+ * @param type compare against currency type, LONG_NAME only or not.
+ * @param currency return value for parsed currency, for generic currency parsing
+ * mode, or null for normal parsing. In generic currency parsing mode, any currency
+ * is parsed, not just the currency that this formatter is set to.
+ * @return length of input that matches, or -1 if match failure
+ */
+ private int compareAffix(String text, int pos, boolean isNegative, boolean isPrefix,
+ String affixPat, boolean complexCurrencyParsing, int type, Currency[] currency) {
+ if (currency != null || currencyChoice != null || (currencySignCount != CURRENCY_SIGN_COUNT_ZERO && complexCurrencyParsing)) {
+ return compareComplexAffix(affixPat, text, pos, type, currency);
+ }
+ if (isPrefix) {
+ return compareSimpleAffix(isNegative ? negativePrefix : positivePrefix, text, pos);
+ } else {
+ return compareSimpleAffix(isNegative ? negativeSuffix : positiveSuffix, text, pos);
+ }
+
+ }
+
+ /**
+ * Check for bidi marks: LRM, RLM, ALM
+ */
+ private static boolean isBidiMark(int c) {
+ return (c==0x200E || c==0x200F || c==0x061C);
+ }
+
+ /**
+ * Remove bidi marks from affix
+ */
+ private static String trimMarksFromAffix(String affix) {
+ boolean hasBidiMark = false;
+ int idx = 0;
+ for (; idx < affix.length(); idx++) {
+ if (isBidiMark(affix.charAt(idx))) {
+ hasBidiMark = true;
+ break;
+ }
+ }
+ if (!hasBidiMark) {
+ return affix;
+ }
+
+ StringBuilder buf = new StringBuilder();
+ buf.append(affix, 0, idx);
+ idx++; // skip the first Bidi mark
+ for (; idx < affix.length(); idx++) {
+ char c = affix.charAt(idx);
+ if (!isBidiMark(c)) {
+ buf.append(c);
+ }
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Return the length matched by the given affix, or -1 if none. Runs of white space in
+ * the affix, match runs of white space in the input. Pattern white space and input
+ * white space are determined differently; see code.
+ *
+ * @param affix pattern string, taken as a literal
+ * @param input input text
+ * @param pos offset into input at which to begin matching
+ * @return length of input that matches, or -1 if match failure
+ */
+ private static int compareSimpleAffix(String affix, String input, int pos) {
+ int start = pos;
+ // Affixes here might consist of sign, currency symbol and related spacing, etc.
+ // For more efficiency we should keep lazily-created trimmed affixes around in
+ // instance variables instead of trimming each time they are used (the next step).
+ String trimmedAffix = (affix.length() > 1)? trimMarksFromAffix(affix): affix;
+ for (int i = 0; i < trimmedAffix.length();) {
+ int c = UTF16.charAt(trimmedAffix, i);
+ int len = UTF16.getCharCount(c);
+ if (PatternProps.isWhiteSpace(c)) {
+ // We may have a pattern like: \u200F and input text like: \u200F Note
+ // that U+200F and U+0020 are Pattern_White_Space but only U+0020 is
+ // UWhiteSpace. So we have to first do a direct match of the run of RULE
+ // whitespace in the pattern, then match any extra characters.
+ boolean literalMatch = false;
+ while (pos < input.length()) {
+ int ic = UTF16.charAt(input, pos);
+ if (ic == c) {
+ literalMatch = true;
+ i += len;
+ pos += len;
+ if (i == trimmedAffix.length()) {
+ break;
+ }
+ c = UTF16.charAt(trimmedAffix, i);
+ len = UTF16.getCharCount(c);
+ if (!PatternProps.isWhiteSpace(c)) {
+ break;
+ }
+ } else if (isBidiMark(ic)) {
+ pos++; // just skip over this input text
+ } else {
+ break;
+ }
+ }
+
+ // Advance over run in trimmedAffix
+ i = skipPatternWhiteSpace(trimmedAffix, i);
+
+ // Advance over run in input text. Must see at least one white space char
+ // in input, unless we've already matched some characters literally.
+ int s = pos;
+ pos = skipUWhiteSpace(input, pos);
+ if (pos == s && !literalMatch) {
+ return -1;
+ }
+ // If we skip UWhiteSpace in the input text, we need to skip it in the
+ // pattern. Otherwise, the previous lines may have skipped over text
+ // (such as U+00A0) that is also in the trimmedAffix.
+ i = skipUWhiteSpace(trimmedAffix, i);
+ } else {
+ boolean match = false;
+ while (pos < input.length()) {
+ int ic = UTF16.charAt(input, pos);
+ if (!match && equalWithSignCompatibility(ic, c)) {
+ i += len;
+ pos += len;
+ match = true;
+ } else if (isBidiMark(ic)) {
+ pos++; // just skip over this input text
+ } else {
+ break;
+ }
+ }
+ if (!match) {
+ return -1;
+ }
+ }
+ }
+ return pos - start;
+ }
+
+ private static boolean equalWithSignCompatibility(int lhs, int rhs) {
+ return lhs == rhs
+ || (minusSigns.contains(lhs) && minusSigns.contains(rhs))
+ || (plusSigns.contains(lhs) && plusSigns.contains(rhs));
+ }
+
+ /**
+ * Skips over a run of zero or more Pattern_White_Space characters at pos in text.
+ */
+ private static int skipPatternWhiteSpace(String text, int pos) {
+ while (pos < text.length()) {
+ int c = UTF16.charAt(text, pos);
+ if (!PatternProps.isWhiteSpace(c)) {
+ break;
+ }
+ pos += UTF16.getCharCount(c);
+ }
+ return pos;
+ }
+
+ /**
+ * Skips over a run of zero or more isUWhiteSpace() characters at pos in text.
+ */
+ private static int skipUWhiteSpace(String text, int pos) {
+ while (pos < text.length()) {
+ int c = UTF16.charAt(text, pos);
+ if (!UCharacter.isUWhiteSpace(c)) {
+ break;
+ }
+ pos += UTF16.getCharCount(c);
+ }
+ return pos;
+ }
+
+ /**
+ * Skips over a run of zero or more bidi marks at pos in text.
+ */
+ private static int skipBidiMarks(String text, int pos) {
+ while (pos < text.length()) {
+ int c = UTF16.charAt(text, pos);
+ if (!isBidiMark(c)) {
+ break;
+ }
+ pos += UTF16.getCharCount(c);
+ }
+ return pos;
+ }
+
+ /**
+ * Returns the length matched by the given affix, or -1 if none.
+ *
+ * @param affixPat pattern string
+ * @param text input text
+ * @param pos offset into input at which to begin matching
+ * @param type parse against currency type, LONG_NAME only or not.
+ * @param currency return value for parsed currency, for generic
+ * currency parsing mode, or null for normal parsing. In generic
+ * currency parsing mode, any currency is parsed, not just the
+ * currency that this formatter is set to.
+ * @return position after the matched text, or -1 if match failure
+ */
+ private int compareComplexAffix(String affixPat, String text, int pos, int type,
+ Currency[] currency) {
+ int start = pos;
+ for (int i = 0; i < affixPat.length() && pos >= 0;) {
+ char c = affixPat.charAt(i++);
+ if (c == QUOTE) {
+ for (;;) {
+ int j = affixPat.indexOf(QUOTE, i);
+ if (j == i) {
+ pos = match(text, pos, QUOTE);
+ i = j + 1;
+ break;
+ } else if (j > i) {
+ pos = match(text, pos, affixPat.substring(i, j));
+ i = j + 1;
+ if (i < affixPat.length() && affixPat.charAt(i) == QUOTE) {
+ pos = match(text, pos, QUOTE);
+ ++i;
+ // loop again
+ } else {
+ break;
+ }
+ } else {
+ // Unterminated quote; should be caught by apply
+ // pattern.
+ throw new RuntimeException();
+ }
+ }
+ continue;
+ }
+
+ String affix = null;
+
+ switch (c) {
+ case CURRENCY_SIGN:
+ // since the currency names in choice format is saved the same way as
+ // other currency names, do not need to do currency choice parsing here.
+ // the general currency parsing parse against all names, including names
+ // in choice format. assert(currency != null || (getCurrency() != null &&
+ // currencyChoice != null));
+ boolean intl = i < affixPat.length() && affixPat.charAt(i) == CURRENCY_SIGN;
+ if (intl) {
+ ++i;
+ }
+ boolean plural = i < affixPat.length() && affixPat.charAt(i) == CURRENCY_SIGN;
+ if (plural) {
+ ++i;
+ intl = false;
+ }
+ // Parse generic currency -- anything for which we have a display name, or
+ // any 3-letter ISO code. Try to parse display name for our locale; first
+ // determine our locale. TODO: use locale in CurrencyPluralInfo
+ ULocale uloc = getLocale(ULocale.VALID_LOCALE);
+ if (uloc == null) {
+ // applyPattern has been called; use the symbols
+ uloc = symbols.getLocale(ULocale.VALID_LOCALE);
+ }
+ // Delegate parse of display name => ISO code to Currency
+ ParsePosition ppos = new ParsePosition(pos);
+ // using Currency.parse to handle mixed style parsing.
+ String iso = Currency.parse(uloc, text, type, ppos);
+
+ // If parse succeeds, populate currency[0]
+ if (iso != null) {
+ if (currency != null) {
+ currency[0] = Currency.getInstance(iso);
+ } else {
+ // The formatter is currency-style but the client has not requested
+ // the value of the parsed currency. In this case, if that value does
+ // not match the formatter's current value, then the parse fails.
+ Currency effectiveCurr = getEffectiveCurrency();
+ if (iso.compareTo(effectiveCurr.getCurrencyCode()) != 0) {
+ pos = -1;
+ continue;
+ }
+ }
+ pos = ppos.getIndex();
+ } else {
+ pos = -1;
+ }
+ continue;
+ case PATTERN_PERCENT:
+ affix = symbols.getPercentString();
+ break;
+ case PATTERN_PER_MILLE:
+ affix = symbols.getPerMillString();
+ break;
+ case PATTERN_PLUS_SIGN:
+ affix = symbols.getPlusSignString();
+ break;
+ case PATTERN_MINUS_SIGN:
+ affix = symbols.getMinusSignString();
+ break;
+ default:
+ // fall through to affix != null test, which will fail
+ break;
+ }
+
+ if (affix != null) {
+ pos = match(text, pos, affix);
+ continue;
+ }
+
+ pos = match(text, pos, c);
+ if (PatternProps.isWhiteSpace(c)) {
+ i = skipPatternWhiteSpace(affixPat, i);
+ }
+ }
+
+ return pos - start;
+ }
+
+ /**
+ * Matches a single character at text[pos] and return the index of the next character
+ * upon success. Return -1 on failure. If ch is a Pattern_White_Space then match a run of
+ * white space in text.
+ */
+ static final int match(String text, int pos, int ch) {
+ if (pos < 0 || pos >= text.length()) {
+ return -1;
+ }
+ pos = skipBidiMarks(text, pos);
+ if (PatternProps.isWhiteSpace(ch)) {
+ // Advance over run of white space in input text
+ // Must see at least one white space char in input
+ int s = pos;
+ pos = skipPatternWhiteSpace(text, pos);
+ if (pos == s) {
+ return -1;
+ }
+ return pos;
+ }
+ if (pos >= text.length() || UTF16.charAt(text, pos) != ch) {
+ return -1;
+ }
+ pos = skipBidiMarks(text, pos + UTF16.getCharCount(ch));
+ return pos;
+ }
+
+ /**
+ * Matches a string at text[pos] and return the index of the next character upon
+ * success. Return -1 on failure. Match a run of white space in str with a run of
+ * white space in text.
+ */
+ static final int match(String text, int pos, String str) {
+ for (int i = 0; i < str.length() && pos >= 0;) {
+ int ch = UTF16.charAt(str, i);
+ i += UTF16.getCharCount(ch);
+ if (isBidiMark(ch)) {
+ continue;
+ }
+ pos = match(text, pos, ch);
+ if (PatternProps.isWhiteSpace(ch)) {
+ i = skipPatternWhiteSpace(str, i);
+ }
+ }
+ return pos;
+ }
+
+ /**
+ * Returns a copy of the decimal format symbols used by this format.
+ *
+ * @return desired DecimalFormatSymbols
+ * @see DecimalFormatSymbols
+ * @stable ICU 2.0
+ */
+ public DecimalFormatSymbols getDecimalFormatSymbols() {
+ try {
+ // don't allow multiple references
+ return (DecimalFormatSymbols) symbols.clone();
+ } catch (Exception foo) {
+ return null; // should never happen
+ }
+ }
+
+ /**
+ * Sets the decimal format symbols used by this format. The format uses a copy of the
+ * provided symbols.
+ *
+ * @param newSymbols desired DecimalFormatSymbols
+ * @see DecimalFormatSymbols
+ * @stable ICU 2.0
+ */
+ public void setDecimalFormatSymbols(DecimalFormatSymbols newSymbols) {
+ symbols = (DecimalFormatSymbols) newSymbols.clone();
+ setCurrencyForSymbols();
+ expandAffixes(null);
+ }
+
+ /**
+ * Update the currency object to match the symbols. This method is used only when the
+ * caller has passed in a symbols object that may not be the default object for its
+ * locale.
+ */
+ private void setCurrencyForSymbols() {
+
+ // Bug 4212072 Update the affix strings according to symbols in order to keep the
+ // affix strings up to date. [Richard/GCL]
+
+ // With the introduction of the Currency object, the currency symbols in the DFS
+ // object are ignored. For backward compatibility, we check any explicitly set DFS
+ // object. If it is a default symbols object for its locale, we change the
+ // currency object to one for that locale. If it is custom, we set the currency to
+ // null.
+ DecimalFormatSymbols def = new DecimalFormatSymbols(symbols.getULocale());
+
+ if (symbols.getCurrencySymbol().equals(def.getCurrencySymbol())
+ && symbols.getInternationalCurrencySymbol()
+ .equals(def.getInternationalCurrencySymbol())) {
+ setCurrency(Currency.getInstance(symbols.getULocale()));
+ } else {
+ setCurrency(null);
+ }
+ }
+
+ /**
+ * Returns the positive prefix.
+ *
+ * <p>Examples: +123, $123, sFr123
+ * @return the prefix
+ * @stable ICU 2.0
+ */
+ public String getPositivePrefix() {
+ return positivePrefix;
+ }
+
+ /**
+ * Sets the positive prefix.
+ *
+ * <p>Examples: +123, $123, sFr123
+ * @param newValue the prefix
+ * @stable ICU 2.0
+ */
+ public void setPositivePrefix(String newValue) {
+ positivePrefix = newValue;
+ posPrefixPattern = null;
+ }
+
+ /**
+ * Returns the negative prefix.
+ *
+ * <p>Examples: -123, ($123) (with negative suffix), sFr-123
+ *
+ * @return the prefix
+ * @stable ICU 2.0
+ */
+ public String getNegativePrefix() {
+ return negativePrefix;
+ }
+
+ /**
+ * Sets the negative prefix.
+ *
+ * <p>Examples: -123, ($123) (with negative suffix), sFr-123
+ * @param newValue the prefix
+ * @stable ICU 2.0
+ */
+ public void setNegativePrefix(String newValue) {
+ negativePrefix = newValue;
+ negPrefixPattern = null;
+ }
+
+ /**
+ * Returns the positive suffix.
+ *
+ * <p>Example: 123%
+ *
+ * @return the suffix
+ * @stable ICU 2.0
+ */
+ public String getPositiveSuffix() {
+ return positiveSuffix;
+ }
+
+ /**
+ * Sets the positive suffix.
+ *
+ * <p>Example: 123%
+ * @param newValue the suffix
+ * @stable ICU 2.0
+ */
+ public void setPositiveSuffix(String newValue) {
+ positiveSuffix = newValue;
+ posSuffixPattern = null;
+ }
+
+ /**
+ * Returns the negative suffix.
+ *
+ * <p>Examples: -123%, ($123) (with positive suffixes)
+ *
+ * @return the suffix
+ * @stable ICU 2.0
+ */
+ public String getNegativeSuffix() {
+ return negativeSuffix;
+ }
+
+ /**
+ * Sets the positive suffix.
+ *
+ * <p>Examples: 123%
+ * @param newValue the suffix
+ * @stable ICU 2.0
+ */
+ public void setNegativeSuffix(String newValue) {
+ negativeSuffix = newValue;
+ negSuffixPattern = null;
+ }
+
+ /**
+ * Returns the multiplier for use in percent, permill, etc. For a percentage, set the
+ * suffixes to have "%" and the multiplier to be 100. (For Arabic, use arabic percent
+ * symbol). For a permill, set the suffixes to have "\u2031" and the multiplier to be
+ * 1000.
+ *
+ * <p>Examples: with 100, 1.23 -> "123", and "123" -> 1.23
+ *
+ * @return the multiplier
+ * @stable ICU 2.0
+ */
+ public int getMultiplier() {
+ return multiplier;
+ }
+
+ /**
+ * Sets the multiplier for use in percent, permill, etc. For a percentage, set the
+ * suffixes to have "%" and the multiplier to be 100. (For Arabic, use arabic percent
+ * symbol). For a permill, set the suffixes to have "\u2031" and the multiplier to be
+ * 1000.
+ *
+ * <p>Examples: with 100, 1.23 -> "123", and "123" -> 1.23
+ *
+ * @param newValue the multiplier
+ * @stable ICU 2.0
+ */
+ public void setMultiplier(int newValue) {
+ if (newValue == 0) {
+ throw new IllegalArgumentException("Bad multiplier: " + newValue);
+ }
+ multiplier = newValue;
+ }
+
+ /**
+ * {@icu} Returns the rounding increment.
+ *
+ * @return A positive rounding increment, or <code>null</code> if a custom rounding
+ * increment is not in effect.
+ * @see #setRoundingIncrement
+ * @see #getRoundingMode
+ * @see #setRoundingMode
+ * @stable ICU 2.0
+ */
+ public java.math.BigDecimal getRoundingIncrement() {
+ if (roundingIncrementICU == null)
+ return null;
+ return roundingIncrementICU.toBigDecimal();
+ }
+
+ /**
+ * {@icu} Sets the rounding increment. In the absence of a rounding increment, numbers
+ * will be rounded to the number of digits displayed.
+ *
+ * @param newValue A positive rounding increment, or <code>null</code> or
+ * <code>BigDecimal(0.0)</code> to use the default rounding increment.
+ * @throws IllegalArgumentException if <code>newValue</code> is < 0.0
+ * @see #getRoundingIncrement
+ * @see #getRoundingMode
+ * @see #setRoundingMode
+ * @stable ICU 2.0
+ */
+ public void setRoundingIncrement(java.math.BigDecimal newValue) {
+ if (newValue == null) {
+ setRoundingIncrement((BigDecimal) null);
+ } else {
+ setRoundingIncrement(new BigDecimal(newValue));
+ }
+ }
+
+ /**
+ * {@icu} Sets the rounding increment. In the absence of a rounding increment, numbers
+ * will be rounded to the number of digits displayed.
+ *
+ * @param newValue A positive rounding increment, or <code>null</code> or
+ * <code>BigDecimal(0.0)</code> to use the default rounding increment.
+ * @throws IllegalArgumentException if <code>newValue</code> is < 0.0
+ * @see #getRoundingIncrement
+ * @see #getRoundingMode
+ * @see #setRoundingMode
+ * @stable ICU 3.6
+ */
+ public void setRoundingIncrement(BigDecimal newValue) {
+ int i = newValue == null ? 0 : newValue.compareTo(BigDecimal.ZERO);
+ if (i < 0) {
+ throw new IllegalArgumentException("Illegal rounding increment");
+ }
+ if (i == 0) {
+ setInternalRoundingIncrement(null);
+ } else {
+ setInternalRoundingIncrement(newValue);
+ }
+ resetActualRounding();
+ }
+
+ /**
+ * {@icu} Sets the rounding increment. In the absence of a rounding increment, numbers
+ * will be rounded to the number of digits displayed.
+ *
+ * @param newValue A positive rounding increment, or 0.0 to use the default
+ * rounding increment.
+ * @throws IllegalArgumentException if <code>newValue</code> is < 0.0
+ * @see #getRoundingIncrement
+ * @see #getRoundingMode
+ * @see #setRoundingMode
+ * @stable ICU 2.0
+ */
+ public void setRoundingIncrement(double newValue) {
+ if (newValue < 0.0) {
+ throw new IllegalArgumentException("Illegal rounding increment");
+ }
+ if (newValue == 0.0d) {
+ setInternalRoundingIncrement((BigDecimal) null);
+ } else {
+ // Should use BigDecimal#valueOf(double) instead of constructor
+ // to avoid the double precision problem.
+ setInternalRoundingIncrement(BigDecimal.valueOf(newValue));
+ }
+ resetActualRounding();
+ }
+
+ /**
+ * Returns the rounding mode.
+ *
+ * @return A rounding mode, between <code>BigDecimal.ROUND_UP</code> and
+ * <code>BigDecimal.ROUND_UNNECESSARY</code>.
+ * @see #setRoundingIncrement
+ * @see #getRoundingIncrement
+ * @see #setRoundingMode
+ * @see java.math.BigDecimal
+ * @stable ICU 2.0
+ */
+ @Override
+ public int getRoundingMode() {
+ return roundingMode;
+ }
+
+ /**
+ * Sets the rounding mode. This has no effect unless the rounding increment is greater
+ * than zero.
+ *
+ * @param roundingMode A rounding mode, between <code>BigDecimal.ROUND_UP</code> and
+ * <code>BigDecimal.ROUND_UNNECESSARY</code>.
+ * @exception IllegalArgumentException if <code>roundingMode</code> is unrecognized.
+ * @see #setRoundingIncrement
+ * @see #getRoundingIncrement
+ * @see #getRoundingMode
+ * @see java.math.BigDecimal
+ * @stable ICU 2.0
+ */
+ @Override
+ public void setRoundingMode(int roundingMode) {
+ if (roundingMode < BigDecimal.ROUND_UP || roundingMode > BigDecimal.ROUND_UNNECESSARY) {
+ throw new IllegalArgumentException("Invalid rounding mode: " + roundingMode);
+ }
+
+ this.roundingMode = roundingMode;
+ resetActualRounding();
+ }
+
+ /**
+ * Returns the width to which the output of <code>format()</code> is padded. The width is
+ * counted in 16-bit code units.
+ *
+ * @return the format width, or zero if no padding is in effect
+ * @see #setFormatWidth
+ * @see #getPadCharacter
+ * @see #setPadCharacter
+ * @see #getPadPosition
+ * @see #setPadPosition
+ * @stable ICU 2.0
+ */
+ public int getFormatWidth() {
+ return formatWidth;
+ }
+
+ /**
+ * Sets the width to which the output of <code>format()</code> is
+ * padded. The width is counted in 16-bit code units. This method
+ * also controls whether padding is enabled.
+ *
+ * @param width the width to which to pad the result of
+ * <code>format()</code>, or zero to disable padding
+ * @exception IllegalArgumentException if <code>width</code> is < 0
+ * @see #getFormatWidth
+ * @see #getPadCharacter
+ * @see #setPadCharacter
+ * @see #getPadPosition
+ * @see #setPadPosition
+ * @stable ICU 2.0
+ */
+ public void setFormatWidth(int width) {
+ if (width < 0) {
+ throw new IllegalArgumentException("Illegal format width");
+ }
+ formatWidth = width;
+ }
+
+ /**
+ * {@icu} Returns the character used to pad to the format width. The default is ' '.
+ *
+ * @return the pad character
+ * @see #setFormatWidth
+ * @see #getFormatWidth
+ * @see #setPadCharacter
+ * @see #getPadPosition
+ * @see #setPadPosition
+ * @stable ICU 2.0
+ */
+ public char getPadCharacter() {
+ return pad;
+ }
+
+ /**
+ * {@icu} Sets the character used to pad to the format width. If padding is not
+ * enabled, then this will take effect if padding is later enabled.
+ *
+ * @param padChar the pad character
+ * @see #setFormatWidth
+ * @see #getFormatWidth
+ * @see #getPadCharacter
+ * @see #getPadPosition
+ * @see #setPadPosition
+ * @stable ICU 2.0
+ */
+ public void setPadCharacter(char padChar) {
+ pad = padChar;
+ }
+
+ /**
+ * {@icu} Returns the position at which padding will take place. This is the location at
+ * which padding will be inserted if the result of <code>format()</code> is shorter
+ * than the format width.
+ *
+ * @return the pad position, one of <code>PAD_BEFORE_PREFIX</code>,
+ * <code>PAD_AFTER_PREFIX</code>, <code>PAD_BEFORE_SUFFIX</code>, or
+ * <code>PAD_AFTER_SUFFIX</code>.
+ * @see #setFormatWidth
+ * @see #getFormatWidth
+ * @see #setPadCharacter
+ * @see #getPadCharacter
+ * @see #setPadPosition
+ * @see #PAD_BEFORE_PREFIX
+ * @see #PAD_AFTER_PREFIX
+ * @see #PAD_BEFORE_SUFFIX
+ * @see #PAD_AFTER_SUFFIX
+ * @stable ICU 2.0
+ */
+ public int getPadPosition() {
+ return padPosition;
+ }
+
+ /**
+ * {@icu} Sets the position at which padding will take place. This is the location at
+ * which padding will be inserted if the result of <code>format()</code> is shorter
+ * than the format width. This has no effect unless padding is enabled.
+ *
+ * @param padPos the pad position, one of <code>PAD_BEFORE_PREFIX</code>,
+ * <code>PAD_AFTER_PREFIX</code>, <code>PAD_BEFORE_SUFFIX</code>, or
+ * <code>PAD_AFTER_SUFFIX</code>.
+ * @exception IllegalArgumentException if the pad position in unrecognized
+ * @see #setFormatWidth
+ * @see #getFormatWidth
+ * @see #setPadCharacter
+ * @see #getPadCharacter
+ * @see #getPadPosition
+ * @see #PAD_BEFORE_PREFIX
+ * @see #PAD_AFTER_PREFIX
+ * @see #PAD_BEFORE_SUFFIX
+ * @see #PAD_AFTER_SUFFIX
+ * @stable ICU 2.0
+ */
+ public void setPadPosition(int padPos) {
+ if (padPos < PAD_BEFORE_PREFIX || padPos > PAD_AFTER_SUFFIX) {
+ throw new IllegalArgumentException("Illegal pad position");
+ }
+ padPosition = padPos;
+ }
+
+ /**
+ * {@icu} Returns whether or not scientific notation is used.
+ *
+ * @return true if this object formats and parses scientific notation
+ * @see #setScientificNotation
+ * @see #getMinimumExponentDigits
+ * @see #setMinimumExponentDigits
+ * @see #isExponentSignAlwaysShown
+ * @see #setExponentSignAlwaysShown
+ * @stable ICU 2.0
+ */
+ public boolean isScientificNotation() {
+ return useExponentialNotation;
+ }
+
+ /**
+ * {@icu} Sets whether or not scientific notation is used. When scientific notation is
+ * used, the effective maximum number of integer digits is <= 8. If the maximum number
+ * of integer digits is set to more than 8, the effective maximum will be 1. This
+ * allows this call to generate a 'default' scientific number format without
+ * additional changes.
+ *
+ * @param useScientific true if this object formats and parses scientific notation
+ * @see #isScientificNotation
+ * @see #getMinimumExponentDigits
+ * @see #setMinimumExponentDigits
+ * @see #isExponentSignAlwaysShown
+ * @see #setExponentSignAlwaysShown
+ * @stable ICU 2.0
+ */
+ public void setScientificNotation(boolean useScientific) {
+ useExponentialNotation = useScientific;
+ }
+
+ /**
+ * {@icu} Returns the minimum exponent digits that will be shown.
+ *
+ * @return the minimum exponent digits that will be shown
+ * @see #setScientificNotation
+ * @see #isScientificNotation
+ * @see #setMinimumExponentDigits
+ * @see #isExponentSignAlwaysShown
+ * @see #setExponentSignAlwaysShown
+ * @stable ICU 2.0
+ */
+ public byte getMinimumExponentDigits() {
+ return minExponentDigits;
+ }
+
+ /**
+ * {@icu} Sets the minimum exponent digits that will be shown. This has no effect
+ * unless scientific notation is in use.
+ *
+ * @param minExpDig a value >= 1 indicating the fewest exponent
+ * digits that will be shown
+ * @exception IllegalArgumentException if <code>minExpDig</code> < 1
+ * @see #setScientificNotation
+ * @see #isScientificNotation
+ * @see #getMinimumExponentDigits
+ * @see #isExponentSignAlwaysShown
+ * @see #setExponentSignAlwaysShown
+ * @stable ICU 2.0
+ */
+ public void setMinimumExponentDigits(byte minExpDig) {
+ if (minExpDig < 1) {
+ throw new IllegalArgumentException("Exponent digits must be >= 1");
+ }
+ minExponentDigits = minExpDig;
+ }
+
+ /**
+ * {@icu} Returns whether the exponent sign is always shown.
+ *
+ * @return true if the exponent is always prefixed with either the localized minus
+ * sign or the localized plus sign, false if only negative exponents are prefixed with
+ * the localized minus sign.
+ * @see #setScientificNotation
+ * @see #isScientificNotation
+ * @see #setMinimumExponentDigits
+ * @see #getMinimumExponentDigits
+ * @see #setExponentSignAlwaysShown
+ * @stable ICU 2.0
+ */
+ public boolean isExponentSignAlwaysShown() {
+ return exponentSignAlwaysShown;
+ }
+
+ /**
+ * {@icu} Sets whether the exponent sign is always shown. This has no effect unless
+ * scientific notation is in use.
+ *
+ * @param expSignAlways true if the exponent is always prefixed with either the
+ * localized minus sign or the localized plus sign, false if only negative exponents
+ * are prefixed with the localized minus sign.
+ * @see #setScientificNotation
+ * @see #isScientificNotation
+ * @see #setMinimumExponentDigits
+ * @see #getMinimumExponentDigits
+ * @see #isExponentSignAlwaysShown
+ * @stable ICU 2.0
+ */
+ public void setExponentSignAlwaysShown(boolean expSignAlways) {
+ exponentSignAlwaysShown = expSignAlways;
+ }
+
+ /**
+ * Returns the grouping size. Grouping size is the number of digits between grouping
+ * separators in the integer portion of a number. For example, in the number
+ * "123,456.78", the grouping size is 3.
+ *
+ * @see #setGroupingSize
+ * @see NumberFormat#isGroupingUsed
+ * @see DecimalFormatSymbols#getGroupingSeparator
+ * @stable ICU 2.0
+ */
+ public int getGroupingSize() {
+ return groupingSize;
+ }
+
+ /**
+ * Sets the grouping size. Grouping size is the number of digits between grouping
+ * separators in the integer portion of a number. For example, in the number
+ * "123,456.78", the grouping size is 3.
+ *
+ * @see #getGroupingSize
+ * @see NumberFormat#setGroupingUsed
+ * @see DecimalFormatSymbols#setGroupingSeparator
+ * @stable ICU 2.0
+ */
+ public void setGroupingSize(int newValue) {
+ groupingSize = (byte) newValue;
+ }
+
+ /**
+ * {@icu} Returns the secondary grouping size. In some locales one grouping interval
+ * is used for the least significant integer digits (the primary grouping size), and
+ * another is used for all others (the secondary grouping size). A formatter
+ * supporting a secondary grouping size will return a positive integer unequal to the
+ * primary grouping size returned by <code>getGroupingSize()</code>. For example, if
+ * the primary grouping size is 4, and the secondary grouping size is 2, then the
+ * number 123456789 formats as "1,23,45,6789", and the pattern appears as "#,##,###0".
+ *
+ * @return the secondary grouping size, or a value less than one if there is none
+ * @see #setSecondaryGroupingSize
+ * @see NumberFormat#isGroupingUsed
+ * @see DecimalFormatSymbols#getGroupingSeparator
+ * @stable ICU 2.0
+ */
+ public int getSecondaryGroupingSize() {
+ return groupingSize2;
+ }
+
+ /**
+ * {@icu} Sets the secondary grouping size. If set to a value less than 1, then
+ * secondary grouping is turned off, and the primary grouping size is used for all
+ * intervals, not just the least significant.
+ *
+ * @see #getSecondaryGroupingSize
+ * @see NumberFormat#setGroupingUsed
+ * @see DecimalFormatSymbols#setGroupingSeparator
+ * @stable ICU 2.0
+ */
+ public void setSecondaryGroupingSize(int newValue) {
+ groupingSize2 = (byte) newValue;
+ }
+
+ /**
+ * {@icu} Returns the MathContext used by this format.
+ *
+ * @return desired MathContext
+ * @see #getMathContext
+ * @stable ICU 4.2
+ */
+ public MathContext getMathContextICU() {
+ return mathContext;
+ }
+
+ /**
+ * {@icu} Returns the MathContext used by this format.
+ *
+ * @return desired MathContext
+ * @see #getMathContext
+ * @stable ICU 4.2
+ */
+ public java.math.MathContext getMathContext() {
+ try {
+ // don't allow multiple references
+ return mathContext == null ? null : new java.math.MathContext(mathContext.getDigits(),
+ java.math.RoundingMode.valueOf(mathContext.getRoundingMode()));
+ } catch (Exception foo) {
+ return null; // should never happen
+ }
+ }
+
+ /**
+ * {@icu} Sets the MathContext used by this format.
+ *
+ * @param newValue desired MathContext
+ * @see #getMathContext
+ * @stable ICU 4.2
+ */
+ public void setMathContextICU(MathContext newValue) {
+ mathContext = newValue;
+ }
+
+ /**
+ * {@icu} Sets the MathContext used by this format.
+ *
+ * @param newValue desired MathContext
+ * @see #getMathContext
+ * @stable ICU 4.2
+ */
+ public void setMathContext(java.math.MathContext newValue) {
+ mathContext = new MathContext(newValue.getPrecision(), MathContext.SCIENTIFIC, false,
+ (newValue.getRoundingMode()).ordinal());
+ }
+
+ /**
+ * Returns the behavior of the decimal separator with integers. (The decimal
+ * separator will always appear with decimals.) <p> Example: Decimal ON: 12345 ->
+ * 12345.; OFF: 12345 -> 12345
+ *
+ * @stable ICU 2.0
+ */
+ public boolean isDecimalSeparatorAlwaysShown() {
+ return decimalSeparatorAlwaysShown;
+ }
+
+ /**
+ * When decimal match is not required, the input does not have to
+ * contain a decimal mark when there is a decimal mark specified in the
+ * pattern.
+ * @param value true if input must contain a match to decimal mark in pattern
+ * Default is false.
+ * @stable ICU 54
+ */
+ public void setDecimalPatternMatchRequired(boolean value) {
+ parseRequireDecimalPoint = value;
+ }
+
+ /**
+ * {@icu} Returns whether the input to parsing must contain a decimal mark if there
+ * is a decimal mark in the pattern.
+ * @return true if input must contain a match to decimal mark in pattern
+ * @stable ICU 54
+ */
+ public boolean isDecimalPatternMatchRequired() {
+ return parseRequireDecimalPoint;
+ }
+
+
+ /**
+ * Sets the behavior of the decimal separator with integers. (The decimal separator
+ * will always appear with decimals.)
+ *
+ * <p>This only affects formatting, and only where there might be no digits after the
+ * decimal point, e.g., if true, 3456.00 -> "3,456." if false, 3456.00 -> "3456" This
+ * is independent of parsing. If you want parsing to stop at the decimal point, use
+ * setParseIntegerOnly.
+ *
+ * <p>
+ * Example: Decimal ON: 12345 -> 12345.; OFF: 12345 -> 12345
+ *
+ * @stable ICU 2.0
+ */
+ public void setDecimalSeparatorAlwaysShown(boolean newValue) {
+ decimalSeparatorAlwaysShown = newValue;
+ }
+
+ /**
+ * {@icu} Returns a copy of the CurrencyPluralInfo used by this format. It might
+ * return null if the decimal format is not a plural type currency decimal
+ * format. Plural type currency decimal format means either the pattern in the decimal
+ * format contains 3 currency signs, or the decimal format is initialized with
+ * PLURALCURRENCYSTYLE.
+ *
+ * @return desired CurrencyPluralInfo
+ * @see CurrencyPluralInfo
+ * @stable ICU 4.2
+ */
+ public CurrencyPluralInfo getCurrencyPluralInfo() {
+ try {
+ // don't allow multiple references
+ return currencyPluralInfo == null ? null :
+ (CurrencyPluralInfo) currencyPluralInfo.clone();
+ } catch (Exception foo) {
+ return null; // should never happen
+ }
+ }
+
+ /**
+ * {@icu} Sets the CurrencyPluralInfo used by this format. The format uses a copy of
+ * the provided information.
+ *
+ * @param newInfo desired CurrencyPluralInfo
+ * @see CurrencyPluralInfo
+ * @stable ICU 4.2
+ */
+ public void setCurrencyPluralInfo(CurrencyPluralInfo newInfo) {
+ currencyPluralInfo = (CurrencyPluralInfo) newInfo.clone();
+ isReadyForParsing = false;
+ }
+
+ /**
+ * Overrides clone.
+ * @stable ICU 2.0
+ */
+ @Override
+ public Object clone() {
+ try {
+ DecimalFormat_ICU58 other = (DecimalFormat_ICU58) super.clone();
+ other.symbols = (DecimalFormatSymbols) symbols.clone();
+ other.digitList = new DigitList(); // fix for JB#5358
+ if (currencyPluralInfo != null) {
+ other.currencyPluralInfo = (CurrencyPluralInfo) currencyPluralInfo.clone();
+ }
+ other.attributes = new ArrayList<FieldPosition>(); // #9240
+ other.currencyUsage = currencyUsage;
+
+ // TODO: We need to figure out whether we share a single copy of DigitList by
+ // multiple cloned copies. format/subformat are designed to use a single
+ // instance, but parse/subparse implementation is not.
+ return other;
+ } catch (Exception e) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Overrides equals.
+ * @stable ICU 2.0
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null)
+ return false;
+ if (!super.equals(obj))
+ return false; // super does class check
+
+ DecimalFormat_ICU58 other = (DecimalFormat_ICU58) obj;
+ // Add the comparison of the four new added fields ,they are posPrefixPattern,
+ // posSuffixPattern, negPrefixPattern, negSuffixPattern. [Richard/GCL]
+ // following are added to accomodate changes for currency plural format.
+ return currencySignCount == other.currencySignCount
+ && (style != NumberFormat.PLURALCURRENCYSTYLE ||
+ equals(posPrefixPattern, other.posPrefixPattern)
+ && equals(posSuffixPattern, other.posSuffixPattern)
+ && equals(negPrefixPattern, other.negPrefixPattern)
+ && equals(negSuffixPattern, other.negSuffixPattern))
+ && multiplier == other.multiplier
+ && groupingSize == other.groupingSize
+ && groupingSize2 == other.groupingSize2
+ && decimalSeparatorAlwaysShown == other.decimalSeparatorAlwaysShown
+ && useExponentialNotation == other.useExponentialNotation
+ && (!useExponentialNotation || minExponentDigits == other.minExponentDigits)
+ && useSignificantDigits == other.useSignificantDigits
+ && (!useSignificantDigits || minSignificantDigits == other.minSignificantDigits
+ && maxSignificantDigits == other.maxSignificantDigits)
+ && symbols.equals(other.symbols)
+ && Utility.objectEquals(currencyPluralInfo, other.currencyPluralInfo)
+ && currencyUsage.equals(other.currencyUsage);
+ }
+
+ // method to unquote the strings and compare
+ private boolean equals(String pat1, String pat2) {
+ if (pat1 == null || pat2 == null) {
+ return (pat1 == null && pat2 == null);
+ }
+ // fast path
+ if (pat1.equals(pat2)) {
+ return true;
+ }
+ return unquote(pat1).equals(unquote(pat2));
+ }
+
+ private String unquote(String pat) {
+ StringBuilder buf = new StringBuilder(pat.length());
+ int i = 0;
+ while (i < pat.length()) {
+ char ch = pat.charAt(i++);
+ if (ch != QUOTE) {
+ buf.append(ch);
+ }
+ }
+ return buf.toString();
+ }
+
+ // protected void handleToString(StringBuffer buf) {
+ // buf.append("\nposPrefixPattern: '" + posPrefixPattern + "'\n");
+ // buf.append("positivePrefix: '" + positivePrefix + "'\n");
+ // buf.append("posSuffixPattern: '" + posSuffixPattern + "'\n");
+ // buf.append("positiveSuffix: '" + positiveSuffix + "'\n");
+ // buf.append("negPrefixPattern: '" +
+ // com.ibm.icu.impl.Utility.format1ForSource(negPrefixPattern) + "'\n");
+ // buf.append("negativePrefix: '" +
+ // com.ibm.icu.impl.Utility.format1ForSource(negativePrefix) + "'\n");
+ // buf.append("negSuffixPattern: '" + negSuffixPattern + "'\n");
+ // buf.append("negativeSuffix: '" + negativeSuffix + "'\n");
+ // buf.append("multiplier: '" + multiplier + "'\n");
+ // buf.append("groupingSize: '" + groupingSize + "'\n");
+ // buf.append("groupingSize2: '" + groupingSize2 + "'\n");
+ // buf.append("decimalSeparatorAlwaysShown: '" + decimalSeparatorAlwaysShown + "'\n");
+ // buf.append("useExponentialNotation: '" + useExponentialNotation + "'\n");
+ // buf.append("minExponentDigits: '" + minExponentDigits + "'\n");
+ // buf.append("useSignificantDigits: '" + useSignificantDigits + "'\n");
+ // buf.append("minSignificantDigits: '" + minSignificantDigits + "'\n");
+ // buf.append("maxSignificantDigits: '" + maxSignificantDigits + "'\n");
+ // buf.append("symbols: '" + symbols + "'");
+ // }
+
+ /**
+ * Overrides hashCode.
+ * @stable ICU 2.0
+ */
+ @Override
+ public int hashCode() {
+ return super.hashCode() * 37 + positivePrefix.hashCode();
+ // just enough fields for a reasonable distribution
+ }
+
+ /**
+ * Synthesizes a pattern string that represents the current state of this Format
+ * object.
+ *
+ * @see #applyPattern
+ * @stable ICU 2.0
+ */
+ public String toPattern() {
+ if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+ // the prefix or suffix pattern might not be defined yet, so they can not be
+ // synthesized, instead, get them directly. but it might not be the actual
+ // pattern used in formatting. the actual pattern used in formatting depends
+ // on the formatted number's plural count.
+ return formatPattern;
+ }
+ return toPattern(false);
+ }
+
+ /**
+ * Synthesizes a localized pattern string that represents the current state of this
+ * Format object.
+ *
+ * @see #applyPattern
+ * @stable ICU 2.0
+ */
+ public String toLocalizedPattern() {
+ if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+ return formatPattern;
+ }
+ return toPattern(true);
+ }
+
+ /**
+ * Expands the affix pattern strings into the expanded affix strings. If any affix
+ * pattern string is null, do not expand it. This method should be called any time the
+ * symbols or the affix patterns change in order to keep the expanded affix strings up
+ * to date. This method also will be called before formatting if format currency
+ * plural names, since the plural name is not a static one, it is based on the
+ * currency plural count, the affix will be known only after the currency plural count
+ * is know. In which case, the parameter 'pluralCount' will be a non-null currency
+ * plural count. In all other cases, the 'pluralCount' is null, which means it is not
+ * needed.
+ */
+ // Bug 4212072 [Richard/GCL]
+ private void expandAffixes(String pluralCount) {
+ // expandAffix() will set currencyChoice to a non-null value if
+ // appropriate AND if it is null.
+ currencyChoice = null;
+
+ // Reuse one StringBuffer for better performance
+ StringBuffer buffer = new StringBuffer();
+ if (posPrefixPattern != null) {
+ expandAffix(posPrefixPattern, pluralCount, buffer);
+ positivePrefix = buffer.toString();
+ }
+ if (posSuffixPattern != null) {
+ expandAffix(posSuffixPattern, pluralCount, buffer);
+ positiveSuffix = buffer.toString();
+ }
+ if (negPrefixPattern != null) {
+ expandAffix(negPrefixPattern, pluralCount, buffer);
+ negativePrefix = buffer.toString();
+ }
+ if (negSuffixPattern != null) {
+ expandAffix(negSuffixPattern, pluralCount, buffer);
+ negativeSuffix = buffer.toString();
+ }
+ }
+
+ /**
+ * Expands an affix pattern into an affix string. All characters in the pattern are
+ * literal unless bracketed by QUOTEs. The following characters outside QUOTE are
+ * recognized: PATTERN_PERCENT, PATTERN_PER_MILLE, PATTERN_MINUS, and
+ * CURRENCY_SIGN. If CURRENCY_SIGN is doubled, it is interpreted as an international
+ * currency sign. If CURRENCY_SIGN is tripled, it is interpreted as currency plural
+ * long names, such as "US Dollars". Any other character outside QUOTE represents
+ * itself. Quoted text must be well-formed.
+ *
+ * This method is used in two distinct ways. First, it is used to expand the stored
+ * affix patterns into actual affixes. For this usage, doFormat must be false. Second,
+ * it is used to expand the stored affix patterns given a specific number (doFormat ==
+ * true), for those rare cases in which a currency format references a ChoiceFormat
+ * (e.g., en_IN display name for INR). The number itself is taken from digitList.
+ * TODO: There are no currency ChoiceFormat patterns, figure out what is still relevant here.
+ *
+ * When used in the first way, this method has a side effect: It sets currencyChoice
+ * to a ChoiceFormat object, if the currency's display name in this locale is a
+ * ChoiceFormat pattern (very rare). It only does this if currencyChoice is null to
+ * start with.
+ *
+ * @param pattern the non-null, possibly empty pattern
+ * @param pluralCount the plural count. It is only used for currency plural format. In
+ * which case, it is the plural count of the currency amount. For example, in en_US,
+ * it is the singular "one", or the plural "other". For all other cases, it is null,
+ * and is not being used.
+ * @param buffer a scratch StringBuffer; its contents will be lost
+ */
+ // Bug 4212072 [Richard/GCL]
+ private void expandAffix(String pattern, String pluralCount, StringBuffer buffer) {
+ buffer.setLength(0);
+ for (int i = 0; i < pattern.length();) {
+ char c = pattern.charAt(i++);
+ if (c == QUOTE) {
+ for (;;) {
+ int j = pattern.indexOf(QUOTE, i);
+ if (j == i) {
+ buffer.append(QUOTE);
+ i = j + 1;
+ break;
+ } else if (j > i) {
+ buffer.append(pattern.substring(i, j));
+ i = j + 1;
+ if (i < pattern.length() && pattern.charAt(i) == QUOTE) {
+ buffer.append(QUOTE);
+ ++i;
+ // loop again
+ } else {
+ break;
+ }
+ } else {
+ // Unterminated quote; should be caught by apply
+ // pattern.
+ throw new RuntimeException();
+ }
+ }
+ continue;
+ }
+
+ switch (c) {
+ case CURRENCY_SIGN:
+ // As of ICU 2.2 we use the currency object, and ignore the currency
+ // symbols in the DFS, unless we have a null currency object. This occurs
+ // if resurrecting a pre-2.2 object or if the user sets a custom DFS.
+ boolean intl = i < pattern.length() && pattern.charAt(i) == CURRENCY_SIGN;
+ boolean plural = false;
+ if (intl) {
+ ++i;
+ if (i < pattern.length() && pattern.charAt(i) == CURRENCY_SIGN) {
+ plural = true;
+ intl = false;
+ ++i;
+ }
+ }
+ String s = null;
+ Currency currency = getCurrency();
+ if (currency != null) {
+ // plural name is only needed when pluralCount != null, which means
+ // when formatting currency plural names. For other cases,
+ // pluralCount == null, and plural names are not needed.
+ if (plural && pluralCount != null) {
+ s = currency.getName(symbols.getULocale(), Currency.PLURAL_LONG_NAME,
+ pluralCount, null);
+ } else if (!intl) {
+ s = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ } else {
+ s = currency.getCurrencyCode();
+ }
+ } else {
+ s = intl ? symbols.getInternationalCurrencySymbol() :
+ symbols.getCurrencySymbol();
+ }
+ // Here is where FieldPosition could be set for CURRENCY PLURAL.
+ buffer.append(s);
+ break;
+ case PATTERN_PERCENT:
+ buffer.append(symbols.getPercentString());
+ break;
+ case PATTERN_PER_MILLE:
+ buffer.append(symbols.getPerMillString());
+ break;
+ case PATTERN_MINUS_SIGN:
+ buffer.append(symbols.getMinusSignString());
+ break;
+ default:
+ buffer.append(c);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Append an affix to the given StringBuffer.
+ *
+ * @param buf
+ * buffer to append to
+ * @param isNegative
+ * @param isPrefix
+ * @param fieldPosition
+ * @param parseAttr
+ */
+ private int appendAffix(StringBuffer buf, boolean isNegative, boolean isPrefix,
+ FieldPosition fieldPosition,
+ boolean parseAttr) {
+ if (currencyChoice != null) {
+ String affixPat = null;
+ if (isPrefix) {
+ affixPat = isNegative ? negPrefixPattern : posPrefixPattern;
+ } else {
+ affixPat = isNegative ? negSuffixPattern : posSuffixPattern;
+ }
+ StringBuffer affixBuf = new StringBuffer();
+ expandAffix(affixPat, null, affixBuf);
+ buf.append(affixBuf);
+ return affixBuf.length();
+ }
+
+ String affix = null;
+ String pattern;
+ if (isPrefix) {
+ affix = isNegative ? negativePrefix : positivePrefix;
+ pattern = isNegative ? negPrefixPattern : posPrefixPattern;
+ } else {
+ affix = isNegative ? negativeSuffix : positiveSuffix;
+ pattern = isNegative ? negSuffixPattern : posSuffixPattern;
+ }
+ // [Spark/CDL] Invoke formatAffix2Attribute to add attributes for affix
+ if (parseAttr) {
+ // Updates for Ticket 11805.
+ int offset = affix.indexOf(symbols.getCurrencySymbol());
+ if (offset > -1) {
+ formatAffix2Attribute(isPrefix, Field.CURRENCY, buf, offset,
+ symbols.getCurrencySymbol().length());
+ }
+ offset = affix.indexOf(symbols.getMinusSignString());
+ if (offset > -1) {
+ formatAffix2Attribute(isPrefix, Field.SIGN, buf, offset,
+ symbols.getMinusSignString().length());
+ }
+ offset = affix.indexOf(symbols.getPercentString());
+ if (offset > -1) {
+ formatAffix2Attribute(isPrefix, Field.PERCENT, buf, offset,
+ symbols.getPercentString().length());
+ }
+ offset = affix.indexOf(symbols.getPerMillString());
+ if (offset > -1) {
+ formatAffix2Attribute(isPrefix, Field.PERMILLE, buf, offset,
+ symbols.getPerMillString().length());
+ }
+ offset = pattern.indexOf("¤¤¤");
+ if (offset > -1) {
+ formatAffix2Attribute(isPrefix, Field.CURRENCY, buf, offset,
+ affix.length() - offset);
+ }
+ }
+
+ // Look for SIGN, PERCENT, PERMILLE in the formatted affix.
+ if (fieldPosition.getFieldAttribute() == NumberFormat.Field.SIGN) {
+ String sign = isNegative ? symbols.getMinusSignString() : symbols.getPlusSignString();
+ int firstPos = affix.indexOf(sign);
+ if (firstPos > -1) {
+ int startPos = buf.length() + firstPos;
+ fieldPosition.setBeginIndex(startPos);
+ fieldPosition.setEndIndex(startPos + sign.length());
+ }
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.PERCENT) {
+ int firstPos = affix.indexOf(symbols.getPercentString());
+ if (firstPos > -1) {
+ int startPos = buf.length() + firstPos;
+ fieldPosition.setBeginIndex(startPos);
+ fieldPosition.setEndIndex(startPos + symbols.getPercentString().length());
+ }
+ } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.PERMILLE) {
+ int firstPos = affix.indexOf(symbols.getPerMillString());
+ if (firstPos > -1) {
+ int startPos = buf.length() + firstPos;
+ fieldPosition.setBeginIndex(startPos);
+ fieldPosition.setEndIndex(startPos + symbols.getPerMillString().length());
+ }
+ } else
+ // If CurrencySymbol or InternationalCurrencySymbol is in the affix, check for currency symbol.
+ // Get spelled out name if "¤¤¤" is in the pattern.
+ if (fieldPosition.getFieldAttribute() == NumberFormat.Field.CURRENCY) {
+ if (affix.indexOf(symbols.getCurrencySymbol()) > -1) {
+ String aff = symbols.getCurrencySymbol();
+ int firstPos = affix.indexOf(aff);
+ int start = buf.length() + firstPos;
+ int end = start + aff.length();
+ fieldPosition.setBeginIndex(start);
+ fieldPosition.setEndIndex(end);
+ } else if (affix.indexOf(symbols.getInternationalCurrencySymbol()) > -1) {
+ String aff = symbols.getInternationalCurrencySymbol();
+ int firstPos = affix.indexOf(aff);
+ int start = buf.length() + firstPos;
+ int end = start + aff.length();
+ fieldPosition.setBeginIndex(start);
+ fieldPosition.setEndIndex(end);
+ } else if (pattern.indexOf("¤¤¤") > -1) {
+ // It's a plural, and we know where it is in the pattern.
+ int firstPos = pattern.indexOf("¤¤¤");
+ int start = buf.length() + firstPos;
+ int end = buf.length() + affix.length(); // This seems clunky and wrong.
+ fieldPosition.setBeginIndex(start);
+ fieldPosition.setEndIndex(end);
+ }
+ }
+
+ buf.append(affix);
+ return affix.length();
+ }
+
+ // Fix for prefix and suffix in Ticket 11805.
+ private void formatAffix2Attribute(boolean isPrefix, Field fieldType,
+ StringBuffer buf, int offset, int symbolSize) {
+ int begin;
+ begin = offset;
+ if (!isPrefix) {
+ begin += buf.length();
+ }
+
+ addAttribute(fieldType, begin, begin + symbolSize);
+ }
+
+ /**
+ * [Spark/CDL] Use this method to add attribute.
+ */
+ private void addAttribute(Field field, int begin, int end) {
+ FieldPosition pos = new FieldPosition(field);
+ pos.setBeginIndex(begin);
+ pos.setEndIndex(end);
+ attributes.add(pos);
+ }
+
+ /**
+ * Formats the object to an attributed string, and return the corresponding iterator.
+ *
+ * @stable ICU 3.6
+ */
+ @Override
+ public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
+ return formatToCharacterIterator(obj, NULL_UNIT);
+ }
+
+ AttributedCharacterIterator formatToCharacterIterator(Object obj, Unit unit) {
+ if (!(obj instanceof Number))
+ throw new IllegalArgumentException();
+ Number number = (Number) obj;
+ StringBuffer text = new StringBuffer();
+ unit.writePrefix(text);
+ attributes.clear();
+ if (obj instanceof BigInteger) {
+ format((BigInteger) number, text, new FieldPosition(0), true);
+ } else if (obj instanceof java.math.BigDecimal) {
+ format((java.math.BigDecimal) number, text, new FieldPosition(0)
+ , true);
+ } else if (obj instanceof Double) {
+ format(number.doubleValue(), text, new FieldPosition(0), true);
+ } else if (obj instanceof Integer || obj instanceof Long) {
+ format(number.longValue(), text, new FieldPosition(0), true);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ unit.writeSuffix(text);
+ AttributedString as = new AttributedString(text.toString());
+
+ // add NumberFormat field attributes to the AttributedString
+ for (int i = 0; i < attributes.size(); i++) {
+ FieldPosition pos = attributes.get(i);
+ Format.Field attribute = pos.getFieldAttribute();
+ as.addAttribute(attribute, attribute, pos.getBeginIndex(), pos.getEndIndex());
+ }
+
+ // return the CharacterIterator from AttributedString
+ return as.getIterator();
+ }
+
+ /**
+ * Appends an affix pattern to the given StringBuffer. Localize unquoted specials.
+ * <p>
+ * <b>Note:</b> This implementation does not support new String localized symbols.
+ */
+ private void appendAffixPattern(StringBuffer buffer, boolean isNegative, boolean isPrefix,
+ boolean localized) {
+ String affixPat = null;
+ if (isPrefix) {
+ affixPat = isNegative ? negPrefixPattern : posPrefixPattern;
+ } else {
+ affixPat = isNegative ? negSuffixPattern : posSuffixPattern;
+ }
+
+ // When there is a null affix pattern, we use the affix itself.
+ if (affixPat == null) {
+ String affix = null;
+ if (isPrefix) {
+ affix = isNegative ? negativePrefix : positivePrefix;
+ } else {
+ affix = isNegative ? negativeSuffix : positiveSuffix;
+ }
+ // Do this crudely for now: Wrap everything in quotes.
+ buffer.append(QUOTE);
+ for (int i = 0; i < affix.length(); ++i) {
+ char ch = affix.charAt(i);
+ if (ch == QUOTE) {
+ buffer.append(ch);
+ }
+ buffer.append(ch);
+ }
+ buffer.append(QUOTE);
+ return;
+ }
+
+ if (!localized) {
+ buffer.append(affixPat);
+ } else {
+ int i, j;
+ for (i = 0; i < affixPat.length(); ++i) {
+ char ch = affixPat.charAt(i);
+ switch (ch) {
+ case QUOTE:
+ j = affixPat.indexOf(QUOTE, i + 1);
+ if (j < 0) {
+ throw new IllegalArgumentException("Malformed affix pattern: " + affixPat);
+ }
+ buffer.append(affixPat.substring(i, j + 1));
+ i = j;
+ continue;
+ case PATTERN_PER_MILLE:
+ ch = symbols.getPerMill();
+ break;
+ case PATTERN_PERCENT:
+ ch = symbols.getPercent();
+ break;
+ case PATTERN_MINUS_SIGN:
+ ch = symbols.getMinusSign();
+ break;
+ }
+ // check if char is same as any other symbol
+ if (ch == symbols.getDecimalSeparator() || ch == symbols.getGroupingSeparator()) {
+ buffer.append(QUOTE);
+ buffer.append(ch);
+ buffer.append(QUOTE);
+ } else {
+ buffer.append(ch);
+ }
+ }
+ }
+ }
+
+ /**
+ * Does the real work of generating a pattern.
+ * <p>
+ * <b>Note:</b> This implementation does not support new String localized symbols.
+ */
+ private String toPattern(boolean localized) {
+ StringBuffer result = new StringBuffer();
+ char zero = localized ? symbols.getZeroDigit() : PATTERN_ZERO_DIGIT;
+ char digit = localized ? symbols.getDigit() : PATTERN_DIGIT;
+ char sigDigit = 0;
+ boolean useSigDig = areSignificantDigitsUsed();
+ if (useSigDig) {
+ sigDigit = localized ? symbols.getSignificantDigit() : PATTERN_SIGNIFICANT_DIGIT;
+ }
+ char group = localized ? symbols.getGroupingSeparator() : PATTERN_GROUPING_SEPARATOR;
+ int i;
+ int roundingDecimalPos = 0; // Pos of decimal in roundingDigits
+ String roundingDigits = null;
+ int padPos = (formatWidth > 0) ? padPosition : -1;
+ String padSpec = (formatWidth > 0)
+ ? new StringBuffer(2).append(localized
+ ? symbols.getPadEscape()
+ : PATTERN_PAD_ESCAPE).append(pad).toString()
+ : null;
+ if (roundingIncrementICU != null) {
+ i = roundingIncrementICU.scale();
+ roundingDigits = roundingIncrementICU.movePointRight(i).toString();
+ roundingDecimalPos = roundingDigits.length() - i;
+ }
+ for (int part = 0; part < 2; ++part) {
+ // variable not used int partStart = result.length();
+ if (padPos == PAD_BEFORE_PREFIX) {
+ result.append(padSpec);
+ }
+
+ // Use original symbols read from resources in pattern eg. use "\u00A4"
+ // instead of "$" in Locale.US [Richard/GCL]
+ appendAffixPattern(result, part != 0, true, localized);
+ if (padPos == PAD_AFTER_PREFIX) {
+ result.append(padSpec);
+ }
+ int sub0Start = result.length();
+ int g = isGroupingUsed() ? Math.max(0, groupingSize) : 0;
+ if (g > 0 && groupingSize2 > 0 && groupingSize2 != groupingSize) {
+ g += groupingSize2;
+ }
+ int maxDig = 0, minDig = 0, maxSigDig = 0;
+ if (useSigDig) {
+ minDig = getMinimumSignificantDigits();
+ maxDig = maxSigDig = getMaximumSignificantDigits();
+ } else {
+ minDig = getMinimumIntegerDigits();
+ maxDig = getMaximumIntegerDigits();
+ }
+ if (useExponentialNotation) {
+ if (maxDig > MAX_SCIENTIFIC_INTEGER_DIGITS) {
+ maxDig = 1;
+ }
+ } else if (useSigDig) {
+ maxDig = Math.max(maxDig, g + 1);
+ } else {
+ maxDig = Math.max(Math.max(g, getMinimumIntegerDigits()), roundingDecimalPos) + 1;
+ }
+ for (i = maxDig; i > 0; --i) {
+ if (!useExponentialNotation && i < maxDig && isGroupingPosition(i)) {
+ result.append(group);
+ }
+ if (useSigDig) {
+ // #@,@### (maxSigDig == 5, minSigDig == 2) 65 4321 (1-based pos,
+ // count from the right) Use # if pos > maxSigDig or 1 <= pos <=
+ // (maxSigDig - minSigDig) Use @ if (maxSigDig - minSigDig) < pos <=
+ // maxSigDig
+ result.append((maxSigDig >= i && i > (maxSigDig - minDig)) ? sigDigit : digit);
+ } else {
+ if (roundingDigits != null) {
+ int pos = roundingDecimalPos - i;
+ if (pos >= 0 && pos < roundingDigits.length()) {
+ result.append((char) (roundingDigits.charAt(pos) - '0' + zero));
+ continue;
+ }
+ }
+ result.append(i <= minDig ? zero : digit);
+ }
+ }
+ if (!useSigDig) {
+ if (getMaximumFractionDigits() > 0 || decimalSeparatorAlwaysShown) {
+ result.append(localized ? symbols.getDecimalSeparator() :
+ PATTERN_DECIMAL_SEPARATOR);
+ }
+ int pos = roundingDecimalPos;
+ for (i = 0; i < getMaximumFractionDigits(); ++i) {
+ if (roundingDigits != null && pos < roundingDigits.length()) {
+ result.append(pos < 0 ? zero :
+ (char) (roundingDigits.charAt(pos) - '0' + zero));
+ ++pos;
+ continue;
+ }
+ result.append(i < getMinimumFractionDigits() ? zero : digit);
+ }
+ }
+ if (useExponentialNotation) {
+ if (localized) {
+ result.append(symbols.getExponentSeparator());
+ } else {
+ result.append(PATTERN_EXPONENT);
+ }
+ if (exponentSignAlwaysShown) {
+ result.append(localized ? symbols.getPlusSign() : PATTERN_PLUS_SIGN);
+ }
+ for (i = 0; i < minExponentDigits; ++i) {
+ result.append(zero);
+ }
+ }
+ if (padSpec != null && !useExponentialNotation) {
+ int add = formatWidth
+ - result.length()
+ + sub0Start
+ - ((part == 0)
+ ? positivePrefix.length() + positiveSuffix.length()
+ : negativePrefix.length() + negativeSuffix.length());
+ while (add > 0) {
+ result.insert(sub0Start, digit);
+ ++maxDig;
+ --add;
+ // Only add a grouping separator if we have at least 2 additional
+ // characters to be added, so we don't end up with ",###".
+ if (add > 1 && isGroupingPosition(maxDig)) {
+ result.insert(sub0Start, group);
+ --add;
+ }
+ }
+ }
+ if (padPos == PAD_BEFORE_SUFFIX) {
+ result.append(padSpec);
+ }
+ // Use original symbols read from resources in pattern eg. use "\u00A4"
+ // instead of "$" in Locale.US [Richard/GCL]
+ appendAffixPattern(result, part != 0, false, localized);
+ if (padPos == PAD_AFTER_SUFFIX) {
+ result.append(padSpec);
+ }
+ if (part == 0) {
+ if (negativeSuffix.equals(positiveSuffix) &&
+ negativePrefix.equals(PATTERN_MINUS_SIGN + positivePrefix)) {
+ break;
+ } else {
+ result.append(localized ? symbols.getPatternSeparator() : PATTERN_SEPARATOR);
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Applies the given pattern to this Format object. A pattern is a short-hand
+ * specification for the various formatting properties. These properties can also be
+ * changed individually through the various setter methods.
+ *
+ * <p>There is no limit to integer digits are set by this routine, since that is the
+ * typical end-user desire; use setMaximumInteger if you want to set a real value. For
+ * negative numbers, use a second pattern, separated by a semicolon
+ *
+ * <p>Example "#,#00.0#" -> 1,234.56
+ *
+ * <p>This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2
+ * fraction digits.
+ *
+ * <p>Example: "#,#00.0#;(#,#00.0#)" for negatives in parentheses.
+ *
+ * <p>In negative patterns, the minimum and maximum counts are ignored; these are
+ * presumed to be set in the positive pattern.
+ *
+ * @stable ICU 2.0
+ */
+ public void applyPattern(String pattern) {
+ applyPattern(pattern, false);
+ }
+
+ /**
+ * Applies the given pattern to this Format object. The pattern is assumed to be in a
+ * localized notation. A pattern is a short-hand specification for the various
+ * formatting properties. These properties can also be changed individually through
+ * the various setter methods.
+ *
+ * <p>There is no limit to integer digits are set by this routine, since that is the
+ * typical end-user desire; use setMaximumInteger if you want to set a real value. For
+ * negative numbers, use a second pattern, separated by a semicolon
+ *
+ * <p>Example "#,#00.0#" -> 1,234.56
+ *
+ * <p>This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2
+ * fraction digits.
+ *
+ * <p>Example: "#,#00.0#;(#,#00.0#)" for negatives in parantheses.
+ *
+ * <p>In negative patterns, the minimum and maximum counts are ignored; these are
+ * presumed to be set in the positive pattern.
+ *
+ * @stable ICU 2.0
+ */
+ public void applyLocalizedPattern(String pattern) {
+ applyPattern(pattern, true);
+ }
+
+ /**
+ * Does the real work of applying a pattern.
+ */
+ private void applyPattern(String pattern, boolean localized) {
+ applyPatternWithoutExpandAffix(pattern, localized);
+ expandAffixAdjustWidth(null);
+ }
+
+ private void expandAffixAdjustWidth(String pluralCount) {
+ // Bug 4212072 Update the affix strings according to symbols in order to keep the
+ // affix strings up to date. [Richard/GCL]
+ expandAffixes(pluralCount);
+
+ // Now that we have the actual prefix and suffix, fix up formatWidth
+ if (formatWidth > 0) {
+ formatWidth += positivePrefix.length() + positiveSuffix.length();
+ }
+ }
+
+ private void applyPatternWithoutExpandAffix(String pattern, boolean localized) {
+ char zeroDigit = PATTERN_ZERO_DIGIT; // '0'
+ char sigDigit = PATTERN_SIGNIFICANT_DIGIT; // '@'
+ char groupingSeparator = PATTERN_GROUPING_SEPARATOR;
+ char decimalSeparator = PATTERN_DECIMAL_SEPARATOR;
+ char percent = PATTERN_PERCENT;
+ char perMill = PATTERN_PER_MILLE;
+ char digit = PATTERN_DIGIT; // '#'
+ char separator = PATTERN_SEPARATOR;
+ String exponent = String.valueOf(PATTERN_EXPONENT);
+ char plus = PATTERN_PLUS_SIGN;
+ char padEscape = PATTERN_PAD_ESCAPE;
+ char minus = PATTERN_MINUS_SIGN; // Bug 4212072 [Richard/GCL]
+ if (localized) {
+ zeroDigit = symbols.getZeroDigit();
+ sigDigit = symbols.getSignificantDigit();
+ groupingSeparator = symbols.getGroupingSeparator();
+ decimalSeparator = symbols.getDecimalSeparator();
+ percent = symbols.getPercent();
+ perMill = symbols.getPerMill();
+ digit = symbols.getDigit();
+ separator = symbols.getPatternSeparator();
+ exponent = symbols.getExponentSeparator();
+ plus = symbols.getPlusSign();
+ padEscape = symbols.getPadEscape();
+ minus = symbols.getMinusSign(); // Bug 4212072 [Richard/GCL]
+ }
+ char nineDigit = (char) (zeroDigit + 9);
+
+ boolean gotNegative = false;
+
+ int pos = 0;
+ // Part 0 is the positive pattern. Part 1, if present, is the negative
+ // pattern.
+ for (int part = 0; part < 2 && pos < pattern.length(); ++part) {
+ // The subpart ranges from 0 to 4: 0=pattern proper, 1=prefix, 2=suffix,
+ // 3=prefix in quote, 4=suffix in quote. Subpart 0 is between the prefix and
+ // suffix, and consists of pattern characters. In the prefix and suffix,
+ // percent, permille, and currency symbols are recognized and translated.
+ int subpart = 1, sub0Start = 0, sub0Limit = 0, sub2Limit = 0;
+
+ // It's important that we don't change any fields of this object
+ // prematurely. We set the following variables for the multiplier, grouping,
+ // etc., and then only change the actual object fields if everything parses
+ // correctly. This also lets us register the data from part 0 and ignore the
+ // part 1, except for the prefix and suffix.
+ StringBuilder prefix = new StringBuilder();
+ StringBuilder suffix = new StringBuilder();
+ int decimalPos = -1;
+ int multpl = 1;
+ int digitLeftCount = 0, zeroDigitCount = 0, digitRightCount = 0, sigDigitCount = 0;
+ byte groupingCount = -1;
+ byte groupingCount2 = -1;
+ int padPos = -1;
+ char padChar = 0;
+ int incrementPos = -1;
+ long incrementVal = 0;
+ byte expDigits = -1;
+ boolean expSignAlways = false;
+ int currencySignCnt = 0;
+
+ // The affix is either the prefix or the suffix.
+ StringBuilder affix = prefix;
+
+ int start = pos;
+
+ PARTLOOP: for (; pos < pattern.length(); ++pos) {
+ char ch = pattern.charAt(pos);
+ switch (subpart) {
+ case 0: // Pattern proper subpart (between prefix & suffix)
+ // Process the digits, decimal, and grouping characters. We record
+ // five pieces of information. We expect the digits to occur in the
+ // pattern ####00.00####, and we record the number of left digits,
+ // zero (central) digits, and right digits. The position of the last
+ // grouping character is recorded (should be somewhere within the
+ // first two blocks of characters), as is the position of the decimal
+ // point, if any (should be in the zero digits). If there is no
+ // decimal point, then there should be no right digits.
+ if (ch == digit) {
+ if (zeroDigitCount > 0 || sigDigitCount > 0) {
+ ++digitRightCount;
+ } else {
+ ++digitLeftCount;
+ }
+ if (groupingCount >= 0 && decimalPos < 0) {
+ ++groupingCount;
+ }
+ } else if ((ch >= zeroDigit && ch <= nineDigit) || ch == sigDigit) {
+ if (digitRightCount > 0) {
+ patternError("Unexpected '" + ch + '\'', pattern);
+ }
+ if (ch == sigDigit) {
+ ++sigDigitCount;
+ } else {
+ ++zeroDigitCount;
+ if (ch != zeroDigit) {
+ int p = digitLeftCount + zeroDigitCount + digitRightCount;
+ if (incrementPos >= 0) {
+ while (incrementPos < p) {
+ incrementVal *= 10;
+ ++incrementPos;
+ }
+ } else {
+ incrementPos = p;
+ }
+ incrementVal += ch - zeroDigit;
+ }
+ }
+ if (groupingCount >= 0 && decimalPos < 0) {
+ ++groupingCount;
+ }
+ } else if (ch == groupingSeparator) {
+ // Bug 4212072 process the Localized pattern like
+ // "'Fr. '#'##0.05;'Fr.-'#'##0.05" (Locale="CH", groupingSeparator
+ // == QUOTE) [Richard/GCL]
+ if (ch == QUOTE && (pos + 1) < pattern.length()) {
+ char after = pattern.charAt(pos + 1);
+ if (!(after == digit || (after >= zeroDigit && after <= nineDigit))) {
+ // A quote outside quotes indicates either the opening
+ // quote or two quotes, which is a quote literal. That is,
+ // we have the first quote in 'do' or o''clock.
+ if (after == QUOTE) {
+ ++pos;
+ // Fall through to append(ch)
+ } else {
+ if (groupingCount < 0) {
+ subpart = 3; // quoted prefix subpart
+ } else {
+ // Transition to suffix subpart
+ subpart = 2; // suffix subpart
+ affix = suffix;
+ sub0Limit = pos--;
+ }
+ continue;
+ }
+ }
+ }
+
+ if (decimalPos >= 0) {
+ patternError("Grouping separator after decimal", pattern);
+ }
+ groupingCount2 = groupingCount;
+ groupingCount = 0;
+ } else if (ch == decimalSeparator) {
+ if (decimalPos >= 0) {
+ patternError("Multiple decimal separators", pattern);
+ }
+ // Intentionally incorporate the digitRightCount, even though it
+ // is illegal for this to be > 0 at this point. We check pattern
+ // syntax below.
+ decimalPos = digitLeftCount + zeroDigitCount + digitRightCount;
+ } else {
+ if (pattern.regionMatches(pos, exponent, 0, exponent.length())) {
+ if (expDigits >= 0) {
+ patternError("Multiple exponential symbols", pattern);
+ }
+ if (groupingCount >= 0) {
+ patternError("Grouping separator in exponential", pattern);
+ }
+ pos += exponent.length();
+ // Check for positive prefix
+ if (pos < pattern.length() && pattern.charAt(pos) == plus) {
+ expSignAlways = true;
+ ++pos;
+ }
+ // Use lookahead to parse out the exponential part of the
+ // pattern, then jump into suffix subpart.
+ expDigits = 0;
+ while (pos < pattern.length() && pattern.charAt(pos) == zeroDigit) {
+ ++expDigits;
+ ++pos;
+ }
+
+ // 1. Require at least one mantissa pattern digit
+ // 2. Disallow "#+ @" in mantissa
+ // 3. Require at least one exponent pattern digit
+ if (((digitLeftCount + zeroDigitCount) < 1 &&
+ (sigDigitCount + digitRightCount) < 1)
+ || (sigDigitCount > 0 && digitLeftCount > 0) || expDigits < 1) {
+ patternError("Malformed exponential", pattern);
+ }
+ }
+ // Transition to suffix subpart
+ subpart = 2; // suffix subpart
+ affix = suffix;
+ sub0Limit = pos--; // backup: for() will increment
+ continue;
+ }
+ break;
+ case 1: // Prefix subpart
+ case 2: // Suffix subpart
+ // Process the prefix / suffix characters Process unquoted characters
+ // seen in prefix or suffix subpart.
+
+ // Several syntax characters implicitly begins the next subpart if we
+ // are in the prefix; otherwise they are illegal if unquoted.
+ if (ch == digit || ch == groupingSeparator || ch == decimalSeparator
+ || (ch >= zeroDigit && ch <= nineDigit) || ch == sigDigit) {
+ // Any of these characters implicitly begins the
+ // next subpart if we are in the prefix
+ if (subpart == 1) { // prefix subpart
+ subpart = 0; // pattern proper subpart
+ sub0Start = pos--; // Reprocess this character
+ continue;
+ } else if (ch == QUOTE) {
+ // Bug 4212072 process the Localized pattern like
+ // "'Fr. '#'##0.05;'Fr.-'#'##0.05" (Locale="CH",
+ // groupingSeparator == QUOTE) [Richard/GCL]
+
+ // A quote outside quotes indicates either the opening quote
+ // or two quotes, which is a quote literal. That is, we have
+ // the first quote in 'do' or o''clock.
+ if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
+ ++pos;
+ affix.append(ch);
+ } else {
+ subpart += 2; // open quote
+ }
+ continue;
+ }
+ patternError("Unquoted special character '" + ch + '\'', pattern);
+ } else if (ch == CURRENCY_SIGN) {
+ // Use lookahead to determine if the currency sign is
+ // doubled or not.
+ boolean doubled = (pos + 1) < pattern.length() &&
+ pattern.charAt(pos + 1) == CURRENCY_SIGN;
+
+ // Bug 4212072 To meet the need of expandAffix(String,
+ // StirngBuffer) [Richard/GCL]
+ if (doubled) {
+ ++pos; // Skip over the doubled character
+ affix.append(ch); // append two: one here, one below
+ if ((pos + 1) < pattern.length() &&
+ pattern.charAt(pos + 1) == CURRENCY_SIGN) {
+ ++pos; // Skip over the tripled character
+ affix.append(ch); // append again
+ currencySignCnt = CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT;
+ } else {
+ currencySignCnt = CURRENCY_SIGN_COUNT_IN_ISO_FORMAT;
+ }
+ } else {
+ currencySignCnt = CURRENCY_SIGN_COUNT_IN_SYMBOL_FORMAT;
+ }
+ // Fall through to append(ch)
+ } else if (ch == QUOTE) {
+ // A quote outside quotes indicates either the opening quote or
+ // two quotes, which is a quote literal. That is, we have the
+ // first quote in 'do' or o''clock.
+ if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
+ ++pos;
+ affix.append(ch); // append two: one here, one below
+ } else {
+ subpart += 2; // open quote
+ }
+ // Fall through to append(ch)
+ } else if (ch == separator) {
+ // Don't allow separators in the prefix, and don't allow
+ // separators in the second pattern (part == 1).
+ if (subpart == 1 || part == 1) {
+ patternError("Unquoted special character '" + ch + '\'', pattern);
+ }
+ sub2Limit = pos++;
+ break PARTLOOP; // Go to next part
+ } else if (ch == percent || ch == perMill) {
+ // Next handle characters which are appended directly.
+ if (multpl != 1) {
+ patternError("Too many percent/permille characters", pattern);
+ }
+ multpl = (ch == percent) ? 100 : 1000;
+ // Convert to non-localized pattern
+ ch = (ch == percent) ? PATTERN_PERCENT : PATTERN_PER_MILLE;
+ // Fall through to append(ch)
+ } else if (ch == minus) {
+ // Convert to non-localized pattern
+ ch = PATTERN_MINUS_SIGN;
+ // Fall through to append(ch)
+ } else if (ch == padEscape) {
+ if (padPos >= 0) {
+ patternError("Multiple pad specifiers", pattern);
+ }
+ if ((pos + 1) == pattern.length()) {
+ patternError("Invalid pad specifier", pattern);
+ }
+ padPos = pos++; // Advance past pad char
+ padChar = pattern.charAt(pos);
+ continue;
+ }
+ affix.append(ch);
+ break;
+ case 3: // Prefix subpart, in quote
+ case 4: // Suffix subpart, in quote
+ // A quote within quotes indicates either the closing quote or two
+ // quotes, which is a quote literal. That is, we have the second quote
+ // in 'do' or 'don''t'.
+ if (ch == QUOTE) {
+ if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
+ ++pos;
+ affix.append(ch);
+ } else {
+ subpart -= 2; // close quote
+ }
+ // Fall through to append(ch)
+ }
+ // NOTE: In ICU 2.2 there was code here to parse quoted percent and
+ // permille characters _within quotes_ and give them special
+ // meaning. This is incorrect, since quoted characters are literals
+ // without special meaning.
+ affix.append(ch);
+ break;
+ }
+ }
+
+ if (subpart == 3 || subpart == 4) {
+ patternError("Unterminated quote", pattern);
+ }
+
+ if (sub0Limit == 0) {
+ sub0Limit = pattern.length();
+ }
+
+ if (sub2Limit == 0) {
+ sub2Limit = pattern.length();
+ }
+
+ // Handle patterns with no '0' pattern character. These patterns are legal,
+ // but must be recodified to make sense. "##.###" -> "#0.###". ".###" ->
+ // ".0##".
+ //
+ // We allow patterns of the form "####" to produce a zeroDigitCount of zero
+ // (got that?); although this seems like it might make it possible for
+ // format() to produce empty strings, format() checks for this condition and
+ // outputs a zero digit in this situation. Having a zeroDigitCount of zero
+ // yields a minimum integer digits of zero, which allows proper round-trip
+ // patterns. We don't want "#" to become "#0" when toPattern() is called (even
+ // though that's what it really is, semantically).
+ if (zeroDigitCount == 0 && sigDigitCount == 0 &&
+ digitLeftCount > 0 && decimalPos >= 0) {
+ // Handle "###.###" and "###." and ".###"
+ int n = decimalPos;
+ if (n == 0)
+ ++n; // Handle ".###"
+ digitRightCount = digitLeftCount - n;
+ digitLeftCount = n - 1;
+ zeroDigitCount = 1;
+ }
+
+ // Do syntax checking on the digits, decimal points, and quotes.
+ if ((decimalPos < 0 && digitRightCount > 0 && sigDigitCount == 0)
+ || (decimalPos >= 0
+ && (sigDigitCount > 0
+ || decimalPos < digitLeftCount
+ || decimalPos > (digitLeftCount + zeroDigitCount)))
+ || groupingCount == 0
+ || groupingCount2 == 0
+ || (sigDigitCount > 0 && zeroDigitCount > 0)
+ || subpart > 2) { // subpart > 2 == unmatched quote
+ patternError("Malformed pattern", pattern);
+ }
+
+ // Make sure pad is at legal position before or after affix.
+ if (padPos >= 0) {
+ if (padPos == start) {
+ padPos = PAD_BEFORE_PREFIX;
+ } else if (padPos + 2 == sub0Start) {
+ padPos = PAD_AFTER_PREFIX;
+ } else if (padPos == sub0Limit) {
+ padPos = PAD_BEFORE_SUFFIX;
+ } else if (padPos + 2 == sub2Limit) {
+ padPos = PAD_AFTER_SUFFIX;
+ } else {
+ patternError("Illegal pad position", pattern);
+ }
+ }
+
+ if (part == 0) {
+ // Set negative affixes temporarily to match the positive
+ // affixes. Fix this up later after processing both parts.
+
+ // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer)
+ // [Richard/GCL]
+ posPrefixPattern = negPrefixPattern = prefix.toString();
+ posSuffixPattern = negSuffixPattern = suffix.toString();
+
+ useExponentialNotation = (expDigits >= 0);
+ if (useExponentialNotation) {
+ minExponentDigits = expDigits;
+ exponentSignAlwaysShown = expSignAlways;
+ }
+ int digitTotalCount = digitLeftCount + zeroDigitCount + digitRightCount;
+ // The effectiveDecimalPos is the position the decimal is at or would be
+ // at if there is no decimal. Note that if decimalPos<0, then
+ // digitTotalCount == digitLeftCount + zeroDigitCount.
+ int effectiveDecimalPos = decimalPos >= 0 ? decimalPos : digitTotalCount;
+ boolean useSigDig = (sigDigitCount > 0);
+ setSignificantDigitsUsed(useSigDig);
+ if (useSigDig) {
+ setMinimumSignificantDigits(sigDigitCount);
+ setMaximumSignificantDigits(sigDigitCount + digitRightCount);
+ } else {
+ int minInt = effectiveDecimalPos - digitLeftCount;
+ setMinimumIntegerDigits(minInt);
+
+ // Upper limit on integer and fraction digits for a Java double
+ // [Richard/GCL]
+ setMaximumIntegerDigits(useExponentialNotation ? digitLeftCount + minInt :
+ DOUBLE_INTEGER_DIGITS);
+ _setMaximumFractionDigits(decimalPos >= 0 ?
+ (digitTotalCount - decimalPos) : 0);
+ setMinimumFractionDigits(decimalPos >= 0 ?
+ (digitLeftCount + zeroDigitCount - decimalPos) : 0);
+ }
+ setGroupingUsed(groupingCount > 0);
+ this.groupingSize = (groupingCount > 0) ? groupingCount : 0;
+ this.groupingSize2 = (groupingCount2 > 0 && groupingCount2 != groupingCount)
+ ? groupingCount2 : 0;
+ this.multiplier = multpl;
+ setDecimalSeparatorAlwaysShown(decimalPos == 0 || decimalPos == digitTotalCount);
+ if (padPos >= 0) {
+ padPosition = padPos;
+ formatWidth = sub0Limit - sub0Start; // to be fixed up below
+ pad = padChar;
+ } else {
+ formatWidth = 0;
+ }
+ if (incrementVal != 0) {
+ // BigDecimal scale cannot be negative (even though this makes perfect
+ // sense), so we need to handle this.
+ int scale = incrementPos - effectiveDecimalPos;
+ roundingIncrementICU = BigDecimal.valueOf(incrementVal, scale > 0 ? scale : 0);
+ if (scale < 0) {
+ roundingIncrementICU = roundingIncrementICU.movePointRight(-scale);
+ }
+ roundingMode = BigDecimal.ROUND_HALF_EVEN;
+ } else {
+ setRoundingIncrement((BigDecimal) null);
+ }
+
+ // Update currency sign count for the new pattern
+ currencySignCount = currencySignCnt;
+ } else {
+ // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer)
+ // [Richard/GCL]
+ negPrefixPattern = prefix.toString();
+ negSuffixPattern = suffix.toString();
+ gotNegative = true;
+ }
+ }
+
+
+ // Bug 4140009 Process the empty pattern [Richard/GCL]
+ if (pattern.length() == 0) {
+ posPrefixPattern = posSuffixPattern = "";
+ setMinimumIntegerDigits(0);
+ setMaximumIntegerDigits(DOUBLE_INTEGER_DIGITS);
+ setMinimumFractionDigits(0);
+ _setMaximumFractionDigits(DOUBLE_FRACTION_DIGITS);
+ }
+
+ // If there was no negative pattern, or if the negative pattern is identical to
+ // the positive pattern, then prepend the minus sign to the positive pattern to
+ // form the negative pattern.
+
+ // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer) [Richard/GCL]
+
+ if (!gotNegative ||
+ (negPrefixPattern.equals(posPrefixPattern)
+ && negSuffixPattern.equals(posSuffixPattern))) {
+ negSuffixPattern = posSuffixPattern;
+ negPrefixPattern = PATTERN_MINUS_SIGN + posPrefixPattern;
+ }
+ setLocale(null, null);
+ // save the pattern
+ formatPattern = pattern;
+
+ // special handlings for currency instance
+ if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
+ // reset rounding increment and max/min fractional digits
+ // by the currency
+ Currency theCurrency = getCurrency();
+ if (theCurrency != null) {
+ setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
+ int d = theCurrency.getDefaultFractionDigits(currencyUsage);
+ setMinimumFractionDigits(d);
+ _setMaximumFractionDigits(d);
+ }
+
+ // initialize currencyPluralInfo if needed
+ if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT
+ && currencyPluralInfo == null) {
+ currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
+ }
+ }
+ resetActualRounding();
+ }
+
+
+ private void patternError(String msg, String pattern) {
+ throw new IllegalArgumentException(msg + " in pattern \"" + pattern + '"');
+ }
+
+
+ // Rewrite the following 4 "set" methods Upper limit on integer and fraction digits
+ // for a Java double [Richard/GCL]
+
+ /**
+ * Sets the maximum number of digits allowed in the integer portion of a number. This
+ * override limits the integer digit count to 309.
+ *
+ * @see NumberFormat#setMaximumIntegerDigits
+ * @stable ICU 2.0
+ */
+ @Override
+ public void setMaximumIntegerDigits(int newValue) {
+ super.setMaximumIntegerDigits(Math.min(newValue, DOUBLE_INTEGER_DIGITS));
+ }
+
+ /**
+ * Sets the minimum number of digits allowed in the integer portion of a number. This
+ * override limits the integer digit count to 309.
+ *
+ * @see NumberFormat#setMinimumIntegerDigits
+ * @stable ICU 2.0
+ */
+ @Override
+ public void setMinimumIntegerDigits(int newValue) {
+ super.setMinimumIntegerDigits(Math.min(newValue, DOUBLE_INTEGER_DIGITS));
+ }
+
+ /**
+ * {@icu} Returns the minimum number of significant digits that will be
+ * displayed. This value has no effect unless {@link #areSignificantDigitsUsed()}
+ * returns true.
+ *
+ * @return the fewest significant digits that will be shown
+ * @stable ICU 3.0
+ */
+ public int getMinimumSignificantDigits() {
+ return minSignificantDigits;
+ }
+
+ /**
+ * {@icu} Returns the maximum number of significant digits that will be
+ * displayed. This value has no effect unless {@link #areSignificantDigitsUsed()}
+ * returns true.
+ *
+ * @return the most significant digits that will be shown
+ * @stable ICU 3.0
+ */
+ public int getMaximumSignificantDigits() {
+ return maxSignificantDigits;
+ }
+
+ /**
+ * {@icu} Sets the minimum number of significant digits that will be displayed. If
+ * <code>min</code> is less than one then it is set to one. If the maximum significant
+ * digits count is less than <code>min</code>, then it is set to <code>min</code>.
+ * This function also enables the use of significant digits by this formatter -
+ * {@link #areSignificantDigitsUsed()} will return true.
+ *
+ * @param min the fewest significant digits to be shown
+ * @stable ICU 3.0
+ */
+ public void setMinimumSignificantDigits(int min) {
+ if (min < 1) {
+ min = 1;
+ }
+ // pin max sig dig to >= min
+ int max = Math.max(maxSignificantDigits, min);
+ minSignificantDigits = min;
+ maxSignificantDigits = max;
+ setSignificantDigitsUsed(true);
+ }
+
+ /**
+ * {@icu} Sets the maximum number of significant digits that will be displayed. If
+ * <code>max</code> is less than one then it is set to one. If the minimum significant
+ * digits count is greater than <code>max</code>, then it is set to <code>max</code>.
+ * This function also enables the use of significant digits by this formatter -
+ * {@link #areSignificantDigitsUsed()} will return true.
+ *
+ * @param max the most significant digits to be shown
+ * @stable ICU 3.0
+ */
+ public void setMaximumSignificantDigits(int max) {
+ if (max < 1) {
+ max = 1;
+ }
+ // pin min sig dig to 1..max
+ int min = Math.min(minSignificantDigits, max);
+ minSignificantDigits = min;
+ maxSignificantDigits = max;
+ setSignificantDigitsUsed(true);
+ }
+
+ /**
+ * {@icu} Returns true if significant digits are in use or false if integer and
+ * fraction digit counts are in use.
+ *
+ * @return true if significant digits are in use
+ * @stable ICU 3.0
+ */
+ public boolean areSignificantDigitsUsed() {
+ return useSignificantDigits;
+ }
+
+ /**
+ * {@icu} Sets whether significant digits are in use, or integer and fraction digit
+ * counts are in use.
+ *
+ * @param useSignificantDigits true to use significant digits, or false to use integer
+ * and fraction digit counts
+ * @stable ICU 3.0
+ */
+ public void setSignificantDigitsUsed(boolean useSignificantDigits) {
+ this.useSignificantDigits = useSignificantDigits;
+ }
+
+ /**
+ * Sets the <tt>Currency</tt> object used to display currency amounts. This takes
+ * effect immediately, if this format is a currency format. If this format is not a
+ * currency format, then the currency object is used if and when this object becomes a
+ * currency format through the application of a new pattern.
+ *
+ * @param theCurrency new currency object to use. Must not be null.
+ * @stable ICU 2.2
+ */
+ @Override
+ public void setCurrency(Currency theCurrency) {
+ // If we are a currency format, then modify our affixes to
+ // encode the currency symbol for the given currency in our
+ // locale, and adjust the decimal digits and rounding for the
+ // given currency.
+
+ super.setCurrency(theCurrency);
+ if (theCurrency != null) {
+ String s = theCurrency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+ symbols.setCurrency(theCurrency);
+ symbols.setCurrencySymbol(s);
+ }
+
+ if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
+ if (theCurrency != null) {
+ setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
+ int d = theCurrency.getDefaultFractionDigits(currencyUsage);
+ setMinimumFractionDigits(d);
+ setMaximumFractionDigits(d);
+ }
+ if (currencySignCount != CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+ // This is not necessary for plural format type
+ // because affixes will be resolved in subformat
+ expandAffixes(null);
+ }
+ }
+ }
+
+ /**
+ * Sets the <tt>Currency Usage</tt> object used to display currency.
+ * This takes effect immediately, if this format is a
+ * currency format.
+ * @param newUsage new currency context object to use.
+ * @stable ICU 54
+ */
+ public void setCurrencyUsage(CurrencyUsage newUsage) {
+ if (newUsage == null) {
+ throw new NullPointerException("return value is null at method AAA");
+ }
+ currencyUsage = newUsage;
+ Currency theCurrency = this.getCurrency();
+
+ // We set rounding/digit based on currency context
+ if (theCurrency != null) {
+ setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
+ int d = theCurrency.getDefaultFractionDigits(currencyUsage);
+ setMinimumFractionDigits(d);
+ _setMaximumFractionDigits(d);
+ }
+ }
+
+ /**
+ * Returns the <tt>Currency Usage</tt> object used to display currency
+ * @stable ICU 54
+ */
+ public CurrencyUsage getCurrencyUsage() {
+ return currencyUsage;
+ }
+
+ /**
+ * Returns the currency in effect for this formatter. Subclasses should override this
+ * method as needed. Unlike getCurrency(), this method should never return null.
+ *
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ @Override
+ protected Currency getEffectiveCurrency() {
+ Currency c = getCurrency();
+ if (c == null) {
+ c = Currency.getInstance(symbols.getInternationalCurrencySymbol());
+ }
+ return c;
+ }
+
+ /**
+ * Sets the maximum number of digits allowed in the fraction portion of a number. This
+ * override limits the fraction digit count to 340.
+ *
+ * @see NumberFormat#setMaximumFractionDigits
+ * @stable ICU 2.0
+ */
+ @Override
+ public void setMaximumFractionDigits(int newValue) {
+ _setMaximumFractionDigits(newValue);
+ resetActualRounding();
+ }
+
+ /*
+ * Internal method for DecimalFormat, setting maximum fractional digits
+ * without triggering actual rounding recalculated.
+ */
+ private void _setMaximumFractionDigits(int newValue) {
+ super.setMaximumFractionDigits(Math.min(newValue, DOUBLE_FRACTION_DIGITS));
+ }
+
+ /**
+ * Sets the minimum number of digits allowed in the fraction portion of a number. This
+ * override limits the fraction digit count to 340.
+ *
+ * @see NumberFormat#setMinimumFractionDigits
+ * @stable ICU 2.0
+ */
+ @Override
+ public void setMinimumFractionDigits(int newValue) {
+ super.setMinimumFractionDigits(Math.min(newValue, DOUBLE_FRACTION_DIGITS));
+ }
+
+ /**
+ * Sets whether {@link #parse(String, ParsePosition)} returns BigDecimal. The
+ * default value is false.
+ *
+ * @param value true if {@link #parse(String, ParsePosition)}
+ * returns BigDecimal.
+ * @stable ICU 3.6
+ */
+ public void setParseBigDecimal(boolean value) {
+ parseBigDecimal = value;
+ }
+
+ /**
+ * Returns whether {@link #parse(String, ParsePosition)} returns BigDecimal.
+ *
+ * @return true if {@link #parse(String, ParsePosition)} returns BigDecimal.
+ * @stable ICU 3.6
+ */
+ public boolean isParseBigDecimal() {
+ return parseBigDecimal;
+ }
+
+ /**
+ * Set the maximum number of exponent digits when parsing a number.
+ * If the limit is set too high, an OutOfMemoryException may be triggered.
+ * The default value is 1000.
+ * @param newValue the new limit
+ * @stable ICU 51
+ */
+ public void setParseMaxDigits(int newValue) {
+ if (newValue > 0) {
+ PARSE_MAX_EXPONENT = newValue;
+ }
+ }
+
+ /**
+ * Get the current maximum number of exponent digits when parsing a
+ * number.
+ * @return the maximum number of exponent digits for parsing
+ * @stable ICU 51
+ */
+ public int getParseMaxDigits() {
+ return PARSE_MAX_EXPONENT;
+ }
+
+ private void writeObject(ObjectOutputStream stream) throws IOException {
+ // Ticket#6449 Format.Field instances are not serializable. When
+ // formatToCharacterIterator is called, attributes (ArrayList) stores
+ // FieldPosition instances with NumberFormat.Field. Because NumberFormat.Field is
+ // not serializable, we need to clear the contents of the list when writeObject is
+ // called. We could remove the field or make it transient, but it will break
+ // serialization compatibility.
+ attributes.clear();
+
+ stream.defaultWriteObject();
+ }
+
+ /**
+ * First, read the default serializable fields from the stream. Then if
+ * <code>serialVersionOnStream</code> is less than 1, indicating that the stream was
+ * written by JDK 1.1, initialize <code>useExponentialNotation</code> to false, since
+ * it was not present in JDK 1.1. Finally, set serialVersionOnStream back to the
+ * maximum allowed value so that default serialization will work properly if this
+ * object is streamed out again.
+ */
+ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ stream.defaultReadObject();
+
+ // Bug 4185761 validate fields [Richard/GCL]
+
+ // We only need to check the maximum counts because NumberFormat .readObject has
+ // already ensured that the maximum is greater than the minimum count.
+
+ // Commented for compatibility with previous version, and reserved for further use
+ // if (getMaximumIntegerDigits() > DOUBLE_INTEGER_DIGITS ||
+ // getMaximumFractionDigits() > DOUBLE_FRACTION_DIGITS) { throw new
+ // InvalidObjectException("Digit count out of range"); }
+
+
+ // Truncate the maximumIntegerDigits to DOUBLE_INTEGER_DIGITS and
+ // maximumFractionDigits to DOUBLE_FRACTION_DIGITS
+
+ if (getMaximumIntegerDigits() > DOUBLE_INTEGER_DIGITS) {
+ setMaximumIntegerDigits(DOUBLE_INTEGER_DIGITS);
+ }
+ if (getMaximumFractionDigits() > DOUBLE_FRACTION_DIGITS) {
+ _setMaximumFractionDigits(DOUBLE_FRACTION_DIGITS);
+ }
+ if (serialVersionOnStream < 2) {
+ exponentSignAlwaysShown = false;
+ setInternalRoundingIncrement(null);
+ roundingMode = BigDecimal.ROUND_HALF_EVEN;
+ formatWidth = 0;
+ pad = ' ';
+ padPosition = PAD_BEFORE_PREFIX;
+ if (serialVersionOnStream < 1) {
+ // Didn't have exponential fields
+ useExponentialNotation = false;
+ }
+ }
+ if (serialVersionOnStream < 3) {
+ // Versions prior to 3 do not store a currency object. Create one to match
+ // the DecimalFormatSymbols object.
+ setCurrencyForSymbols();
+ }
+ if (serialVersionOnStream < 4) {
+ currencyUsage = CurrencyUsage.STANDARD;
+ }
+ serialVersionOnStream = currentSerialVersion;
+ digitList = new DigitList();
+
+ if (roundingIncrement != null) {
+ setInternalRoundingIncrement(new BigDecimal(roundingIncrement));
+ }
+ resetActualRounding();
+ }
+
+ private void setInternalRoundingIncrement(BigDecimal value) {
+ roundingIncrementICU = value;
+ roundingIncrement = value == null ? null : value.toBigDecimal();
+ }
+
+ // ----------------------------------------------------------------------
+ // INSTANCE VARIABLES
+ // ----------------------------------------------------------------------
+
+ private transient DigitList digitList = new DigitList();
+
+ /**
+ * The symbol used as a prefix when formatting positive numbers, e.g. "+".
+ *
+ * @serial
+ * @see #getPositivePrefix
+ */
+ private String positivePrefix = "";
+
+ /**
+ * The symbol used as a suffix when formatting positive numbers. This is often an
+ * empty string.
+ *
+ * @serial
+ * @see #getPositiveSuffix
+ */
+ private String positiveSuffix = "";
+
+ /**
+ * The symbol used as a prefix when formatting negative numbers, e.g. "-".
+ *
+ * @serial
+ * @see #getNegativePrefix
+ */
+ private String negativePrefix = "-";
+
+ /**
+ * The symbol used as a suffix when formatting negative numbers. This is often an
+ * empty string.
+ *
+ * @serial
+ * @see #getNegativeSuffix
+ */
+ private String negativeSuffix = "";
+
+ /**
+ * The prefix pattern for non-negative numbers. This variable corresponds to
+ * <code>positivePrefix</code>.
+ *
+ * <p>This pattern is expanded by the method <code>expandAffix()</code> to
+ * <code>positivePrefix</code> to update the latter to reflect changes in
+ * <code>symbols</code>. If this variable is <code>null</code> then
+ * <code>positivePrefix</code> is taken as a literal value that does not change when
+ * <code>symbols</code> changes. This variable is always <code>null</code> for
+ * <code>DecimalFormat</code> objects older than stream version 2 restored from
+ * stream.
+ *
+ * @serial
+ */
+ // [Richard/GCL]
+ private String posPrefixPattern;
+
+ /**
+ * The suffix pattern for non-negative numbers. This variable corresponds to
+ * <code>positiveSuffix</code>. This variable is analogous to
+ * <code>posPrefixPattern</code>; see that variable for further documentation.
+ *
+ * @serial
+ */
+ // [Richard/GCL]
+ private String posSuffixPattern;
+
+ /**
+ * The prefix pattern for negative numbers. This variable corresponds to
+ * <code>negativePrefix</code>. This variable is analogous to
+ * <code>posPrefixPattern</code>; see that variable for further documentation.
+ *
+ * @serial
+ */
+ // [Richard/GCL]
+ private String negPrefixPattern;
+
+ /**
+ * The suffix pattern for negative numbers. This variable corresponds to
+ * <code>negativeSuffix</code>. This variable is analogous to
+ * <code>posPrefixPattern</code>; see that variable for further documentation.
+ *
+ * @serial
+ */
+ // [Richard/GCL]
+ private String negSuffixPattern;
+
+ /**
+ * Formatter for ChoiceFormat-based currency names. If this field is not null, then
+ * delegate to it to format currency symbols.
+ * TODO: This is obsolete: Remove, and design extensible serialization. ICU ticket #12090.
+ *
+ * @since ICU 2.6
+ */
+ private ChoiceFormat currencyChoice;
+
+ /**
+ * The multiplier for use in percent, permill, etc.
+ *
+ * @serial
+ * @see #getMultiplier
+ */
+ private int multiplier = 1;
+
+ /**
+ * The number of digits between grouping separators in the integer portion of a
+ * number. Must be greater than 0 if <code>NumberFormat.groupingUsed</code> is true.
+ *
+ * @serial
+ * @see #getGroupingSize
+ * @see NumberFormat#isGroupingUsed
+ */
+ private byte groupingSize = 3; // invariant, > 0 if useThousands
+
+ /**
+ * The secondary grouping size. This is only used for Hindi numerals, which use a
+ * primary grouping of 3 and a secondary grouping of 2, e.g., "12,34,567". If this
+ * value is less than 1, then secondary grouping is equal to the primary grouping.
+ *
+ */
+ private byte groupingSize2 = 0;
+
+ /**
+ * If true, forces the decimal separator to always appear in a formatted number, even
+ * if the fractional part of the number is zero.
+ *
+ * @serial
+ * @see #isDecimalSeparatorAlwaysShown
+ */
+ private boolean decimalSeparatorAlwaysShown = false;
+
+ /**
+ * The <code>DecimalFormatSymbols</code> object used by this format. It contains the
+ * symbols used to format numbers, e.g. the grouping separator, decimal separator, and
+ * so on.
+ *
+ * @serial
+ * @see #setDecimalFormatSymbols
+ * @see DecimalFormatSymbols
+ */
+ private DecimalFormatSymbols symbols = null; // LIU new DecimalFormatSymbols();
+
+ /**
+ * True to use significant digits rather than integer and fraction digit counts.
+ *
+ * @serial
+ * @since ICU 3.0
+ */
+ private boolean useSignificantDigits = false;
+
+ /**
+ * The minimum number of significant digits to show. Must be >= 1 and <=
+ * maxSignificantDigits. Ignored unless useSignificantDigits == true.
+ *
+ * @serial
+ * @since ICU 3.0
+ */
+ private int minSignificantDigits = 1;
+
+ /**
+ * The maximum number of significant digits to show. Must be >=
+ * minSignficantDigits. Ignored unless useSignificantDigits == true.
+ *
+ * @serial
+ * @since ICU 3.0
+ */
+ private int maxSignificantDigits = 6;
+
+ /**
+ * True to force the use of exponential (i.e. scientific) notation
+ * when formatting numbers.
+ *
+ *<p> Note that the JDK 1.2 public API provides no way to set this
+ * field, even though it is supported by the implementation and
+ * the stream format. The intent is that this will be added to the
+ * API in the future.
+ *
+ * @serial
+ */
+ private boolean useExponentialNotation; // Newly persistent in JDK 1.2
+
+ /**
+ * The minimum number of digits used to display the exponent when a number is
+ * formatted in exponential notation. This field is ignored if
+ * <code>useExponentialNotation</code> is not true.
+ *
+ * <p>Note that the JDK 1.2 public API provides no way to set this field, even though
+ * it is supported by the implementation and the stream format. The intent is that
+ * this will be added to the API in the future.
+ *
+ * @serial
+ */
+ private byte minExponentDigits; // Newly persistent in JDK 1.2
+
+ /**
+ * If true, the exponent is always prefixed with either the plus sign or the minus
+ * sign. Otherwise, only negative exponents are prefixed with the minus sign. This has
+ * no effect unless <code>useExponentialNotation</code> is true.
+ *
+ * @serial
+ * @since AlphaWorks NumberFormat
+ */
+ private boolean exponentSignAlwaysShown = false;
+
+ /**
+ * The value to which numbers are rounded during formatting. For example, if the
+ * rounding increment is 0.05, then 13.371 would be formatted as 13.350, assuming 3
+ * fraction digits. Has the value <code>null</code> if rounding is not in effect, or a
+ * positive value if rounding is in effect. Default value <code>null</code>.
+ *
+ * @serial
+ * @since AlphaWorks NumberFormat
+ */
+ // Note: this is kept in sync with roundingIncrementICU.
+ // it is only kept around to avoid a conversion when formatting a java.math.BigDecimal
+ private java.math.BigDecimal roundingIncrement = null;
+
+ /**
+ * The value to which numbers are rounded during formatting. For example, if the
+ * rounding increment is 0.05, then 13.371 would be formatted as 13.350, assuming 3
+ * fraction digits. Has the value <code>null</code> if rounding is not in effect, or a
+ * positive value if rounding is in effect. Default value <code>null</code>. WARNING:
+ * the roundingIncrement value is the one serialized.
+ *
+ * @serial
+ * @since AlphaWorks NumberFormat
+ */
+ private transient BigDecimal roundingIncrementICU = null;
+
+ /**
+ * The rounding mode. This value controls any rounding operations which occur when
+ * applying a rounding increment or when reducing the number of fraction digits to
+ * satisfy a maximum fraction digits limit. The value may assume any of the
+ * <code>BigDecimal</code> rounding mode values. Default value
+ * <code>BigDecimal.ROUND_HALF_EVEN</code>.
+ *
+ * @serial
+ * @since AlphaWorks NumberFormat
+ */
+ private int roundingMode = BigDecimal.ROUND_HALF_EVEN;
+
+ /**
+ * Operations on <code>BigDecimal</code> numbers are controlled by a {@link
+ * MathContext} object, which provides the context (precision and other information)
+ * for the operation. The default <code>MathContext</code> settings are
+ * <code>digits=0, form=PLAIN, lostDigits=false, roundingMode=ROUND_HALF_UP</code>;
+ * these settings perform fixed point arithmetic with unlimited precision, as defined
+ * for the original BigDecimal class in Java 1.1 and Java 1.2
+ */
+ // context for plain unlimited math
+ private MathContext mathContext = new MathContext(0, MathContext.PLAIN);
+
+ /**
+ * The padded format width, or zero if there is no padding. Must be >= 0. Default
+ * value zero.
+ *
+ * @serial
+ * @since AlphaWorks NumberFormat
+ */
+ private int formatWidth = 0;
+
+ /**
+ * The character used to pad the result of format to <code>formatWidth</code>, if
+ * padding is in effect. Default value ' '.
+ *
+ * @serial
+ * @since AlphaWorks NumberFormat
+ */
+ private char pad = ' ';
+
+ /**
+ * The position in the string at which the <code>pad</code> character will be
+ * inserted, if padding is in effect. Must have a value from
+ * <code>PAD_BEFORE_PREFIX</code> to <code>PAD_AFTER_SUFFIX</code>. Default value
+ * <code>PAD_BEFORE_PREFIX</code>.
+ *
+ * @serial
+ * @since AlphaWorks NumberFormat
+ */
+ private int padPosition = PAD_BEFORE_PREFIX;
+
+ /**
+ * True if {@link #parse(String, ParsePosition)} to return BigDecimal rather than
+ * Long, Double or BigDecimal except special values. This property is introduced for
+ * J2SE 5 compatibility support.
+ *
+ * @serial
+ * @since ICU 3.6
+ * @see #setParseBigDecimal(boolean)
+ * @see #isParseBigDecimal()
+ */
+ private boolean parseBigDecimal = false;
+
+ /**
+ * The currency usage for the NumberFormat(standard or cash usage).
+ * It is used as STANDARD by default
+ * @since ICU 54
+ */
+ private CurrencyUsage currencyUsage = CurrencyUsage.STANDARD;
+
+ // ----------------------------------------------------------------------
+
+ static final int currentSerialVersion = 4;
+
+ /**
+ * The internal serial version which says which version was written Possible values
+ * are:
+ *
+ * <ul>
+ *
+ * <li><b>0</b> (default): versions before JDK 1.2
+ *
+ * <li><b>1</b>: version from JDK 1.2 and later, which includes the two new fields
+ * <code>useExponentialNotation</code> and <code>minExponentDigits</code>.
+ *
+ * <li><b>2</b>: version on AlphaWorks, which adds roundingMode, formatWidth, pad,
+ * padPosition, exponentSignAlwaysShown, roundingIncrement.
+ *
+ * <li><b>3</b>: ICU 2.2. Adds currency object.
+ *
+ * <li><b>4</b>: ICU 54. Adds currency usage(standard vs cash)
+ *
+ * </ul>
+ *
+ * @serial
+ */
+ private int serialVersionOnStream = currentSerialVersion;
+
+ // ----------------------------------------------------------------------
+ // CONSTANTS
+ // ----------------------------------------------------------------------
+
+ /**
+ * {@icu} Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
+ * specify pad characters inserted before the prefix.
+ *
+ * @see #setPadPosition
+ * @see #getPadPosition
+ * @see #PAD_AFTER_PREFIX
+ * @see #PAD_BEFORE_SUFFIX
+ * @see #PAD_AFTER_SUFFIX
+ * @stable ICU 2.0
+ */
+ public static final int PAD_BEFORE_PREFIX = 0;
+
+ /**
+ * {@icu} Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
+ * specify pad characters inserted after the prefix.
+ *
+ * @see #setPadPosition
+ * @see #getPadPosition
+ * @see #PAD_BEFORE_PREFIX
+ * @see #PAD_BEFORE_SUFFIX
+ * @see #PAD_AFTER_SUFFIX
+ * @stable ICU 2.0
+ */
+ public static final int PAD_AFTER_PREFIX = 1;
+
+ /**
+ * {@icu} Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
+ * specify pad characters inserted before the suffix.
+ *
+ * @see #setPadPosition
+ * @see #getPadPosition
+ * @see #PAD_BEFORE_PREFIX
+ * @see #PAD_AFTER_PREFIX
+ * @see #PAD_AFTER_SUFFIX
+ * @stable ICU 2.0
+ */
+ public static final int PAD_BEFORE_SUFFIX = 2;
+
+ /**
+ * {@icu} Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
+ * specify pad characters inserted after the suffix.
+ *
+ * @see #setPadPosition
+ * @see #getPadPosition
+ * @see #PAD_BEFORE_PREFIX
+ * @see #PAD_AFTER_PREFIX
+ * @see #PAD_BEFORE_SUFFIX
+ * @stable ICU 2.0
+ */
+ public static final int PAD_AFTER_SUFFIX = 3;
+
+ // Constants for characters used in programmatic (unlocalized) patterns.
+ static final char PATTERN_ZERO_DIGIT = '0';
+ static final char PATTERN_ONE_DIGIT = '1';
+ static final char PATTERN_TWO_DIGIT = '2';
+ static final char PATTERN_THREE_DIGIT = '3';
+ static final char PATTERN_FOUR_DIGIT = '4';
+ static final char PATTERN_FIVE_DIGIT = '5';
+ static final char PATTERN_SIX_DIGIT = '6';
+ static final char PATTERN_SEVEN_DIGIT = '7';
+ static final char PATTERN_EIGHT_DIGIT = '8';
+ static final char PATTERN_NINE_DIGIT = '9';
+ static final char PATTERN_GROUPING_SEPARATOR = ',';
+ static final char PATTERN_DECIMAL_SEPARATOR = '.';
+ static final char PATTERN_DIGIT = '#';
+ static final char PATTERN_SIGNIFICANT_DIGIT = '@';
+ static final char PATTERN_EXPONENT = 'E';
+ static final char PATTERN_PLUS_SIGN = '+';
+ static final char PATTERN_MINUS_SIGN = '-';
+
+ // Affix
+ private static final char PATTERN_PER_MILLE = '\u2030';
+ private static final char PATTERN_PERCENT = '%';
+ static final char PATTERN_PAD_ESCAPE = '*';
+
+ // Other
+ private static final char PATTERN_SEPARATOR = ';';
+
+ // Pad escape is package private to allow access by DecimalFormatSymbols.
+ // Also plus sign. Also exponent.
+
+ /**
+ * The CURRENCY_SIGN is the standard Unicode symbol for currency. It is used in
+ * patterns and substitued with either the currency symbol, or if it is doubled, with
+ * the international currency symbol. If the CURRENCY_SIGN is seen in a pattern, then
+ * the decimal separator is replaced with the monetary decimal separator.
+ *
+ * The CURRENCY_SIGN is not localized.
+ */
+ private static final char CURRENCY_SIGN = '\u00A4';
+
+ private static final char QUOTE = '\'';
+
+ /**
+ * Upper limit on integer and fraction digits for a Java double [Richard/GCL]
+ */
+ static final int DOUBLE_INTEGER_DIGITS = 309;
+ static final int DOUBLE_FRACTION_DIGITS = 340;
+
+ /**
+ * When someone turns on scientific mode, we assume that more than this number of
+ * digits is due to flipping from some other mode that didn't restrict the maximum,
+ * and so we force 1 integer digit. We don't bother to track and see if someone is
+ * using exponential notation with more than this number, it wouldn't make sense
+ * anyway, and this is just to make sure that someone turning on scientific mode with
+ * default settings doesn't end up with lots of zeroes.
+ */
+ static final int MAX_SCIENTIFIC_INTEGER_DIGITS = 8;
+
+ // Proclaim JDK 1.1 serial compatibility.
+ private static final long serialVersionUID = 864413376551465018L;
+
+ private ArrayList<FieldPosition> attributes = new ArrayList<FieldPosition>();
+
+ // The following are used in currency format
+
+ // -- triple currency sign char array
+ // private static final char[] tripleCurrencySign = {0xA4, 0xA4, 0xA4};
+ // -- triple currency sign string
+ // private static final String tripleCurrencyStr = new String(tripleCurrencySign);
+ //
+ // -- default currency plural pattern char array
+ // private static final char[] defaultCurrencyPluralPatternChar =
+ // {0, '.', '#', '#', ' ', 0xA4, 0xA4, 0xA4};
+ // -- default currency plural pattern string
+ // private static final String defaultCurrencyPluralPattern =
+ // new String(defaultCurrencyPluralPatternChar);
+
+ // pattern used in this formatter
+ private String formatPattern = "";
+ // style is only valid when decimal formatter is constructed by
+ // DecimalFormat(pattern, decimalFormatSymbol, style)
+ private int style = NumberFormat.NUMBERSTYLE;
+ /**
+ * Represents whether this is a currency format, and which currency format style. 0:
+ * not currency format type; 1: currency style -- symbol name, such as "$" for US
+ * dollar. 2: currency style -- ISO name, such as USD for US dollar. 3: currency style
+ * -- plural long name, such as "US Dollar" for "1.00 US Dollar", or "US Dollars" for
+ * "3.00 US Dollars".
+ */
+ private int currencySignCount = CURRENCY_SIGN_COUNT_ZERO;
+
+ /**
+ * For parsing purposes, we need to remember all prefix patterns and suffix patterns
+ * of every currency format pattern, including the pattern of the default currency
+ * style, ISO currency style, and plural currency style. The patterns are set through
+ * applyPattern. The following are used to represent the affix patterns in currency
+ * plural formats.
+ */
+ private static final class AffixForCurrency {
+ // negative prefix pattern
+ private String negPrefixPatternForCurrency = null;
+ // negative suffix pattern
+ private String negSuffixPatternForCurrency = null;
+ // positive prefix pattern
+ private String posPrefixPatternForCurrency = null;
+ // positive suffix pattern
+ private String posSuffixPatternForCurrency = null;
+ private final int patternType;
+
+ public AffixForCurrency(String negPrefix, String negSuffix, String posPrefix,
+ String posSuffix, int type) {
+ negPrefixPatternForCurrency = negPrefix;
+ negSuffixPatternForCurrency = negSuffix;
+ posPrefixPatternForCurrency = posPrefix;
+ posSuffixPatternForCurrency = posSuffix;
+ patternType = type;
+ }
+
+ public String getNegPrefix() {
+ return negPrefixPatternForCurrency;
+ }
+
+ public String getNegSuffix() {
+ return negSuffixPatternForCurrency;
+ }
+
+ public String getPosPrefix() {
+ return posPrefixPatternForCurrency;
+ }
+
+ public String getPosSuffix() {
+ return posSuffixPatternForCurrency;
+ }
+
+ public int getPatternType() {
+ return patternType;
+ }
+ }
+
+ // Affix pattern set for currency. It is a set of AffixForCurrency, each element of
+ // the set saves the negative prefix, negative suffix, positive prefix, and positive
+ // suffix of a pattern.
+ private transient Set<AffixForCurrency> affixPatternsForCurrency = null;
+
+ // For currency parsing. Since currency parsing needs to parse against all currency
+ // patterns, before the parsing, we need to set up the affix patterns for all currencies.
+ private transient boolean isReadyForParsing = false;
+
+ // Information needed for DecimalFormat to format/parse currency plural.
+ private CurrencyPluralInfo currencyPluralInfo = null;
+
+ /**
+ * Unit is an immutable class for the textual representation of a unit, in
+ * particular its prefix and suffix.
+ *
+ * @author rocketman
+ *
+ */
+ static class Unit {
+ private final String prefix;
+ private final String suffix;
+
+ public Unit(String prefix, String suffix) {
+ this.prefix = prefix;
+ this.suffix = suffix;
+ }
+
+ public void writeSuffix(StringBuffer toAppendTo) {
+ toAppendTo.append(suffix);
+ }
+
+ public void writePrefix(StringBuffer toAppendTo) {
+ toAppendTo.append(prefix);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Unit)) {
+ return false;
+ }
+ Unit other = (Unit) obj;
+ return prefix.equals(other.prefix) && suffix.equals(other.suffix);
+ }
+ @Override
+ public String toString() {
+ return prefix + "/" + suffix;
+ }
+ }
+
+ static final Unit NULL_UNIT = new Unit("", "");
+
+ // Note about rounding implementation
+ //
+ // The original design intended to skip rounding operation when roundingIncrement is not
+ // set. However, rounding may need to occur when fractional digits exceed the width of
+ // fractional part of pattern.
+ //
+ // DigitList class has built-in rounding mechanism, using ROUND_HALF_EVEN. This implementation
+ // forces non-null roundingIncrement if the setting is other than ROUND_HALF_EVEN, otherwise,
+ // when rounding occurs in DigitList by pattern's fractional digits' width, the result
+ // does not match the rounding mode.
+ //
+ // Ideally, all rounding operation should be done in one place like ICU4C trunk does
+ // (ICU4C rounding implementation was rewritten recently). This is intrim implemetation
+ // to fix various issues. In the future, we should entire implementation of rounding
+ // in this class, like ICU4C did.
+ //
+ // Once we fully implement rounding logic in DigitList, then following fields and methods
+ // should be gone.
+
+ private transient BigDecimal actualRoundingIncrementICU = null;
+ private transient java.math.BigDecimal actualRoundingIncrement = null;
+
+ /*
+ * The actual rounding increment as a double.
+ */
+ private transient double roundingDouble = 0.0;
+
+ /*
+ * If the roundingDouble is the reciprocal of an integer (the most common case!), this
+ * is set to be that integer. Otherwise it is 0.0.
+ */
+ private transient double roundingDoubleReciprocal = 0.0;
+
+ /*
+ * Set roundingDouble, roundingDoubleReciprocal and actualRoundingIncrement
+ * based on rounding mode and width of fractional digits. Whenever setting affecting
+ * rounding mode, rounding increment and maximum width of fractional digits, then
+ * this method must be called.
+ *
+ * roundingIncrementICU is the field storing the custom rounding increment value,
+ * while actual rounding increment could be larger.
+ */
+ private void resetActualRounding() {
+ if (roundingIncrementICU != null) {
+ BigDecimal byWidth = getMaximumFractionDigits() > 0 ?
+ BigDecimal.ONE.movePointLeft(getMaximumFractionDigits()) : BigDecimal.ONE;
+ if (roundingIncrementICU.compareTo(byWidth) >= 0) {
+ actualRoundingIncrementICU = roundingIncrementICU;
+ } else {
+ actualRoundingIncrementICU = byWidth.equals(BigDecimal.ONE) ? null : byWidth;
+ }
+ } else {
+ if (roundingMode == BigDecimal.ROUND_HALF_EVEN || isScientificNotation()) {
+ // This rounding fix is irrelevant if mode is ROUND_HALF_EVEN as DigitList
+ // does ROUND_HALF_EVEN for us. This rounding fix won't work at all for
+ // scientific notation.
+ actualRoundingIncrementICU = null;
+ } else {
+ if (getMaximumFractionDigits() > 0) {
+ actualRoundingIncrementICU = BigDecimal.ONE.movePointLeft(getMaximumFractionDigits());
+ } else {
+ actualRoundingIncrementICU = BigDecimal.ONE;
+ }
+ }
+ }
+
+ if (actualRoundingIncrementICU == null) {
+ setRoundingDouble(0.0d);
+ actualRoundingIncrement = null;
+ } else {
+ setRoundingDouble(actualRoundingIncrementICU.doubleValue());
+ actualRoundingIncrement = actualRoundingIncrementICU.toBigDecimal();
+ }
+ }
+
+ static final double roundingIncrementEpsilon = 0.000000001;
+
+ private void setRoundingDouble(double newValue) {
+ roundingDouble = newValue;
+ if (roundingDouble > 0.0d) {
+ double rawRoundedReciprocal = 1.0d / roundingDouble;
+ roundingDoubleReciprocal = Math.rint(rawRoundedReciprocal);
+ if (Math.abs(rawRoundedReciprocal - roundingDoubleReciprocal) > roundingIncrementEpsilon) {
+ roundingDoubleReciprocal = 0.0d;
+ }
+ } else {
+ roundingDoubleReciprocal = 0.0d;
+ }
+ }
+}
+
+// eof
* <p>To format a Measure object, first create a formatter
* object using a MeasureFormat factory method. Then use that
* object's format or formatMeasures methods.
- *
+ *
* Here is sample code:
* <pre>
* MeasureFormat fmtFr = MeasureFormat.getInstance(
* ULocale.FRENCH, FormatWidth.SHORT);
* Measure measure = new Measure(23, MeasureUnit.CELSIUS);
- *
+ *
* // Output: 23 °C
* System.out.println(fmtFr.format(measure));
*
*
* // Output: 70 °F
* System.out.println(fmtFr.format(measureF));
- *
+ *
* MeasureFormat fmtFrFull = MeasureFormat.getInstance(
* ULocale.FRENCH, FormatWidth.WIDE);
* // Output: 70 pieds et 5,3 pouces
* System.out.println(fmtFrFull.formatMeasures(
* new Measure(70, MeasureUnit.FOOT),
* new Measure(5.3, MeasureUnit.INCH)));
- *
+ *
* // Output: 1 pied et 1 pouce
* System.out.println(fmtFrFull.formatMeasures(
* new Measure(1, MeasureUnit.FOOT),
* new Measure(1, MeasureUnit.INCH)));
- *
+ *
* MeasureFormat fmtFrNarrow = MeasureFormat.getInstance(
ULocale.FRENCH, FormatWidth.NARROW);
* // Output: 1′ 1″
* System.out.println(fmtFrNarrow.formatMeasures(
* new Measure(1, MeasureUnit.FOOT),
* new Measure(1, MeasureUnit.INCH)));
- *
- *
+ *
+ *
* MeasureFormat fmtEn = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.WIDE);
- *
+ *
* // Output: 1 inch, 2 feet
* fmtEn.formatMeasures(
* new Measure(1, MeasureUnit.INCH),
* This class is immutable and thread-safe so long as its deprecated subclass,
* TimeUnitFormat, is never used. TimeUnitFormat is not thread-safe, and is
* mutable. Although this class has existing subclasses, this class does not support new
- * sub-classes.
+ * sub-classes.
*
* @see com.ibm.icu.text.UFormat
* @author Alan Liu
/**
* Formatting width enum.
- *
+ *
* @stable ICU 53
*/
// Be sure to update MeasureUnitTest.TestSerialFormatWidthEnum
/**
* Spell out everything.
- *
+ *
* @stable ICU 53
*/
- WIDE(ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE),
+ WIDE(ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE),
/**
* Abbreviate when possible.
- *
+ *
* @stable ICU 53
*/
- SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE),
+ SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE),
/**
* Brief. Use only a symbol for the unit when possible.
- *
+ *
* @stable ICU 53
*/
NARROW(ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE),
* Identical to NARROW except when formatMeasures is called with
* an hour and minute; minute and second; or hour, minute, and second Measures.
* In these cases formatMeasures formats as 5:37:23 instead of 5h, 37m, 23s.
- *
+ *
* @stable ICU 53
*/
NUMERIC(ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE);
* If the pos argument identifies a NumberFormat field,
* then its indices are set to the beginning and end of the first such field
* encountered. MeasureFormat itself does not supply any fields.
- *
+ *
* Calling a
* <code>formatMeasures</code> method is preferred over calling
* this method as they give better performance.
- *
+ *
* @param obj must be a Collection<? extends Measure>, Measure[], or Measure object.
* @param toAppendTo Formatted string appended here.
* @param pos Identifies a field in the formatted text.
* @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition)
- *
+ *
* @stable ICU53
*/
@Override
} else if (obj instanceof Measure){
toAppendTo.append(formatMeasure((Measure) obj, numberFormat, new StringBuilder(), fpos));
} else {
- throw new IllegalArgumentException(obj.toString());
+ throw new IllegalArgumentException(obj.toString());
}
if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
pos.setBeginIndex(fpos.getBeginIndex() + prevLength);
* and using the appropriate Number values. Typically the units should be
* in descending order, with all but the last Measure having integer values
* (eg, not “3.2 feet, 2 inches”).
- *
+ *
* @param measures a sequence of one or more measures.
* @return the formatted string.
* @stable ICU 53
* <br>Note: If the format doesn’t have enough decimals, or lowValue ≥ highValue,
* the result will be a degenerate range, like “5-5 meters”.
* <br>Currency Units are not yet supported.
- *
+ *
* @param lowValue low value in range
* @param highValue high value in range
* @return the formatted string.
}
final double lowDouble = lowNumber.doubleValue();
- String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble,
+ String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble,
lowFpos.getCountVisibleFractionDigits(), lowFpos.getFractionDigits()));
final double highDouble = highNumber.doubleValue();
- String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble,
+ String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble,
highFpos.getCountVisibleFractionDigits(), highFpos.getFractionDigits()));
final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(getLocale());
result.append(affix.substring(pos+replacement.length()));
}
}
-
+
/**
- * Formats a single measure per unit.
- *
+ * Formats a single measure per unit.
+ *
* An example of such a formatted string is "3.5 meters per second."
*
* @param measure the measure object. In above example, 3.5 meters.
/**
* Formats a sequence of measures.
- *
+ *
* If the fieldPosition argument identifies a NumberFormat field,
* then its indices are set to the beginning and end of the first such field
* encountered. MeasureFormat itself does not supply any fields.
- *
+ *
* @param appendTo the formatted string appended here.
* @param fieldPosition Identifies a field in the formatted text.
* @param measures the measures to format.
}
MeasureFormat rhs = (MeasureFormat) other;
// A very slow but safe implementation.
- return getWidth() == rhs.getWidth()
- && getLocale().equals(rhs.getLocale())
+ return getWidth() == rhs.getWidth()
+ && getLocale().equals(rhs.getLocale())
&& getNumberFormat().equals(rhs.getNumberFormat());
}
@Override
public final int hashCode() {
// A very slow but safe implementation.
- return (getLocale().hashCode() * 31
+ return (getLocale().hashCode() * 31
+ getNumberFormat().hashCode()) * 31 + getWidth().hashCode();
}
return pattern;
}
- private String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
if (index != StandardPlural.OTHER_INDEX) {
String pattern = getFormatterOrNull(unit, width, index);
if (pattern != null) {
suffix = pattern.substring(pos+3);
}
}
+ @Override
public String toString() {
return prefix + "; " + suffix;
}
if (fieldPositionFoundIndex == -1) {
results[i] = formatMeasure(measures[i], nf, new StringBuilder(), fpos).toString();
if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
- fieldPositionFoundIndex = i;
+ fieldPositionFoundIndex = i;
}
} else {
results[i] = formatMeasure(measures[i], nf);
// if hour-minute-second
if (startIndex == 0 && endIndex == 2) {
return formatNumeric(
- d,
+ d,
numericFormatters.getHourMinuteSecond(),
DateFormat.Field.SECOND,
hms[endIndex],
// if minute-second
if (startIndex == 1 && endIndex == 2) {
return formatNumeric(
- d,
+ d,
numericFormatters.getMinuteSecond(),
DateFormat.Field.SECOND,
hms[endIndex],
// if hour-minute
if (startIndex == 0 && endIndex == 1) {
return formatNumeric(
- d,
+ d,
numericFormatters.getHourMinute(),
DateFormat.Field.MINUTE,
hms[endIndex],
public MeasureProxy() {
}
+ @Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeByte(0); // version
out.writeUTF(locale.toLanguageTag());
out.writeObject(keyValues);
}
+ @Override
@SuppressWarnings("unchecked")
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
in.readByte(); // version.
import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.MessagePattern.ArgType;
import com.ibm.icu.text.MessagePattern.Part;
-import com.ibm.icu.text.PluralRules.FixedDecimal;
+import com.ibm.icu.text.PluralRules.IFixedDecimal;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.ULocale;
assert context.number.doubleValue() == number; // argument number minus the offset
context.numberString = context.formatter.format(context.number);
if(context.formatter instanceof DecimalFormat) {
- FixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
+ IFixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
return rules.select(dec);
} else {
return rules.select(number);
}
/**
- * {@icu} Sets whether strict parsing is in effect. When this is true, the
- * following conditions cause a parse failure (examples use the pattern "#,##0.#"):<ul>
+ * {@icu} Sets whether strict parsing is in effect. When this is true, the string
+ * is required to be a stronger match to the pattern than when lenient parsing is in
+ * effect. More specifically, the following conditions cause a parse failure relative
+ * to lenient mode (examples use the pattern "#,##0.#"):<ul>
+ * <li>The presence and position of special symbols, including currency, must match the
+ * pattern.<br>
+ * '123-' fails (the minus sign is expected in the prefix, not suffix)</li>
* <li>Leading or doubled grouping separators<br>
* ',123' and '1,,234" fail</li>
* <li>Groups of incorrect length when grouping is used<br>
/**
* Returns the maximum number of digits allowed in the integer portion of a
* number. The default value is 40, which subclasses can override.
- * When formatting, the exact behavior when this value is exceeded is
- * subclass-specific. When parsing, this has no effect.
+ *
+ * When formatting, if the number of digits exceeds this value, the highest-
+ * significance digits are truncated until the limit is reached, in accordance
+ * with UTS#35.
+ *
+ * This setting has no effect on parsing.
+ *
* @return the maximum number of integer digits
* @see #setMaximumIntegerDigits
* @stable ICU 2.0
f.setDecimalSeparatorAlwaysShown(false);
f.setParseIntegerOnly(true);
}
-
if (choice == CASHCURRENCYSTYLE) {
f.setCurrencyUsage(CurrencyUsage.CASH);
}
+ if (choice == PLURALCURRENCYSTYLE) {
+ f.setCurrencyPluralInfo(CurrencyPluralInfo.getInstance(desiredLocale));
+ }
format = f;
}
// TODO: the actual locale of the *pattern* may differ from that
* @param choice the pattern format.
* @return the pattern
* @stable ICU 3.2
+ * @internal
+ * @deprecated This API is ICU internal only.
*/
- protected static String getPattern(ULocale forLocale, int choice) {
+ @Deprecated
+ public static String getPattern(ULocale forLocale, int choice) {
/* for ISOCURRENCYSTYLE and PLURALCURRENCYSTYLE,
* the pattern is the same as the pattern of CURRENCYSTYLE
* but by replacing the single currency sign with
switch (choice) {
case NUMBERSTYLE:
case INTEGERSTYLE:
+ case PLURALCURRENCYSTYLE:
patternKey = "decimalFormat";
break;
case CURRENCYSTYLE:
break;
case CASHCURRENCYSTYLE:
case ISOCURRENCYSTYLE:
- case PLURALCURRENCYSTYLE:
case STANDARDCURRENCYSTYLE:
patternKey = "currencyFormat";
break;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.PluralRules.FixedDecimal;
+import com.ibm.icu.text.PluralRules.IFixedDecimal;
+import com.ibm.icu.text.PluralRules.Operand;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
private final class PluralSelectorAdapter implements PluralSelector {
@Override
public String select(Object context, double number) {
- FixedDecimal dec = (FixedDecimal) context;
- assert dec.source == (dec.isNegative ? -number : number);
+ IFixedDecimal dec = (IFixedDecimal) context;
+ assert dec.getPluralOperand(Operand.n) == Math.abs(number);
return pluralRules.select(dec);
}
}
} else {
numberString = numberFormat.format(numberMinusOffset);
}
- FixedDecimal dec;
+ IFixedDecimal dec;
if(numberFormat instanceof DecimalFormat) {
dec = ((DecimalFormat) numberFormat).getFixedDecimal(numberMinusOffset);
} else {
private static final long serialVersionUID = 9163464945387899416L;
@Override
- public boolean isFulfilled(FixedDecimal n) {
+ public boolean isFulfilled(IFixedDecimal n) {
return true;
}
*/
public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE));
- private enum Operand {
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public static enum Operand {
+ /** The double value of the entire number. */
n,
+ /** The integer value, with the fraction digits truncated off. */
i,
+ /** All visible fraction digits as an integer, including trailing zeros. */
f,
+ /** Visible fraction digits, not including trailing zeros. */
t,
+ /** Number of visible fraction digits. */
v,
w,
/* deprecated */
* @deprecated This API is ICU internal only.
*/
@Deprecated
- public static class FixedDecimal extends Number implements Comparable<FixedDecimal> {
+ public static interface IFixedDecimal {
+ public double getPluralOperand(Operand operand);
+ public boolean isNaN();
+ public boolean isInfinite();
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public static class FixedDecimal extends Number implements Comparable<FixedDecimal>, IFixedDecimal {
private static final long serialVersionUID = -4756200506571685661L;
/**
* @internal
* @internal
* @deprecated This API is ICU internal only.
*/
+ @Override
@Deprecated
- public double get(Operand operand) {
+ public double getPluralOperand(Operand operand) {
switch(operand) {
default: return source;
case i: return integerValue;
) throws IOException, ClassNotFoundException {
throw new NotSerializableException();
}
+
+ /* (non-Javadoc)
+ * @see com.ibm.icu.text.PluralRules.IFixedDecimal#isNaN()
+ */
+ @Override
+ public boolean isNaN() {
+ return Double.isNaN(source);
+ }
+
+ /* (non-Javadoc)
+ * @see com.ibm.icu.text.PluralRules.IFixedDecimal#isInfinite()
+ */
+ @Override
+ public boolean isInfinite() {
+ return Double.isInfinite(source);
+ }
}
/**
* Returns true if the number fulfills the constraint.
* @param n the number to test, >= 0.
*/
- boolean isFulfilled(FixedDecimal n);
+ boolean isFulfilled(IFixedDecimal n);
/*
* Returns false if an unlimited number of values fulfills the
}
@Override
- public boolean isFulfilled(FixedDecimal number) {
- double n = number.get(operand);
+ public boolean isFulfilled(IFixedDecimal number) {
+ double n = number.getPluralOperand(operand);
if ((integersOnly && (n - (long)n) != 0.0
- || operand == Operand.j && number.visibleDecimalDigitCount != 0)) {
+ || operand == Operand.j && number.getPluralOperand(Operand.v) != 0)) {
return !inRange;
}
if (mod != 0) {
}
@Override
- public boolean isFulfilled(FixedDecimal n) {
+ public boolean isFulfilled(IFixedDecimal n) {
return a.isFulfilled(n)
&& b.isFulfilled(n);
}
}
@Override
- public boolean isFulfilled(FixedDecimal n) {
+ public boolean isFulfilled(IFixedDecimal n) {
return a.isFulfilled(n)
|| b.isFulfilled(n);
}
return keyword;
}
- public boolean appliesTo(FixedDecimal n) {
+ public boolean appliesTo(IFixedDecimal n) {
return constraint.isFulfilled(n);
}
return this;
}
- private Rule selectRule(FixedDecimal n) {
+ private Rule selectRule(IFixedDecimal n) {
for (Rule rule : rules) {
if (rule.appliesTo(n)) {
return rule;
return null;
}
- public String select(FixedDecimal n) {
- if (Double.isInfinite(n.source) || Double.isNaN(n.source)) {
+ public String select(IFixedDecimal n) {
+ if (n.isInfinite() || n.isNaN()) {
return KEYWORD_OTHER;
}
Rule r = selectRule(n);
return null;
}
- public boolean select(FixedDecimal sample, String keyword) {
+ public boolean select(IFixedDecimal sample, String keyword) {
for (Rule rule : rules) {
if (rule.getKeyword().equals(keyword) && rule.appliesTo(sample)) {
return true;
}
@SuppressWarnings("unused")
- private boolean addConditional(Set<FixedDecimal> toAddTo, Set<FixedDecimal> others, double trial) {
+ private boolean addConditional(Set<IFixedDecimal> toAddTo, Set<IFixedDecimal> others, double trial) {
boolean added;
- FixedDecimal toAdd = new FixedDecimal(trial);
+ IFixedDecimal toAdd = new FixedDecimal(trial);
if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) {
others.add(toAdd);
added = true;
* @deprecated This API is ICU internal only.
*/
@Deprecated
- public String select(FixedDecimal number) {
+ public String select(IFixedDecimal number) {
return rules.select(number);
}
public StringBuffer format(com.ibm.icu.math.BigDecimal number,
StringBuffer toAppendTo,
FieldPosition pos) {
- if (MIN_VALUE.compareTo(number) >= 0 || MAX_VALUE.compareTo(number) <= 0) {
+ if (MIN_VALUE.compareTo(number) > 0 || MAX_VALUE.compareTo(number) < 0) {
// We're outside of our normal range that this framework can handle.
// The DecimalFormat will provide more accurate results.
return getDecimalFormat().format(number, toAppendTo, pos);
import java.text.CharacterIterator;
import java.util.Map;
+import com.ibm.icu.impl.number.Parse;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.util.ULocale;
int start = iterator.getRunStart(NumberFormat.Field.EXPONENT_SIGN);
int limit = iterator.getRunLimit(NumberFormat.Field.EXPONENT_SIGN);
int aChar = char32AtAndAdvance(iterator);
- if (DecimalFormat.minusSigns.contains(aChar)) {
+ if (Parse.UNISET_MINUS.contains(aChar)) {
append(
iterator,
copyFromOffset,
start,
result);
result.append(SUPERSCRIPT_MINUS_SIGN);
- } else if (DecimalFormat.plusSigns.contains(aChar)) {
+ } else if (Parse.UNISET_PLUS.contains(aChar)) {
append(
iterator,
copyFromOffset,
*/
@Deprecated
public static String parse(ULocale locale, String text, int type, ParsePosition pos) {
- List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale);
- if (currencyTrieVec == null) {
- TextTrieMap<CurrencyStringInfo> currencyNameTrie =
- new TextTrieMap<CurrencyStringInfo>(true);
- TextTrieMap<CurrencyStringInfo> currencySymbolTrie =
- new TextTrieMap<CurrencyStringInfo>(false);
- currencyTrieVec = new ArrayList<TextTrieMap<CurrencyStringInfo>>();
- currencyTrieVec.add(currencySymbolTrie);
- currencyTrieVec.add(currencyNameTrie);
- setupCurrencyTrieVec(locale, currencyTrieVec);
- CURRENCY_NAME_CACHE.put(locale, currencyTrieVec);
- }
-
+ List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = getCurrencyTrieVec(locale);
int maxLength = 0;
String isoResult = null;
return isoResult;
}
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public static TextTrieMap<CurrencyStringInfo>.ParseState openParseState(
+ ULocale locale, int startingCp, int type) {
+ List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = getCurrencyTrieVec(locale);
+ if (type == Currency.LONG_NAME) {
+ return currencyTrieVec.get(0).openParseState(startingCp);
+ } else {
+ return currencyTrieVec.get(1).openParseState(startingCp);
+ }
+ }
+
+ private static List<TextTrieMap<CurrencyStringInfo>> getCurrencyTrieVec(ULocale locale) {
+ List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale);
+ if (currencyTrieVec == null) {
+ TextTrieMap<CurrencyStringInfo> currencyNameTrie =
+ new TextTrieMap<CurrencyStringInfo>(true);
+ TextTrieMap<CurrencyStringInfo> currencySymbolTrie =
+ new TextTrieMap<CurrencyStringInfo>(false);
+ currencyTrieVec = new ArrayList<TextTrieMap<CurrencyStringInfo>>();
+ currencyTrieVec.add(currencySymbolTrie);
+ currencyTrieVec.add(currencyNameTrie);
+ setupCurrencyTrieVec(locale, currencyTrieVec);
+ CURRENCY_NAME_CACHE.put(locale, currencyTrieVec);
+ }
+ return currencyTrieVec;
+ }
+
private static void setupCurrencyTrieVec(ULocale locale,
List<TextTrieMap<CurrencyStringInfo>> trieVec) {
}
}
- private static final class CurrencyStringInfo {
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Deprecated
+ public static final class CurrencyStringInfo {
private String isoCode;
private String currencyString;
*/
public Measure(Number number, MeasureUnit unit) {
if (number == null || unit == null) {
- throw new NullPointerException();
+ throw new NullPointerException("Number and MeasureUnit must not be null");
}
this.number = number;
this.unit = unit;
set pattern +0;-#
begin
format output breaks
-6 \u200F+\u0666 JK
--6 \u200F-\u0666 JK
+6 \u061C+\u0666 JK
+-6 \u061C-\u0666 K
test basic patterns
set locale fr_FR
12345 2345.000
72.1234 72.1234
+test patterns with no '0' symbols
+set locale en_US
+begin
+pattern format output breaks
+# 514.23 514
+# 0.23 0
+# 0 0
+# 1 1
+##.# 514.23 514.2
+##.# 0.23 0.2
+##.# 0 0
+##.# 1 1
+#.# 514.23 514.2
+#.# 0.23 0.2
+#.# 0 0
+#.# 1 1
+.# 514.23 514.2
+.# 0.23 .2
+.# 0 .0
+.# 1 1.0
+#. 514.23 514.
+#. 0.23 0.
+#. 0 0.
+#. 1 1.
+. 514.23 514.
+. 0.23 0.
+. 0 0.
+. 1 1.
+
+test behavior on numbers approaching zero
+set locale en
+begin
+pattern format output breaks
+#.## 0.01 0.01
+#.## 0.001 0
+#.## 0 0
+#.00 0.01 .01
+#.00 0.001 .00
+#.00 0 .00
+0.00 0.01 0.01
+0.00 0.001 0.00
+0.00 0 0.00
+
+// Not in official spec, but needed for backwards compatibility
+test patterns with leading grouping separator
+set locale en_US
+begin
+pattern format output breaks
+,##0 1234.56 1,235
+'#',## 3456 #34,56
+
+test patterns with valid and invalid quote marks
+set locale et
+begin
+pattern format output breaks
+'# 1 fail
+''# 1 '1
+'''# 1 fail
+''''# 1 ''1
+'''''# 1 fail
+'-''-'# 1 -'-1
+// K doesn't know the locale symbol for et
+-'-'# 1 −-1 K
+'#'# 1 #1
+''#'' 1 '1'
+''#- 1 '1− K
+'-'#- 1 -1− K
+-#'-' 1 −1- K
+
test int64
set locale en
begin
0.05E0 12301.2 1,25E4 JK
##0.000#E0 0.17 170,0E-3
// JDK doesn't support significant digits in exponents
+@@@E0 6.235 6,24E0 K
@@@E0 6235 6,24E3 K
@@@#E0 6200 6,20E3 K
@@@#E0 6201 6,201E3 K
@@@#E0 6201.7 6,202E3 K
@@@#E00 6201.7 6,202E03 K
@@@#E+00 6201.7 6,202E+03 K
+// If no zeros are specified, significant digits is fraction length plus 1
+#.##E0 52413 5,24E4
+###.##E0 52413 52,4E3 K
+#E0 52413 5,2413E4 K
+0E0 52413 5E4
+
+test scientific with grouping
+set locale en
+set pattern #,##0.000E0
+begin
+format output breaks
+// J throws an IllegalArgumentException when parsing the pattern.
+1 1.000E0 J
+11 11.00E0 J
+111 111.0E0 J
+// K doesn't print the grouping separator ("1111E0")
+1111 1,111E0 JK
+// K prints too many digits ("1.1111E4")
+11111 1.111E4 JK
+111111 11.11E4 JK
+1111111 111.1E4 JK
+11111111 1,111E4 JK
+111111111 1.111E8 JK
test percents
set locale fr
// In J ICU adds padding as if 'EUR' is only 2 chars (2 * 0xa4)
\u00a4\u00a4 **####0.00 433.0 EUR *433,00 JK
// In J ICU adds padding as if 'EUR' is only 2 chars (2 * 0xa4)
-\u00a4\u00a4 **#######0 433.0 EUR *433,00 JK
+\u00a4\u00a4 **#######0 433.0 EUR ****433 JK
test padding and currencies
begin
set format 299792458.0
begin
minIntegerDigits maxIntegerDigits minFractionDigits maxFractionDigits output breaks
+// JDK gives 2.99792458E8 (maxInt + maxFrac instead of minInt + maxFrac)
+1 1000 0 5 2.99792E8 K
// JDK gives .3E9 instead of unlimited precision.
0 1 0 0 2.99792458E8 K
1 1 0 0 3E8
// JDK gives E0 instead of allowing for unlimited precision
-0 0 0 0 2.99792458E8 K
-// JDK gives .299792E9
-0 1 0 5 2.9979E8 K
+// S obeys the maximum integer digits and returns .299792458E9
+0 0 0 0 2.99792458E8 KS
+// JDK and S give .299792E9
+0 1 0 5 2.9979E8 KS
// JDK gives 300E6
0 3 0 0 299.792458E6 K
// JDK gives 299.8E6 (maybe maxInt + maxFrac instead of minInt + maxFrac)?
4 4 0 0 2998E5
0 0 1 5 .29979E9
// JDK gives E0
-0 0 1 0 2.99792458E8 K
+// S obeys the maximum integer digits
+0 0 1 0 2.99792458E8 KS
// JDK gives .2998E9
-0 0 0 4 2.998E8 K
+0 0 0 4 2.998E8 KS
+// S correctly formats this as 29.979246E7.
// JDK uses 8 + 6 for significant digits instead of 2 + 6
-2 8 1 6 2.9979246E8 K
+// J and C return 2.9979246E8.
+2 8 1 6 29.979246E7 CJK
// Treat max int digits > 8 as being the same as min int digits.
// This behavior is not spelled out in the specification.
// JDK fails here because it tries to use 9 + 6 = 15 sig digits.
begin
minIntegerDigits maxIntegerDigits minFractionDigits maxFractionDigits output breaks
// JDK gives E0
-0 0 0 0 2.9979245E7 K
+// S obeys the max integer digits and prints 0.299...
+0 0 0 0 2.9979245E7 KS
// JDK gives .3E8
0 1 0 0 2.9979245E7 K
// JDK gives 2998E4.
set locale en
set pattern #,##0.###
begin
-format maxIntegerDigits output
-123 1 3
-123 -2147483648 0
-12345 1 5
-12345 -2147483648 0
-5.3 1 5.3
-5.3 -2147483648 .3
+format maxIntegerDigits output breaks
+123 1 3
+0 0 0
+// S ignores max integer if it is less than zero and prints "123"
+123 -2147483648 0 S
+12345 1 5
+12345 -2147483648 0 S
+5.3 1 5.3
+5.3 -2147483648 .3 S
test patterns with zero
set locale en
set format 0
begin
-pattern output
+pattern output breaks
#.# 0
#. 0.
.# .0
# 0
+#,##0.00 0.00
+#,###.00 .00
00.000E00 00.000E00
0.####E0 0E0
##0.######E000 0E000
0.001234 0.001234 K
0.0012345 0.0012345 K
0.00123456 0.0012346 K
--43 -43.0 K
--43.7 -43.7 K
+-43 -43.0
+-43.7 -43.7
-43.76 -43.76 K
-43.762 -43.762 K
-43.7626 -43.763 K
1,2345,6789 4
1,23,45,6789 4 K 2
1,23,45,6789 4 K 2 2
-123,456789 6 K 6 3
+123,456789 6 6 3
123456789 6 JK 6 4
test multiplier setters
23 -12 -276
23 -1 -23
// ICU4J and JDK throw exception on zero multiplier. ICU4C does not.
-23 0 23 JK
+23 0 23 JKS
23 1 23
23 12 276
-23 12 -276
set format 1357
begin
padCharacter formatWidth output breaks
-* 8 bill1357 K
+* 8 bill1357
* 9 *bill1357 K
^ 10 ^^bill1357 K
output breaks useScientific
186283.00
1.86E5 K 1
-186283.00 K 0
+186283.00 0
test rounding mode setters
set locale en_US
-1.49 down -1 K
1.01 up 1.5 K
1.49 down 1 K
--1.01 ceiling -1 K
--1.49 floor -1.5 K
+-1.01 ceiling -1
+-1.49 floor -1.5
test currency usage setters
-// TODO: Find a country and currency where standard and cash differ
set locale CH
-set currency CHF
set pattern \u00a4\u00a4 0
begin
-format currencyUsage output breaks
-0.37 standard CHF 0.37 K
-// TODO: Get the rounding data into ICU4C and ICU4J
-0.37 cash CHF 0.35 CJK
+format currency currencyUsage output breaks
+0.37 CHF standard CHF 0.37 K
+0.37 CHF cash CHF 0.35 CK
+1.234 CZK standard CZK 1.23 K
+1.234 CZK cash CZK 1
+
+test currency usage to pattern
+set locale en
+begin
+currency currencyUsage toPattern breaks
+// These work in J, but it prepends an extra hash sign to the pattern.
+// K does not support this feature.
+USD standard 0.00 JK
+CHF standard 0.00 JK
+CZK standard 0.00 JK
+USD cash 0.00 JK
+CHF cash 0.05 JK
+CZK cash 0 JK
+
+test currency rounding
+set locale en
+set currency USD
+begin
+pattern format output breaks
+# 123 123 S
+// Currency rounding should always override the pattern.
+// K prints the currency in ISO format for some reason.
+\u00a4# 123 $123.00 K
+\u00a4#.000 123 $123.00 K
+\u00a4#.## 123 $123.00 K
test exponent parameter setters
set locale en_US
decimalSeparatorAlwaysShown exponentSignAlwaysShown minimumExponentDigits output breaks
0 0 2 3E08 K
0 1 3 3E+008 K
-// ICU DecimalFormat J does not honor decimalSeparatorAlwaysShown
-// for scientific notation. But JDK DecimalFormat does honor
// decimalSeparatorAlwaysShown K=JDK; C=ICU4C; J=ICU4J
// See ticket 11621
-1 0 2 3.E08 JK
-1 1 3 3.E+008 JK
+1 0 2 3.E08 K
+1 1 3 3.E+008 K
1 0 1 3.E8
0 0 1 3E8
// decimalSeparatorAlwaysShown off by default
299792458 3E8
299000000 2.99E8
-299792458 3.E8 J 1
+299792458 3.E8 1
test pad position setters
set locale en_US
set pattern [0.00];(#)
begin
format output breaks
-Inf [\u221e] K
+Inf [\u221e]
-Inf (\u221e) K
NaN NaN K
locale pattern format output breaks
en #0% 0.4376 44%
// This next test breaks JDK. JDK doesn't multiply by 100.
-// It also is now broken in ICU4J until #10368 is fixed.
-fa \u0025\u00a0\u0023\u0030 0.4376 \u200e\u066a\u00a0\u06f4\u06f4 JK
+fa \u0025\u00a0\u0023\u0030 0.4376 \u200e\u066a\u00a0\u06f4\u06f4 K
test toPattern
set locale en
begin
pattern toPattern breaks
+// All of the "S" failures in this section are because of functionally equivalent patterns
// JDK doesn't support any patterns with padding or both negative prefix and suffix
// Breaks ICU4J See ticket 11671
**0,000 **0,000 JK
**##0,000 **##0,000 K
**###0,000 **###0,000 K
-**####0,000 **#,##0,000 K
+**####0,000 **#,##0,000 KS
###,000. #,000.
-0,000 #0,000
+0,000 #0,000 S
.00 #.00
-000 #000
-000,000 #,000,000
+000 #000 S
+000,000 #,000,000 S
pp#,000 pp#,000
-00.## #00.##
+00.## #00.## S
#,#00.025 #,#00.025
// No secondary grouping in JDK
#,##,###.02500 #,##,###.02500 K
pp#,000;(#) pp#,000;(#,000) K
-**####,##,##0.0##;(#) **#,##,##,##0.0##;**(##,##,##0.0##) K
+**####,##,##0.0##;(#) **#,##,##,##0.0##;**(##,##,##0.0##) KS
// No significant digits in JDK
@@### @@### K
@,@#,### @,@#,### K
0.00E0 0.00E0
-@@@##E0 @@@##E0 K
+// The following one works in JDK, probably because
+// it just returns the same string
+@@@##E0 @@@##E0
###0.00#E0 ###0.00#E0
##00.00#E0 ##00.00#E0
0.00E+00 0.00E+00 K
// J requires prefix and suffix for lenient parsing, but C doesn't
5,347.25 5347.25 JK
(5,347.25 -5347.25 J
--5,347.25 fail
+// S is successful at parsing this as -5347.25 in lenient mode
+-5,347.25 fail S
+3.52E4 35200
(34.8E-3) -0.0348
// JDK stops parsing at the spaces. JDK doesn't see space as a grouping separator
// J doesn't allow trailing separators before E but C does
(34,,25,E-1) -342.5 J
(34 25 E-1) -342.5 JK
-(34,,25 E-1) -3425 J
+(34,,25 E-1) -342.5 JK
// Spaces are not allowed after exponent symbol
// C parses up to the E but J bails
(34 25E -1) -3425 JK
set pattern #,##0.0###+;#-
begin
parse output breaks
-// C sees this as -3426, don't understand why
-3426 -3426 JK
+// C sees this as -3426, don't understand why.
+// J and K just bail.
+3426 3426 JKC
3426+ 3426
-// J bails, but JDK will parse up to the space and get 34.
-// C sees -34
-34 d1+ -34 JK
+// J bails; C and K see -34
+34 d1+ 34 JKC
// JDK sees this as -1234 for some reason
// J bails b/c of trailing separators
// C parses until trailing separators, but sees -1234
-1,234,,,+ -1234 JK
+1,234,,,+ 1234 JKC
1,234- -1234
// J bails because of trailing separators
1,234,- -1234 J
test parse strict
set locale en
-set pattern +#,##0.0###;(#)
+set pattern +#,##,##0.0###;(#)
set lenient 0
+set minGroupingDigits 2
begin
parse output breaks
+123d5 123
+5347.25 5347.25
// separators in wrong place cause failure, no separators ok.
-+5,347.25 5347.25
-(5347.25) -5347.25
-(5,347.25) -5347.25
++65,347.25 65347.25
+(65347.25) -65347.25
+(65,347.25) -65347.25
// JDK does allow separators in the wrong place and parses as -5347.25
(53,47.25) fail K
// strict requires prefix or suffix
-5,347.25 fail
+65,347.25 fail
+3.52E4 35200
(34.8E-3) -0.0348
(3425E-1) -342.5
// Strict doesn't allow separators in sci notation.
-(3,425) -3425
-// JDK allows separators in sci notation and parses as -342.5
-(3,425E-1) fail K
+(63,425) -63425
+// JDK and S allow separators in sci notation and parses as -342.5
+(63,425E-1) fail KS
// Both prefix and suffix needed for strict.
// JDK accepts this and parses as -342.5
(3425E-1 fail K
+3.52EE4 3.52
-+1,234,567.8901 1234567.8901
++12,34,567.8901 1234567.8901
// With strict digit separators don't have to be the right type
// JDK doesn't acknowledge space as a separator
-+1 234 567.8901 1234567.8901 K
++12 34 567.8901 1234567.8901 K
// In general the grouping separators have to match their expected
// location exactly. The only exception is when string being parsed
// have no separators at all.
-+1,234,567.8901 1234567.8901
-// JDK doesn't require separators to be in the right place
++12,345.67 12345.67
+// JDK doesn't require separators to be in the right place.
+1,23,4567.8901 fail K
++1,234,567.8901 fail K
+1234,567.8901 fail K
+1,234567.8901 fail K
+1234567.8901 1234567.8901
+// Minimum grouping is not satisfied below, but that's ok
+// because minimum grouping is optional.
++1,234.5 1234.5
// Comma after decimal means parse to a comma
-+123,456.78,9 123456.78
-// A decimal after a decimal means bail
-// JDK parses as 123456.78
-+123,456.78.9 fail K
++1,23,456.78,9 123456.78
+// J fails upon seeing the second decimal point
++1,23,456.78.9 123456.78 J
+79 79
+79 79
+ 79 fail
// JDK parses as -1945
(1,945d1) fail K
+test parse strict without prefix/suffix
+set locale en
+set pattern #
+set lenient 0
+begin
+parse output breaks
+12.34 12.34
+-12.34 -12.34
++12.34 12.34 JK
+$12.34 fail
+
test parse integer only
set locale en
set pattern 0.00
begin
parse output breaks
35 35
-+35 fail
+// S accepts leading plus signs
++35 35 CJK
-35 -35
2.63 2
-39.99 -39
set locale en
begin
parse output outputCurrency breaks
-// See ticket 11735
-53.45 fail USD J
+// Fixed in ticket 11735
+53.45 fail USD
test parse strange prefix
set locale en
set negativeSuffix 9N
begin
parse output breaks
+// S is the only implementation that passes these cases.
// C consumes the '9' as a digit and assumes number is negative
// J and JDK bail
-// 6549K 654 CJK
+6549K 654 CJK
// C consumes the '9' as a digit and assumes number is negative
// J and JDK bail
-// 6549N -654 CJK
+6549N -654 CJK
test really strange prefix
set locale en
8245 45
2845 -45
+test parse pattern with quotes
+set locale en
+set pattern '-'#y
+begin
+parse output
+-45y 45
+
+test parse with locale symbols
+// The grouping separator in it_CH is an apostrophe
+set locale it_CH
+set pattern #
+begin
+parse output breaks
+१३ 13
+१३.३१ 13.31
+// J and K stop parsing at the apostrophe
+123'456 123456 JK
+524'1.3 5241.3 JK
+३'१ 31 JK
+
+test parse with European-style comma/period
+set locale pt
+set pattern #
+begin
+parse output breaks
+// J and K get 123
+123.456 123456 JK
+123,456 123.456
+987,654.321 987.654
+987,654 321 987.654
+// J and K get 987
+987.654,321 987654.321 JK
+
test select
set locale sr
begin
test parse currency ISO
set pattern 0.00 \u00a4\u00a4;(#) \u00a4\u00a4
-set locale en_US
+set locale en_GB
begin
parse output outputCurrency breaks
-$53.45 53.45 USD
+53.45 fail GBP
+£53.45 53.45 GBP
+$53.45 fail USD
53.45 USD 53.45 USD
+53.45 GBP 53.45 GBP
USD 53.45 53.45 USD J
-53.45USD fail USD
+53.45USD 53.45 USD CJ
USD53.45 53.45 USD
(7.92) USD -7.92 USD
-(7.92) EUR -7.92 EUR
+(7.92) GBP -7.92 GBP
(7.926) USD -7.926 USD
-(7.926 USD) fail USD
-(USD 7.926) fail USD
-USD (7.926) fail USD
-USD (7.92) fail USD
-(7.92)USD fail USD
-USD(7.92) fail USD
+(7.926 USD) -7.926 USD CJ
+(USD 7.926) -7.926 USD CJ
+USD (7.926) -7.926 USD CJ
+USD (7.92) -7.92 USD CJ
+(7.92)USD -7.92 USD CJ
+USD(7.92) -7.92 USD CJ
(8) USD -8 USD
--8 USD fail USD
+-8 USD -8 USD CJ
67 USD 67 USD
53.45$ fail USD
US Dollars 53.45 53.45 USD J
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
-53.45US Dollars fail USD
+53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
US Dollat53.45 fail USD
-53.45US Dollar fail USD
-US Dollars (53.45) fail USD
+53.45US Dollar 53.45 USD CJ
+US Dollars (53.45) -53.45 USD CJ
(53.45) US Dollars -53.45 USD
-US Dollar (53.45) fail USD
+(53.45) Euros -53.45 EUR
+US Dollar (53.45) -53.45 USD CJ
(53.45) US Dollar -53.45 USD
-US Dollars(53.45) fail USD
-(53.45)US Dollars fail USD
-US Dollar(53.45) fail USD
+US Dollars(53.45) -53.45 USD CJ
+(53.45)US Dollars -53.45 USD CJ
+US Dollar(53.45) -53.45 USD CJ
US Dollat(53.45) fail USD
-(53.45)US Dollar fail USD
+(53.45)US Dollar -53.45 USD CJ
test parse currency ISO negative
set pattern 0.00 \u00a4\u00a4;-# \u00a4\u00a4
-set locale en_US
+set locale en_GB
begin
parse output outputCurrency breaks
-$53.45 53.45 USD
+53.45 fail GBP
+£53.45 53.45 GBP
+$53.45 fail USD
53.45 USD 53.45 USD
+53.45 GBP 53.45 GBP
USD 53.45 53.45 USD J
-53.45USD fail USD
+53.45USD 53.45 USD CJ
USD53.45 53.45 USD
-7.92 USD -7.92 USD
--7.92 EUR -7.92 EUR
+-7.92 GBP -7.92 GBP
-7.926 USD -7.926 USD
-USD -7.926 fail USD
--7.92USD fail USD
-USD-7.92 fail USD
+USD -7.926 -7.926 USD CJ
+-7.92USD -7.92 USD CJ
+USD-7.92 -7.92 USD CJ
-8 USD -8 USD
67 USD 67 USD
53.45$ fail USD
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
-53.45US Dollars fail USD
+53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
US Dollat53.45 fail USD
-53.45US Dollar fail USD
+53.45US Dollar 53.45 USD CJ
test parse currency long
set pattern 0.00 \u00a4\u00a4\u00a4;(#) \u00a4\u00a4\u00a4
-set locale en_US
+set locale en_GB
begin
parse output outputCurrency breaks
-$53.45 53.45 USD
+// J throws a NullPointerException on the first case
+53.45 fail GBP
+£53.45 53.45 GBP
+$53.45 fail USD
53.45 USD 53.45 USD
+53.45 GBP 53.45 GBP
USD 53.45 53.45 USD J
-// See ticket 11735
-53.45USD fail USD J
+53.45USD 53.45 USD CJ
USD53.45 53.45 USD
(7.92) USD -7.92 USD
+(7.92) GBP -7.92 GBP
(7.926) USD -7.926 USD
-(7.926 USD) fail USD
-(USD 7.926) fail USD
-USD (7.926) fail USD
-USD (7.92) fail USD
-(7.92)USD fail USD
-USD(7.92) fail USD
+(7.926 USD) -7.926 USD CJ
+(USD 7.926) -7.926 USD CJ
+USD (7.926) -7.926 USD CJ
+USD (7.92) -7.92 USD CJ
+(7.92)USD -7.92 USD CJ
+USD(7.92) -7.92 USD CJ
(8) USD -8 USD
-// See ticket 11735
--8 USD fail USD J
+-8 USD -8 USD CJ
67 USD 67 USD
-// See ticket 11735
-53.45$ fail USD J
+// J throws a NullPointerException on the next case
+53.45$ fail USD
US Dollars 53.45 53.45 USD J
53.45 US Dollars 53.45 USD
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
-// See ticket 11735
-53.45US Dollars fail USD J
+53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
US Dollat53.45 fail USD
-// See ticket 11735
-53.45US Dollar fail USD J
+53.45US Dollar 53.45 USD CJ
test parse currency short
set pattern 0.00 \u00a4;(#) \u00a4
-set locale en_US
+set locale en_GB
begin
parse output outputCurrency breaks
-$53.45 53.45 USD
+53.45 fail GBP
+£53.45 53.45 GBP
+$53.45 fail USD
53.45 USD 53.45 USD
+53.45 GBP 53.45 GBP
USD 53.45 53.45 USD J
-53.45USD fail USD
+53.45USD 53.45 USD CJ
USD53.45 53.45 USD
(7.92) USD -7.92 USD
+(7.92) GBP -7.92 GBP
(7.926) USD -7.926 USD
-(7.926 USD) fail USD
-(USD 7.926) fail USD
-USD (7.926) fail USD
-USD (7.92) fail USD
-(7.92)USD fail USD
-USD(7.92) fail USD
+(7.926 USD) -7.926 USD CJ
+(USD 7.926) -7.926 USD CJ
+USD (7.926) -7.926 USD CJ
+USD (7.92) -7.92 USD CJ
+(7.92)USD -7.92 USD CJ
+USD(7.92) -7.92 USD CJ
(8) USD -8 USD
--8 USD fail USD
+-8 USD -8 USD CJ
67 USD 67 USD
53.45$ fail USD
US Dollars 53.45 53.45 USD J
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
-53.45US Dollars fail USD
+53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
US Dollat53.45 fail USD
-53.45US Dollar fail USD
+53.45US Dollar 53.45 USD CJ
test parse currency short prefix
set pattern \u00a40.00;(\u00a4#)
-set locale en_US
+set locale en_GB
begin
parse output outputCurrency breaks
-$53.45 53.45 USD
-53.45 USD fail USD
+53.45 fail GBP
+£53.45 53.45 GBP
+$53.45 fail USD
+53.45 USD 53.45 USD CJ
+53.45 GBP 53.45 GBP CJ
USD 53.45 53.45 USD J
-53.45USD fail USD
+53.45USD 53.45 USD CJ
USD53.45 53.45 USD
-(7.92) USD fail USD
-(7.926) USD fail USD
-(7.926 USD) fail USD
+// S fails these because '(' is an incomplete prefix.
+(7.92) USD -7.92 USD CJS
+(7.92) GBP -7.92 GBP CJS
+(7.926) USD -7.926 USD CJS
+(7.926 USD) -7.926 USD CJS
(USD 7.926) -7.926 USD J
-USD (7.926) fail USD
-USD (7.92) fail USD
-(7.92)USD fail USD
-USD(7.92) fail USD
-(8) USD fail USD
--8 USD fail USD
-67 USD fail USD
+USD (7.926) -7.926 USD CJS
+USD (7.92) -7.92 USD CJS
+(7.92)USD -7.92 USD CJS
+USD(7.92) -7.92 USD CJS
+(8) USD -8 USD CJS
+-8 USD -8 USD CJ
+67 USD 67 USD CJ
53.45$ fail USD
US Dollars 53.45 53.45 USD J
53.45 US Dollars 53.45 USD
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
-53.45US Dollars fail USD
+53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
-53.45US Dollar fail USD
+53.45US Dollar 53.45 USD CJ
test format foreign currency
set locale fa_IR
+set currency IRR
begin
pattern format output breaks
\u00a4\u00a4\u00a4 0.00;\u00a4\u00a4\u00a4 # 1235 \u0631\u06cc\u0627\u0644 \u0627\u06cc\u0631\u0627\u0646 \u06F1\u06F2\u06F3\u06F5 K
Euro 7.82 7.82 EUR
Euros 7.82 7.82 EUR
+test parse currency without currency mode
+// Should accept a symbol associated with the currency specified by the API,
+// but should not traverse the full currency data.
+set locale en_US
+set pattern \u00a4#,##0.00
+begin
+parse currency output breaks
+$52.41 USD 52.41
+USD52.41 USD 52.41 K
+\u20ac52.41 USD fail
+EUR52.41 USD fail
+$52.41 EUR fail
+USD52.41 EUR fail
+\u20ac52.41 EUR 52.41 K
+EUR52.41 EUR 52.41
+
test parse currency ISO strict
set pattern 0.00 \u00a4\u00a4;(#) \u00a4\u00a4
set locale en_US
format output breaks
-0.99 -0 JK
+test parse decimalPatternMatchRequired
+set locale en
+set decimalPatternMatchRequired 1
+begin
+pattern parse output breaks
+// K doesn't support this feature.
+0 123 123
+0 123. fail JK
+0 1.23 fail JK
+0 -513 -513
+0 -513. fail JK
+0 -5.13 fail JK
+0.0 123 fail K
+0.0 123. 123
+0.0 1.23 1.23
+0.0 -513 fail K
+0.0 -513. -513
+0.0 -5.13 -5.13
+
+test parse minus sign
+set locale en
+set pattern #
+begin
+parse output breaks
+-123 -123
+- 123 -123 JK
+ -123 -123 JK
+ - 123 -123 JK
+123- -123 JK
+123 - -123 JK
+
+test parse case sensitive
+set locale en
+set lenient 1
+set pattern Aa#
+begin
+parse parseCaseSensitive output breaks
+Aa1.23 1 1.23
+Aa1.23 0 1.23
+AA1.23 1 fail
+// J and K do not support case-insensitive parsing for prefix/suffix.
+// J supports it for the exponent separator, but not K.
+AA1.23 0 1.23 JK
+aa1.23 1 fail
+aa1.23 0 1.23 JK
+Aa1.23E3 1 1230
+Aa1.23E3 0 1230
+Aa1.23e3 1 1.23 J
+Aa1.23e3 0 1230 K
+NaN 1 NaN K
+NaN 0 NaN K
+nan 1 fail
+nan 0 NaN JK
+
+test parse infinity and scientific notation overflow
+set locale en
+begin
+parse output breaks
+NaN NaN K
+// JDK returns zero
+1E999999999999999 Inf K
+-1E999999999999999 -Inf K
+1E-99999999999999 0.0
+// Note: The test suite code doesn't properly check for 0.0 vs. -0.0
+-1E-99999999999999 -0.0
+1E2147483648 Inf K
+1E2147483647 Inf K
+1E2147483646 1E2147483646
+1E-2147483649 0
+1E-2147483648 0
+// S returns zero here
+1E-2147483647 1E-2147483647 S
+1E-2147483646 1E-2147483646
+
+test format push limits
+set locale en
+set minFractionDigits 2
+set roundingMode halfDown
+begin
+maxFractionDigits format output breaks
+100 987654321987654321 987654321987654321.00
+100 987654321.987654321 987654321.987654321
+100 9999999999999.9950000000001 9999999999999.9950000000001
+2 9999999999999.9950000000001 10000000000000.00
+2 9999999.99499999 9999999.99
+// K doesn't support halfDowm rounding mode?
+2 9999999.995 9999999.99 K
+2 9999999.99500001 10000000.00
+100 56565656565656565656565656565656565656565656565656565656565656 56565656565656565656565656565656565656565656565656565656565656.00
+100 454545454545454545454545454545.454545454545454545454545454545 454545454545454545454545454545.454545454545454545454545454545
+100 0.0000000000000000000123 0.0000000000000000000123
+100 -78787878787878787878787878787878 -78787878787878787878787878787878.00
+100 -8989898989898989898989.8989898989898989 -8989898989898989898989.8989898989898989
+
+test ticket 11230
+set locale en
+set pattern ###
+begin
+parse output breaks
+// K and J return null; S returns 99
+ 9 9 9 JKS
+// K and J return null
+ 9 999 9999 JK
+
DecimalFormatSymbols US = new DecimalFormatSymbols(Locale.US);
DecimalFormat f = new DecimalFormat("#,##,###", US);
expect(f, new Long(123456789), "12,34,56,789");
- expectPat(f, "#,##,###");
+ expectPat(f, "#,##,##0");
f.applyPattern("#,###");
f.setSecondaryGroupingSize(4);
expect(f, new Long(123456789), "12,3456,789");
- expectPat(f, "#,####,###");
+ expectPat(f, "#,####,##0");
// On Sun JDK 1.2-1.3, the hi_IN locale uses '0' for a zero digit,
// but on IBM JDK 1.2-1.3, the locale uses U+0966.
fmt.setFormatWidth(16);
// 12 34567890123456
- expectPat(fmt, "AA*^#,###,##0.00ZZ");
+ expectPat(fmt, "AA*^#####,##0.00ZZ");
}
private void expectPat(DecimalFormat fmt, String exp) {
expect(new DecimalFormat[] { new DecimalFormat("#E0", US),
new DecimalFormat("##E0", US),
new DecimalFormat("####E0", US),
- new DecimalFormat("0E0", US),
- new DecimalFormat("00E0", US),
- new DecimalFormat("000E0", US),
+ new DecimalFormat("0E0", US),
+ new DecimalFormat("00E0", US),
+ new DecimalFormat("000E0", US),
},
new Long(45678000),
new String[] { "4.5678E7",
"45.678E6",
"4567.8E4",
"5E7",
- "46E6",
+ "46E6",
"457E5",
}
);
new Long(-1000000000), "(1,000,000,000.00)",
});
}
-
+
private void expect(NumberFormat fmt, Object[] data) {
for (int i=0; i<data.length; i+=2) {
expect(fmt, (Number) data[i], (String) data[i+1]);
}
}
-
+
private void expect(Object fmto, Object numo, Object expo) {
NumberFormat fmt = null, fmts[] = null;
Number num = null, nums[] = null;
if (!s.equals("-0.1")) {
errln("FAIL");
}
- }
+ }
@Test
public void TestBigDecimalJ28() {
import java.text.CharacterIterator;
import java.text.FieldPosition;
import java.text.ParsePosition;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
import java.util.Locale;
-import java.util.Map;
import org.junit.Test;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.text.CompactDecimalFormat;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
-import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.DecimalFormat;
import com.ibm.icu.text.NumberFormat;
-import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ULocale;
{1234567890123f, "1.2T"},
{12345678901234f, "12T"},
{123456789012345f, "120T"},
- {12345678901234567890f, "12000000T"},
+ {12345678901234567890f, "12,000,000T"},
};
Object[][] SerbianTestDataShort = {
- {1234, "1,2\u00A0\u0445\u0438\u0459."},
+ {1, "1"},
+ {12, "12"},
+ {123, "120"},
{12345, "12\u00a0хиљ."},
{20789, "21\u00a0хиљ."},
{123456, "120\u00a0хиљ."},
};
Object[][] SerbianTestDataLong = {
+ {1, "1"},
+ {12, "12"},
+ {123, "120"},
{1234, "1,2 хиљаде"},
{12345, "12 хиљада"},
{21789, "22 хиљаде"},
};
Object[][] SerbianTestDataLongNegative = {
+ {-1, "-1"},
+ {-12, "-12"},
+ {-123, "-120"},
{-1234, "-1,2 хиљаде"},
{-12345, "-12 хиљада"},
{-21789, "-22 хиљаде"},
};
Object[][] JapaneseTestData = {
+ {1f, "1"},
+ {12f, "12"},
+ {123f, "120"},
{1234f, "1200"},
{12345f, "1.2万"},
{123456f, "12万"},
};
Object[][] ChineseCurrencyTestData = {
- // The first one should really have a ¥ in front, but the CLDR data is
- // incorrect. See http://unicode.org/cldr/trac/ticket/9298 and update
- // this test case when the CLDR ticket is fixed.
+ {new CurrencyAmount(1f, Currency.getInstance("CNY")), "¥1"},
+ {new CurrencyAmount(12f, Currency.getInstance("CNY")), "¥12"},
+ {new CurrencyAmount(123f, Currency.getInstance("CNY")), "¥120"},
{new CurrencyAmount(1234f, Currency.getInstance("CNY")), "¥1.2千"},
{new CurrencyAmount(12345f, Currency.getInstance("CNY")), "¥1.2万"},
{new CurrencyAmount(123456f, Currency.getInstance("CNY")), "¥12万"},
{new CurrencyAmount(123456789012345f, Currency.getInstance("CNY")), "¥120兆"},
};
Object[][] GermanCurrencyTestData = {
+ {new CurrencyAmount(1f, Currency.getInstance("EUR")), "1 €"},
+ {new CurrencyAmount(12f, Currency.getInstance("EUR")), "12 €"},
+ {new CurrencyAmount(123f, Currency.getInstance("EUR")), "120 €"},
{new CurrencyAmount(1234f, Currency.getInstance("EUR")), "1,2 Tsd. €"},
{new CurrencyAmount(12345f, Currency.getInstance("EUR")), "12 Tsd. €"},
{new CurrencyAmount(123456f, Currency.getInstance("EUR")), "120 Tsd. €"},
{new CurrencyAmount(123456789012345f, Currency.getInstance("EUR")), "120 Bio. €"},
};
Object[][] EnglishCurrencyTestData = {
+ {new CurrencyAmount(1f, Currency.getInstance("USD")), "$1"},
+ {new CurrencyAmount(12f, Currency.getInstance("USD")), "$12"},
+ {new CurrencyAmount(123f, Currency.getInstance("USD")), "$120"},
{new CurrencyAmount(1234f, Currency.getInstance("USD")), "$1.2K"},
{new CurrencyAmount(12345f, Currency.getInstance("USD")), "$12K"},
{new CurrencyAmount(123456f, Currency.getInstance("USD")), "$120K"},
};
Object[][] SwahiliTestData = {
+ {1f, "1"},
+ {12f, "12"},
+ {123f, "120"},
{1234f, "elfu\u00a01.2"},
{12345f, "elfu\u00a012"},
{123456f, "elfu\u00A0120"},
{123456789012f, "B120"},
{1234567890123f, "T1.2"},
{12345678901234f, "T12"},
- {12345678901234567890f, "T12000000"},
+ {12345678901234567890f, "T12,000,000"},
};
Object[][] CsTestDataShort = {
+ {1, "1"},
+ {12, "12"},
+ {123, "120"},
{1000, "1\u00a0tis."},
{1500, "1,5\u00a0tis."},
{5000, "5\u00a0tis."},
};
Object[][] SwahiliTestDataNegative = {
- {-1234f, "elfu\u00a0-1.2"},
- {-12345f, "elfu\u00a0-12"},
- {-123456f, "elfu\u00A0-120"},
- {-1234567f, "M-1.2"},
- {-12345678f, "M-12"},
- {-123456789f, "M-120"},
- {-1234567890f, "B-1.2"},
- {-12345678901f, "B-12"},
- {-123456789012f, "B-120"},
- {-1234567890123f, "T-1.2"},
- {-12345678901234f, "T-12"},
- {-12345678901234567890f, "T-12000000"},
+ {-1f, "-1"},
+ {-12f, "-12"},
+ {-123f, "-120"},
+ {-1234f, "-elfu\u00a01.2"},
+ {-12345f, "-elfu\u00a012"},
+ {-123456f, "-elfu\u00A0120"},
+ {-1234567f, "-M1.2"},
+ {-12345678f, "-M12"},
+ {-123456789f, "-M120"},
+ {-1234567890f, "-B1.2"},
+ {-12345678901f, "-B12"},
+ {-123456789012f, "-B120"},
+ {-1234567890123f, "-T1.2"},
+ {-12345678901234f, "-T12"},
+ {-12345678901234567890f, "-T12,000,000"},
};
Object[][] TestACoreCompactFormatList = {
{2000, "2Ks$s"},
};
- @Test
- public void TestACoreCompactFormat() {
- Map<String,String[][]> affixes = new HashMap();
- affixes.put("one", new String[][] {
- {"","",}, {"","",}, {"","",},
- {"","K"}, {"","K"}, {"","K"},
- {"","M"}, {"","M"}, {"","M"},
- {"","B"}, {"","B"}, {"","B"},
- {"","T"}, {"","T"}, {"","T"},
- });
- affixes.put("other", new String[][] {
- {"","",}, {"","",}, {"","",},
- {"","Ks"}, {"","Ks"}, {"","Ks"},
- {"","Ms"}, {"","Ms"}, {"","Ms"},
- {"","Bs"}, {"","Bs"}, {"","Bs"},
- {"","Ts"}, {"","Ts"}, {"","Ts"},
- });
-
- Map<String,String[]> currencyAffixes = new HashMap();
- currencyAffixes.put("one", new String[] {"", "$"});
- currencyAffixes.put("other", new String[] {"", "$s"});
-
- long[] divisors = new long[] {
- 0,0,0,
- 1000, 1000, 1000,
- 1000000, 1000000, 1000000,
- 1000000000L, 1000000000L, 1000000000L,
- 1000000000000L, 1000000000000L, 1000000000000L};
- long[] divisors_err = new long[] {
- 0,0,0,
- 13, 13, 13,
- 1000000, 1000000, 1000000,
- 1000000000L, 1000000000L, 1000000000L,
- 1000000000000L, 1000000000000L, 1000000000000L};
- checkCore(affixes, null, divisors, TestACoreCompactFormatList);
- checkCore(affixes, currencyAffixes, divisors, TestACoreCompactFormatListCurrency);
- try {
- checkCore(affixes, null, divisors_err, TestACoreCompactFormatList);
- } catch(AssertionError e) {
- // Exception expected, thus return.
- return;
- }
- fail("Error expected but passed");
- }
-
- private void checkCore(Map<String, String[][]> affixes, Map<String, String[]> currencyAffixes, long[] divisors, Object[][] testItems) {
- Collection<String> debugCreationErrors = new LinkedHashSet();
- CompactDecimalFormat cdf = new CompactDecimalFormat(
- "#,###.00",
- DecimalFormatSymbols.getInstance(new ULocale("fr")),
- CompactStyle.SHORT, PluralRules.createRules("one: j is 1 or f is 1"),
- divisors, affixes, currencyAffixes,
- debugCreationErrors
- );
- if (debugCreationErrors.size() != 0) {
- for (String s : debugCreationErrors) {
- errln("Creation error: " + s);
- }
- } else {
- checkCdf("special cdf ", cdf, testItems);
- }
- }
+ // TODO(sffc): Re-write these tests for the new CompactDecimalFormat pipeline
+
+// @Test
+// public void TestACoreCompactFormat() {
+// Map<String,String[][]> affixes = new HashMap();
+// affixes.put("one", new String[][] {
+// {"","",}, {"","",}, {"","",},
+// {"","K"}, {"","K"}, {"","K"},
+// {"","M"}, {"","M"}, {"","M"},
+// {"","B"}, {"","B"}, {"","B"},
+// {"","T"}, {"","T"}, {"","T"},
+// });
+// affixes.put("other", new String[][] {
+// {"","",}, {"","",}, {"","",},
+// {"","Ks"}, {"","Ks"}, {"","Ks"},
+// {"","Ms"}, {"","Ms"}, {"","Ms"},
+// {"","Bs"}, {"","Bs"}, {"","Bs"},
+// {"","Ts"}, {"","Ts"}, {"","Ts"},
+// });
+//
+// Map<String,String[]> currencyAffixes = new HashMap();
+// currencyAffixes.put("one", new String[] {"", "$"});
+// currencyAffixes.put("other", new String[] {"", "$s"});
+//
+// long[] divisors = new long[] {
+// 0,0,0,
+// 1000, 1000, 1000,
+// 1000000, 1000000, 1000000,
+// 1000000000L, 1000000000L, 1000000000L,
+// 1000000000000L, 1000000000000L, 1000000000000L};
+// long[] divisors_err = new long[] {
+// 0,0,0,
+// 13, 13, 13,
+// 1000000, 1000000, 1000000,
+// 1000000000L, 1000000000L, 1000000000L,
+// 1000000000000L, 1000000000000L, 1000000000000L};
+// checkCore(affixes, null, divisors, TestACoreCompactFormatList);
+// checkCore(affixes, currencyAffixes, divisors, TestACoreCompactFormatListCurrency);
+// try {
+// checkCore(affixes, null, divisors_err, TestACoreCompactFormatList);
+// } catch(AssertionError e) {
+// // Exception expected, thus return.
+// return;
+// }
+// fail("Error expected but passed");
+// }
+
+// private void checkCore(Map<String, String[][]> affixes, Map<String, String[]> currencyAffixes, long[] divisors, Object[][] testItems) {
+// Collection<String> debugCreationErrors = new LinkedHashSet();
+// CompactDecimalFormat cdf = new CompactDecimalFormat(
+// "#,###.00",
+// DecimalFormatSymbols.getInstance(new ULocale("fr")),
+// CompactStyle.SHORT, PluralRules.createRules("one: j is 1 or f is 1"),
+// divisors, affixes, currencyAffixes,
+// debugCreationErrors
+// );
+// if (debugCreationErrors.size() != 0) {
+// for (String s : debugCreationErrors) {
+// errln("Creation error: " + s);
+// }
+// } else {
+// checkCdf("special cdf ", cdf, testItems);
+// }
+// }
@Test
public void TestDefaultSignificantDigits() {
- // We are expecting two significant digits as default.
+ // We are expecting two significant digits for compact formats with one or two zeros,
+ // and rounded to the unit for compact formats with three or more zeros.
CompactDecimalFormat cdf =
CompactDecimalFormat.getInstance(ULocale.ENGLISH, CompactStyle.SHORT);
+ assertEquals("Default significant digits", "120K", cdf.format(123456));
assertEquals("Default significant digits", "12K", cdf.format(12345));
assertEquals("Default significant digits", "1.2K", cdf.format(1234));
assertEquals("Default significant digits", "120", cdf.format(123));
CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(
ULocale.ENGLISH, CompactStyle.LONG);
BigInteger source_int = new BigInteger("31415926535897932384626433");
- assertEquals("BigInteger format wrong: ", "31,000,000,000,000 trillion",
+ cdf.setMaximumFractionDigits(0);
+ assertEquals("BigInteger format wrong: ", "31,415,926,535,898 trillion",
cdf.format(source_int));
BigDecimal source_dec = new BigDecimal(source_int);
- assertEquals("BigDecimal format wrong: ", "31,000,000,000,000 trillion",
+ assertEquals("BigDecimal format wrong: ", "31,415,926,535,898 trillion",
cdf.format(source_dec));
}
result = cdf.format(new CurrencyAmount(43000f, Currency.getInstance("USD")));
assertEquals("CDF should correctly format 43000 with currency in 'ar'", "US$ ٤٣ ألف", result);
result = cdf.format(new CurrencyAmount(-43000f, Currency.getInstance("USD")));
- assertEquals("CDF should correctly format -43000 with currency in 'ar'", "US$ -٤٣ ألف", result);
+ assertEquals("CDF should correctly format -43000 with currency in 'ar'", "-US$ ٤٣ ألف", result);
// Extra locale with different positive/negative formats
cdf = CompactDecimalFormat.getInstance(new ULocale("fi"), CompactDecimalFormat.CompactStyle.SHORT);
result = cdf.format(new CurrencyAmount(123000, Currency.getInstance("EUR")));
assertEquals("CDF should correctly format 123000 with currency in 'it'", "120000 €", result);
}
+
+ @Test
+ public void TestBug11319() {
+ if (logKnownIssue("11319", "CDF does not fall back from zh-Hant-HK to zh-Hant")) {
+ return;
+ }
+
+ CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(new ULocale("zh-Hant-HK"), CompactStyle.SHORT);
+ String result = cdf.format(958000000L);
+ assertEquals("CDF should correctly format 958 million in zh-Hant-HK", "9.6億", result);
+ }
+
+ @Test
+ public void TestBug12975() {
+ ULocale locale = new ULocale("it");
+ CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(locale, CompactStyle.SHORT);
+ String resultCdf = cdf.format(120000);
+ DecimalFormat df = (DecimalFormat) DecimalFormat.getInstance(locale);
+ String resultDefault = df.format(120000);
+ assertEquals("CompactDecimalFormat should use default pattern when compact pattern is unavailable",
+ resultDefault, resultCdf);
+ }
+
+ @Test
+ public void TestBug11534() {
+ ULocale locale = new ULocale("pt_PT");
+ CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(locale, CompactStyle.SHORT);
+ String result = cdf.format(1000);
+ assertEquals("pt_PT should fall back to pt", "1 mil", result);
+ }
+
+ @Test
+ public void TestBug12181() {
+ ULocale loc = ULocale.ENGLISH;
+ CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(loc, CompactStyle.SHORT);
+ String s = cdf.format(-1500);
+ assertEquals("Should work with negative numbers", "-1.5K", s);
+ }
}
* <p>
* In addition each attribute is listed in the fieldOrdering static array which specifies
* The order that attributes are printed whenever there is a test failure.
- * <p>
+ * <p>
* To add a new attribute, first create a public field for it.
* Next, add the attribute name to the fieldOrdering array.
* Finally, create a setter method for it.
- *
+ *
* @author rocketman
*/
-public class NumberFormatTestData {
-
+public class DataDrivenNumberFormatTestData {
+
/**
* The locale.
*/
public ULocale locale = null;
-
+
/**
* The currency.
*/
public Currency currency = null;
-
+
/**
* The pattern to initialize the formatter, for example 0.00"
*/
public String pattern = null;
-
+
/**
* The value to format as a string. For example 1234.5 would be "1234.5"
*/
public String format = null;
-
+
/**
* The formatted value.
*/
public String output = null;
-
+
/**
* Field for arbitrary comments.
*/
public String comment = null;
-
+
public Integer minIntegerDigits = null;
public Integer maxIntegerDigits = null;
public Integer minFractionDigits = null;
public String plural = null;
public Integer parseIntegerOnly = null;
public Integer decimalPatternMatchRequired = null;
+ public Integer parseCaseSensitive = null;
public Integer parseNoExponent = null;
public String outputCurrency = null;
-
-
-
+
+
+
/**
* nothing or empty means that test ought to work for both C and JAVA;
* "C" means test is known to fail in C. "J" means test is known to fail in JAVA.
* "CJ" means test is known to fail for both languages.
*/
public String breaks = null;
-
+
private static Map<String, Integer> roundingModeMap =
new HashMap<String, Integer>();
-
+
static {
roundingModeMap.put("ceiling", BigDecimal.ROUND_CEILING);
roundingModeMap.put("floor", BigDecimal.ROUND_FLOOR);
roundingModeMap.put("halfUp", BigDecimal.ROUND_HALF_UP);
roundingModeMap.put("unnecessary", BigDecimal.ROUND_UNNECESSARY);
}
-
+
private static Map<String, Currency.CurrencyUsage> currencyUsageMap =
new HashMap<String, Currency.CurrencyUsage>();
-
+
static {
currencyUsageMap.put("standard", Currency.CurrencyUsage.STANDARD);
currencyUsageMap.put("cash", Currency.CurrencyUsage.CASH);
}
-
+
private static Map<String, Integer> padPositionMap =
new HashMap<String, Integer>();
-
+
static {
// TODO: Fix so that it doesn't depend on DecimalFormat.
padPositionMap.put("beforePrefix", DecimalFormat.PAD_BEFORE_PREFIX);
padPositionMap.put("beforeSuffix", DecimalFormat.PAD_BEFORE_SUFFIX);
padPositionMap.put("afterSuffix", DecimalFormat.PAD_AFTER_SUFFIX);
}
-
+
private static Map<String, Integer> formatStyleMap =
new HashMap<String, Integer>();
-
+
static {
formatStyleMap.put("decimal", NumberFormat.NUMBERSTYLE);
formatStyleMap.put("currency", NumberFormat.CURRENCYSTYLE);
formatStyleMap.put("currencyAccounting", NumberFormat.ACCOUNTINGCURRENCYSTYLE);
formatStyleMap.put("cashCurrency", NumberFormat.CASHCURRENCYSTYLE);
}
-
+
// Add any new fields here. On test failures, fields are printed in the same order they
// appear here.
private static String[] fieldOrdering = {
"parseNoExponent",
"outputCurrency"
};
-
+
static {
HashSet<String> set = new HashSet<String>();
for (String s : fieldOrdering) {
if (!set.add(s)) {
- throw new ExceptionInInitializerError(s + "is a duplicate field.");
+ throw new ExceptionInInitializerError(s + "is a duplicate field.");
}
}
}
-
+
private static <T> T fromString(Map<String, T> map, String key) {
T value = map.get(key);
if (value == null) {
}
return value;
}
-
+
// start field setters.
// add setter for each new field in this block.
-
+
public void setLocale(String value) {
locale = new ULocale(value);
}
-
+
public void setCurrency(String value) {
currency = Currency.getInstance(value);
}
-
+
public void setPattern(String value) {
pattern = value;
}
-
+
public void setFormat(String value) {
format = value;
}
-
+
public void setOutput(String value) {
output = value;
}
-
+
public void setComment(String value) {
comment = value;
}
-
+
public void setMinIntegerDigits(String value) {
minIntegerDigits = Integer.valueOf(value);
}
-
+
public void setMaxIntegerDigits(String value) {
maxIntegerDigits = Integer.valueOf(value);
}
-
+
public void setMinFractionDigits(String value) {
minFractionDigits = Integer.valueOf(value);
}
-
+
public void setMaxFractionDigits(String value) {
maxFractionDigits = Integer.valueOf(value);
}
-
+
public void setMinGroupingDigits(String value) {
minGroupingDigits = Integer.valueOf(value);
}
-
+
public void setBreaks(String value) {
breaks = value;
}
-
+
public void setUseSigDigits(String value) {
useSigDigits = Integer.valueOf(value);
}
-
+
public void setMinSigDigits(String value) {
minSigDigits = Integer.valueOf(value);
}
-
+
public void setMaxSigDigits(String value) {
maxSigDigits = Integer.valueOf(value);
}
-
+
public void setUseGrouping(String value) {
useGrouping = Integer.valueOf(value);
}
-
+
public void setMultiplier(String value) {
multiplier = Integer.valueOf(value);
}
-
+
public void setRoundingIncrement(String value) {
roundingIncrement = Double.valueOf(value);
}
-
+
public void setFormatWidth(String value) {
formatWidth = Integer.valueOf(value);
}
-
+
public void setPadCharacter(String value) {
padCharacter = value;
}
-
+
public void setUseScientific(String value) {
useScientific = Integer.valueOf(value);
}
-
+
public void setGrouping(String value) {
grouping = Integer.valueOf(value);
}
-
+
public void setGrouping2(String value) {
grouping2 = Integer.valueOf(value);
}
-
+
public void setRoundingMode(String value) {
roundingMode = fromString(roundingModeMap, value);
}
-
+
public void setCurrencyUsage(String value) {
currencyUsage = fromString(currencyUsageMap, value);
}
-
+
public void setMinimumExponentDigits(String value) {
minimumExponentDigits = Integer.valueOf(value);
}
-
+
public void setExponentSignAlwaysShown(String value) {
exponentSignAlwaysShown = Integer.valueOf(value);
}
-
+
public void setDecimalSeparatorAlwaysShown(String value) {
decimalSeparatorAlwaysShown = Integer.valueOf(value);
}
-
+
public void setPadPosition(String value) {
padPosition = fromString(padPositionMap, value);
}
-
+
public void setPositivePrefix(String value) {
positivePrefix = value;
}
-
+
public void setPositiveSuffix(String value) {
positiveSuffix = value;
}
-
+
public void setNegativePrefix(String value) {
negativePrefix = value;
}
-
+
public void setNegativeSuffix(String value) {
negativeSuffix = value;
}
-
+
public void setLocalizedPattern(String value) {
localizedPattern = value;
}
-
+
public void setToPattern(String value) {
toPattern = value;
}
-
+
public void setToLocalizedPattern(String value) {
toLocalizedPattern = value;
}
-
+
public void setStyle(String value) {
style = fromString(formatStyleMap, value);
}
-
+
public void setParse(String value) {
parse = value;
}
-
+
public void setLenient(String value) {
lenient = Integer.valueOf(value);
}
-
+
public void setPlural(String value) {
plural = value;
}
-
+
public void setParseIntegerOnly(String value) {
parseIntegerOnly = Integer.valueOf(value);
}
-
+
+ public void setParseCaseSensitive(String value) {
+ parseCaseSensitive = Integer.valueOf(value);
+ }
+
public void setDecimalPatternMatchRequired(String value) {
decimalPatternMatchRequired = Integer.valueOf(value);
}
-
+
public void setParseNoExponent(String value) {
parseNoExponent = Integer.valueOf(value);
}
-
+
public void setOutputCurrency(String value) {
outputCurrency = value;
}
-
+
// end field setters.
-
+
// start of field clearers
// Add clear methods that can be set in one test and cleared
// in the next i.e the breaks field.
-
+
public void clearBreaks() {
breaks = null;
}
-
+
public void clearUseGrouping() {
useGrouping = null;
}
-
+
public void clearGrouping2() {
grouping2 = null;
}
-
+
public void clearGrouping() {
grouping = null;
}
-
+
public void clearMinGroupingDigits() {
minGroupingDigits = null;
}
-
+
public void clearUseScientific() {
useScientific = null;
}
-
+
public void clearDecimalSeparatorAlwaysShown() {
decimalSeparatorAlwaysShown = null;
}
-
+
// end field clearers
-
+
public void setField(String fieldName, String valueString)
throws NoSuchMethodException {
Method m = getClass().getMethod(
throw new RuntimeException(e);
}
}
-
+
public void clearField(String fieldName)
throws NoSuchMethodException {
Method m = getClass().getMethod(fieldToClearer(fieldName));
throw new RuntimeException(e);
}
}
-
- public String toString() {
+
+ @Override
+ public String toString() {
StringBuilder result = new StringBuilder();
result.append("{");
boolean first = true;
+ Character.toUpperCase(fieldName.charAt(0))
+ fieldName.substring(1);
}
-
+
private static String fieldToClearer(String fieldName) {
return "clear"
+ Character.toUpperCase(fieldName.charAt(0))
package com.ibm.icu.dev.test.format;
import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
* A collection of methods to run the data driven number format test suite.
*/
public class DataDrivenNumberFormatTestUtility {
-
+
/**
* Base class for code under test.
*/
public static abstract class CodeUnderTest {
-
+
/**
* Returns the ID of the code under test. This ID is used to identify
* tests that are known to fail for this particular code under test.
public Character Id() {
return null;
}
-
+
/**
* Runs a single formatting test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
- public String format(NumberFormatTestData tuple) {
+ public String format(DataDrivenNumberFormatTestData tuple) {
+ if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
-
+
/**
* Runs a single toPattern test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
- public String toPattern(NumberFormatTestData tuple) {
+ public String toPattern(DataDrivenNumberFormatTestData tuple) {
+ if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
-
+
/**
* Runs a single parse test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
- public String parse(NumberFormatTestData tuple) {
+ public String parse(DataDrivenNumberFormatTestData tuple) {
+ if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
-
+
/**
* Runs a single parse currency test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
- public String parseCurrency(NumberFormatTestData tuple) {
+ public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
+ if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
-
+
/**
* Runs a single select test. On success, returns null.
* On failure, returns the error. This implementation just returns null.
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
- public String select(NumberFormatTestData tuple) {
+ public String select(DataDrivenNumberFormatTestData tuple) {
+ if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
}
-
+
private static enum RunMode {
SKIP_KNOWN_FAILURES,
- INCLUDE_KNOWN_FAILURES
+ INCLUDE_KNOWN_FAILURES
}
-
+
private final CodeUnderTest codeUnderTest;
private String fileLine = null;
private int fileLineNumber = 0;
- private String fileTestName = "";
- private NumberFormatTestData tuple = new NumberFormatTestData();
-
+ private String fileTestName = "";
+ private DataDrivenNumberFormatTestData tuple = new DataDrivenNumberFormatTestData();
+
/**
* Runs all the tests in the data driven test suite against codeUnderTest.
* @param fileName The name of the test file. A relative file name under
* com/ibm/icu/dev/data such as "data.txt"
* @param codeUnderTest the code under test
*/
-
+
static void runSuite(
String fileName, CodeUnderTest codeUnderTest) {
new DataDrivenNumberFormatTestUtility(codeUnderTest)
.run(fileName, RunMode.SKIP_KNOWN_FAILURES);
}
-
+
/**
* Runs every format test in data driven test suite including those
- * that are known to fail.
- *
+ * that are known to fail. If a test is supposed to fail but actually
+ * passes, an error is printed.
+ *
* @param fileName The name of the test file. A relative file name under
* com/ibm/icu/dev/data such as "data.txt"
* @param codeUnderTest the code under test
new DataDrivenNumberFormatTestUtility(codeUnderTest)
.run(fileName, RunMode.INCLUDE_KNOWN_FAILURES);
}
-
+
private DataDrivenNumberFormatTestUtility(
CodeUnderTest codeUnderTest) {
this.codeUnderTest = codeUnderTest;
}
-
+
private void run(String fileName, RunMode runMode) {
Character codeUnderTestIdObj = codeUnderTest.Id();
char codeUnderTestId =
if (fileLine != null && fileLine.charAt(0) == '\uFEFF') {
fileLine = fileLine.substring(1);
}
-
+
int state = 0;
List<String> columnValues;
List<String> columnNames = null;
if (state == 0) {
if (fileLine.startsWith("test ")) {
fileTestName = fileLine;
- tuple = new NumberFormatTestData();
+ tuple = new DataDrivenNumberFormatTestData();
} else if (fileLine.startsWith("set ")) {
if (!setTupleField()) {
return;
return;
}
}
- if (runMode == RunMode.INCLUDE_KNOWN_FAILURES
- || !breaks(codeUnderTestId)) {
- String errorMessage = isPass(tuple);
- if (errorMessage != null) {
- showError(errorMessage);
+ if (runMode == RunMode.INCLUDE_KNOWN_FAILURES || !breaks(codeUnderTestId)) {
+ String errorMessage;
+ Exception err = null;
+ boolean shouldFail = (tuple.output != null && tuple.output.equals("fail"))
+ ? !breaks(codeUnderTestId)
+ : breaks(codeUnderTestId);
+ try {
+ errorMessage = isPass(tuple);
+ } catch (Exception e) {
+ err = e;
+ errorMessage = "Exception: " + e + ": " + e.getCause();
+ }
+ if (shouldFail && errorMessage == null) {
+ showError("Expected failure, but passed");
+ } else if (!shouldFail && errorMessage != null) {
+ if (err != null) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ PrintStream ps = new PrintStream(os);
+ err.printStackTrace(ps);
+ String stackTrace = os.toString();
+ showError(errorMessage + " Stack trace: " + stackTrace.substring(0, 500));
+ } else {
+ showError(errorMessage);
+ }
}
}
}
fileLine = null;
}
} catch (Exception e) {
- showError(e.toString());
- e.printStackTrace();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ PrintStream ps = new PrintStream(os);
+ e.printStackTrace(ps);
+ String stackTrace = os.toString();
+ showError("MAJOR ERROR: " + e.toString() + " Stack trace: " + stackTrace.substring(0,500));
} finally {
try {
if (in != null) {
private static boolean isSpace(char c) {
return (c == 0x09 || c == 0x20 || c == 0x3000);
}
-
+
private boolean setTupleField() {
List<String> parts = splitBy(3, (char) 0x20);
if (parts.size() < 3) {
}
return setField(parts.get(1), parts.get(2));
}
-
+
private boolean setField(String name, String value) {
try {
tuple.setField(name, Utility.unescape(value));
return false;
}
}
-
+
private boolean clearField(String name) {
try {
tuple.clearField(name);
return false;
}
}
-
+
private void showError(String message) {
TestFmwk.errln(String.format("line %d: %s\n%s\n%s", fileLineNumber, Utility.escape(message), fileTestName,fileLine));
}
-
+
private List<String> splitBy(char delimiter) {
return splitBy(Integer.MAX_VALUE, delimiter);
}
-
+
private List<String> splitBy(int max, char delimiter) {
- ArrayList<String> result = new ArrayList<String>();
+ ArrayList<String> result = new ArrayList<String>();
int colIdx = 0;
int colStart = 0;
int len = fileLine.length();
}
result.add(fileLine.substring(colStart, len));
return result;
- }
+ }
private boolean readLine(BufferedReader in) throws IOException {
String line = in.readLine();
fileLine = idx == 0 ? "" : line;
return true;
}
-
- private String isPass(NumberFormatTestData tuple) {
+
+ private String isPass(DataDrivenNumberFormatTestData tuple) {
StringBuilder result = new StringBuilder();
if (tuple.format != null && tuple.output != null) {
String errorMessage = codeUnderTest.format(tuple);
* Corporation and others. All Rights Reserved.
**/
-/**
+/**
* Port From: JDK 1.4b1 : java.text.Format.IntlTestDecimalFormatAPI
* Source File: java/text/format/IntlTestDecimalFormatAPI.java
**/
-
+
/*
@test 1.4 98/03/06
@summary test International Decimal Format API
public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
{
/**
- * Problem 1: simply running
- * decF4.setRoundingMode(java.math.BigDecimal.ROUND_HALF_UP) does not work
+ * Problem 1: simply running
+ * decF4.setRoundingMode(java.math.BigDecimal.ROUND_HALF_UP) does not work
* as decF4.setRoundingIncrement(.0001) must also be run.
- * Problem 2: decF4.format(8.88885) does not return 8.8889 as expected.
- * You must run decF4.format(new BigDecimal(Double.valueOf(8.88885))) in
+ * Problem 2: decF4.format(8.88885) does not return 8.8889 as expected.
+ * You must run decF4.format(new BigDecimal(Double.valueOf(8.88885))) in
* order for this to work as expected.
- * Problem 3: There seems to be no way to set half up to be the default
+ * Problem 3: There seems to be no way to set half up to be the default
* rounding mode.
- * We solved the problem with the code at the bottom of this page however
+ * We solved the problem with the code at the bottom of this page however
* this is not quite general purpose enough to include in icu4j. A static
- * setDefaultRoundingMode function would solve the problem nicely. Also
- * decimal places past 20 are not handled properly. A small ammount of work
+ * setDefaultRoundingMode function would solve the problem nicely. Also
+ * decimal places past 20 are not handled properly. A small ammount of work
* would make bring this up to snuff.
*/
@Test
// problem 2
double number = 8.88885;
String expected = "8.8889";
-
+
String pat = ",##0.0000";
DecimalFormat dec = new DecimalFormat(pat);
dec.setRoundingMode(BigDecimal.ROUND_HALF_UP);
if (!str.equals(expected)) {
errln("Fail: " + number + " x \"" + pat + "\" = \"" +
str + "\", expected \"" + expected + "\"");
- }
+ }
pat = ",##0.0001";
dec = new DecimalFormat(pat);
if (!str.equals(expected)) {
errln("Fail: " + number + " x \"" + pat + "\" = \"" +
str + "\", expected \"" + expected + "\"");
- }
-
+ }
+
// testing 20 decimal places
pat = ",##0.00000000000000000001";
dec = new DecimalFormat(pat);
BigDecimal bignumber = new BigDecimal("8.888888888888888888885");
expected = "8.88888888888888888889";
-
+
dec.setRoundingMode(BigDecimal.ROUND_HALF_UP);
- str = dec.format(bignumber);
+ str = dec.format(bignumber);
if (!str.equals(expected)) {
errln("Fail: " + bignumber + " x \"" + pat + "\" = \"" +
str + "\", expected \"" + expected + "\"");
- }
-
+ }
+
}
- /**
- * This test checks various generic API methods in DecimalFormat to achieve
+ /**
+ * This test checks various generic API methods in DecimalFormat to achieve
* 100% API coverage.
*/
@Test
DecimalFormat decfmt = new DecimalFormat();
MathContext resultICU;
- MathContext comp1 = new MathContext(0, MathContext.PLAIN);
+ MathContext comp1 = new MathContext(0, MathContext.PLAIN, false, MathContext.ROUND_HALF_EVEN);
resultICU = decfmt.getMathContextICU();
if ((comp1.getDigits() != resultICU.getDigits()) ||
(comp1.getForm() != resultICU.getForm()) ||
" / expected: " + comp1.toString());
}
- MathContext comp2 = new MathContext(5, MathContext.ENGINEERING);
+ MathContext comp2 = new MathContext(5, MathContext.ENGINEERING, false, MathContext.ROUND_HALF_EVEN);
decfmt.setMathContextICU(comp2);
resultICU = decfmt.getMathContextICU();
if ((comp2.getDigits() != resultICU.getDigits()) ||
// get default rounding increment
r1 = pat.getRoundingIncrement();
- // set rounding mode with zero increment. Rounding
+ // set rounding mode with zero increment. Rounding
// increment should be set by this operation
pat.setRoundingMode(BigDecimal.ROUND_UP);
r2 = pat.getRoundingIncrement();
}
}
}
-
+
@Test
public void testJB6648()
{
DecimalFormat df = new DecimalFormat();
df.setParseStrict(true);
-
+
String numstr = new String();
-
+
String[] patterns = {
"0",
"00",
"000",
"0,000",
"0.0",
- "#000.0"
+ "#000.0"
};
-
+
for(int i=0; i < patterns.length; i++) {
df.applyPattern(patterns[i]);
- numstr = df.format(5);
+ numstr = df.format(5);
try {
Number n = df.parse(numstr);
logln("INFO: Parsed " + numstr + " -> " + n);
} catch (ParseException pe) {
errln("ERROR: Failed round trip with strict parsing.");
- }
+ }
}
-
+
df.applyPattern(patterns[1]);
- numstr = "005";
+ numstr = "005";
try {
Number n = df.parse(numstr);
logln("INFO: Successful parse for " + numstr + " with strict parse enabled. Number is " + n);
} catch (ParseException pe) {
errln("ERROR: Parse Exception encountered in strict mode: numstr -> " + numstr);
- }
-
+ }
+
}
}
s2 = pat.toPattern();
logln("Extracted pattern is " + s2);
if (!s2.equals(p1)) {
- errln("ERROR: toPattern() result did not match pattern applied");
+ errln("ERROR: toPattern() result did not match pattern applied: " + p1 + " vs " + s2);
}
String p2 = new String("#,##0.0# FF;(#,##0.0# FF)");
String s3;
s3 = pat.toLocalizedPattern();
logln("Extracted pattern is " + s3);
- if (!s3.equals(p2)) {
- errln("ERROR: toLocalizedPattern() result did not match pattern applied");
- }
+ assertEquals("ERROR: toLocalizedPattern() result did not match pattern applied", p2, s3);
// ======= Test getStaticClassID()
*******************************************************************************
*/
-/**
+/**
* Port From: ICU4C v1.8.1 : format : IntlTestDecimalFormatSymbols
* Source File: $ICU4CRoot/source/test/intltest/tsdcfmsy.cpp
**/
* Test the API of DecimalFormatSymbols; primarily a simple get/set set.
*/
@Test
- public void TestSymbols() {
- DecimalFormatSymbols fr = new DecimalFormatSymbols(Locale.FRENCH);
+ public void TestSymbols() {
+ DecimalFormatSymbols fr = new DecimalFormatSymbols(Locale.FRENCH);
DecimalFormatSymbols en = new DecimalFormatSymbols(Locale.ENGLISH);
-
+
if (en.equals(fr)) {
errln("ERROR: English DecimalFormatSymbols equal to French");
}
-
+
// just do some VERY basic tests to make sure that get/set work
-
+
char zero = en.getZeroDigit();
fr.setZeroDigit(zero);
if (fr.getZeroDigit() != en.getZeroDigit()) {
errln("ERROR: get/set ZeroDigit failed");
}
-
+
char group = en.getGroupingSeparator();
fr.setGroupingSeparator(group);
if (fr.getGroupingSeparator() != en.getGroupingSeparator()) {
errln("ERROR: get/set GroupingSeparator failed");
}
-
+
char decimal = en.getDecimalSeparator();
fr.setDecimalSeparator(decimal);
if (fr.getDecimalSeparator() != en.getDecimalSeparator()) {
errln("ERROR: get/set DecimalSeparator failed");
}
-
+
char perMill = en.getPerMill();
fr.setPerMill(perMill);
if (fr.getPerMill() != en.getPerMill()) {
errln("ERROR: get/set PerMill failed");
}
-
+
char percent = en.getPercent();
fr.setPercent(percent);
if (fr.getPercent() != en.getPercent()) {
errln("ERROR: get/set Percent failed");
}
-
+
char digit = en.getDigit();
fr.setDigit(digit);
if (fr.getPercent() != en.getPercent()) {
errln("ERROR: get/set Percent failed");
}
-
+
char patternSeparator = en.getPatternSeparator();
fr.setPatternSeparator(patternSeparator);
if (fr.getPatternSeparator() != en.getPatternSeparator()) {
errln("ERROR: get/set PatternSeparator failed");
}
-
+
String infinity = en.getInfinity();
fr.setInfinity(infinity);
String infinity2 = fr.getInfinity();
if (!infinity.equals(infinity2)) {
errln("ERROR: get/set Infinity failed");
}
-
+
String nan = en.getNaN();
fr.setNaN(nan);
String nan2 = fr.getNaN();
if (!nan.equals(nan2)) {
errln("ERROR: get/set NaN failed");
}
-
+
char minusSign = en.getMinusSign();
fr.setMinusSign(minusSign);
if (fr.getMinusSign() != en.getMinusSign()) {
errln("ERROR: get/set MinusSign failed");
}
-
+
// char exponential = en.getExponentialSymbol();
// fr.setExponentialSymbol(exponential);
// if(fr.getExponentialSymbol() != en.getExponentialSymbol()) {
// errln("ERROR: get/set Exponential failed");
// }
-
+
//DecimalFormatSymbols foo = new DecimalFormatSymbols(); //The variable is never used
-
+
en = (DecimalFormatSymbols) fr.clone();
-
+
if (!en.equals(fr)) {
errln("ERROR: Clone failed");
}
-
+
DecimalFormatSymbols sym = new DecimalFormatSymbols(Locale.US);
-
+
verify(34.5, "00.00", sym, "34.50");
sym.setDecimalSeparator('S');
verify(34.5, "00.00", sym, "34S50");
sym.setPercent('P');
verify(34.5, "00 %", sym, "3450 P");
sym.setCurrencySymbol("D");
- verify(34.5, "\u00a4##.##", sym, "D34.5");
+ verify(34.5, "\u00a4##.##", sym, "D34.50");
sym.setGroupingSeparator('|');
verify(3456.5, "0,000.##", sym, "3|456S5");
}
-
+
/** helper functions**/
public void verify(double value, String pattern, DecimalFormatSymbols sym, String expected) {
DecimalFormat df = new DecimalFormat(pattern, sym);
FieldPosition pos = new FieldPosition(-1);
buffer = df.format(value, buffer, pos);
if(!buffer.toString().equals(expected)){
- errln("ERROR: format failed after setSymbols()\n Expected" +
+ errln("ERROR: format failed after setSymbols()\n Expected" +
expected + ", Got " + buffer);
}
}
import org.junit.Test;
import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.dev.test.serializable.FormatHandler;
import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
import com.ibm.icu.impl.Pair;
import com.ibm.icu.impl.Utility;
}
}
+ @Test
+ public void testBug11966() {
+ Locale locale = new Locale("en", "AU");
+ MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE);
+ // Should not throw an exception.
+ }
+
// DO NOT DELETE THIS FUNCTION! It may appear as dead code, but we use this to generate code
// for MeasureFormat during the release process.
static Map<MeasureUnit, Pair<MeasureUnit, MeasureUnit>> getUnitsToPerParts() {
public static class MeasureFormatHandler implements SerializableTestUtility.Handler
{
+ FormatHandler.NumberFormatHandler nfh = new FormatHandler.NumberFormatHandler();
+
@Override
public Object[] getTestObjects()
{
MeasureFormat b1 = (MeasureFormat) b;
return a1.getLocale().equals(b1.getLocale())
&& a1.getWidth().equals(b1.getWidth())
- && a1.getNumberFormat().equals(b1.getNumberFormat())
- ;
+ && nfh.hasSameBehavior(a1.getNumberFormat(), b1.getNumberFormat());
}
}
}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.format;
+
+import java.math.BigDecimal;
+import java.text.ParsePosition;
+
+import org.junit.Test;
+
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.DecimalFormat_ICU58;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ULocale;
+
+public class NumberFormatDataDrivenTest {
+
+ private static ULocale EN = new ULocale("en");
+
+ private static Number toNumber(String s) {
+ if (s.equals("NaN")) {
+ return Double.NaN;
+ } else if (s.equals("-Inf")) {
+ return Double.NEGATIVE_INFINITY;
+ } else if (s.equals("Inf")) {
+ return Double.POSITIVE_INFINITY;
+ }
+ return new BigDecimal(s);
+ }
+
+ private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU58 =
+ new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
+ @Override
+ public Character Id() {
+ return 'J';
+ }
+
+ @Override
+ public String format(DataDrivenNumberFormatTestData tuple) {
+ DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
+ String actual = fmt.format(toNumber(tuple.format));
+ String expected = tuple.output;
+ if (!expected.equals(actual)) {
+ return "Expected " + expected + ", got " + actual;
+ }
+ return null;
+ }
+
+ @Override
+ public String toPattern(DataDrivenNumberFormatTestData tuple) {
+ DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
+ StringBuilder result = new StringBuilder();
+ if (tuple.toPattern != null) {
+ String expected = tuple.toPattern;
+ String actual = fmt.toPattern();
+ if (!expected.equals(actual)) {
+ result.append("Expected toPattern=" + expected + ", got " + actual);
+ }
+ }
+ if (tuple.toLocalizedPattern != null) {
+ String expected = tuple.toLocalizedPattern;
+ String actual = fmt.toLocalizedPattern();
+ if (!expected.equals(actual)) {
+ result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
+ }
+ }
+ return result.length() == 0 ? null : result.toString();
+ }
+
+ @Override
+ public String parse(DataDrivenNumberFormatTestData tuple) {
+ DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
+ ParsePosition ppos = new ParsePosition(0);
+ Number actual = fmt.parse(tuple.parse, ppos);
+ if (ppos.getIndex() == 0) {
+ return "Parse failed; got " + actual + ", but expected " + tuple.output;
+ }
+ if (tuple.output.equals("fail")) {
+ return null;
+ }
+ Number expected = toNumber(tuple.output);
+ // number types cannot be compared, this is the best we can do.
+ if (expected.doubleValue() != actual.doubleValue()
+ && !Double.isNaN(expected.doubleValue())
+ && !Double.isNaN(expected.doubleValue())) {
+ return "Expected: " + expected + ", got: " + actual;
+ }
+ return null;
+ }
+
+ @Override
+ public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
+ DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
+ ParsePosition ppos = new ParsePosition(0);
+ CurrencyAmount currAmt = fmt.parseCurrency(tuple.parse, ppos);
+ if (ppos.getIndex() == 0) {
+ return "Parse failed; got " + currAmt + ", but expected " + tuple.output;
+ }
+ if (tuple.output.equals("fail")) {
+ return null;
+ }
+ Number expected = toNumber(tuple.output);
+ Number actual = currAmt.getNumber();
+ // number types cannot be compared, this is the best we can do.
+ if (expected.doubleValue() != actual.doubleValue()
+ && !Double.isNaN(expected.doubleValue())
+ && !Double.isNaN(expected.doubleValue())) {
+ return "Expected: " + expected + ", got: " + actual;
+ }
+
+ if (!tuple.outputCurrency.equals(currAmt.getCurrency().toString())) {
+ return "Expected currency: " + tuple.outputCurrency + ", got: " + currAmt.getCurrency();
+ }
+ return null;
+ }
+
+ /**
+ * @param tuple
+ * @return
+ */
+ private DecimalFormat_ICU58 createDecimalFormat(DataDrivenNumberFormatTestData tuple) {
+
+ DecimalFormat_ICU58 fmt =
+ new DecimalFormat_ICU58(
+ tuple.pattern == null ? "0" : tuple.pattern,
+ new DecimalFormatSymbols(tuple.locale == null ? EN : tuple.locale));
+ adjustDecimalFormat(tuple, fmt);
+ return fmt;
+ }
+ /**
+ * @param tuple
+ * @param fmt
+ */
+ private void adjustDecimalFormat(
+ DataDrivenNumberFormatTestData tuple, DecimalFormat_ICU58 fmt) {
+ if (tuple.minIntegerDigits != null) {
+ fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
+ }
+ if (tuple.maxIntegerDigits != null) {
+ fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
+ }
+ if (tuple.minFractionDigits != null) {
+ fmt.setMinimumFractionDigits(tuple.minFractionDigits);
+ }
+ if (tuple.maxFractionDigits != null) {
+ fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
+ }
+ if (tuple.currency != null) {
+ fmt.setCurrency(tuple.currency);
+ }
+ if (tuple.minGroupingDigits != null) {
+ // Oops we don't support this.
+ }
+ if (tuple.useSigDigits != null) {
+ fmt.setSignificantDigitsUsed(tuple.useSigDigits != 0);
+ }
+ if (tuple.minSigDigits != null) {
+ fmt.setMinimumSignificantDigits(tuple.minSigDigits);
+ }
+ if (tuple.maxSigDigits != null) {
+ fmt.setMaximumSignificantDigits(tuple.maxSigDigits);
+ }
+ if (tuple.useGrouping != null) {
+ fmt.setGroupingUsed(tuple.useGrouping != 0);
+ }
+ if (tuple.multiplier != null) {
+ fmt.setMultiplier(tuple.multiplier);
+ }
+ if (tuple.roundingIncrement != null) {
+ fmt.setRoundingIncrement(tuple.roundingIncrement.doubleValue());
+ }
+ if (tuple.formatWidth != null) {
+ fmt.setFormatWidth(tuple.formatWidth);
+ }
+ if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
+ fmt.setPadCharacter(tuple.padCharacter.charAt(0));
+ }
+ if (tuple.useScientific != null) {
+ fmt.setScientificNotation(tuple.useScientific != 0);
+ }
+ if (tuple.grouping != null) {
+ fmt.setGroupingSize(tuple.grouping);
+ }
+ if (tuple.grouping2 != null) {
+ fmt.setSecondaryGroupingSize(tuple.grouping2);
+ }
+ if (tuple.roundingMode != null) {
+ fmt.setRoundingMode(tuple.roundingMode);
+ }
+ if (tuple.currencyUsage != null) {
+ fmt.setCurrencyUsage(tuple.currencyUsage);
+ }
+ if (tuple.minimumExponentDigits != null) {
+ fmt.setMinimumExponentDigits(tuple.minimumExponentDigits.byteValue());
+ }
+ if (tuple.exponentSignAlwaysShown != null) {
+ fmt.setExponentSignAlwaysShown(tuple.exponentSignAlwaysShown != 0);
+ }
+ if (tuple.decimalSeparatorAlwaysShown != null) {
+ fmt.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
+ }
+ if (tuple.padPosition != null) {
+ fmt.setPadPosition(tuple.padPosition);
+ }
+ if (tuple.positivePrefix != null) {
+ fmt.setPositivePrefix(tuple.positivePrefix);
+ }
+ if (tuple.positiveSuffix != null) {
+ fmt.setPositiveSuffix(tuple.positiveSuffix);
+ }
+ if (tuple.negativePrefix != null) {
+ fmt.setNegativePrefix(tuple.negativePrefix);
+ }
+ if (tuple.negativeSuffix != null) {
+ fmt.setNegativeSuffix(tuple.negativeSuffix);
+ }
+ if (tuple.localizedPattern != null) {
+ fmt.applyLocalizedPattern(tuple.localizedPattern);
+ }
+ int lenient = tuple.lenient == null ? 1 : tuple.lenient.intValue();
+ fmt.setParseStrict(lenient == 0);
+ if (tuple.parseIntegerOnly != null) {
+ fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
+ }
+ if (tuple.parseCaseSensitive != null) {
+ // Not supported.
+ }
+ if (tuple.decimalPatternMatchRequired != null) {
+ fmt.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0);
+ }
+ if (tuple.parseNoExponent != null) {
+ // Oops, not supported for now
+ }
+ }
+ };
+
+ private DataDrivenNumberFormatTestUtility.CodeUnderTest JDK =
+ new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
+ @Override
+ public Character Id() {
+ return 'K';
+ }
+
+ @Override
+ public String format(DataDrivenNumberFormatTestData tuple) {
+ java.text.DecimalFormat fmt = createDecimalFormat(tuple);
+ String actual = fmt.format(toNumber(tuple.format));
+ String expected = tuple.output;
+ if (!expected.equals(actual)) {
+ return "Expected " + expected + ", got " + actual;
+ }
+ return null;
+ }
+
+ @Override
+ public String toPattern(DataDrivenNumberFormatTestData tuple) {
+ java.text.DecimalFormat fmt = createDecimalFormat(tuple);
+ StringBuilder result = new StringBuilder();
+ if (tuple.toPattern != null) {
+ String expected = tuple.toPattern;
+ String actual = fmt.toPattern();
+ if (!expected.equals(actual)) {
+ result.append("Expected toPattern=" + expected + ", got " + actual);
+ }
+ }
+ if (tuple.toLocalizedPattern != null) {
+ String expected = tuple.toLocalizedPattern;
+ String actual = fmt.toLocalizedPattern();
+ if (!expected.equals(actual)) {
+ result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
+ }
+ }
+ return result.length() == 0 ? null : result.toString();
+ }
+
+ @Override
+ public String parse(DataDrivenNumberFormatTestData tuple) {
+ java.text.DecimalFormat fmt = createDecimalFormat(tuple);
+ ParsePosition ppos = new ParsePosition(0);
+ Number actual = fmt.parse(tuple.parse, ppos);
+ if (ppos.getIndex() == 0) {
+ return "Parse failed; got " + actual + ", but expected " + tuple.output;
+ }
+ if (tuple.output.equals("fail")) {
+ return null;
+ }
+ Number expected = toNumber(tuple.output);
+ // number types cannot be compared, this is the best we can do.
+ if (expected.doubleValue() != actual.doubleValue()
+ && !Double.isNaN(expected.doubleValue())
+ && !Double.isNaN(expected.doubleValue())) {
+ return "Expected: " + expected + ", got: " + actual;
+ }
+ return null;
+ }
+
+ /**
+ * @param tuple
+ * @return
+ */
+ private java.text.DecimalFormat createDecimalFormat(DataDrivenNumberFormatTestData tuple) {
+ java.text.DecimalFormat fmt =
+ new java.text.DecimalFormat(
+ tuple.pattern == null ? "0" : tuple.pattern,
+ new java.text.DecimalFormatSymbols(
+ (tuple.locale == null ? EN : tuple.locale).toLocale()));
+ adjustDecimalFormat(tuple, fmt);
+ return fmt;
+ }
+
+ /**
+ * @param tuple
+ * @param fmt
+ */
+ private void adjustDecimalFormat(
+ DataDrivenNumberFormatTestData tuple, java.text.DecimalFormat fmt) {
+ if (tuple.minIntegerDigits != null) {
+ fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
+ }
+ if (tuple.maxIntegerDigits != null) {
+ fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
+ }
+ if (tuple.minFractionDigits != null) {
+ fmt.setMinimumFractionDigits(tuple.minFractionDigits);
+ }
+ if (tuple.maxFractionDigits != null) {
+ fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
+ }
+ if (tuple.currency != null) {
+ fmt.setCurrency(java.util.Currency.getInstance(tuple.currency.toString()));
+ }
+ if (tuple.minGroupingDigits != null) {
+ // Oops we don't support this.
+ }
+ if (tuple.useSigDigits != null) {
+ // Oops we don't support this
+ }
+ if (tuple.minSigDigits != null) {
+ // Oops we don't support this
+ }
+ if (tuple.maxSigDigits != null) {
+ // Oops we don't support this
+ }
+ if (tuple.useGrouping != null) {
+ fmt.setGroupingUsed(tuple.useGrouping != 0);
+ }
+ if (tuple.multiplier != null) {
+ fmt.setMultiplier(tuple.multiplier);
+ }
+ if (tuple.roundingIncrement != null) {
+ // Not supported
+ }
+ if (tuple.formatWidth != null) {
+ // Not supported
+ }
+ if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
+ // Not supported
+ }
+ if (tuple.useScientific != null) {
+ // Not supported
+ }
+ if (tuple.grouping != null) {
+ fmt.setGroupingSize(tuple.grouping);
+ }
+ if (tuple.grouping2 != null) {
+ // Not supported
+ }
+ if (tuple.roundingMode != null) {
+ // Not supported
+ }
+ if (tuple.currencyUsage != null) {
+ // Not supported
+ }
+ if (tuple.minimumExponentDigits != null) {
+ // Not supported
+ }
+ if (tuple.exponentSignAlwaysShown != null) {
+ // Not supported
+ }
+ if (tuple.decimalSeparatorAlwaysShown != null) {
+ fmt.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
+ }
+ if (tuple.padPosition != null) {
+ // Not supported
+ }
+ if (tuple.positivePrefix != null) {
+ fmt.setPositivePrefix(tuple.positivePrefix);
+ }
+ if (tuple.positiveSuffix != null) {
+ fmt.setPositiveSuffix(tuple.positiveSuffix);
+ }
+ if (tuple.negativePrefix != null) {
+ fmt.setNegativePrefix(tuple.negativePrefix);
+ }
+ if (tuple.negativeSuffix != null) {
+ fmt.setNegativeSuffix(tuple.negativeSuffix);
+ }
+ if (tuple.localizedPattern != null) {
+ fmt.applyLocalizedPattern(tuple.localizedPattern);
+ }
+
+ // lenient parsing not supported by JDK
+ if (tuple.parseIntegerOnly != null) {
+ fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
+ }
+ if (tuple.parseCaseSensitive != null) {
+ // Not supported.
+ }
+ if (tuple.decimalPatternMatchRequired != null) {
+ // Oops, not supported
+ }
+ if (tuple.parseNoExponent != null) {
+ // Oops, not supported for now
+ }
+ }
+ };
+
+ @Test
+ public void TestDataDrivenICU58() {
+ DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
+ "numberformattestspecification.txt", ICU58);
+ }
+
+ @Test
+ public void TestDataDrivenJDK() {
+ DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
+ "numberformattestspecification.txt", JDK);
+ }
+
+ @Test
+ public void TestDataDrivenShane() {
+ ShanesDataDrivenTester.run();
+ }
+}
*******************************************************************************
*/
-/**
+/**
* Port From: ICU4C v1.8.1 : format : NumberFormatRegressionTest
* Source File: $ICU4CRoot/source/test/intltest/numrgts.cpp
**/
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.ULocale;
-/**
+/**
* Performs regression test for MessageFormat
**/
public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
errln("FAIL");
}
}
-
+
/**
* DateFormat should call setIntegerParseOnly(TRUE) on adopted
* NumberFormat objects.
*/
@Test
public void TestJ691() {
-
+
Locale loc = new Locale("fr", "CH");
-
+
// set up the input date string & expected output
String udt = "11.10.2000";
String exp = "11.10.00";
-
+
// create a Calendar for this locale
Calendar cal = Calendar.getInstance(loc);
-
+
// create a NumberFormat for this locale
NumberFormat nf = NumberFormat.getInstance(loc);
-
+
// *** Here's the key: We don't want to have to do THIS:
//nf.setParseIntegerOnly(true);
// However with changes to fr_CH per cldrbug:9370 we have to do the following:
// create the DateFormat
DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, loc);
-
+
df.setCalendar(cal);
df.setNumberFormat(nf);
-
+
// set parsing to lenient & parse
Date ulocdat = new Date();
df.setLenient(true);
}
// format back to a string
String outString = df.format(ulocdat);
-
+
if (!outString.equals(exp)) {
errln("FAIL: " + udt + " => " + outString);
}
}
-
+
/**
* Test getIntegerInstance();
*/
@Test
public void Test4408066() {
-
+
NumberFormat nf1 = NumberFormat.getIntegerInstance();
NumberFormat nf2 = NumberFormat.getIntegerInstance(Locale.CHINA);
-
+
//test isParseIntegerOnly
if (!nf1.isParseIntegerOnly() || !nf2.isParseIntegerOnly()) {
errln("Failed : Integer Number Format Instance should set setParseIntegerOnly(true)");
}
-
+
//Test format
{
double[] data = {
- -3.75, -2.5, -1.5,
- -1.25, 0, 1.0,
- 1.25, 1.5, 2.5,
+ -3.75, -2.5, -1.5,
+ -1.25, 0, 1.0,
+ 1.25, 1.5, 2.5,
3.75, 10.0, 255.5
};
String[] expected = {
"1", "2", "2",
"4", "10", "256"
};
-
+
for (int i = 0; i < data.length; ++i) {
String result = nf1.format(data[i]);
if (!result.equals(expected[i])) {
- errln("Failed => Source: " + Double.toString(data[i])
+ errln("Failed => Source: " + Double.toString(data[i])
+ ";Formatted : " + result
+ ";but expectted: " + expected[i]);
}
//Test parse, Parsing should stop at "."
{
String data[] = {
- "-3.75", "-2.5", "-1.5",
- "-1.25", "0", "1.0",
- "1.25", "1.5", "2.5",
+ "-3.75", "-2.5", "-1.5",
+ "-1.25", "0", "1.0",
+ "1.25", "1.5", "2.5",
"3.75", "10.0", "255.5"
};
long[] expected = {
1, 1, 2,
3, 10, 255
};
-
+
for (int i = 0; i < data.length; ++i) {
Number n = null;
try {
errln("Failed: Integer Number Format should parse string to Long/Integer");
}
if (n.longValue() != expected[i]) {
- errln("Failed=> Source: " + data[i]
+ errln("Failed=> Source: " + data[i]
+ ";result : " + n.toString()
+ ";expected :" + Long.toString(expected[i]));
}
}
}
}
-
+
//Test New serialized DecimalFormat(2.0) read old serialized forms of DecimalFormat(1.3.1.1)
@Test
public void TestSerialization() throws IOException{
byte[][] contents = NumberFormatSerialTestData.getContent();
double data = 1234.56;
String[] expected = {
- "1,234.56", "$1,234.56", "123,456%", "1.23456E3"};
+ "1,234.56", "$1,234.56", "1.23456E3", "1,234.56"};
for (int i = 0; i < 4; ++i) {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(contents[i]));
try {
NumberFormat format = (NumberFormat) ois.readObject();
String result = format.format(data);
- if (result.equals(expected[i])) {
- logln("OK: Deserialized bogus NumberFormat(new version read old version)");
- } else {
- errln("FAIL: the test data formats are not euqal");
- }
+ assertEquals("Deserialization new version should read old version", expected[i], result);
} catch (Exception e) {
warnln("FAIL: " + e.getMessage());
}
try {
Number n = nfmt.parse(data[i]);
if (expected[i] != n.doubleValue()) {
- errln("Failed: Parsed result for " + data[i] + ": "
+ errln("Failed: Parsed result for " + data[i] + ": "
+ n.doubleValue() + " / expected: " + expected[i]);
}
} catch (ParseException pe) {
@Test
public void TestSurrogatesParsing() { // Test parsing of numbers that use digits from the supplemental planes.
final String[] data = {
- "1\ud801\udca2,3\ud801\udca45.67", //
- "\ud801\udca1\ud801\udca2,\ud801\udca3\ud801\udca4\ud801\udca5.\ud801\udca6\ud801\udca7\ud801\udca8", //
+ "1\ud801\udca2,3\ud801\udca45.67", //
+ "\ud801\udca1\ud801\udca2,\ud801\udca3\ud801\udca4\ud801\udca5.\ud801\udca6\ud801\udca7\ud801\udca8", //
"\ud835\udfd2.\ud835\udfd7E-\ud835\udfd1",
"\ud835\udfd3.8E-0\ud835\udfd0"
};
try {
Number n = nfmt.parse(data[i]);
if (expected[i] != n.doubleValue()) {
- errln("Failed: Parsed result for " + data[i] + ": "
+ errln("Failed: Parsed result for " + data[i] + ": "
+ n.doubleValue() + " / expected: " + expected[i]);
}
} catch (ParseException pe) {
void checkNBSPPatternRtNum(String testcase, NumberFormat nf, double myNumber) {
String myString = nf.format(myNumber);
-
+
double aNumber;
try {
aNumber = nf.parse(myString).doubleValue();
public void TestNBSPInPattern() {
NumberFormat nf = null;
String testcase;
-
-
+
+
testcase="ar_AE UNUM_CURRENCY";
nf = NumberFormat.getCurrencyInstance(new ULocale("ar_AE"));
checkNBSPPatternRT(testcase, nf);
- // if we don't have CLDR 1.6 data, bring out the problem anyways
-
+ // if we don't have CLDR 1.6 data, bring out the problem anyways
+
String SPECIAL_PATTERN = "\u00A4\u00A4'\u062f.\u0625.\u200f\u00a0'###0.00";
testcase = "ar_AE special pattern: " + SPECIAL_PATTERN;
nf = new DecimalFormat();
((DecimalFormat)nf).applyPattern(SPECIAL_PATTERN);
checkNBSPPatternRT(testcase, nf);
-
+
}
/*
errln("FAIL: Parsed result: " + num + " - expected: " + val);
}
}
+
+ @Test
+ public void TestAffixesNoCurrency() {
+ ULocale locale = new ULocale("en");
+ DecimalFormat nf = (DecimalFormat) NumberFormat.getInstance(locale, NumberFormat.PLURALCURRENCYSTYLE);
+ assertEquals(
+ "Positive suffix should contain the single currency sign when no currency is set",
+ " \u00A4",
+ nf.getPositiveSuffix());
+ }
}
public class NumberFormatSerialTestData {
//get Content
public static byte[][] getContent() {
- return content;
+ return content;
}
//NumberFormat.getInstance(Locale.US)
};
- //NumberFormat.getPercentInstance(Locale.US)
- static byte[] percentInstance = new byte[]{
- -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
- 116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1,
- 3, 98, -40, 114, 48, 58, 2, 0, 22, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83,
- 101, 112, 97, 114, 97, 116, 111, 114, 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 90,
- 0, 23, 101, 120, 112, 111, 110, 101, 110, 116, 83, 105, 103, 110, 65, 108, 119, 97, 121, 115,
- 83, 104, 111, 119, 110, 73, 0, 11, 102, 111, 114, 109, 97, 116, 87, 105, 100, 116, 104, 66,
- 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 83, 105, 122, 101, 66, 0, 13, 103, 114, 111,
- 117, 112, 105, 110, 103, 83, 105, 122, 101, 50, 66, 0, 17, 109, 105, 110, 69, 120, 112, 111,
- 110, 101, 110, 116, 68, 105, 103, 105, 116, 115, 73, 0, 10, 109, 117, 108, 116, 105, 112, 108,
- 105, 101, 114, 67, 0, 3, 112, 97, 100, 73, 0, 11, 112, 97, 100, 80, 111, 115, 105, 116,
- 105, 111, 110, 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 73, 0,
- 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101,
- 97, 109, 90, 0, 22, 117, 115, 101, 69, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 78,
- 111, 116, 97, 116, 105, 111, 110, 76, 0, 16, 110, 101, 103, 80, 114, 101, 102, 105, 120, 80,
- 97, 116, 116, 101, 114, 110, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47,
- 83, 116, 114, 105, 110, 103, 59, 76, 0, 16, 110, 101, 103, 83, 117, 102, 102, 105, 120, 80,
- 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 1, 76, 0, 14, 110, 101, 103, 97, 116, 105,
- 118, 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 1, 76, 0, 14, 110, 101, 103, 97,
- 116, 105, 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 1, 76, 0, 16, 112, 111,
- 115, 80, 114, 101, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 1, 76,
- 0, 16, 112, 111, 115, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0,
- 126, 0, 1, 76, 0, 14, 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120,
- 113, 0, 126, 0, 1, 76, 0, 14, 112, 111, 115, 105, 116, 105, 118, 101, 83, 117, 102, 102,
- 105, 120, 113, 0, 126, 0, 1, 76, 0, 17, 114, 111, 117, 110, 100, 105, 110, 103, 73, 110,
- 99, 114, 101, 109, 101, 110, 116, 116, 0, 22, 76, 106, 97, 118, 97, 47, 109, 97, 116, 104,
- 47, 66, 105, 103, 68, 101, 99, 105, 109, 97, 108, 59, 76, 0, 7, 115, 121, 109, 98, 111,
- 108, 115, 116, 0, 39, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 116, 101,
- 120, 116, 47, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98,
- 111, 108, 115, 59, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
- 116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, 109, 97, 116, -33, -10, -77,
- -65, 19, 125, 7, -24, 3, 0, 11, 90, 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 85,
- 115, 101, 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103,
- 105, 116, 115, 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105,
- 116, 115, 73, 0, 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, 110,
- 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, 101,
- 103, 101, 114, 68, 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116,
- 105, 111, 110, 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103,
- 101, 114, 68, 105, 103, 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114,
- 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109,
- 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97,
- 114, 115, 101, 73, 110, 116, 101, 103, 101, 114, 79, 110, 108, 121, 73, 0, 21, 115, 101, 114,
- 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 120, 114,
- 0, 16, 106, 97, 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, 116, -5, -40,
- -68, 18, -23, 15, 24, 67, 2, 0, 0, 120, 112, 1, 0, 127, 0, 0, 0, 0, 0, 0,
- 1, 53, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 120, 0, 0,
- 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 100, 0, 32, 0, 0, 0, 0, 0, 0, 0,
- 6, 0, 0, 0, 2, 0, 116, 0, 1, 45, 116, 0, 1, 37, 116, 0, 1, 45, 116, 0,
- 1, 37, 116, 0, 0, 113, 0, 126, 0, 8, 116, 0, 0, 116, 0, 1, 37, 112, 115, 114,
- 0, 37, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68,
- 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 111, 108, 115, 80,
- 29, 23, -103, 8, 104, -109, -100, 2, 0, 18, 67, 0, 16, 100, 101, 99, 105, 109, 97, 108,
- 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, 105, 103, 105, 116, 67, 0, 11,
- 101, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, 103, 114, 111, 117, 112, 105,
- 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, 105, 110, 117, 115, 83,
- 105, 103, 110, 67, 0, 17, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97,
- 116, 111, 114, 67, 0, 9, 112, 97, 100, 69, 115, 99, 97, 112, 101, 67, 0, 16, 112, 97,
- 116, 116, 101, 114, 110, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 7, 112, 101, 114,
- 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, 101, 110, 116, 67, 0, 8, 112, 108, 117,
- 115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111,
- 110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 9, 122, 101, 114, 111, 68, 105, 103, 105,
- 116, 76, 0, 3, 78, 97, 78, 113, 0, 126, 0, 1, 76, 0, 14, 99, 117, 114, 114, 101,
- 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 1, 76, 0, 17, 101, 120, 112,
- 111, 110, 101, 110, 116, 83, 101, 112, 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 1, 76,
- 0, 8, 105, 110, 102, 105, 110, 105, 116, 121, 113, 0, 126, 0, 1, 76, 0, 18, 105, 110,
- 116, 108, 67, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0,
- 1, 120, 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 46, 0, 42, 0, 59, 32,
- 48, 0, 37, 0, 43, 0, 0, 0, 2, 0, 48, 116, 0, 3, -17, -65, -67, 116, 0, 1,
- 36, 116, 0, 1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
- };
-
//NumberFormat.getScientificInstance(Locale.US)
static byte[] scientificInstance = new byte[]{
-84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
};
- final static byte[][] content = {generalInstance, currencyInstance, percentInstance, scientificInstance};
+ static byte[] icu58Latest = {
+ -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101,
+ 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 3, 98, -40,
+ 114, 48, 58, 3, 0, 36, 73, 0, 18, 80, 65, 82, 83, 69, 95, 77, 65, 88, 95, 69, 88, 80, 79, 78,
+ 69, 78, 84, 73, 0, 17, 99, 117, 114, 114, 101, 110, 99, 121, 83, 105, 103, 110, 67, 111, 117,
+ 110, 116, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114,
+ 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 90, 0, 23, 101, 120, 112, 111, 110, 101,
+ 110, 116, 83, 105, 103, 110, 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 73, 0, 11, 102,
+ 111, 114, 109, 97, 116, 87, 105, 100, 116, 104, 66, 0, 12, 103, 114, 111, 117, 112, 105, 110,
+ 103, 83, 105, 122, 101, 66, 0, 13, 103, 114, 111, 117, 112, 105, 110, 103, 83, 105, 122, 101,
+ 50, 73, 0, 20, 109, 97, 120, 83, 105, 103, 110, 105, 102, 105, 99, 97, 110, 116, 68, 105, 103,
+ 105, 116, 115, 66, 0, 17, 109, 105, 110, 69, 120, 112, 111, 110, 101, 110, 116, 68, 105, 103,
+ 105, 116, 115, 73, 0, 20, 109, 105, 110, 83, 105, 103, 110, 105, 102, 105, 99, 97, 110, 116, 68,
+ 105, 103, 105, 116, 115, 73, 0, 10, 109, 117, 108, 116, 105, 112, 108, 105, 101, 114, 67, 0, 3,
+ 112, 97, 100, 73, 0, 11, 112, 97, 100, 80, 111, 115, 105, 116, 105, 111, 110, 90, 0, 15, 112,
+ 97, 114, 115, 101, 66, 105, 103, 68, 101, 99, 105, 109, 97, 108, 90, 0, 24, 112, 97, 114, 115,
+ 101, 82, 101, 113, 117, 105, 114, 101, 68, 101, 99, 105, 109, 97, 108, 80, 111, 105, 110, 116,
+ 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 73, 0, 21, 115, 101, 114,
+ 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 73, 0, 5,
+ 115, 116, 121, 108, 101, 90, 0, 22, 117, 115, 101, 69, 120, 112, 111, 110, 101, 110, 116, 105,
+ 97, 108, 78, 111, 116, 97, 116, 105, 111, 110, 90, 0, 20, 117, 115, 101, 83, 105, 103, 110, 105,
+ 102, 105, 99, 97, 110, 116, 68, 105, 103, 105, 116, 115, 76, 0, 10, 97, 116, 116, 114, 105, 98,
+ 117, 116, 101, 115, 116, 0, 21, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 65, 114, 114,
+ 97, 121, 76, 105, 115, 116, 59, 76, 0, 14, 99, 117, 114, 114, 101, 110, 99, 121, 67, 104, 111,
+ 105, 99, 101, 116, 0, 24, 76, 106, 97, 118, 97, 47, 116, 101, 120, 116, 47, 67, 104, 111, 105,
+ 99, 101, 70, 111, 114, 109, 97, 116, 59, 76, 0, 18, 99, 117, 114, 114, 101, 110, 99, 121, 80,
+ 108, 117, 114, 97, 108, 73, 110, 102, 111, 116, 0, 37, 76, 99, 111, 109, 47, 105, 98, 109, 47,
+ 105, 99, 117, 47, 116, 101, 120, 116, 47, 67, 117, 114, 114, 101, 110, 99, 121, 80, 108, 117,
+ 114, 97, 108, 73, 110, 102, 111, 59, 76, 0, 13, 99, 117, 114, 114, 101, 110, 99, 121, 85, 115,
+ 97, 103, 101, 116, 0, 41, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116,
+ 105, 108, 47, 67, 117, 114, 114, 101, 110, 99, 121, 36, 67, 117, 114, 114, 101, 110, 99, 121,
+ 85, 115, 97, 103, 101, 59, 76, 0, 13, 102, 111, 114, 109, 97, 116, 80, 97, 116, 116, 101, 114,
+ 110, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103,
+ 59, 76, 0, 11, 109, 97, 116, 104, 67, 111, 110, 116, 101, 120, 116, 116, 0, 30, 76, 99, 111,
+ 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 109, 97, 116, 104, 47, 77, 97, 116, 104, 67, 111,
+ 110, 116, 101, 120, 116, 59, 76, 0, 16, 110, 101, 103, 80, 114, 101, 102, 105, 120, 80, 97, 116,
+ 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 16, 110, 101, 103, 83, 117, 102, 102, 105, 120,
+ 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 14, 110, 101, 103, 97, 116, 105, 118,
+ 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, 14, 110, 101, 103, 97, 116, 105,
+ 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, 16, 112, 111, 115, 80, 114,
+ 101, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 16, 112, 111,
+ 115, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 14,
+ 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0,
+ 14, 112, 111, 115, 105, 116, 105, 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 5, 76,
+ 0, 17, 114, 111, 117, 110, 100, 105, 110, 103, 73, 110, 99, 114, 101, 109, 101, 110, 116, 116,
+ 0, 22, 76, 106, 97, 118, 97, 47, 109, 97, 116, 104, 47, 66, 105, 103, 68, 101, 99, 105, 109, 97,
+ 108, 59, 76, 0, 7, 115, 121, 109, 98, 111, 108, 115, 116, 0, 39, 76, 99, 111, 109, 47, 105, 98,
+ 109, 47, 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114,
+ 109, 97, 116, 83, 121, 109, 98, 111, 108, 115, 59, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98,
+ 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114,
+ 109, 97, 116, -33, -10, -77, -65, 19, 125, 7, -24, 3, 0, 14, 90, 0, 12, 103, 114, 111, 117, 112,
+ 105, 110, 103, 85, 115, 101, 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110,
+ 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105,
+ 103, 105, 116, 115, 73, 0, 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111,
+ 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116,
+ 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116,
+ 105, 111, 110, 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103,
+ 101, 114, 68, 105, 103, 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114,
+ 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109,
+ 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97,
+ 114, 115, 101, 73, 110, 116, 101, 103, 101, 114, 79, 110, 108, 121, 90, 0, 11, 112, 97, 114,
+ 115, 101, 83, 116, 114, 105, 99, 116, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115,
+ 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 76, 0, 21, 99, 97, 112, 105, 116, 97, 108,
+ 105, 122, 97, 116, 105, 111, 110, 83, 101, 116, 116, 105, 110, 103, 116, 0, 33, 76, 99, 111,
+ 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 105, 115, 112, 108, 97,
+ 121, 67, 111, 110, 116, 101, 120, 116, 59, 76, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 116,
+ 0, 27, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 67,
+ 117, 114, 114, 101, 110, 99, 121, 59, 120, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105,
+ 99, 117, 46, 116, 101, 120, 116, 46, 85, 70, 111, 114, 109, 97, 116, -69, 26, -15, 32, -39, 7,
+ 93, -64, 2, 0, 2, 76, 0, 12, 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, 101, 116, 0, 26,
+ 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 85, 76, 111,
+ 99, 97, 108, 101, 59, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126,
+ 0, 13, 120, 114, 0, 16, 106, 97, 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97,
+ 116, -5, -40, -68, 18, -23, 15, 24, 67, 2, 0, 0, 120, 112, 112, 112, 1, 3, 127, 0, 0, 0, 3, 0,
+ 0, 1, 53, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 126, 114, 0, 31, 99, 111, 109, 46,
+ 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 105, 115, 112, 108, 97, 121, 67,
+ 111, 110, 116, 101, 120, 116, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 114, 0, 14, 106, 97, 118,
+ 97, 46, 108, 97, 110, 103, 46, 69, 110, 117, 109, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 112,
+ 116, 0, 19, 67, 65, 80, 73, 84, 65, 76, 73, 90, 65, 84, 73, 79, 78, 95, 78, 79, 78, 69, 115,
+ 114, 0, 45, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, 108, 46, 77,
+ 101, 97, 115, 117, 114, 101, 85, 110, 105, 116, 36, 77, 101, 97, 115, 117, 114, 101, 85, 110,
+ 105, 116, 80, 114, 111, 120, 121, -55, -70, 119, -8, -15, 121, 121, -30, 12, 0, 0, 120, 112,
+ 119, 18, 0, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 0, 3, 85, 83, 68, 0, 0, 120, 120, 0, 0,
+ 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 32, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 115, 114, 0, 19, 106, 97, 118, 97, 46,
+ 117, 116, 105, 108, 46, 65, 114, 114, 97, 121, 76, 105, 115, 116, 120, -127, -46, 29, -103, -57,
+ 97, -99, 3, 0, 1, 73, 0, 4, 115, 105, 122, 101, 120, 112, 0, 0, 0, 0, 119, 4, 0, 0, 0, 0, 120,
+ 112, 112, 126, 114, 0, 39, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105,
+ 108, 46, 67, 117, 114, 114, 101, 110, 99, 121, 36, 67, 117, 114, 114, 101, 110, 99, 121, 85,
+ 115, 97, 103, 101, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 113, 0, 126, 0, 17, 116, 0, 8, 83, 84,
+ 65, 78, 68, 65, 82, 68, 116, 0, 9, 35, 44, 35, 35, 48, 46, 35, 35, 35, 115, 114, 0, 28, 99, 111,
+ 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 109, 97, 116, 104, 46, 77, 97, 116, 104, 67, 111,
+ 110, 116, 101, 120, 116, 99, 105, 109, 109, 99, 49, 48, 48, 2, 0, 4, 73, 0, 6, 100, 105, 103,
+ 105, 116, 115, 73, 0, 4, 102, 111, 114, 109, 90, 0, 10, 108, 111, 115, 116, 68, 105, 103, 105,
+ 116, 115, 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 120, 112, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 116, 0, 1, 45, 116, 0, 0, 116, 0, 1, 45, 116, 0, 0, 116, 0, 0,
+ 113, 0, 126, 0, 31, 116, 0, 0, 116, 0, 0, 112, 115, 114, 0, 37, 99, 111, 109, 46, 105, 98, 109,
+ 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109,
+ 97, 116, 83, 121, 109, 98, 111, 108, 115, 80, 29, 23, -103, 8, 104, -109, -100, 2, 0, 38, 67, 0,
+ 16, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100,
+ 105, 103, 105, 116, 67, 0, 11, 101, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17,
+ 103, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109,
+ 105, 110, 117, 115, 83, 105, 103, 110, 67, 0, 25, 109, 111, 110, 101, 116, 97, 114, 121, 71,
+ 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 17, 109,
+ 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 112, 97,
+ 100, 69, 115, 99, 97, 112, 101, 67, 0, 16, 112, 97, 116, 116, 101, 114, 110, 83, 101, 112, 97,
+ 114, 97, 116, 111, 114, 67, 0, 7, 112, 101, 114, 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99,
+ 101, 110, 116, 67, 0, 8, 112, 108, 117, 115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105,
+ 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 8, 115,
+ 105, 103, 68, 105, 103, 105, 116, 67, 0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 116, 76, 0,
+ 3, 78, 97, 78, 113, 0, 126, 0, 5, 76, 0, 12, 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108,
+ 101, 113, 0, 126, 0, 13, 76, 0, 15, 99, 117, 114, 114, 101, 110, 99, 121, 80, 97, 116, 116, 101,
+ 114, 110, 113, 0, 126, 0, 5, 91, 0, 19, 99, 117, 114, 114, 101, 110, 99, 121, 83, 112, 99, 65,
+ 102, 116, 101, 114, 83, 121, 109, 116, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103,
+ 47, 83, 116, 114, 105, 110, 103, 59, 91, 0, 20, 99, 117, 114, 114, 101, 110, 99, 121, 83, 112,
+ 99, 66, 101, 102, 111, 114, 101, 83, 121, 109, 113, 0, 126, 0, 38, 76, 0, 14, 99, 117, 114, 114,
+ 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 5, 76, 0, 22, 100, 101, 99, 105,
+ 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0,
+ 126, 0, 5, 91, 0, 12, 100, 105, 103, 105, 116, 83, 116, 114, 105, 110, 103, 115, 113, 0, 126, 0,
+ 38, 91, 0, 6, 100, 105, 103, 105, 116, 115, 116, 0, 2, 91, 67, 76, 0, 26, 101, 120, 112, 111,
+ 110, 101, 110, 116, 77, 117, 108, 116, 105, 112, 108, 105, 99, 97, 116, 105, 111, 110, 83, 105,
+ 103, 110, 113, 0, 126, 0, 5, 76, 0, 17, 101, 120, 112, 111, 110, 101, 110, 116, 83, 101, 112,
+ 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 5, 76, 0, 23, 103, 114, 111, 117, 112, 105, 110,
+ 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5,
+ 76, 0, 8, 105, 110, 102, 105, 110, 105, 116, 121, 113, 0, 126, 0, 5, 76, 0, 18, 105, 110, 116,
+ 108, 67, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 5, 76, 0,
+ 11, 109, 105, 110, 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, 76, 0, 31, 109,
+ 111, 110, 101, 116, 97, 114, 121, 71, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114,
+ 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, 76, 0, 23, 109, 111, 110,
+ 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103,
+ 113, 0, 126, 0, 5, 76, 0, 13, 112, 101, 114, 77, 105, 108, 108, 83, 116, 114, 105, 110, 103,
+ 113, 0, 126, 0, 5, 76, 0, 13, 112, 101, 114, 99, 101, 110, 116, 83, 116, 114, 105, 110, 103,
+ 113, 0, 126, 0, 5, 76, 0, 10, 112, 108, 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0,
+ 5, 76, 0, 15, 114, 101, 113, 117, 101, 115, 116, 101, 100, 76, 111, 99, 97, 108, 101, 116, 0,
+ 18, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 76, 111, 99, 97, 108, 101, 59, 76, 0, 7,
+ 117, 108, 111, 99, 97, 108, 101, 113, 0, 126, 0, 13, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111,
+ 99, 97, 108, 101, 113, 0, 126, 0, 13, 120, 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 44, 0, 46,
+ 0, 42, 0, 59, 32, 48, 0, 37, 0, 43, 0, 0, 0, 8, 0, 64, 0, 48, 116, 0, 3, 78, 97, 78, 115, 114,
+ 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, 108, 46, 85, 76,
+ 111, 99, 97, 108, 101, 51, -114, -10, 104, 70, -48, 11, -31, 2, 0, 1, 76, 0, 8, 108, 111, 99,
+ 97, 108, 101, 73, 68, 113, 0, 126, 0, 5, 120, 112, 116, 0, 5, 101, 110, 95, 85, 83, 112, 117,
+ 114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 83, 116, 114, 105, 110, 103,
+ 59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0, 120, 112, 0, 0, 0, 3, 116, 0, 6, 91, 58, 94,
+ 83, 58, 93, 116, 0, 9, 91, 58, 100, 105, 103, 105, 116, 58, 93, 116, 0, 2, -62, -96, 117, 113,
+ 0, 126, 0, 46, 0, 0, 0, 3, 113, 0, 126, 0, 48, 113, 0, 126, 0, 49, 113, 0, 126, 0, 50, 116, 0,
+ 1, 36, 116, 0, 1, 46, 117, 113, 0, 126, 0, 46, 0, 0, 0, 10, 116, 0, 1, 48, 116, 0, 1, 49, 116,
+ 0, 1, 50, 116, 0, 1, 51, 116, 0, 1, 52, 116, 0, 1, 53, 116, 0, 1, 54, 116, 0, 1, 55, 116, 0, 1,
+ 56, 116, 0, 1, 57, 117, 114, 0, 2, 91, 67, -80, 38, 102, -80, -30, 93, -124, -84, 2, 0, 0, 120,
+ 112, 0, 0, 0, 10, 0, 48, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 116, 0,
+ 2, -61, -105, 116, 0, 1, 69, 116, 0, 1, 44, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
+ 116, 0, 1, 45, 113, 0, 126, 0, 69, 113, 0, 126, 0, 53, 116, 0, 3, -30, -128, -80, 116, 0, 1, 37,
+ 116, 0, 1, 43, 115, 114, 0, 16, 106, 97, 118, 97, 46, 117, 116, 105, 108, 46, 76, 111, 99, 97,
+ 108, 101, 126, -8, 17, 96, -100, 48, -7, -20, 3, 0, 6, 73, 0, 8, 104, 97, 115, 104, 99, 111,
+ 100, 101, 76, 0, 7, 99, 111, 117, 110, 116, 114, 121, 113, 0, 126, 0, 5, 76, 0, 10, 101, 120,
+ 116, 101, 110, 115, 105, 111, 110, 115, 113, 0, 126, 0, 5, 76, 0, 8, 108, 97, 110, 103, 117, 97,
+ 103, 101, 113, 0, 126, 0, 5, 76, 0, 6, 115, 99, 114, 105, 112, 116, 113, 0, 126, 0, 5, 76, 0, 7,
+ 118, 97, 114, 105, 97, 110, 116, 113, 0, 126, 0, 5, 120, 112, -1, -1, -1, -1, 116, 0, 2, 85, 83,
+ 116, 0, 0, 116, 0, 2, 101, 110, 113, 0, 126, 0, 79, 113, 0, 126, 0, 79, 120, 115, 113, 0, 126,
+ 0, 43, 113, 0, 126, 0, 45, 113, 0, 126, 0, 44, 120
+ };
+
+ static byte[] newFromPattern = {
+ -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101,
+ 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 3, 98, -40,
+ 114, 48, 58, 3, 0, 1, 73, 0, 18, 105, 99, 117, 77, 97, 116, 104, 67, 111, 110, 116, 101, 120,
+ 116, 70, 111, 114, 109, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
+ 116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, 109, 97, 116, -33, -10, -77,
+ -65, 19, 125, 7, -24, 3, 0, 14, 90, 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 85, 115, 101,
+ 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115,
+ 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 73, 0,
+ 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 105,
+ 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, 101, 103, 101, 114, 68,
+ 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105,
+ 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103,
+ 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111,
+ 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109, 117, 109, 73, 110, 116,
+ 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97, 114, 115, 101, 73, 110,
+ 116, 101, 103, 101, 114, 79, 110, 108, 121, 90, 0, 11, 112, 97, 114, 115, 101, 83, 116, 114,
+ 105, 99, 116, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110,
+ 83, 116, 114, 101, 97, 109, 76, 0, 21, 99, 97, 112, 105, 116, 97, 108, 105, 122, 97, 116, 105,
+ 111, 110, 83, 101, 116, 116, 105, 110, 103, 116, 0, 33, 76, 99, 111, 109, 47, 105, 98, 109, 47,
+ 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 105, 115, 112, 108, 97, 121, 67, 111, 110, 116,
+ 101, 120, 116, 59, 76, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 116, 0, 27, 76, 99, 111, 109,
+ 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 67, 117, 114, 114, 101, 110, 99,
+ 121, 59, 120, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120,
+ 116, 46, 85, 70, 111, 114, 109, 97, 116, -69, 26, -15, 32, -39, 7, 93, -64, 2, 0, 2, 76, 0, 12,
+ 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, 101, 116, 0, 26, 76, 99, 111, 109, 47, 105, 98,
+ 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 85, 76, 111, 99, 97, 108, 101, 59, 76, 0, 11,
+ 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 120, 114, 0, 16, 106, 97,
+ 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, 116, -5, -40, -68, 18, -23, 15, 24,
+ 67, 2, 0, 0, 120, 112, 112, 112, 1, 3, 40, 0, 0, 0, 3, 0, 0, 0, 40, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 1, 0, 0, 0, 0, 0, 2, 126, 114, 0, 31, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116,
+ 101, 120, 116, 46, 68, 105, 115, 112, 108, 97, 121, 67, 111, 110, 116, 101, 120, 116, 0, 0, 0,
+ 0, 0, 0, 0, 0, 18, 0, 0, 120, 114, 0, 14, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 69, 110,
+ 117, 109, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 112, 116, 0, 19, 67, 65, 80, 73, 84, 65, 76,
+ 73, 90, 65, 84, 73, 79, 78, 95, 78, 79, 78, 69, 112, 120, 0, 0, 0, 0, 119, 4, 0, 0, 0, 0, 115,
+ 114, 0, 34, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 105, 109, 112, 108, 46, 110,
+ 117, 109, 98, 101, 114, 46, 80, 114, 111, 112, 101, 114, 116, 105, 101, 115, 56, -42, 52, -54,
+ -104, -87, -46, 123, 3, 0, 0, 120, 112, 119, 8, 0, 0, 0, 0, 0, 0, 0, 7, 116, 0, 12, 103, 114,
+ 111, 117, 112, 105, 110, 103, 83, 105, 122, 101, 115, 114, 0, 17, 106, 97, 118, 97, 46, 108, 97,
+ 110, 103, 46, 73, 110, 116, 101, 103, 101, 114, 18, -30, -96, -92, -9, -127, -121, 56, 2, 0, 1,
+ 73, 0, 5, 118, 97, 108, 117, 101, 120, 114, 0, 16, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46,
+ 78, 117, 109, 98, 101, 114, -122, -84, -107, 29, 11, -108, -32, -117, 2, 0, 0, 120, 112, 0, 0,
+ 0, 3, 116, 0, 20, 109, 105, 110, 105, 109, 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105,
+ 103, 105, 116, 115, 115, 113, 0, 126, 0, 15, 0, 0, 0, 2, 116, 0, 15, 112, 97, 100, 100, 105,
+ 110, 103, 76, 111, 99, 97, 116, 105, 111, 110, 126, 114, 0, 64, 99, 111, 109, 46, 105, 98, 109,
+ 46, 105, 99, 117, 46, 105, 109, 112, 108, 46, 110, 117, 109, 98, 101, 114, 46, 102, 111, 114,
+ 109, 97, 116, 116, 101, 114, 115, 46, 80, 97, 100, 100, 105, 110, 103, 70, 111, 114, 109, 97,
+ 116, 36, 80, 97, 100, 100, 105, 110, 103, 76, 111, 99, 97, 116, 105, 111, 110, 0, 0, 0, 0, 0, 0,
+ 0, 0, 18, 0, 0, 120, 113, 0, 126, 0, 9, 116, 0, 12, 65, 70, 84, 69, 82, 95, 80, 82, 69, 70, 73,
+ 88, 116, 0, 13, 112, 97, 100, 100, 105, 110, 103, 83, 116, 114, 105, 110, 103, 116, 0, 1, 42,
+ 116, 0, 12, 112, 97, 100, 100, 105, 110, 103, 87, 105, 100, 116, 104, 115, 113, 0, 126, 0, 15,
+ 0, 0, 0, 16, 116, 0, 21, 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120,
+ 80, 97, 116, 116, 101, 114, 110, 116, 0, 2, 65, 45, 116, 0, 21, 112, 111, 115, 105, 116, 105,
+ 118, 101, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 116, 0, 3, 98, -62, -92,
+ 120, 115, 114, 0, 37, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116,
+ 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 111, 108, 115,
+ 80, 29, 23, -103, 8, 104, -109, -100, 2, 0, 38, 67, 0, 16, 100, 101, 99, 105, 109, 97, 108, 83,
+ 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, 105, 103, 105, 116, 67, 0, 11, 101, 120,
+ 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, 103, 114, 111, 117, 112, 105, 110, 103,
+ 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, 105, 110, 117, 115, 83, 105, 103, 110,
+ 67, 0, 25, 109, 111, 110, 101, 116, 97, 114, 121, 71, 114, 111, 117, 112, 105, 110, 103, 83,
+ 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 17, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101,
+ 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 112, 97, 100, 69, 115, 99, 97, 112, 101, 67, 0, 16,
+ 112, 97, 116, 116, 101, 114, 110, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 7, 112, 101,
+ 114, 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, 101, 110, 116, 67, 0, 8, 112, 108, 117,
+ 115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111,
+ 110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 8, 115, 105, 103, 68, 105, 103, 105, 116, 67,
+ 0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 116, 76, 0, 3, 78, 97, 78, 116, 0, 18, 76, 106, 97,
+ 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 12, 97, 99, 116,
+ 117, 97, 108, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 76, 0, 15, 99, 117, 114, 114, 101,
+ 110, 99, 121, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 33, 91, 0, 19, 99, 117, 114, 114,
+ 101, 110, 99, 121, 83, 112, 99, 65, 102, 116, 101, 114, 83, 121, 109, 116, 0, 19, 91, 76, 106,
+ 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 91, 0, 20, 99, 117,
+ 114, 114, 101, 110, 99, 121, 83, 112, 99, 66, 101, 102, 111, 114, 101, 83, 121, 109, 113, 0,
+ 126, 0, 34, 76, 0, 14, 99, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0,
+ 126, 0, 33, 76, 0, 22, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111,
+ 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 91, 0, 12, 100, 105, 103, 105, 116, 83,
+ 116, 114, 105, 110, 103, 115, 113, 0, 126, 0, 34, 91, 0, 6, 100, 105, 103, 105, 116, 115, 116,
+ 0, 2, 91, 67, 76, 0, 26, 101, 120, 112, 111, 110, 101, 110, 116, 77, 117, 108, 116, 105, 112,
+ 108, 105, 99, 97, 116, 105, 111, 110, 83, 105, 103, 110, 113, 0, 126, 0, 33, 76, 0, 17, 101,
+ 120, 112, 111, 110, 101, 110, 116, 83, 101, 112, 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 33,
+ 76, 0, 23, 103, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83,
+ 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 8, 105, 110, 102, 105, 110, 105, 116, 121,
+ 113, 0, 126, 0, 33, 76, 0, 18, 105, 110, 116, 108, 67, 117, 114, 114, 101, 110, 99, 121, 83,
+ 121, 109, 98, 111, 108, 113, 0, 126, 0, 33, 76, 0, 11, 109, 105, 110, 117, 115, 83, 116, 114,
+ 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 31, 109, 111, 110, 101, 116, 97, 114, 121, 71, 114,
+ 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110,
+ 103, 113, 0, 126, 0, 33, 76, 0, 23, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97,
+ 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 13, 112, 101,
+ 114, 77, 105, 108, 108, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 13, 112, 101,
+ 114, 99, 101, 110, 116, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 10, 112, 108,
+ 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 15, 114, 101, 113, 117, 101,
+ 115, 116, 101, 100, 76, 111, 99, 97, 108, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 117, 116,
+ 105, 108, 47, 76, 111, 99, 97, 108, 101, 59, 76, 0, 7, 117, 108, 111, 99, 97, 108, 101, 113, 0,
+ 126, 0, 5, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 120,
+ 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 44, 0, 46, 0, 42, 0, 59, 32, 48, 0, 37, 0, 43, 0, 0,
+ 0, 8, 0, 64, 0, 48, 116, 0, 3, 78, 97, 78, 115, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46,
+ 105, 99, 117, 46, 117, 116, 105, 108, 46, 85, 76, 111, 99, 97, 108, 101, 51, -114, -10, 104, 70,
+ -48, 11, -31, 2, 0, 1, 76, 0, 8, 108, 111, 99, 97, 108, 101, 73, 68, 113, 0, 126, 0, 33, 120,
+ 112, 116, 0, 5, 101, 110, 95, 85, 83, 112, 117, 114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108,
+ 97, 110, 103, 46, 83, 116, 114, 105, 110, 103, 59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0,
+ 120, 112, 0, 0, 0, 3, 116, 0, 6, 91, 58, 94, 83, 58, 93, 116, 0, 9, 91, 58, 100, 105, 103, 105,
+ 116, 58, 93, 116, 0, 2, -62, -96, 117, 113, 0, 126, 0, 42, 0, 0, 0, 3, 113, 0, 126, 0, 44, 113,
+ 0, 126, 0, 45, 113, 0, 126, 0, 46, 116, 0, 1, 36, 116, 0, 1, 46, 117, 113, 0, 126, 0, 42, 0, 0,
+ 0, 10, 116, 0, 1, 48, 116, 0, 1, 49, 116, 0, 1, 50, 116, 0, 1, 51, 116, 0, 1, 52, 116, 0, 1, 53,
+ 116, 0, 1, 54, 116, 0, 1, 55, 116, 0, 1, 56, 116, 0, 1, 57, 117, 114, 0, 2, 91, 67, -80, 38,
+ 102, -80, -30, 93, -124, -84, 2, 0, 0, 120, 112, 0, 0, 0, 10, 0, 48, 0, 49, 0, 50, 0, 51, 0, 52,
+ 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 116, 0, 2, -61, -105, 116, 0, 1, 69, 116, 0, 1, 44, 116, 0,
+ 3, -30, -120, -98, 116, 0, 3, 85, 83, 68, 116, 0, 1, 45, 113, 0, 126, 0, 65, 113, 0, 126, 0, 49,
+ 116, 0, 3, -30, -128, -80, 116, 0, 1, 37, 116, 0, 1, 43, 115, 114, 0, 16, 106, 97, 118, 97, 46,
+ 117, 116, 105, 108, 46, 76, 111, 99, 97, 108, 101, 126, -8, 17, 96, -100, 48, -7, -20, 3, 0, 6,
+ 73, 0, 8, 104, 97, 115, 104, 99, 111, 100, 101, 76, 0, 7, 99, 111, 117, 110, 116, 114, 121, 113,
+ 0, 126, 0, 33, 76, 0, 10, 101, 120, 116, 101, 110, 115, 105, 111, 110, 115, 113, 0, 126, 0, 33,
+ 76, 0, 8, 108, 97, 110, 103, 117, 97, 103, 101, 113, 0, 126, 0, 33, 76, 0, 6, 115, 99, 114, 105,
+ 112, 116, 113, 0, 126, 0, 33, 76, 0, 7, 118, 97, 114, 105, 97, 110, 116, 113, 0, 126, 0, 33,
+ 120, 112, -1, -1, -1, -1, 116, 0, 2, 85, 83, 116, 0, 0, 116, 0, 2, 101, 110, 113, 0, 126, 0, 75,
+ 113, 0, 126, 0, 75, 120, 115, 113, 0, 126, 0, 39, 113, 0, 126, 0, 41, 113, 0, 126, 0, 40, 120
+ };
+
+ final static byte[][] content = {
+ generalInstance,
+ currencyInstance,
+ scientificInstance,
+ icu58Latest,
+ newFromPattern
+ };
}
package com.ibm.icu.dev.test.format;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.math.BigInteger;
+import java.math.RoundingMode;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.Format;
import com.ibm.icu.impl.LocaleUtility;
import com.ibm.icu.impl.data.ResourceReader;
import com.ibm.icu.impl.data.TokenIterator;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
import com.ibm.icu.math.BigDecimal;
import com.ibm.icu.math.MathContext;
import com.ibm.icu.text.CompactDecimalFormat;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.text.RuleBasedNumberFormat;
import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ULocale;
public class NumberFormatTest extends TestFmwk {
- private static ULocale EN = new ULocale("en");
-
- private static Number toNumber(String s) {
- if (s.equals("NaN")) {
- return Double.NaN;
- } else if (s.equals("-Inf")) {
- return Double.NEGATIVE_INFINITY;
- } else if (s.equals("Inf")) {
- return Double.POSITIVE_INFINITY;
- }
- return new BigDecimal(s);
- }
-
-
- private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU =
- new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
- @Override
- public Character Id() { return 'J'; }
-
- @Override
- public String format(NumberFormatTestData tuple) {
- DecimalFormat fmt = newDecimalFormat(tuple);
- String actual = fmt.format(toNumber(tuple.format));
- String expected = tuple.output;
- if (!expected.equals(actual)) {
- return "Expected " + expected + ", got " + actual;
- }
- return null;
- }
-
- @Override
- public String toPattern(NumberFormatTestData tuple) {
- DecimalFormat fmt = newDecimalFormat(tuple);
- StringBuilder result = new StringBuilder();
- if (tuple.toPattern != null) {
- String expected = tuple.toPattern;
- String actual = fmt.toPattern();
- if (!expected.equals(actual)) {
- result.append("Expected toPattern=" + expected + ", got " + actual);
- }
- }
- if (tuple.toLocalizedPattern != null) {
- String expected = tuple.toLocalizedPattern;
- String actual = fmt.toLocalizedPattern();
- if (!expected.equals(actual)) {
- result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
- }
- }
- return result.length() == 0 ? null : result.toString();
- }
-
- @Override
- public String parse(NumberFormatTestData tuple) {
- DecimalFormat fmt = newDecimalFormat(tuple);
- ParsePosition ppos = new ParsePosition(0);
- Number actual = fmt.parse(tuple.parse, ppos);
- if (ppos.getIndex() == 0) {
- if (!tuple.output.equals("fail")) {
- return "Parse error expected.";
- }
- return null;
- }
- if (tuple.output.equals("fail")) {
- return "Parse succeeded: "+actual+", but was expected to fail.";
- }
- Number expected = toNumber(tuple.output);
- // number types cannot be compared, this is the best we can do.
- if (expected.doubleValue() != (actual.doubleValue())) {
- return "Expected: " + expected + ", got: " + actual;
- }
- return null;
- }
-
- @Override
- public String parseCurrency(NumberFormatTestData tuple) {
- DecimalFormat fmt = newDecimalFormat(tuple);
- ParsePosition ppos = new ParsePosition(0);
- CurrencyAmount currAmt = fmt.parseCurrency(tuple.parse, ppos);
- if (ppos.getIndex() == 0) {
- if (!tuple.output.equals("fail")) {
- return "Parse error expected.";
- }
- return null;
- }
- if (tuple.output.equals("fail")) {
- return "Parse succeeded: "+currAmt+", but was expected to fail.";
- }
- Number expected = toNumber(tuple.output);
- Number actual = currAmt.getNumber();
- // number types cannot be compared, this is the best we can do.
- if (expected.doubleValue() != (actual.doubleValue())) {
- return "Expected: " + expected + ", got: " + actual;
- }
-
- if (!tuple.outputCurrency.equals(currAmt.getCurrency().toString())) {
- return "Expected currency: " + tuple.outputCurrency + ", got: " + currAmt.getCurrency();
- }
- return null;
- }
-
- /**
- * @param tuple
- * @return
- */
- private DecimalFormat newDecimalFormat(NumberFormatTestData tuple) {
-
- DecimalFormat fmt = new DecimalFormat(
- tuple.pattern == null ? "0" : tuple.pattern,
- new DecimalFormatSymbols(tuple.locale == null ? EN : tuple.locale));
- adjustDecimalFormat(tuple, fmt);
- return fmt;
- }
- /**
- * @param tuple
- * @param fmt
- */
- private void adjustDecimalFormat(NumberFormatTestData tuple, DecimalFormat fmt) {
- if (tuple.minIntegerDigits != null) {
- fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
- }
- if (tuple.maxIntegerDigits != null) {
- fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
- }
- if (tuple.minFractionDigits != null) {
- fmt.setMinimumFractionDigits(tuple.minFractionDigits);
- }
- if (tuple.maxFractionDigits != null) {
- fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
- }
- if (tuple.currency != null) {
- fmt.setCurrency(tuple.currency);
- }
- if (tuple.minGroupingDigits != null) {
- // Oops we don't support this.
- }
- if (tuple.useSigDigits != null) {
- fmt.setSignificantDigitsUsed(
- tuple.useSigDigits != 0);
- }
- if (tuple.minSigDigits != null) {
- fmt.setMinimumSignificantDigits(tuple.minSigDigits);
- }
- if (tuple.maxSigDigits != null) {
- fmt.setMaximumSignificantDigits(tuple.maxSigDigits);
- }
- if (tuple.useGrouping != null) {
- fmt.setGroupingUsed(tuple.useGrouping != 0);
- }
- if (tuple.multiplier != null) {
- fmt.setMultiplier(tuple.multiplier);
- }
- if (tuple.roundingIncrement != null) {
- fmt.setRoundingIncrement(tuple.roundingIncrement.doubleValue());
- }
- if (tuple.formatWidth != null) {
- fmt.setFormatWidth(tuple.formatWidth);
- }
- if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
- fmt.setPadCharacter(tuple.padCharacter.charAt(0));
- }
- if (tuple.useScientific != null) {
- fmt.setScientificNotation(tuple.useScientific != 0);
- }
- if (tuple.grouping != null) {
- fmt.setGroupingSize(tuple.grouping);
- }
- if (tuple.grouping2 != null) {
- fmt.setSecondaryGroupingSize(tuple.grouping2);
- }
- if (tuple.roundingMode != null) {
- fmt.setRoundingMode(tuple.roundingMode);
- }
- if (tuple.currencyUsage != null) {
- fmt.setCurrencyUsage(tuple.currencyUsage);
- } if (tuple.minimumExponentDigits != null) {
- fmt.setMinimumExponentDigits(
- tuple.minimumExponentDigits.byteValue());
- }
- if (tuple.exponentSignAlwaysShown != null) {
- fmt.setExponentSignAlwaysShown(
- tuple.exponentSignAlwaysShown != 0);
- }
- if (tuple.decimalSeparatorAlwaysShown != null) {
- fmt.setDecimalSeparatorAlwaysShown(
- tuple.decimalSeparatorAlwaysShown != 0);
- }
- if (tuple.padPosition != null) {
- fmt.setPadPosition(tuple.padPosition);
- }
- if (tuple.positivePrefix != null) {
- fmt.setPositivePrefix(tuple.positivePrefix);
- }
- if (tuple.positiveSuffix != null) {
- fmt.setPositiveSuffix(tuple.positiveSuffix);
- }
- if (tuple.negativePrefix != null) {
- fmt.setNegativePrefix(tuple.negativePrefix);
- }
- if (tuple.negativeSuffix != null) {
- fmt.setNegativeSuffix(tuple.negativeSuffix);
- }
- if (tuple.localizedPattern != null) {
- fmt.applyLocalizedPattern(tuple.localizedPattern);
- }
- int lenient = tuple.lenient == null ? 1 : tuple.lenient.intValue();
- fmt.setParseStrict(lenient == 0);
- if (tuple.parseIntegerOnly != null) {
- fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
- }
- if (tuple.decimalPatternMatchRequired != null) {
- fmt.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0);
- }
- if (tuple.parseNoExponent != null) {
- // Oops, not supported for now
- }
- }
- };
-
-
- private DataDrivenNumberFormatTestUtility.CodeUnderTest JDK =
- new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
- @Override
- public Character Id() { return 'K'; }
-
- @Override
- public String format(NumberFormatTestData tuple) {
- java.text.DecimalFormat fmt = newDecimalFormat(tuple);
- String actual = fmt.format(toNumber(tuple.format));
- String expected = tuple.output;
- if (!expected.equals(actual)) {
- return "Expected " + expected + ", got " + actual;
- }
- return null;
- }
-
- @Override
- public String toPattern(NumberFormatTestData tuple) {
- java.text.DecimalFormat fmt = newDecimalFormat(tuple);
- StringBuilder result = new StringBuilder();
- if (tuple.toPattern != null) {
- String expected = tuple.toPattern;
- String actual = fmt.toPattern();
- if (!expected.equals(actual)) {
- result.append("Expected toPattern=" + expected + ", got " + actual);
- }
- }
- if (tuple.toLocalizedPattern != null) {
- String expected = tuple.toLocalizedPattern;
- String actual = fmt.toLocalizedPattern();
- if (!expected.equals(actual)) {
- result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
- }
- }
- return result.length() == 0 ? null : result.toString();
- }
-
- @Override
- public String parse(NumberFormatTestData tuple) {
- java.text.DecimalFormat fmt = newDecimalFormat(tuple);
- ParsePosition ppos = new ParsePosition(0);
- Number actual = fmt.parse(tuple.parse, ppos);
- if (ppos.getIndex() == 0) {
- if (!tuple.output.equals("fail")) {
- return "Parse error expected.";
- }
- return null;
- }
- if (tuple.output.equals("fail")) {
- return "Parse succeeded: "+actual+", but was expected to fail.";
- }
- Number expected = toNumber(tuple.output);
- // number types cannot be compared, this is the best we can do.
- if (expected.doubleValue() != actual.doubleValue()) {
- return "Expected: " + expected + ", got: " + actual;
- }
- return null;
- }
-
-
-
- /**
- * @param tuple
- * @return
- */
- private java.text.DecimalFormat newDecimalFormat(NumberFormatTestData tuple) {
- java.text.DecimalFormat fmt = new java.text.DecimalFormat(
- tuple.pattern == null ? "0" : tuple.pattern,
- new java.text.DecimalFormatSymbols(
- (tuple.locale == null ? EN : tuple.locale).toLocale()));
- adjustDecimalFormat(tuple, fmt);
- return fmt;
- }
-
- /**
- * @param tuple
- * @param fmt
- */
- private void adjustDecimalFormat(NumberFormatTestData tuple, java.text.DecimalFormat fmt) {
- if (tuple.minIntegerDigits != null) {
- fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
- }
- if (tuple.maxIntegerDigits != null) {
- fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
- }
- if (tuple.minFractionDigits != null) {
- fmt.setMinimumFractionDigits(tuple.minFractionDigits);
- }
- if (tuple.maxFractionDigits != null) {
- fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
- }
- if (tuple.currency != null) {
- fmt.setCurrency(java.util.Currency.getInstance(tuple.currency.toString()));
- }
- if (tuple.minGroupingDigits != null) {
- // Oops we don't support this.
- }
- if (tuple.useSigDigits != null) {
- // Oops we don't support this
- }
- if (tuple.minSigDigits != null) {
- // Oops we don't support this
- }
- if (tuple.maxSigDigits != null) {
- // Oops we don't support this
- }
- if (tuple.useGrouping != null) {
- fmt.setGroupingUsed(tuple.useGrouping != 0);
- }
- if (tuple.multiplier != null) {
- fmt.setMultiplier(tuple.multiplier);
- }
- if (tuple.roundingIncrement != null) {
- // Not supported
- }
- if (tuple.formatWidth != null) {
- // Not supported
- }
- if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
- // Not supported
- }
- if (tuple.useScientific != null) {
- // Not supported
- }
- if (tuple.grouping != null) {
- fmt.setGroupingSize(tuple.grouping);
- }
- if (tuple.grouping2 != null) {
- // Not supported
- }
- if (tuple.roundingMode != null) {
- // Not supported
- }
- if (tuple.currencyUsage != null) {
- // Not supported
- }
- if (tuple.minimumExponentDigits != null) {
- // Not supported
- }
- if (tuple.exponentSignAlwaysShown != null) {
- // Not supported
- }
- if (tuple.decimalSeparatorAlwaysShown != null) {
- fmt.setDecimalSeparatorAlwaysShown(
- tuple.decimalSeparatorAlwaysShown != 0);
- }
- if (tuple.padPosition != null) {
- // Not supported
- }
- if (tuple.positivePrefix != null) {
- fmt.setPositivePrefix(tuple.positivePrefix);
- }
- if (tuple.positiveSuffix != null) {
- fmt.setPositiveSuffix(tuple.positiveSuffix);
- }
- if (tuple.negativePrefix != null) {
- fmt.setNegativePrefix(tuple.negativePrefix);
- }
- if (tuple.negativeSuffix != null) {
- fmt.setNegativeSuffix(tuple.negativeSuffix);
- }
- if (tuple.localizedPattern != null) {
- fmt.applyLocalizedPattern(tuple.localizedPattern);
- }
-
- // lenient parsing not supported by JDK
- if (tuple.parseIntegerOnly != null) {
- fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
- }
- if (tuple.decimalPatternMatchRequired != null) {
- // Oops, not supported
- }
- if (tuple.parseNoExponent != null) {
- // Oops, not supported for now
- }
- }
- };
-
@Test
public void TestRoundingScientific10542() {
DecimalFormat format =
DecimalFormatSymbols sym = new DecimalFormatSymbols(Locale.US);
final String pat[] = { "#.#", "#.", ".#", "#" };
int pat_length = pat.length;
- final String newpat[] = { "#0.#", "#0.", "#.0", "#" };
+ final String newpat[] = { "0.#", "0.", "#.0", "0" };
final String num[] = { "0", "0.", ".0", "0" };
for (int i=0; i<pat_length; ++i)
{
{"$124", "4", "-1"},
{"$124 $124", "4", "-1"},
{"$124 ", "4", "-1"},
+ {"$124 ", "4", "-1"},
{"$ 124 ", "5", "-1"},
{"$\u00A0124 ", "5", "-1"},
- {" $ 124 ", "0", "0"}, // TODO: need to handle space correctly
- {"124$", "0", "3"}, // TODO: need to handle space correctly
- // {"124 $", "5", "-1"}, TODO: OK or NOT?
- {"124 $", "0", "3"},
+ {" $ 124 ", "6", "-1"},
+ {"124$", "3", "-1"},
+ {"124 $", "3", "-1"},
+ {"$124\u200D", "4", "-1"},
+ {"$\u200D124", "5", "-1"},
};
NumberFormat foo = NumberFormat.getCurrencyInstance();
for (int i = 0; i < DATA.length; ++i) {
}
}
+ @Test
+ public void TestSpaceParsingStrict() {
+ // All trailing grouping separators should be ignored in strict mode, not just the first.
+ Object[][] cases = {
+ {"123 ", 3, -1},
+ {"123 ", 3, -1},
+ {"123 ,", 3, -1},
+ {"123,", 3, -1},
+ {"123, ", 3, -1},
+ {"123,,", 3, -1},
+ {"123,, ", 3, -1},
+ {"123 ,", 3, -1},
+ {"123, ", 3, -1},
+ {"123, 456", 3, -1},
+ {"123 456", 0, 8} // TODO: Does this behavior make sense?
+ };
+ DecimalFormat df = new DecimalFormat("#,###");
+ df.setParseStrict(true);
+ for (Object[] cas : cases) {
+ String input = (String) cas[0];
+ int expectedIndex = (Integer) cas[1];
+ int expectedErrorIndex = (Integer) cas[2];
+ ParsePosition ppos = new ParsePosition(0);
+ df.parse(input, ppos);
+ assertEquals("Failed on index: '" + input + "'", expectedIndex, ppos.getIndex());
+ assertEquals("Failed on error: '" + input + "'", expectedErrorIndex, ppos.getErrorIndex());
+ }
+ }
@Test
public void TestMultiCurrencySign() {
}
try {
// mix style parsing
- for (int k=3; k<=5; ++k) {
+ for (int k=3; k<=4; ++k) {
// DATA[i][3] is the currency format result using a
// single currency sign.
// DATA[i][4] is the currency format result using
// double currency sign.
// DATA[i][5] is the currency format result using
// triple currency sign.
+ // ICU 59: long name parsing requires currency mode.
String oneCurrencyFormat = DATA[i][k];
if (fmt.parse(oneCurrencyFormat).doubleValue() !=
numberToBeFormat.doubleValue()) {
if (!strBuf.equals(formatResult)) {
errln("FAIL: localeID: " + localeString + ", expected(" + formatResult.length() + "): \"" + formatResult + "\", actual(" + strBuf.length() + "): \"" + strBuf + "\"");
}
- try {
- // test parsing, and test parsing for all currency formats.
- for (int j = 3; j < 6; ++j) {
- // DATA[i][3] is the currency format result using
- // CURRENCYSTYLE formatter.
- // DATA[i][4] is the currency format result using
- // ISOCURRENCYSTYLE formatter.
- // DATA[i][5] is the currency format result using
- // PLURALCURRENCYSTYLE formatter.
- String oneCurrencyFormatResult = DATA[i][j];
- Number val = numFmt.parse(oneCurrencyFormatResult);
- if (val.doubleValue() != numberToBeFormat.doubleValue()) {
- errln("FAIL: getCurrencyFormat of locale " + localeString + " failed roundtripping the number. val=" + val + "; expected: " + numberToBeFormat);
- }
+ // test parsing, and test parsing for all currency formats.
+ for (int j = 3; j < 6; ++j) {
+ // DATA[i][3] is the currency format result using
+ // CURRENCYSTYLE formatter.
+ // DATA[i][4] is the currency format result using
+ // ISOCURRENCYSTYLE formatter.
+ // DATA[i][5] is the currency format result using
+ // PLURALCURRENCYSTYLE formatter.
+ String oneCurrencyFormatResult = DATA[i][j];
+ CurrencyAmount val = numFmt.parseCurrency(oneCurrencyFormatResult, null);
+ if (val.getNumber().doubleValue() != numberToBeFormat.doubleValue()) {
+ errln("FAIL: getCurrencyFormat of locale " + localeString + " failed roundtripping the number. val=" + val + "; expected: " + numberToBeFormat);
}
}
- catch (ParseException e) {
- errln("FAIL: " + e.getMessage());
- }
}
}
}
public void TestMiscCurrencyParsing() {
String[][] DATA = {
// each has: string to be parsed, parsed position, error position
- {"1.00 ", "0", "4"},
- {"1.00 UAE dirha", "0", "4"},
- {"1.00 us dollar", "14", "-1"},
- {"1.00 US DOLLAR", "14", "-1"},
- {"1.00 usd", "0", "4"},
+ {"1.00 ", "4", "-1", "0", "5"},
+ {"1.00 UAE dirha", "4", "-1", "0", "14"},
+ {"1.00 us dollar", "4", "-1", "14", "-1"},
+ {"1.00 US DOLLAR", "4", "-1", "14", "-1"},
+ {"1.00 usd", "4", "-1", "8", "-1"},
+ {"1.00 USD", "4", "-1", "8", "-1"},
};
ULocale locale = new ULocale("en_US");
for (int i=0; i<DATA.length; ++i) {
String stringToBeParsed = DATA[i][0];
int parsedPosition = Integer.parseInt(DATA[i][1]);
int errorIndex = Integer.parseInt(DATA[i][2]);
+ int currParsedPosition = Integer.parseInt(DATA[i][3]);
+ int currErrorIndex = Integer.parseInt(DATA[i][4]);
NumberFormat numFmt = NumberFormat.getInstance(locale, NumberFormat.CURRENCYSTYLE);
ParsePosition parsePosition = new ParsePosition(0);
Number val = numFmt.parse(stringToBeParsed, parsePosition);
if (parsePosition.getIndex() != parsedPosition ||
parsePosition.getErrorIndex() != errorIndex) {
- errln("FAIL: parse failed. expected error position: " + errorIndex + "; actual: " + parsePosition.getErrorIndex());
- errln("FAIL: parse failed. expected position: " + parsedPosition +"; actual: " + parsePosition.getIndex());
+ errln("FAIL: parse failed on case "+i+". expected position: " + parsedPosition +"; actual: " + parsePosition.getIndex());
+ errln("FAIL: parse failed on case "+i+". expected error position: " + errorIndex + "; actual: " + parsePosition.getErrorIndex());
}
if (parsePosition.getErrorIndex() == -1 &&
val.doubleValue() != 1.00) {
errln("FAIL: parse failed. expected 1.00, actual:" + val);
}
+ parsePosition = new ParsePosition(0);
+ CurrencyAmount amt = numFmt.parseCurrency(stringToBeParsed, parsePosition);
+ if (parsePosition.getIndex() != currParsedPosition ||
+ parsePosition.getErrorIndex() != currErrorIndex) {
+ errln("FAIL: parseCurrency failed on case "+i+". expected error position: " + currErrorIndex + "; actual: " + parsePosition.getErrorIndex());
+ errln("FAIL: parseCurrency failed on case "+i+". expected position: " + currParsedPosition +"; actual: " + parsePosition.getIndex());
+ }
+ if (parsePosition.getErrorIndex() == -1 &&
+ amt.getNumber().doubleValue() != 1.00) {
+ errln("FAIL: parseCurrency failed. expected 1.00, actual:" + val);
+ }
}
}
public int getCurExpectVal() { return curExpectVal; }
public String getCurExpectCurr() { return curExpectCurr; }
}
+ // Note: In cases where the number occurs before the currency sign, non-currency mode will parse the number
+ // and stop when it reaches the currency symbol.
final ParseCurrencyItem[] parseCurrencyItems = {
new ParseCurrencyItem( "en_US", "dollars2", "$2.00", 5, 2, 5, 2, "USD" ),
new ParseCurrencyItem( "en_US", "dollars4", "$4", 2, 4, 2, 4, "USD" ),
- new ParseCurrencyItem( "en_US", "dollars9", "9\u00A0$", 0, 0, 0, 0, "" ),
+ new ParseCurrencyItem( "en_US", "dollars9", "9\u00A0$", 1, 9, 3, 9, "USD" ),
new ParseCurrencyItem( "en_US", "pounds3", "\u00A33.00", 0, 0, 5, 3, "GBP" ),
new ParseCurrencyItem( "en_US", "pounds5", "\u00A35", 0, 0, 2, 5, "GBP" ),
- new ParseCurrencyItem( "en_US", "pounds7", "7\u00A0\u00A3", 0, 0, 0, 0, "" ),
+ new ParseCurrencyItem( "en_US", "pounds7", "7\u00A0\u00A3", 1, 7, 3, 7, "GBP" ),
new ParseCurrencyItem( "en_US", "euros8", "\u20AC8", 0, 0, 2, 8, "EUR" ),
new ParseCurrencyItem( "en_GB", "pounds3", "\u00A33.00", 5, 3, 5, 3, "GBP" ),
new ParseCurrencyItem( "en_GB", "pounds5", "\u00A35", 2, 5, 2, 5, "GBP" ),
- new ParseCurrencyItem( "en_GB", "pounds7", "7\u00A0\u00A3", 0, 0, 0, 0, "" ),
- new ParseCurrencyItem( "en_GB", "euros4", "4,00\u00A0\u20AC", 0, 0, 0, 0, "" ),
- new ParseCurrencyItem( "en_GB", "euros6", "6\u00A0\u20AC", 0, 0, 0, 0, "" ),
+ new ParseCurrencyItem( "en_GB", "pounds7", "7\u00A0\u00A3", 1, 7, 3, 7, "GBP" ),
+ new ParseCurrencyItem( "en_GB", "euros4", "4,00\u00A0\u20AC", 4,400, 6,400, "EUR" ),
+ new ParseCurrencyItem( "en_GB", "euros6", "6\u00A0\u20AC", 1, 6, 3, 6, "EUR" ),
new ParseCurrencyItem( "en_GB", "euros8", "\u20AC8", 0, 0, 2, 8, "EUR" ),
new ParseCurrencyItem( "en_GB", "dollars4", "US$4", 0, 0, 4, 4, "USD" ),
new ParseCurrencyItem( "fr_FR", "euros4", "4,00\u00A0\u20AC", 6, 4, 6, 4, "EUR" ),
new ParseCurrencyItem( "fr_FR", "euros6", "6\u00A0\u20AC", 3, 6, 3, 6, "EUR" ),
- new ParseCurrencyItem( "fr_FR", "euros8", "\u20AC8", 0, 0, 0, 0, "" ),
+ new ParseCurrencyItem( "fr_FR", "euros8", "\u20AC8", 0, 0, 2, 8, "EUR" ),
new ParseCurrencyItem( "fr_FR", "dollars2", "$2.00", 0, 0, 0, 0, "" ),
new ParseCurrencyItem( "fr_FR", "dollars4", "$4", 0, 0, 0, 0, "" ),
};
}
}
+ @Test
+ public void TestParseCurrencyWithWhitespace() {
+ DecimalFormat df = new DecimalFormat("#,##0.00 ¤¤");
+ ParsePosition ppos = new ParsePosition(0);
+ df.parseCurrency("1.00 us denmark", ppos);
+ assertEquals("Expected to fail on 'us denmark' string", 9, ppos.getErrorIndex());
+ }
+
@Test
public void TestParseCurrPatternWithDecStyle() {
String currpat = "¤#,##0.00";
DecimalFormat f = new DecimalFormat("#,##,###", US);
expect(f, 123456789L, "12,34,56,789");
- expectPat(f, "#,##,###");
+ expectPat(f, "#,##,##0");
f.applyPattern("#,###");
f.setSecondaryGroupingSize(4);
expect(f, 123456789L, "12,3456,789");
- expectPat(f, "#,####,###");
+ expectPat(f, "#,####,##0");
NumberFormat g = NumberFormat.getInstance(new Locale("hi", "IN"));
String out = "";
errln("FAIL Pattern rt \"" + pat + "\" . \"" + pat2 + "\"");
}
// Make sure digit counts match what we expect
+ if (i == 0) continue; // outputs to 1,1,0,0 since at least one min digit is required.
if (df.getMinimumIntegerDigits() != DIGITS[4 * i]
|| df.getMaximumIntegerDigits() != DIGITS[4 * i + 1]
|| df.getMinimumFractionDigits() != DIGITS[4 * i + 2]
UnicodeString pattern(patternChars);
expectPad(fmt, pattern , DecimalFormat.kPadBeforePrefix, 4, padString);
*/
+
+ // Test multi-char padding sequence specified via pattern
+ expect2(new DecimalFormat("*'😃'####.00", US), 1.1, "😃😃😃1.10");
}
/**
// 12 3456789012345
expectPat(fmt, "AA*^####,##0.00ZZ"); // This is the interesting case
+ // The new implementation produces "AA*^#####,##0.00ZZ", which is functionally equivalent
+ // to what the old implementation produced, "AA*^#,###,##0.00ZZ"
fmt.setFormatWidth(16);
// 12 34567890123456
- expectPat(fmt, "AA*^#,###,##0.00ZZ");
+ //expectPat(fmt, "AA*^#,###,##0.00ZZ");
+ expectPat(fmt, "AA*^#####,##0.00ZZ");
}
@Test
errln("FAIL: isScientificNotation = true, expect false");
}
- df.applyPattern("0.00000");
+ // Create a new instance to flush out currency info
+ df = new DecimalFormat("0.00000", US);
df.setScientificNotation(true);
if (!df.isScientificNotation()) {
errln("FAIL: isScientificNotation = false, expect true");
}
}
+ @Test
+ public void TestParseNull() throws ParseException {
+ DecimalFormat df = new DecimalFormat();
+ try {
+ df.parse(null);
+ fail("df.parse(null) didn't throw an exception");
+ } catch (IllegalArgumentException e){}
+ try {
+ df.parse(null, null);
+ fail("df.parse(null) didn't throw an exception");
+ } catch (IllegalArgumentException e){}
+ try {
+ df.parseCurrency(null, null);
+ fail("df.parse(null) didn't throw an exception");
+ } catch (IllegalArgumentException e){}
+ }
+
@Test
public void TestWhiteSpaceParsing() {
DecimalFormatSymbols US = new DecimalFormatSymbols(Locale.US);
int n = 1234;
expect(fmt, "a b1234c ", n);
expect(fmt, "a b1234c ", n);
+ expect(fmt, "ab1234", n);
+
+ fmt.applyPattern("a b #");
+ expect(fmt, "ab1234", n);
+ expect(fmt, "ab 1234", n);
+ expect(fmt, "a b1234", n);
+ expect(fmt, "a b1234", n);
}
/**
// Support methods
//------------------------------------------------------------------
- // Format-Parse test
+ /** Format-Parse test */
public void expect2(NumberFormat fmt, Number n, String exp) {
// Don't round-trip format test, since we explicitly do it
expect(fmt, n, exp, false);
expect(fmt, exp, n);
}
- // Format-Parse test
+ /** Format-Parse test */
public void expect3(NumberFormat fmt, Number n, String exp) {
// Don't round-trip format test, since we explicitly do it
expect_rbnf(fmt, n, exp, false);
expect_rbnf(fmt, exp, n);
}
- // Format-Parse test (convenience)
+ /** Format-Parse test (convenience) */
public void expect2(NumberFormat fmt, double n, String exp) {
expect2(fmt, new Double(n), exp);
}
- // Format-Parse test (convenience)
+ /** RBNF Format-Parse test (convenience) */
public void expect3(NumberFormat fmt, double n, String exp) {
expect3(fmt, new Double(n), exp);
}
- // Format-Parse test (convenience)
+ /** Format-Parse test (convenience) */
public void expect2(NumberFormat fmt, long n, String exp) {
expect2(fmt, new Long(n), exp);
}
- // Format-Parse test (convenience)
+ /** RBNF Format-Parse test (convenience) */
public void expect3(NumberFormat fmt, long n, String exp) {
expect3(fmt, new Long(n), exp);
}
- // Format test
+ /** Format test */
public void expect(NumberFormat fmt, Number n, String exp, boolean rt) {
StringBuffer saw = new StringBuffer();
FieldPosition pos = new FieldPosition(0);
", FAIL " + n + " x " + pat + " = \"" + saw + "\", expected \"" + exp + "\"");
}
}
- // Format test
+ /** RBNF format test */
public void expect_rbnf(NumberFormat fmt, Number n, String exp, boolean rt) {
StringBuffer saw = new StringBuffer();
FieldPosition pos = new FieldPosition(0);
}
}
- // Format test (convenience)
+ /** Format test (convenience) */
public void expect(NumberFormat fmt, Number n, String exp) {
expect(fmt, n, exp, true);
}
- // Format test (convenience)
+ /** Format test (convenience) */
public void expect(NumberFormat fmt, double n, String exp) {
expect(fmt, new Double(n), exp);
}
- // Format test (convenience)
+ /** Format test (convenience) */
public void expect(NumberFormat fmt, long n, String exp) {
expect(fmt, new Long(n), exp);
}
- // Parse test
+ /** Parse test */
public void expect(NumberFormat fmt, String str, Number n) {
Number num = null;
try {
}
}
- // Parse test
+ /** RBNF Parse test */
public void expect_rbnf(NumberFormat fmt, String str, Number n) {
Number num = null;
try {
}
}
- // Parse test (convenience)
+ /** Parse test (convenience) */
public void expect(NumberFormat fmt, String str, double n) {
expect(fmt, str, new Double(n));
}
- // Parse test (convenience)
+ /** Parse test (convenience) */
public void expect(NumberFormat fmt, String str, long n) {
expect(fmt, str, new Long(n));
}
}
}
+ @Test
+ public void TestScientificWithGrouping() {
+ DecimalFormat df = new DecimalFormat("#,##0.000E0");
+ expect2(df, 123, "123.0E0");
+ expect2(df, 1234, "1,234E0");
+ expect2(df, 12340, "1.234E4");
+ }
+
@Test
public void TestStrictParse() {
String[] pass = {
"00", // leading zero before zero - used to be error - see ticket #7913
"012", // leading zero before digit - used to be error - see ticket #7913
"0,456", // leading zero before group separator - used to be error - see ticket #7913
+ "999,999", // see ticket #6863
+ "-99,999", // see ticket #6863
+ "-999,999", // see ticket #6863
+ "-9,999,999", // see ticket #6863
};
String[] fail = {
"1,2", // wrong number of digits after group separator
"00E2", // leading zeroes now allowed in strict mode - see ticket #
};
String[] scientificFail = {
- "1,234E2", // group separators with exponent fail
};
nf = (DecimalFormat) NumberFormat.getInstance(Locale.ENGLISH);
// For valid array, it is displayed as {min value, max value}
// Tests when "if (minimumIntegerDigits > maximumIntegerDigits)" is true
int[][] cases = { { -1, 0 }, { 0, 1 }, { 1, 0 }, { 2, 0 }, { 2, 1 }, { 10, 0 } };
- int[] expectedMax = { 0, 1, 1, 2, 2, 10 };
+ int[] expectedMax = { 1, 1, 1, 2, 2, 10 };
if (cases.length != expectedMax.length) {
errln("Can't continue test case method TestSetMinimumIntegerDigits "
+ "since the test case arrays are unequal.");
} else {
for (int i = 0; i < cases.length; i++) {
- nf.setMaximumIntegerDigits(cases[i][1]);
nf.setMinimumIntegerDigits(cases[i][0]);
+ nf.setMaximumIntegerDigits(cases[i][1]);
if (nf.getMaximumIntegerDigits() != expectedMax[i]) {
errln("NumberFormat.setMinimumIntegerDigits(int newValue "
- + "did not return an expected result for parameter " + cases[i][1] + " and " + cases[i][0]
+ + "did not return an expected result for parameter " + cases[i][0] + " and " + cases[i][1]
+ " and expected " + expectedMax[i] + " but got " + nf.getMaximumIntegerDigits());
}
}
}
}
+ /*
+ * This feature had to do with a limitation in DigitList.java that no longer exists in the
+ * new implementation.
+ *
@Test
public void TestParseMaxDigits() {
DecimalFormat fmt = new DecimalFormat();
fmt.setParseMaxDigits(-1);
- /* Default value is 1000 */
+ // Default value is 1000
if (fmt.getParseMaxDigits() != 1000) {
errln("Fail valid value checking in setParseMaxDigits.");
}
}
}
+ */
private static class FormatCharItrTestThread implements Runnable {
private final NumberFormat fmt;
}
}
+ @Test
+ public void TestCurrencyWithMinMaxFractionDigits() {
+ DecimalFormat df = new DecimalFormat();
+ df.applyPattern("¤#,##0.00");
+ df.setCurrency(Currency.getInstance("USD"));
+ assertEquals("Basic currency format fails", "$1.23", df.format(1.234));
+ df.setMaximumFractionDigits(4);
+ assertEquals("Currency with max fraction == 4", "$1.234", df.format(1.234));
+ df.setMinimumFractionDigits(4);
+ assertEquals("Currency with min fraction == 4", "$1.2340", df.format(1.234));
+ }
+
@Test
public void TestParseRequiredDecimalPoint() {
String[] testPattern = { "00.####", "00.0", "00" };
String value2Parse = "99";
+ String value2ParseWithDecimal = "99.9";
double parseValue = 99;
+ double parseValueWithDecimal = 99.9;
DecimalFormat parser = new DecimalFormat();
double result;
boolean hasDecimalPoint;
TestFmwk.errln("Parsing " + value2Parse + " should have succeeded with " + testPattern[i] +
" and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired());
}
+ try {
+ result = parser.parse(value2ParseWithDecimal).doubleValue();
+ assertEquals("wrong parsed value", parseValueWithDecimal, result);
+ } catch (ParseException e) {
+ TestFmwk.errln("Parsing " + value2ParseWithDecimal + " should have succeeded with " + testPattern[i] +
+ " and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired());
+ }
parser.setDecimalPatternMatchRequired(true);
try {
} catch (ParseException e) {
// OK, should fail
}
+ try {
+ result = parser.parse(value2ParseWithDecimal).doubleValue();
+ if(!hasDecimalPoint){
+ TestFmwk.errln("Parsing " + value2ParseWithDecimal + " should NOT have succeeded with " + testPattern[i] +
+ " and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired());
+ }
+ } catch (ParseException e) {
+ // OK, should fail
+ }
}
-
- }
-
-
- //TODO(junit): investigate
- @Test
- public void TestDataDrivenICU() {
- DataDrivenNumberFormatTestUtility.runSuite(
- "numberformattestspecification.txt", ICU);
}
- //TODO(junit): investigate
- @Test
- public void TestDataDrivenJDK() {
- DataDrivenNumberFormatTestUtility.runSuite(
- "numberformattestspecification.txt", JDK);
- }
-
-
@Test
public void TestCurrFmtNegSameAsPositive() {
DecimalFormatSymbols decfmtsym = DecimalFormatSymbols.getInstance(Locale.US);
decfmtsym.setMinusSign('\u200B'); // ZERO WIDTH SPACE, in ICU4J cannot set to empty string
- DecimalFormat decfmt = new DecimalFormat("\u00A4#,##0.00;\u00A4#,##0.00", decfmtsym);
+ DecimalFormat decfmt = new DecimalFormat("\u00A4#,##0.00;-\u00A4#,##0.00", decfmtsym);
String currFmtResult = decfmt.format(-100.0);
if (!currFmtResult.equals("\u200B$100.00")) {
errln("decfmt.toPattern results wrong, expected \u200B$100.00, got " + currFmtResult);
@Test
public void TestNumberFormatTestDataToString() {
- new NumberFormatTestData().toString();
+ new DataDrivenNumberFormatTestData().toString();
}
// Testing for Issue 11805.
result.get(i).value);
}
}
- // TODO: restore when #11914 is fixed.
- // assertTrue("Comparing vector results for " + formattedOutput,
- // expected.containsAll(result));
+ assertTrue("Comparing vector results for " + formattedOutput, expected.containsAll(result));
}
// Testing for Issue 11914, missing FieldPositions for some field types.
public void TestStringSymbols() {
DecimalFormatSymbols symbols = new DecimalFormatSymbols(ULocale.US);
+ // Attempt digits with multiple code points.
String[] customDigits = {"(0)", "(1)", "(2)", "(3)", "(4)", "(5)", "(6)", "(7)", "(8)", "(9)"};
symbols.setDigitStrings(customDigits);
- symbols.setDecimalSeparatorString("~~");
- symbols.setGroupingSeparatorString("^^");
-
DecimalFormat fmt = new DecimalFormat("#,##0.0#", symbols);
+ expect2(fmt, 1234567.89, "(1),(2)(3)(4),(5)(6)(7).(8)(9)");
+
+ // Scientific notation should work.
+ fmt.applyPattern("@@@E0");
+ expect2(fmt, 1230000, "(1).(2)(3)E(6)");
- expect2(fmt, 1234567.89, "(1)^^(2)(3)(4)^^(5)(6)(7)~~(8)(9)");
+ // Grouping and decimal with multiple code points are not supported during parsing.
+ symbols.setDecimalSeparatorString("~~");
+ symbols.setGroupingSeparatorString("^^");
+ fmt.setDecimalFormatSymbols(symbols);
+ fmt.applyPattern("#,##0.0#");
+ assertEquals("Custom decimal and grouping separator string with multiple characters",
+ fmt.format(1234567.89), "(1)^^(2)(3)(4)^^(5)(6)(7)~~(8)(9)");
+
+ // Digits starting at U+1D7CE MATHEMATICAL BOLD DIGIT ZERO
+ // These are all single code points, so parsing will work.
+ for (int i=0; i<10; i++) customDigits[i] = new String(Character.toChars(0x1D7CE+i));
+ symbols.setDigitStrings(customDigits);
+ symbols.setDecimalSeparatorString("😁");
+ symbols.setGroupingSeparatorString("😎");
+ fmt.setDecimalFormatSymbols(symbols);
+ expect2(fmt, 1234.56, "𝟏😎𝟐𝟑𝟒😁𝟓𝟔");
}
@Test
public void TestArabicCurrencyPatternInfo() {
ULocale arLocale = new ULocale("ar");
-
+
DecimalFormatSymbols symbols = new DecimalFormatSymbols(arLocale);
String currSpacingPatn = symbols.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, true);
if (currSpacingPatn==null || currSpacingPatn.length() == 0) {
errln("locale ar, getPatternForCurrencySpacing returns null or 0-length string");
}
-
+
DecimalFormat currAcctFormat = (DecimalFormat)NumberFormat.getInstance(arLocale, NumberFormat.ACCOUNTINGCURRENCYSTYLE);
String currAcctPatn = currAcctFormat.toPattern();
if (currAcctPatn==null || currAcctPatn.length() == 0) {
errln("locale ar, toPattern for ACCOUNTINGCURRENCYSTYLE returns null or 0-length string");
}
}
+
+ @Test
+ public void Test10436() {
+ DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(Locale.ENGLISH);
+ df.setRoundingMode(MathContext.ROUND_CEILING);
+ df.setMinimumFractionDigits(0);
+ df.setMaximumFractionDigits(0);
+ assertEquals("-.99 should round toward infinity", "-0", df.format(-0.99));
+ }
+
+ @Test
+ public void Test10765() {
+ NumberFormat fmt = NumberFormat.getInstance(new ULocale("en"));
+ fmt.setMinimumIntegerDigits(10);
+ FieldPosition pos = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR);
+ fmt.format(1234, new StringBuffer(), pos);
+ assertEquals("FieldPosition should report the first occurence", 1, pos.getBeginIndex());
+ assertEquals("FieldPosition should report the first occurence", 2, pos.getEndIndex());
+ }
+
+ @Test
+ public void Test10997() {
+ NumberFormat fmt = NumberFormat.getCurrencyInstance(new ULocale("en-US"));
+ fmt.setMinimumFractionDigits(4);
+ fmt.setMaximumFractionDigits(4);
+ String str1 = fmt.format(new CurrencyAmount(123.45, Currency.getInstance("USD")));
+ String str2 = fmt.format(new CurrencyAmount(123.45, Currency.getInstance("EUR")));
+ assertEquals("minFrac 4 should be respected in default currency", "$123.4500", str1);
+ assertEquals("minFrac 4 should be respected in different currency", "€123.4500", str2);
+ }
+
+ @Test
+ public void Test11020() {
+ DecimalFormatSymbols sym = new DecimalFormatSymbols(ULocale.FRANCE);
+ DecimalFormat fmt = new DecimalFormat("0.05E0", sym);
+ String result = fmt.format(12301.2).replace('\u00a0', ' ');
+ assertEquals("Rounding increment should be applied after magnitude scaling", "1,25E4", result);
+ }
+
+ @Test
+ public void Test11025() {
+ String pattern = "¤¤ **####0.00";
+ DecimalFormatSymbols sym = new DecimalFormatSymbols(ULocale.FRANCE);
+ DecimalFormat fmt = new DecimalFormat(pattern, sym);
+ String result = fmt.format(433.0);
+ assertEquals("Number should be padded to 11 characters", "EUR *433,00", result);
+ }
+
+ @Test
+ public void Test11640() {
+ DecimalFormat df = (DecimalFormat) NumberFormat.getInstance();
+ df.applyPattern("¤¤¤ 0");
+ String result = df.getPositivePrefix();
+ assertEquals("Triple-currency should give long name on getPositivePrefix", "US dollars ", result);
+ }
+
+ @Test
+ public void Test11645() {
+ String pattern = "#,##0.0#";
+ DecimalFormat fmt = (DecimalFormat) NumberFormat.getInstance();
+ fmt.applyPattern(pattern);
+ DecimalFormat fmtCopy;
+
+ final int newMultiplier = 37;
+ fmtCopy = (DecimalFormat) fmt.clone();
+ assertNotEquals("Value before setter", fmtCopy.getMultiplier(), newMultiplier);
+ fmtCopy.setMultiplier(newMultiplier);
+ assertEquals("Value after setter", fmtCopy.getMultiplier(), newMultiplier);
+ fmtCopy.applyPattern(pattern);
+ assertEquals("Value after applyPattern", fmtCopy.getMultiplier(), newMultiplier);
+ assertFalse("multiplier", fmt.equals(fmtCopy));
+
+ final int newRoundingMode = RoundingMode.CEILING.ordinal();
+ fmtCopy = (DecimalFormat) fmt.clone();
+ assertNotEquals("Value before setter", fmtCopy.getRoundingMode(), newRoundingMode);
+ fmtCopy.setRoundingMode(newRoundingMode);
+ assertEquals("Value after setter", fmtCopy.getRoundingMode(), newRoundingMode);
+ fmtCopy.applyPattern(pattern);
+ assertEquals("Value after applyPattern", fmtCopy.getRoundingMode(), newRoundingMode);
+ assertFalse("roundingMode", fmt.equals(fmtCopy));
+
+ final Currency newCurrency = Currency.getInstance("EAT");
+ fmtCopy = (DecimalFormat) fmt.clone();
+ assertNotEquals("Value before setter", fmtCopy.getCurrency(), newCurrency);
+ fmtCopy.setCurrency(newCurrency);
+ assertEquals("Value after setter", fmtCopy.getCurrency(), newCurrency);
+ fmtCopy.applyPattern(pattern);
+ assertEquals("Value after applyPattern", fmtCopy.getCurrency(), newCurrency);
+ assertFalse("currency", fmt.equals(fmtCopy));
+
+ final CurrencyUsage newCurrencyUsage = CurrencyUsage.CASH;
+ fmtCopy = (DecimalFormat) fmt.clone();
+ assertNotEquals("Value before setter", fmtCopy.getCurrencyUsage(), newCurrencyUsage);
+ fmtCopy.setCurrencyUsage(CurrencyUsage.CASH);
+ assertEquals("Value after setter", fmtCopy.getCurrencyUsage(), newCurrencyUsage);
+ fmtCopy.applyPattern(pattern);
+ assertEquals("Value after applyPattern", fmtCopy.getCurrencyUsage(), newCurrencyUsage);
+ assertFalse("currencyUsage", fmt.equals(fmtCopy));
+ }
+
+ @Test
+ public void Test11646() {
+ DecimalFormatSymbols symbols = new DecimalFormatSymbols(new ULocale("en_US"));
+ String pattern = "\u00a4\u00a4\u00a4 0.00 %\u00a4\u00a4";
+ DecimalFormat fmt = new DecimalFormat(pattern, symbols);
+
+ // Test equality with affixes. set affix methods can't capture special
+ // characters which is why equality should fail.
+ {
+ DecimalFormat fmtCopy = (DecimalFormat) fmt.clone();
+ assertEquals("", fmt, fmtCopy);
+ fmtCopy.setPositivePrefix(fmtCopy.getPositivePrefix());
+ assertNotEquals("", fmt, fmtCopy);
+ }
+ {
+ DecimalFormat fmtCopy = (DecimalFormat) fmt.clone();
+ assertEquals("", fmt, fmtCopy);
+ fmtCopy.setPositiveSuffix(fmtCopy.getPositiveSuffix());
+ assertNotEquals("", fmt, fmtCopy);
+ }
+ {
+ DecimalFormat fmtCopy = (DecimalFormat) fmt.clone();
+ assertEquals("", fmt, fmtCopy);
+ fmtCopy.setNegativePrefix(fmtCopy.getNegativePrefix());
+ assertNotEquals("", fmt, fmtCopy);
+ }
+ {
+ DecimalFormat fmtCopy = (DecimalFormat) fmt.clone();
+ assertEquals("", fmt, fmtCopy);
+ fmtCopy.setNegativeSuffix(fmtCopy.getNegativeSuffix());
+ assertNotEquals("", fmt, fmtCopy);
+ }
+ }
+
+ @Test
+ public void Test11648() {
+ DecimalFormat df = new DecimalFormat("0.00");
+ df.setScientificNotation(true);
+ String pat = df.toPattern();
+ assertEquals("A valid scientific notation pattern should be produced", "0.00E0", pat);
+ }
+
+ @Test
+ public void Test11649() {
+ String pattern = "\u00a4\u00a4\u00a4 0.00";
+ DecimalFormat fmt = new DecimalFormat(pattern);
+ fmt.setCurrency(Currency.getInstance("USD"));
+ assertEquals("Triple currency sign should format long name", "US dollars 12.34", fmt.format(12.34));
+
+ String newPattern = fmt.toPattern();
+ assertEquals("Should produce a valid pattern", pattern, newPattern);
+
+ DecimalFormat fmt2 = new DecimalFormat(newPattern);
+ fmt2.setCurrency(Currency.getInstance("USD"));
+ assertEquals("Triple currency sign pattern should round-trip", "US dollars 12.34", fmt2.format(12.34));
+ }
+
+ @Test
+ public void Test11686() {
+ DecimalFormat df = new DecimalFormat();
+ df.setPositiveSuffix("0K");
+ df.setNegativeSuffix("0N");
+ expect2(df, 123, "1230K");
+ expect2(df, -123, "1230N");
+ }
+
+ @Test
+ public void Test11839() {
+ DecimalFormatSymbols dfs = new DecimalFormatSymbols(ULocale.ENGLISH);
+ dfs.setMinusSign('∸');
+ dfs.setPlusSign('∔'); // ∔ U+2214 DOT PLUS
+ DecimalFormat df = new DecimalFormat("0.00+;0.00-", dfs);
+ String result = df.format(-1.234);
+ assertEquals("Locale-specific minus sign should be used", "1.23∸", result);
+ result = df.format(1.234);
+ assertEquals("Locale-specific plus sign should be used", "1.23∔", result);
+ }
+
+ @Test
+ public void Test12753() {
+ ULocale locale = new ULocale("en-US");
+ DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
+ symbols.setDecimalSeparator('*');
+ DecimalFormat df = new DecimalFormat("0.00", symbols);
+ df.setDecimalPatternMatchRequired(true);
+ try {
+ df.parse("123");
+ fail("Parsing integer succeeded even though setDecimalPatternMatchRequired was set");
+ } catch (ParseException e) {
+ // Parse failed (expected)
+ }
+ }
+
+ @Test
+ public void Test12962() {
+ String pat = "**0.00";
+ DecimalFormat df = new DecimalFormat(pat);
+ String newPat = df.toPattern();
+ assertEquals("Format width changed upon calling applyPattern", pat.length(), newPat.length());
+ }
+
+ @Test
+ public void Test10354() {
+ DecimalFormatSymbols dfs = new DecimalFormatSymbols();
+ dfs.setNaN("");
+ DecimalFormat df = new DecimalFormat();
+ df.setDecimalFormatSymbols(dfs);
+ try {
+ df.formatToCharacterIterator(Double.NaN);
+ // pass
+ } catch (IllegalArgumentException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Test
+ public void Test11913() {
+ NumberFormat df = DecimalFormat.getInstance();
+ String result = df.format(new BigDecimal("1.23456789E400"));
+ assertEquals("Should format more than 309 digits", "12,345,678", result.substring(0, 10));
+ assertEquals("Should format more than 309 digits", 534, result.length());
+ }
+
+ @Test
+ public void Test12045() {
+ if (logKnownIssue("12045", "XSU is missing from fr")) { return; }
+
+ NumberFormat nf = NumberFormat.getInstance(new ULocale("fr"), NumberFormat.PLURALCURRENCYSTYLE);
+ ParsePosition ppos = new ParsePosition(0);
+ try {
+ CurrencyAmount result = nf.parseCurrency("2,34 XSU", ppos);
+ assertEquals("Parsing should succeed on XSU",
+ new CurrencyAmount(2.34, Currency.getInstance("XSU")), result);
+ // pass
+ } catch (Exception e) {
+ throw new AssertionError("Should have been able to parse XSU", e);
+ }
+ }
+
+ @Test
+ public void Test11739() {
+ NumberFormat nf = NumberFormat.getCurrencyInstance(new ULocale("sr_BA"));
+ ((DecimalFormat) nf).applyPattern("0.0 ¤¤¤");
+ ParsePosition ppos = new ParsePosition(0);
+ CurrencyAmount result = nf.parseCurrency("1.500 амерички долар", ppos);
+ assertEquals("Should parse to 1500 USD", new CurrencyAmount(1500, Currency.getInstance("USD")), result);
+ }
+
+ @Test
+ public void Test11647() {
+ DecimalFormat df = new DecimalFormat();
+ df.applyPattern("¤¤¤¤#");
+ String actual = df.format(123);
+ assertEquals("Should replace 4 currency signs with U+FFFD", "\uFFFD123", actual);
+ }
+
+ @Test
+ public void Test12567() {
+ DecimalFormat df1 = (DecimalFormat) NumberFormat.getInstance(NumberFormat.PLURALCURRENCYSTYLE);
+ DecimalFormat df2 = (DecimalFormat) NumberFormat.getInstance(NumberFormat.NUMBERSTYLE);
+ df2.setCurrency(df1.getCurrency());
+ df2.setCurrencyPluralInfo(df1.getCurrencyPluralInfo());
+ df1.applyPattern("0.00");
+ df2.applyPattern("0.00");
+ assertEquals("df1 == df2", df1, df2);
+ assertEquals("df2 == df1", df2, df1);
+ df2.setPositivePrefix("abc");
+ assertNotEquals("df1 != df2", df1, df2);
+ assertNotEquals("df2 != df1", df2, df1);
+ }
+
+ @Test
+ public void testPercentZero() {
+ DecimalFormat df = (DecimalFormat) NumberFormat.getPercentInstance();
+ String actual = df.format(0);
+ assertEquals("Should have one zero digit", "0%", actual);
+ }
+
+ @Test
+ public void testCurrencyZeroRounding() {
+ DecimalFormat df = (DecimalFormat) NumberFormat.getCurrencyInstance();
+ df.setMaximumFractionDigits(0);
+ String actual = df.format(0);
+ assertEquals("Should have zero fraction digits", "$0", actual);
+ }
+
+ @Test
+ public void testCustomCurrencySymbol() {
+ DecimalFormat df = (DecimalFormat) NumberFormat.getCurrencyInstance();
+ df.setCurrency(Currency.getInstance("USD"));
+ DecimalFormatSymbols symbols = df.getDecimalFormatSymbols();
+ symbols.setCurrencySymbol("#");
+ df.setDecimalFormatSymbols(symbols);
+ String actual = df.format(123);
+ assertEquals("Should use '#' instad of '$'", "#123.00", actual);
+ }
+
+ @Test
+ public void TestBasicSerializationRoundTrip() throws IOException, ClassNotFoundException {
+ DecimalFormat df0 = new DecimalFormat("A-**#####,#00.00b¤");
+
+ // Write to byte stream
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(df0);
+ oos.flush();
+ baos.close();
+ byte[] bytes = baos.toByteArray();
+
+ // Read from byte stream
+ ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
+ Object obj = ois.readObject();
+ ois.close();
+ DecimalFormat df1 = (DecimalFormat) obj;
+
+ // Test equality
+ assertEquals("Did not round-trip through serialization", df0, df1);
+
+ // Test basic functionality
+ String str0 = df0.format(12345.67);
+ String str1 = df1.format(12345.67);
+ assertEquals("Serialized formatter does not produce same output", str0, str1);
+ }
+
+ @Test
+ public void testGetSetCurrency() {
+ DecimalFormat df = new DecimalFormat("¤#");
+ assertEquals("Currency should start out null", null, df.getCurrency());
+ Currency curr = Currency.getInstance("EUR");
+ df.setCurrency(curr);
+ assertEquals("Currency should equal EUR after set", curr, df.getCurrency());
+ String result = df.format(123);
+ assertEquals("Currency should format as expected in EUR", "€123.00", result);
+ }
+
+ @Test
+ public void testRoundingModeSetters() {
+ DecimalFormat df1 = new DecimalFormat();
+ DecimalFormat df2 = new DecimalFormat();
+
+ df1.setRoundingMode(java.math.BigDecimal.ROUND_CEILING);
+ assertNotEquals("Rounding mode was set to a non-default", df1, df2);
+ df2.setRoundingMode(com.ibm.icu.math.BigDecimal.ROUND_CEILING);
+ assertEquals("Rounding mode from icu.math and java.math should be the same", df1, df2);
+ df2.setRoundingMode(java.math.RoundingMode.CEILING.ordinal());
+ assertEquals("Rounding mode ordinal from java.math.RoundingMode should be the same", df1, df2);
+ }
+
+ @Test
+ public void testSignificantDigitsMode() {
+ String[][] allExpected = {
+ {"12340.0", "12340.0", "12340.0"},
+ {"1234.0", "1234.0", "1234.0"},
+ {"123.4", "123.4", "123.4"},
+ {"12.34", "12.34", "12.34"},
+ {"1.234", "1.23", "1.23"},
+ {"0.1234", "0.12", "0.123"},
+ {"0.01234", "0.01", "0.0123"},
+ {"0.001234", "0.00", "0.00123"}
+ };
+
+ DecimalFormat df = new DecimalFormat();
+ df.setMinimumFractionDigits(1);
+ df.setMaximumFractionDigits(2);
+ df.setMinimumSignificantDigits(3);
+ df.setMaximumSignificantDigits(4);
+ df.setGroupingUsed(false);
+
+ SignificantDigitsMode[] modes = new SignificantDigitsMode[] {
+ SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION,
+ SignificantDigitsMode.RESPECT_MAXIMUM_FRACTION,
+ SignificantDigitsMode.ENSURE_MINIMUM_SIGNIFICANT
+ };
+
+ for (double d = 12340.0, i=0; d > 0.001; d /= 10, i++) {
+ for (int j=0; j<modes.length; j++) {
+ SignificantDigitsMode mode = modes[j];
+ df.setSignificantDigitsMode(mode);
+ String expected = allExpected[(int)i][j];
+ String actual = df.format(d);
+ assertEquals("Significant digits mode getter is broken",
+ mode, df.getSignificantDigitsMode());
+ assertEquals("Significant digits output differs for "+i+", "+j,
+ expected, actual);
+ }
+ }
+ }
+
+ @Test
+ public void testParseNoExponent() throws ParseException {
+ DecimalFormat df = new DecimalFormat();
+ assertEquals("Parse no exponent has wrong default", false, df.getParseNoExponent());
+ Number result1 = df.parse("123E4");
+ df.setParseNoExponent(true);
+ assertEquals("Parse no exponent getter is broken", true, df.getParseNoExponent());
+ Number result2 = df.parse("123E4");
+ assertEquals("Exponent did not parse before setParseNoExponent", result1, new Long(1230000));
+ assertEquals("Exponent parsed after setParseNoExponent", result2, new Long(123));
+ }
+
+ @Test
+ public void testMinimumGroupingDigits() {
+ String[][] allExpected = {
+ {"123", "123"},
+ {"1,230", "1230"},
+ {"12,300", "12,300"},
+ {"1,23,000", "1,23,000"}
+ };
+
+ DecimalFormat df = new DecimalFormat("#,##,##0");
+ assertEquals("Minimum grouping digits has wrong default", 1, df.getMinimumGroupingDigits());
+
+ for (int l = 123, i=0; l <= 123000; l *= 10, i++) {
+ df.setMinimumGroupingDigits(1);
+ assertEquals("Minimum grouping digits getter is broken", 1, df.getMinimumGroupingDigits());
+ String actual = df.format(l);
+ assertEquals("Output is wrong for 1, "+i, allExpected[i][0], actual);
+ df.setMinimumGroupingDigits(2);
+ assertEquals("Minimum grouping digits getter is broken", 2, df.getMinimumGroupingDigits());
+ actual = df.format(l);
+ assertEquals("Output is wrong for 2, "+i, allExpected[i][1], actual);
+ }
+ }
+
+ @Test
+ public void testParseCaseSensitive() {
+ String[] patterns = {"a#b", "A#B"};
+ String[] inputs = {"a500b", "A500b", "a500B", "a500e10b", "a500E10b"};
+ int[][] expectedParsePositions = {
+ {5, 5, 5, 8, 8}, // case insensitive, pattern 0
+ {5, 0, 4, 4, 8}, // case sensitive, pattern 0
+ {5, 5, 5, 8, 8}, // case insensitive, pattern 1
+ {0, 4, 0, 0, 0}, // case sensitive, pattern 1
+ };
+
+ for (int p = 0; p < patterns.length; p++) {
+ String pat = patterns[p];
+ DecimalFormat df = new DecimalFormat(pat);
+ assertEquals("parseCaseSensitive default is wrong", false, df.getParseCaseSensitive());
+ for (int i = 0; i < inputs.length; i++) {
+ String inp = inputs[i];
+ df.setParseCaseSensitive(false);
+ assertEquals("parseCaseSensitive getter is broken", false, df.getParseCaseSensitive());
+ ParsePosition actualInsensitive = new ParsePosition(0);
+ df.parse(inp, actualInsensitive);
+ assertEquals("Insensitive, pattern "+p+", input "+i,
+ expectedParsePositions[p*2][i], actualInsensitive.getIndex());
+ df.setParseCaseSensitive(true);
+ assertEquals("parseCaseSensitive getter is broken", true, df.getParseCaseSensitive());
+ ParsePosition actualSensitive = new ParsePosition(0);
+ df.parse(inp, actualSensitive);
+ assertEquals("Sensitive, pattern "+p+", input "+i,
+ expectedParsePositions[p*2+1][i], actualSensitive.getIndex());
+ }
+ }
+ }
}
# Basics
fp: "0.####" 0.10005 "0.1" 0.1
fp: - 0.10006 "0.1001" 0.1001
-pat: - "#0.####"
+pat: - "0.####"
fp: "#.####" 0.10005 "0.1" 0.1
-pat: - "#0.####"
+pat: - "0.####"
rt: "0" 1234 "1234"
-pat: - "#0"
+pat: - "0"
# Significant digits
fp: "@@@" 1.234567 "1.23" 1.23
pat: "##@@##" "@@##"
pat: "@@.@@" err # decimal sep. disallowed in sig. digits
-pat: "@#@" err # only one cluster of sig. digits
+# The new pattern parser treats this the same as "@@#"
+#pat: "@#@" err # only one cluster of sig. digits
pat: "@@0" err # either @ or 0, not both
# NumberRegression/Test4140009
* Corporation and others. All Rights Reserved.
**/
-/**
+/**
* Port From: JDK 1.4b1 : java.text.Format.NumberRegression
* Source File: java/text/format/NumberRegression.java
**/
-
+
/**
* @test 1.49 01/05/21
* @bug 4052223 4059870 4061302 4062486 4066646 4068693 4070798 4071005 4071014
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
try {
dfFoo.applyPattern("0000;-000");
- if (!dfFoo.toPattern().equals("#0000"))
+ if (!dfFoo.toPattern().equals("0000"))
errln("dfFoo.toPattern : " + dfFoo.toPattern());
logln(dfFoo.format(42));
logln(dfFoo.format(-42));
dfFoo.applyPattern("000;-000");
- if (!dfFoo.toPattern().equals("#000"))
+ if (!dfFoo.toPattern().equals("000"))
errln("dfFoo.toPattern : " + dfFoo.toPattern());
logln(dfFoo.format(42));
logln(dfFoo.format(-42));
dfFoo.applyPattern("000;-0000");
- if (!dfFoo.toPattern().equals("#000"))
+ if (!dfFoo.toPattern().equals("000"))
errln("dfFoo.toPattern : " + dfFoo.toPattern());
logln(dfFoo.format(42));
logln(dfFoo.format(-42));
dfFoo.applyPattern("0000;-000");
- if (!dfFoo.toPattern().equals("#0000"))
+ if (!dfFoo.toPattern().equals("0000"))
errln("dfFoo.toPattern : " + dfFoo.toPattern());
logln(dfFoo.format(42));
logln(dfFoo.format(-42));
errln("getMaximumIntegerDigits() returns " +
nf.getMaximumIntegerDigits());
}
-
+
/**
* Locale data should use generic currency symbol
*
public void Test4122840()
{
Locale[] locales = NumberFormat.getAvailableLocales();
-
+
for (int i = 0; i < locales.length; i++) {
ICUResourceBundle rb = (ICUResourceBundle)ICUResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,locales[i]);
" does not contain generic currency symbol:" +
pattern );
}
-
+
// Create a DecimalFormat using the pattern we got and format a number
DecimalFormatSymbols symbols = new DecimalFormatSymbols(locales[i]);
DecimalFormat fmt1 = new DecimalFormat(pattern, symbols);
-
+
String result1 = fmt1.format(1.111);
-
+
//
// Now substitute in the locale's currency symbol and create another
// pattern. Replace the decimal separator with the monetary separator.
for (int j = 0; j < buf.length(); j++) {
if (buf.charAt(j) == '\u00a4') {
String cur = "'" + symbols.getCurrencySymbol() + "'";
- buf.replace(j, j+1, cur);
+ buf.replace(j, j+1, cur);
j += cur.length() - 1;
}
}
DecimalFormat fmt2 = new DecimalFormat(buf.toString(), symbols);
// Actual width of decimal fractions and rounding option are inherited
- // from the currency, not the pattern itself. So we need to force
+ // from the currency, not the pattern itself. So we need to force
// maximum/minimumFractionDigits and rounding option for the second
// DecimalForamt instance. The fix for ticket#7282 requires this test
// code change to make it work properly.
- fmt2.setMaximumFractionDigits(fmt1.getMaximumFractionDigits());
- fmt2.setMinimumFractionDigits(fmt1.getMinimumFractionDigits());
- fmt2.setRoundingIncrement(fmt1.getRoundingIncrement());
+ if (symbols.getCurrency() != null) {
+ fmt2.setMaximumFractionDigits(symbols.getCurrency().getDefaultFractionDigits());
+ fmt2.setMinimumFractionDigits(symbols.getCurrency().getDefaultFractionDigits());
+ fmt2.setRoundingIncrement(symbols.getCurrency().getRoundingIncrement());
+ } else {
+ fmt2.setMaximumFractionDigits(fmt1.getMinimumFractionDigits());
+ fmt2.setMinimumFractionDigits(fmt1.getMaximumFractionDigits());
+ fmt2.setRoundingIncrement(fmt1.getRoundingIncrement());
+ }
String result2 = fmt2.format(1.111);
-
+
// NOTE: en_IN is a special case (ChoiceFormat currency display name)
if (!result1.equals(result2) &&
!locales[i].toString().equals("en_IN")) {
}
}
}
-
+
/**
* DecimalFormat.format() delivers wrong string.
*/
@Test
public void Test4134034() {
DecimalFormat nf = new DecimalFormat("##,###,###.00");
-
+
String f = nf.format(9.02);
if (f.equals("9.02")) logln(f + " ok"); else errln("9.02 -> " + f + "; want 9.02");
*
* JDK 1.1.6 Bug, did NOT occur in 1.1.5
* Possibly related to bug 4125885.
- *
+ *
* This class demonstrates a regression in version 1.1.6
* of DecimalFormat class.
- *
+ *
* 1.1.6 Results
* Value 1.2 Format #.00 Result '01.20' !!!wrong
* Value 1.2 Format 0.00 Result '001.20' !!!wrong
* Value 1.2 Format 00.00 Result '0001.20' !!!wrong
* Value 1.2 Format #0.0# Result '1.2'
* Value 1.2 Format #0.00 Result '001.20' !!!wrong
- *
+ *
* 1.1.5 Results
* Value 1.2 Format #.00 Result '1.20'
* Value 1.2 Format 0.00 Result '1.20'
String out = nf.format(pi);
String pat = nf.toPattern();
double val = nf.parse(out).doubleValue();
-
+
nf.applyPattern(pat);
String out2 = nf.format(pi);
String pat2 = nf.toPattern();
double val2 = nf.parse(out2).doubleValue();
-
+
if (!pat.equals(pat2))
errln("Fail with \"" + PATS[i] + "\": Patterns should concur, \"" +
pat + "\" vs. \"" + pat2 + "\"");
logln("Applying pattern \"" + pattern + "\"");
sdf.applyPattern(pattern);
int minIntDig = sdf.getMinimumIntegerDigits();
- if (minIntDig != 0) {
+ // In ICU 58 and older, this case returned 0.
+ // Now it returns 1 instead, since the pattern parser enforces at least 1 min digit.
+ if (minIntDig != 1) {
errln("Test failed");
errln(" Minimum integer digits : " + minIntDig);
errln(" new pattern: " + sdf.toPattern());
e.printStackTrace();
}
logln("The string " + s + " parsed as " + n);
- if (n.doubleValue() != dbl) {
- errln("Round trip failure");
- }
+ assertEquals("Round trip failure", dbl, n.doubleValue());
}
/**
@Test
public void Test4167494() throws Exception {
NumberFormat fmt = NumberFormat.getInstance(Locale.US);
-
+
double a = Double.MAX_VALUE;
String s = fmt.format(a);
double b = fmt.parse(s).doubleValue();
@Test
public void Test4176114() {
String[] DATA = {
- "00", "#00",
- "000", "#000", // No grouping
- "#000", "#000", // No grouping
+ "00", "00",
+ "000", "000", // No grouping
+ "#000", "000", // No grouping
"#,##0", "#,##0",
"#,000", "#,000",
- "0,000", "#0,000",
- "00,000", "#00,000",
- "000,000", "#,000,000",
- "0,000,000,000,000.0000", "#0,000,000,000,000.0000", // Reported
+ "0,000", "0,000",
+ "00,000", "00,000",
+ "000,000", "000,000",
+ "0,000,000,000,000.0000", "0,000,000,000,000.0000", // Reported
};
for (int i=0; i<DATA.length; i+=2) {
DecimalFormat df = new DecimalFormat(DATA[i]);
}
}
- @Test
- public void Test4185761() throws IOException, ClassNotFoundException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(baos);
-
- NumberFormat nf = NumberFormat.getInstance(Locale.US);
-
- // Set special values we are going to search for in the output byte stream
- // These are all legal values.
- nf.setMinimumIntegerDigits(0x111); // Keep under 309
- nf.setMaximumIntegerDigits(0x112); // Keep under 309
- nf.setMinimumFractionDigits(0x113); // Keep under 340
- nf.setMaximumFractionDigits(0x114); // Keep under 340
-
- oos.writeObject(nf);
- oos.flush();
- baos.close();
-
- byte[] bytes = baos.toByteArray();
-
- // Scan for locations of min/max int/fract values in the byte array.
- // At the moment (ICU4J 2.1), there is only one instance of each target pair
- // in the byte stream, so assume first match is it. Note this is not entirely
- // failsafe, and needs to be checked if we change the package or structure of
- // this class.
- // Current positions are 890, 880, 886, 876
- int[] offsets = new int[4];
- for (int i = 0; i < bytes.length - 1; ++i) {
- if (bytes[i] == 0x01) { // high byte
- for (int j = 0; j < offsets.length; ++j) {
- if ((offsets[j] == 0) && (bytes[i+1] == (0x11 + j))) { // low byte
- offsets[j] = i;
- break;
- }
- }
- }
- }
-
- {
- ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
- Object o = ois.readObject();
- ois.close();
-
- if (!nf.equals(o)) {
- errln("Fail: NumberFormat serialization/equality bug");
- } else {
- logln("NumberFormat serialization/equality is OKAY.");
- }
- }
-
- // Change the values in the byte stream so that min > max.
- // Numberformat should catch this and throw an exception.
- for (int i = 0; i < offsets.length; ++i) {
- bytes[offsets[i]] = (byte)(4 - i);
- }
-
- {
- ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
- try {
- NumberFormat format = (NumberFormat) ois.readObject();
- logln("format: " + format.format(1234.56)); //fix "The variable is never used"
- errln("FAIL: Deserialized bogus NumberFormat with minXDigits > maxXDigits");
- } catch (InvalidObjectException e) {
- logln("Ok: " + e.getMessage());
- }
- }
-
- // Set values so they are too high, but min <= max
- // Format should pass the min <= max test, and DecimalFormat should reset to current maximum
- // (for compatibility with versions streamed out before the maximums were imposed).
- for (int i = 0; i < offsets.length; ++i) {
- bytes[offsets[i]] = 4;
- }
-
- {
- ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
- NumberFormat format = (NumberFormat) ois.readObject();
- //For compatibility with previous version
- if ((format.getMaximumIntegerDigits() != 309)
- || format.getMaximumFractionDigits() != 340) {
- errln("FAIL: Deserialized bogus NumberFormat with values out of range," +
- " intMin: " + format.getMinimumIntegerDigits() +
- " intMax: " + format.getMaximumIntegerDigits() +
- " fracMin: " + format.getMinimumFractionDigits() +
- " fracMax: " + format.getMaximumFractionDigits());
- } else {
- logln("Ok: Digit count out of range");
- }
- }
- }
-
-
/**
* Some DecimalFormatSymbols changes are not picked up by DecimalFormat.
* This includes the minus sign, currency symbol, international currency
fmt.getPositiveSuffix() + ", exp ^");
}
sym.setPercent('%');
-
+
fmt.applyPattern("#\u2030");
sym.setPerMill('^');
fmt.setDecimalFormatSymbols(sym);
System.out.println("\n Test skipped for release 2.2");
return;
}
-
+
// Since the pattern logic has changed, make sure that patterns round
// trip properly. Test stream in/out integrity too.
Locale[] avail = NumberFormat.getAvailableLocales();
break;
}
DecimalFormat df = (DecimalFormat) nf;
-
+
// Test toPattern/applyPattern round trip
String pat = df.toPattern();
DecimalFormatSymbols symb = new DecimalFormatSymbols(avail[i]);
DecimalFormat f2 = new DecimalFormat(pat, symb);
+ f2.setCurrency(df.getCurrency()); // Currency does not travel with the pattern string
if (!df.equals(f2)) {
errln("FAIL: " + avail[i] + " #" + j + " -> \"" + pat +
"\" -> \"" + f2.toPattern() + '"');
pat = df.toLocalizedPattern();
try{
f2.applyLocalizedPattern(pat);
-
+
String s1 = f2.format(123456);
String s2 = df.format(123456);
if(!s1.equals(s2)){
errln("FAIL: " + avail[i] + " #" + j + " -> localized \"" + s2 +
"\" -> \"" + s2 + '"'+ " in locale "+df.getLocale(ULocale.ACTUAL_LOCALE));
-
- }
- if (!df.equals(f2)) {
- errln("FAIL: " + avail[i] + " #" + j + " -> localized \"" + pat +
- "\" -> \"" + f2.toLocalizedPattern() + '"'+ " in locale "+df.getLocale(ULocale.ACTUAL_LOCALE));
- errln("s1: "+s1+" s2: "+s2);
+
}
-
+
+ // Equality of formatter objects is NOT guaranteed across toLocalizedPattern/applyLocalizedPattern.
+ // However, equality of relevant properties is guaranteed.
+ assertEquals("Localized FAIL on posPrefix", df.getPositivePrefix(), f2.getPositivePrefix());
+ assertEquals("Localized FAIL on posSuffix", df.getPositiveSuffix(), f2.getPositiveSuffix());
+ assertEquals("Localized FAIL on negPrefix", df.getNegativePrefix(), f2.getNegativePrefix());
+ assertEquals("Localized FAIL on negSuffix", df.getNegativeSuffix(), f2.getNegativeSuffix());
+ assertEquals("Localized FAIL on groupingSize", df.getGroupingSize(), f2.getGroupingSize());
+ assertEquals("Localized FAIL on secondaryGroupingSize", df.getSecondaryGroupingSize(), f2.getSecondaryGroupingSize());
+ assertEquals("Localized FAIL on minFrac", df.getMinimumFractionDigits(), f2.getMinimumFractionDigits());
+ assertEquals("Localized FAIL on maxFrac", df.getMaximumFractionDigits(), f2.getMaximumFractionDigits());
+ assertEquals("Localized FAIL on minInt", df.getMinimumIntegerDigits(), f2.getMinimumIntegerDigits());
+ assertEquals("Localized FAIL on maxInt", df.getMaximumIntegerDigits(), f2.getMaximumIntegerDigits());
+
}catch(IllegalArgumentException ex){
- errln(ex.getMessage()+" for locale "+ df.getLocale(ULocale.ACTUAL_LOCALE));
+ throw new AssertionError("For locale " + avail[i], ex);
}
-
+
// Test writeObject/readObject round trip
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sym = new DecimalFormatSymbols(Locale.US);
for (int j=0; j<SPECIALS.length; ++j) {
char special = SPECIALS[j];
- String pat = "'" + special + "'#0'" + special + "'";
+ String pat = "'" + special + "'0'" + special + "'";
try {
fmt = new DecimalFormat(pat, sym);
String pat2 = fmt.toPattern();
String str = Long.toString(DATA[i]);
for (int m = 1; m <= 100; m++) {
fmt.setMultiplier(m);
- long n = ((Number) fmt.parse(str)).longValue();
+ long n = fmt.parse(str).longValue();
if (n > 0 != DATA[i] > 0) {
errln("\"" + str + "\" parse(x " + fmt.getMultiplier() +
") => " + n);
new Double(1.006), "1.01",
};
NumberFormat fmt = NumberFormat.getInstance(Locale.US);
- fmt.setMaximumFractionDigits(2);
+ fmt.setMaximumFractionDigits(2);
for (int i=0; i<DATA.length; i+=2) {
String s = fmt.format(((Double) DATA[i]).doubleValue());
if (!s.equals(DATA[i+1])) {
- errln("FAIL: Got " + s + ", exp " + DATA[i+1]);
+ errln("FAIL: Got " + s + ", exp " + DATA[i+1]);
}
}
}
-
+
/**
* 4243011: Formatting .5 rounds to "1" instead of "0"
*/
public void Test4243011() {
double DATA[] = {0.5, 1.5, 2.5, 3.5, 4.5};
String EXPECTED[] = {"0.", "2.", "2.", "4.", "4."};
-
+
DecimalFormat format = new DecimalFormat("0.");
for (int i = 0; i < DATA.length; i++) {
String result = format.format(DATA[i]);
}
}
}
-
+
/**
* 4243108: format(0.0) gives "0.1" if preceded by parse("99.99")
*/
} catch (ParseException e) {
errln("Caught a ParseException:");
e.printStackTrace();
- }
+ }
result = f.format(0.0);
if (result.equals("0")) {
logln("OK: got " + result);
errln("FAIL: got " + result);
}
}
-
+
/**
* 4330377: DecimalFormat engineering notation gives incorrect results
*/
}
*/
}
-
+
/**
* 4233840: NumberFormat does not round correctly
*/
NumberFormat nf = new DecimalFormat("0.##", new DecimalFormatSymbols(Locale.US));
nf.setMinimumFractionDigits(2);
-
+
String result = nf.format(f);
-
+
if (!result.equals("0.01")) {
errln("FAIL: input: " + f + ", expected: 0.01, got: " + result);
}
}
-
+
/**
* 4241880: Decimal format doesnt round a double properly when the number is less than 1
*/
* For serialization
*/
private static final long serialVersionUID = 1251303884737169952L;
- public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
+ @Override
+ public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
- public StringBuffer format(long number,StringBuffer toAppendTo, FieldPosition pos) {
+ @Override
+ public StringBuffer format(long number,StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
- public Number parse(String text, ParsePosition parsePosition) {
+ @Override
+ public Number parse(String text, ParsePosition parsePosition) {
return new Integer(0);
}
- public StringBuffer format(java.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
+ @Override
+ public StringBuffer format(java.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
- public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
+ @Override
+ public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
- public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
+ @Override
+ public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.format;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.ParseException;
+import java.text.ParsePosition;
+
+import com.ibm.icu.dev.test.format.DataDrivenNumberFormatTestUtility.CodeUnderTest;
+import com.ibm.icu.impl.number.Endpoint;
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantity1;
+import com.ibm.icu.impl.number.FormatQuantity2;
+import com.ibm.icu.impl.number.FormatQuantity3;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.Parse;
+import com.ibm.icu.impl.number.Parse.ParseMode;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.text.DecimalFormat;
+import com.ibm.icu.text.DecimalFormat.PropertySetter;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ULocale;
+
+public class ShanesDataDrivenTester extends CodeUnderTest {
+ static final String dataPath =
+ "../../../icu4j-core-tests/src/com/ibm/icu/dev/data/numberformattestspecification.txt";
+
+ public static void run() {
+ CodeUnderTest tester = new ShanesDataDrivenTester();
+ DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(dataPath, tester);
+ }
+
+ @Override
+ public Character Id() {
+ return 'S';
+ }
+
+ /**
+ * Runs a single formatting test. On success, returns null. On failure, returns the error. This
+ * implementation just returns null. Subclasses should override.
+ *
+ * @param tuple contains the parameters of the format test.
+ */
+ @Override
+ public String format(DataDrivenNumberFormatTestData tuple) {
+ String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+ ULocale locale = (tuple.locale == null) ? ULocale.ENGLISH : tuple.locale;
+ Properties properties = PatternString.parseToProperties(pattern, tuple.currency != null);
+ propertiesFromTuple(tuple, properties);
+ Format fmt = Endpoint.fromBTA(properties, locale);
+ FormatQuantity q1, q2, q3;
+ if (tuple.format.equals("NaN")) {
+ q1 = q2 = new FormatQuantity1(Double.NaN);
+ q3 = new FormatQuantity2(Double.NaN);
+ } else if (tuple.format.equals("-Inf")) {
+ q1 = q2 = new FormatQuantity1(Double.NEGATIVE_INFINITY);
+ q3 = new FormatQuantity1(Double.NEGATIVE_INFINITY);
+ } else if (tuple.format.equals("Inf")) {
+ q1 = q2 = new FormatQuantity1(Double.POSITIVE_INFINITY);
+ q3 = new FormatQuantity1(Double.POSITIVE_INFINITY);
+ } else {
+ BigDecimal d = new BigDecimal(tuple.format);
+ if (d.precision() <= 16) {
+ q1 = new FormatQuantity1(d);
+ q2 = new FormatQuantity1(Double.parseDouble(tuple.format));
+ q3 = new FormatQuantity4(d);
+ } else {
+ q1 = new FormatQuantity1(d);
+ q2 = new FormatQuantity3(d);
+ q3 = new FormatQuantity4(d); // duplicate values so no null
+ }
+ }
+ String expected = tuple.output;
+ String actual1 = fmt.format(q1);
+ if (!expected.equals(actual1)) {
+ return "Expected \"" + expected + "\", got \"" + actual1 + "\" on FormatQuantity1 BigDecimal";
+ }
+ String actual2 = fmt.format(q2);
+ if (!expected.equals(actual2)) {
+ return "Expected \"" + expected + "\", got \"" + actual2 + "\" on FormatQuantity1 double";
+ }
+ String actual3 = fmt.format(q3);
+ if (!expected.equals(actual3)) {
+ return "Expected \"" + expected + "\", got \"" + actual3 + "\" on FormatQuantity4 BigDecimal";
+ }
+ return null;
+ }
+
+ /**
+ * Runs a single toPattern test. On success, returns null. On failure, returns the error. This
+ * implementation just returns null. Subclasses should override.
+ *
+ * @param tuple contains the parameters of the format test.
+ */
+ @Override
+ public String toPattern(DataDrivenNumberFormatTestData tuple) {
+ String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+ final Properties properties;
+ DecimalFormat df;
+ try {
+ properties = PatternString.parseToProperties(pattern, tuple.currency != null);
+ propertiesFromTuple(tuple, properties);
+ // TODO: Use PatternString.propertiesToString() directly. (How to deal with CurrencyUsage?)
+ df = new DecimalFormat();
+ df.setProperties(
+ new PropertySetter() {
+ @Override
+ public void set(Properties props) {
+ props.copyFrom(properties);
+ }
+ });
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ return e.getLocalizedMessage();
+ }
+
+ if (tuple.toPattern != null) {
+ String expected = tuple.toPattern;
+ String actual = df.toPattern();
+ if (!expected.equals(actual)) {
+ return "Expected toPattern='" + expected + "'; got '" + actual + "'";
+ }
+ }
+ if (tuple.toLocalizedPattern != null) {
+ String expected = tuple.toLocalizedPattern;
+ String actual = PatternString.propertiesToString(properties);
+ if (!expected.equals(actual)) {
+ return "Expected toLocalizedPattern='" + expected + "'; got '" + actual + "'";
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Runs a single parse test. On success, returns null. On failure, returns the error. This
+ * implementation just returns null. Subclasses should override.
+ *
+ * @param tuple contains the parameters of the format test.
+ */
+ @Override
+ public String parse(DataDrivenNumberFormatTestData tuple) {
+ String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+ Properties properties;
+ ParsePosition ppos = new ParsePosition(0);
+ Number actual;
+ try {
+ properties = PatternString.parseToProperties(pattern, tuple.currency != null);
+ propertiesFromTuple(tuple, properties);
+ actual =
+ Parse.parse(
+ tuple.parse, ppos, properties, DecimalFormatSymbols.getInstance(tuple.locale));
+ } catch (IllegalArgumentException e) {
+ return "parse exception: " + e.getMessage();
+ }
+ if (actual == null && ppos.getIndex() != 0) {
+ throw new AssertionError("Error: value is null but parse position is not zero");
+ }
+ if (ppos.getIndex() == 0) {
+ return "Parse failed; got " + actual + ", but expected " + tuple.output;
+ }
+ if (tuple.output.equals("NaN")) {
+ if (!Double.isNaN(actual.doubleValue())) {
+ return "Expected NaN, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("Inf")) {
+ if (!Double.isInfinite(actual.doubleValue())
+ || Double.compare(actual.doubleValue(), 0.0) < 0) {
+ return "Expected Inf, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("-Inf")) {
+ if (!Double.isInfinite(actual.doubleValue())
+ || Double.compare(actual.doubleValue(), 0.0) > 0) {
+ return "Expected -Inf, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("fail")) {
+ return null;
+ } else if (new BigDecimal(tuple.output).compareTo(new BigDecimal(actual.toString())) != 0) {
+ return "Expected: " + tuple.output + ", got: " + actual;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Runs a single parse currency test. On success, returns null. On failure, returns the error.
+ * This implementation just returns null. Subclasses should override.
+ *
+ * @param tuple contains the parameters of the format test.
+ */
+ @Override
+ public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
+ String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+ Properties properties;
+ ParsePosition ppos = new ParsePosition(0);
+ CurrencyAmount actual;
+ try {
+ properties = PatternString.parseToProperties(pattern, tuple.currency != null);
+ propertiesFromTuple(tuple, properties);
+ actual =
+ Parse.parseCurrency(
+ tuple.parse, ppos, properties, DecimalFormatSymbols.getInstance(tuple.locale));
+ } catch (ParseException e) {
+ e.printStackTrace();
+ return "parse exception: " + e.getMessage();
+ }
+ if (ppos.getIndex() == 0 || actual.getCurrency().getCurrencyCode().equals("XXX")) {
+ return "Parse failed; got " + actual + ", but expected " + tuple.output;
+ }
+ BigDecimal expectedNumber = new BigDecimal(tuple.output);
+ if (expectedNumber.compareTo(new BigDecimal(actual.getNumber().toString())) != 0) {
+ return "Wrong number: Expected: " + expectedNumber + ", got: " + actual;
+ }
+ String expectedCurrency = tuple.outputCurrency;
+ if (!expectedCurrency.equals(actual.getCurrency().toString())) {
+ return "Wrong currency: Expected: " + expectedCurrency + ", got: " + actual;
+ }
+ return null;
+ }
+
+ /**
+ * Runs a single select test. On success, returns null. On failure, returns the error. This
+ * implementation just returns null. Subclasses should override.
+ *
+ * @param tuple contains the parameters of the format test.
+ */
+ @Override
+ public String select(DataDrivenNumberFormatTestData tuple) {
+ return null;
+ }
+
+ private static void propertiesFromTuple(
+ DataDrivenNumberFormatTestData tuple, Properties properties) {
+ if (tuple.minIntegerDigits != null) {
+ properties.setMinimumIntegerDigits(tuple.minIntegerDigits);
+ }
+ if (tuple.maxIntegerDigits != null) {
+ properties.setMaximumIntegerDigits(tuple.maxIntegerDigits);
+ }
+ if (tuple.minFractionDigits != null) {
+ properties.setMinimumFractionDigits(tuple.minFractionDigits);
+ }
+ if (tuple.maxFractionDigits != null) {
+ properties.setMaximumFractionDigits(tuple.maxFractionDigits);
+ }
+ if (tuple.currency != null) {
+ properties.setCurrency(tuple.currency);
+ }
+ if (tuple.minGroupingDigits != null) {
+ properties.setMinimumGroupingDigits(tuple.minGroupingDigits);
+ }
+ if (tuple.useSigDigits != null) {
+ // TODO
+ }
+ if (tuple.minSigDigits != null) {
+ properties.setMinimumSignificantDigits(tuple.minSigDigits);
+ }
+ if (tuple.maxSigDigits != null) {
+ properties.setMaximumSignificantDigits(tuple.maxSigDigits);
+ }
+ if (tuple.useGrouping != null && tuple.useGrouping == 0) {
+ properties.setGroupingSize(Integer.MAX_VALUE);
+ properties.setSecondaryGroupingSize(Integer.MAX_VALUE);
+ }
+ if (tuple.multiplier != null) {
+ properties.setMultiplier(new BigDecimal(tuple.multiplier));
+ }
+ if (tuple.roundingIncrement != null) {
+ properties.setRoundingIncrement(new BigDecimal(tuple.roundingIncrement.toString()));
+ }
+ if (tuple.formatWidth != null) {
+ properties.setFormatWidth(tuple.formatWidth);
+ }
+ if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
+ properties.setPadString(tuple.padCharacter.toString());
+ }
+ if (tuple.useScientific != null) {
+ properties.setMinimumExponentDigits(
+ tuple.useScientific != 0 ? 1 : Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
+ }
+ if (tuple.grouping != null) {
+ properties.setGroupingSize(tuple.grouping);
+ }
+ if (tuple.grouping2 != null) {
+ properties.setSecondaryGroupingSize(tuple.grouping2);
+ }
+ if (tuple.roundingMode != null) {
+ properties.setRoundingMode(RoundingMode.valueOf(tuple.roundingMode));
+ }
+ if (tuple.currencyUsage != null) {
+ properties.setCurrencyUsage(tuple.currencyUsage);
+ }
+ if (tuple.minimumExponentDigits != null) {
+ properties.setMinimumExponentDigits(tuple.minimumExponentDigits.byteValue());
+ }
+ if (tuple.exponentSignAlwaysShown != null) {
+ properties.setExponentSignAlwaysShown(tuple.exponentSignAlwaysShown != 0);
+ }
+ if (tuple.decimalSeparatorAlwaysShown != null) {
+ properties.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
+ }
+ if (tuple.padPosition != null) {
+ properties.setPadPosition(PadPosition.fromOld(tuple.padPosition));
+ }
+ if (tuple.positivePrefix != null) {
+ properties.setPositivePrefix(tuple.positivePrefix);
+ }
+ if (tuple.positiveSuffix != null) {
+ properties.setPositiveSuffix(tuple.positiveSuffix);
+ }
+ if (tuple.negativePrefix != null) {
+ properties.setNegativePrefix(tuple.negativePrefix);
+ }
+ if (tuple.negativeSuffix != null) {
+ properties.setNegativeSuffix(tuple.negativeSuffix);
+ }
+ if (tuple.localizedPattern != null) {
+ // TODO
+ }
+ if (tuple.lenient != null) {
+ properties.setParseMode(tuple.lenient == 0 ? ParseMode.STRICT : ParseMode.LENIENT);
+ }
+ if (tuple.parseIntegerOnly != null) {
+ properties.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
+ }
+ if (tuple.parseCaseSensitive != null) {
+ properties.setParseCaseSensitive(tuple.parseCaseSensitive != 0);
+ }
+ if (tuple.decimalPatternMatchRequired != null) {
+ properties.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0);
+ }
+ if (tuple.parseNoExponent != null) {
+ properties.setParseNoExponent(tuple.parseNoExponent != 0);
+ }
+ }
+}
errln("parsed argument " + parsedArgs[0] + " != " + num);
}
}
- catch (Exception e) {
- errln("parse of '" + result + " returned exception: " + e.getMessage());
+ catch (ParseException e) {
+ errln("parse of '" + result + "' returned exception: "
+ + e.getMessage() + " " + e.getErrorOffset());
}
}
}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.AffixPatternUtils;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.ULocale;
+
+public class AffixPatternUtilsTest {
+
+ @Test
+ public void testEscape() {
+ Object[][] cases = {
+ {"", ""},
+ {"abc", "abc"},
+ {"-", "'-'"},
+ {"-!", "'-'!"},
+ {"−", "−"},
+ {"---", "'---'"},
+ {"-%-", "'-%-'"},
+ {"'", "''"},
+ {"-'", "'-'''"},
+ {"-'-", "'-''-'"},
+ {"a-'-", "a'-''-'"}
+ };
+
+ StringBuilder sb = new StringBuilder();
+ for (Object[] cas : cases) {
+ String input = (String) cas[0];
+ String expected = (String) cas[1];
+ sb.setLength(0);
+ AffixPatternUtils.escape(input, sb);
+ assertEquals(expected, sb.toString());
+ }
+ }
+
+ @Test
+ public void testUnescape() {
+ Object[][] cases = {
+ {"", false, 0, ""},
+ {"abc", false, 3, "abc"},
+ {"-", false, 1, "−"},
+ {"-!", false, 2, "−!"},
+ {"+", false, 1, "\u061C+"},
+ {"+!", false, 2, "\u061C+!"},
+ {"‰", false, 1, "؉"},
+ {"‰!", false, 2, "؉!"},
+ {"-x", false, 2, "−x"},
+ {"'-'x", false, 2, "-x"},
+ {"'--''-'-x", false, 6, "--'-−x"},
+ {"''", false, 1, "'"},
+ {"''''", false, 2, "''"},
+ {"''''''", false, 3, "'''"},
+ {"''x''", false, 3, "'x'"},
+ {"¤", true, 1, "$"},
+ {"¤¤", true, 2, "XXX"},
+ {"¤¤¤", true, 3, "long name"},
+ {"¤¤¤¤", true, 4, "\uFFFD"},
+ {"¤¤¤¤¤", true, 5, "\uFFFD"},
+ {"¤¤¤a¤¤¤¤", true, 8, "long namea\uFFFD"},
+ {"a¤¤¤¤b¤¤¤¤¤c", true, 12, "a\uFFFDb\uFFFDc"},
+ {"¤!", true, 2, "$!"},
+ {"¤¤!", true, 3, "XXX!"},
+ {"¤¤¤!", true, 4, "long name!"},
+ {"-¤¤", true, 3, "−XXX"},
+ {"¤¤-", true, 3, "XXX−"},
+ {"'¤'", false, 1, "¤"},
+ {"%", false, 1, "٪\u061C"},
+ {"'%'", false, 1, "%"},
+ {"¤'-'%", true, 3, "$-٪\u061C"}
+ };
+
+ // ar_SA has an interesting percent sign and various Arabic letter marks
+ DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("ar_SA"));
+ NumberStringBuilder sb = new NumberStringBuilder();
+
+ for (Object[] cas : cases) {
+ String input = (String) cas[0];
+ boolean curr = (Boolean) cas[1];
+ int length = (Integer) cas[2];
+ String output = (String) cas[3];
+
+ assertEquals(
+ "Currency on <" + input + ">", curr, AffixPatternUtils.hasCurrencySymbols(input));
+ assertEquals("Length on <" + input + ">", length, AffixPatternUtils.unescapedLength(input));
+
+ sb.clear();
+ AffixPatternUtils.unescape(input, symbols, "$", "XXX", "long name", "−", sb);
+ assertEquals("Output on <" + input + ">", output, sb.toString());
+ }
+ }
+
+ @Test
+ public void testInvalid() {
+ String[] invalidExamples = {"'", "x'", "'x", "'x''", "''x'"};
+ DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("en_US"));
+ NumberStringBuilder sb = new NumberStringBuilder();
+
+ for (String str : invalidExamples) {
+ try {
+ AffixPatternUtils.hasCurrencySymbols(str);
+ fail("No exception was thrown on an invalid string");
+ } catch (IllegalArgumentException e) {
+ // OK
+ }
+ try {
+ AffixPatternUtils.unescapedLength(str);
+ fail("No exception was thrown on an invalid string");
+ } catch (IllegalArgumentException e) {
+ // OK
+ }
+ try {
+ AffixPatternUtils.unescape(str, symbols, "$", "XXX", "long name", "−", sb);
+ fail("No exception was thrown on an invalid string");
+ } catch (IllegalArgumentException e) {
+ // OK
+ }
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.Test;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.impl.number.Endpoint;
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantity1;
+import com.ibm.icu.impl.number.FormatQuantity2;
+import com.ibm.icu.impl.number.FormatQuantity3;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+
+/** TODO: This is a temporary name for this class. Suggestions for a better name? */
+public class FormatQuantityTest extends TestFmwk {
+
+ @Test
+ public void testBehavior() throws ParseException {
+
+ // Make a list of several formatters to test the behavior of FormatQuantity.
+ List<Format> formats = new ArrayList<Format>();
+
+ Properties properties = new Properties();
+ Format ndf = Endpoint.fromBTA(properties);
+ formats.add(ndf);
+
+ properties =
+ new Properties()
+ .setMinimumSignificantDigits(3)
+ .setMaximumSignificantDigits(3)
+ .setCompactStyle(CompactStyle.LONG);
+ Format cdf = Endpoint.fromBTA(properties);
+ formats.add(cdf);
+
+ properties =
+ new Properties()
+ .setMinimumExponentDigits(1)
+ .setMaximumIntegerDigits(3)
+ .setMaximumFractionDigits(1);
+ Format exf = Endpoint.fromBTA(properties);
+ formats.add(exf);
+
+ properties = new Properties().setRoundingIncrement(new BigDecimal("0.5"));
+ Format rif = Endpoint.fromBTA(properties);
+ formats.add(rif);
+
+ String[] cases = {
+ "1.0",
+ "2.01",
+ "1234.56",
+ "3000.0",
+ "0.00026418",
+ "0.01789261",
+ "468160.0",
+ "999000.0",
+ "999900.0",
+ "999990.0",
+ "0.0",
+ "12345678901.0",
+ "-5193.48",
+ };
+
+ String[] hardCases = {
+ "9999999999999900.0",
+ "789000000000000000000000.0",
+ "789123123567853156372158.0",
+ "987654321987654321987654321987654321987654311987654321.0",
+ };
+
+ String[] doubleCases = {
+ "512.0000000000017",
+ "4095.9999999999977",
+ "4095.999999999998",
+ "4095.9999999999986",
+ "4095.999999999999",
+ "4095.9999999999995",
+ "4096.000000000001",
+ "4096.000000000002",
+ "4096.000000000003",
+ "4096.000000000004",
+ "4096.000000000005",
+ "4096.0000000000055",
+ "4096.000000000006",
+ "4096.000000000007",
+ };
+
+ int i = 0;
+ for (String str : cases) {
+ testFormatQuantity(i++, str, formats, 0);
+ }
+
+ i = 0;
+ for (String str : hardCases) {
+ testFormatQuantity(i++, str, formats, 1);
+ }
+
+ i = 0;
+ for (String str : doubleCases) {
+ testFormatQuantity(i++, str, formats, 2);
+ }
+ }
+
+ static void testFormatQuantity(int t, String str, List<Format> formats, int mode) {
+ if (mode == 2) {
+ assertEquals("Double is not valid", Double.toString(Double.parseDouble(str)), str);
+ }
+
+ List<FormatQuantity> qs = new ArrayList<FormatQuantity>();
+ BigDecimal d = new BigDecimal(str);
+ qs.add(new FormatQuantity1(d));
+ if (mode == 0) qs.add(new FormatQuantity2(d));
+ qs.add(new FormatQuantity3(d));
+ qs.add(new FormatQuantity4(d));
+
+ if (new BigDecimal(Double.toString(d.doubleValue())).compareTo(d) == 0) {
+ double dv = d.doubleValue();
+ qs.add(new FormatQuantity1(dv));
+ if (mode == 0) qs.add(new FormatQuantity2(dv));
+ qs.add(new FormatQuantity3(dv));
+ qs.add(new FormatQuantity4(dv));
+ }
+
+ if (new BigDecimal(Long.toString(d.longValue())).compareTo(d) == 0) {
+ double lv = d.longValue();
+ qs.add(new FormatQuantity1(lv));
+ if (mode == 0) qs.add(new FormatQuantity2(lv));
+ qs.add(new FormatQuantity3(lv));
+ qs.add(new FormatQuantity4(lv));
+ }
+
+ testFormatQuantityExpectedOutput(qs.get(0), str);
+
+ if (qs.size() == 1) {
+ return;
+ }
+
+ for (int i = 1; i < qs.size(); i++) {
+ FormatQuantity q0 = qs.get(0);
+ FormatQuantity q1 = qs.get(i);
+ testFormatQuantityExpectedOutput(q1, str);
+ testFormatQuantityRounding(q0, q1);
+ testFormatQuantityRoundingInterval(q0, q1);
+ testFormatQuantityMath(q0, q1);
+ testFormatQuantityWithFormats(q0, q1, formats);
+ }
+ }
+
+ private static void testFormatQuantityExpectedOutput(FormatQuantity rq, String expected) {
+ StringBuilder sb = new StringBuilder();
+ FormatQuantity q0 = rq.clone();
+ // Force an accurate double
+ q0.roundToInfinity();
+ q0.setIntegerFractionLength(1, Integer.MAX_VALUE, 1, Integer.MAX_VALUE);
+ for (int m = q0.getUpperDisplayMagnitude(); m >= q0.getLowerDisplayMagnitude(); m--) {
+ sb.append(q0.getDigit(m));
+ if (m == 0) sb.append('.');
+ }
+ if (q0.isNegative()) {
+ sb.insert(0, '-');
+ }
+ String actual = sb.toString();
+ assertEquals("Unexpected output from simple string conversion (" + q0 + ")", expected, actual);
+ }
+
+ private static final MathContext MATH_CONTEXT_HALF_EVEN =
+ new MathContext(0, RoundingMode.HALF_EVEN);
+ private static final MathContext MATH_CONTEXT_CEILING = new MathContext(0, RoundingMode.CEILING);
+ private static final MathContext MATH_CONTEXT_PRECISION =
+ new MathContext(3, RoundingMode.HALF_UP);
+
+ private static void testFormatQuantityRounding(FormatQuantity rq0, FormatQuantity rq1) {
+ FormatQuantity q0 = rq0.clone();
+ FormatQuantity q1 = rq1.clone();
+ q0.roundToMagnitude(-1, MATH_CONTEXT_HALF_EVEN);
+ q1.roundToMagnitude(-1, MATH_CONTEXT_HALF_EVEN);
+ testFormatQuantityBehavior(q0, q1);
+
+ q0 = rq0.clone();
+ q1 = rq1.clone();
+ q0.roundToMagnitude(-1, MATH_CONTEXT_CEILING);
+ q1.roundToMagnitude(-1, MATH_CONTEXT_CEILING);
+ testFormatQuantityBehavior(q0, q1);
+
+ q0 = rq0.clone();
+ q1 = rq1.clone();
+ q0.roundToMagnitude(-1, MATH_CONTEXT_PRECISION);
+ q1.roundToMagnitude(-1, MATH_CONTEXT_PRECISION);
+ testFormatQuantityBehavior(q0, q1);
+ }
+
+ private static void testFormatQuantityRoundingInterval(FormatQuantity rq0, FormatQuantity rq1) {
+ FormatQuantity q0 = rq0.clone();
+ FormatQuantity q1 = rq1.clone();
+ q0.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_HALF_EVEN);
+ q1.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_HALF_EVEN);
+ testFormatQuantityBehavior(q0, q1);
+
+ q0 = rq0.clone();
+ q1 = rq1.clone();
+ q0.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_CEILING);
+ q1.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_CEILING);
+ testFormatQuantityBehavior(q0, q1);
+ }
+
+ private static void testFormatQuantityMath(FormatQuantity rq0, FormatQuantity rq1) {
+ FormatQuantity q0 = rq0.clone();
+ FormatQuantity q1 = rq1.clone();
+ q0.adjustMagnitude(-3);
+ q1.adjustMagnitude(-3);
+ testFormatQuantityBehavior(q0, q1);
+
+ q0 = rq0.clone();
+ q1 = rq1.clone();
+ q0.multiplyBy(new BigDecimal("3.14159"));
+ q1.multiplyBy(new BigDecimal("3.14159"));
+ testFormatQuantityBehavior(q0, q1);
+ }
+
+ private static void testFormatQuantityWithFormats(
+ FormatQuantity rq0, FormatQuantity rq1, List<Format> formats) {
+ for (Format format : formats) {
+ FormatQuantity q0 = rq0.clone();
+ FormatQuantity q1 = rq1.clone();
+ String s1 = format.format(q0);
+ String s2 = format.format(q1);
+ assertEquals("Different output from formatter (" + q0 + ", " + q1 + ")", s1, s2);
+ }
+ }
+
+ private static void testFormatQuantityBehavior(FormatQuantity rq0, FormatQuantity rq1) {
+ FormatQuantity q0 = rq0.clone();
+ FormatQuantity q1 = rq1.clone();
+
+ assertEquals("Different sign (" + q0 + ", " + q1 + ")", q0.isNegative(), q1.isNegative());
+
+ assertEquals(
+ "Different fingerprint (" + q0 + ", " + q1 + ")",
+ q0.getPositionFingerprint(),
+ q1.getPositionFingerprint());
+
+ assertEquals(
+ "Different upper range of digits (" + q0 + ", " + q1 + ")",
+ q0.getUpperDisplayMagnitude(),
+ q1.getUpperDisplayMagnitude());
+
+ assertDoubleEquals(
+ "Different double values (" + q0 + ", " + q1 + ")", q0.toDouble(), q1.toDouble());
+
+ assertBigDecimalEquals(
+ "Different BigDecimal values (" + q0 + ", " + q1 + ")",
+ q0.toBigDecimal(),
+ q1.toBigDecimal());
+
+ int equalityDigits = Math.min(q0.maxRepresentableDigits(), q1.maxRepresentableDigits());
+ for (int m = q0.getUpperDisplayMagnitude(), i = 0;
+ m >= Math.min(q0.getLowerDisplayMagnitude(), q1.getLowerDisplayMagnitude())
+ && i < equalityDigits;
+ m--, i++) {
+ assertEquals(
+ "Different digit at magnitude " + m + " (" + q0 + ", " + q1 + ")",
+ q0.getDigit(m),
+ q1.getDigit(m));
+ }
+
+ if (rq0 instanceof FormatQuantity4) {
+ String message = ((FormatQuantity4) rq0).checkHealth();
+ if (message != null) errln(message);
+ }
+ if (rq1 instanceof FormatQuantity4) {
+ String message = ((FormatQuantity4) rq1).checkHealth();
+ if (message != null) errln(message);
+ }
+ }
+
+ @Test
+ public void testSwitchStorage() {
+ FormatQuantity4 fq = new FormatQuantity4();
+
+ fq.setToLong(1234123412341234L);
+ assertFalse("Should not be using byte array", fq.usingBytes());
+ assertBigDecimalEquals("Failed on initialize", "1234123412341234", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ // Long -> Bytes
+ fq.appendDigit((byte) 5, 0, true);
+ assertTrue("Should be using byte array", fq.usingBytes());
+ assertBigDecimalEquals("Failed on multiply", "12341234123412345", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ // Bytes -> Long
+ fq.roundToMagnitude(5, MATH_CONTEXT_HALF_EVEN);
+ assertFalse("Should not be using byte array", fq.usingBytes());
+ assertBigDecimalEquals("Failed on round", "12341234123400000", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ }
+
+ @Test
+ public void testAppend() {
+ FormatQuantity4 fq = new FormatQuantity4();
+ fq.appendDigit((byte) 1, 0, true);
+ assertBigDecimalEquals("Failed on append", "1.", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ fq.appendDigit((byte) 2, 0, true);
+ assertBigDecimalEquals("Failed on append", "12.", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ fq.appendDigit((byte) 3, 1, true);
+ assertBigDecimalEquals("Failed on append", "1203.", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ fq.appendDigit((byte) 0, 1, true);
+ assertBigDecimalEquals("Failed on append", "120300.", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ fq.appendDigit((byte) 4, 0, true);
+ assertBigDecimalEquals("Failed on append", "1203004.", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ fq.appendDigit((byte) 0, 0, true);
+ assertBigDecimalEquals("Failed on append", "12030040.", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ fq.appendDigit((byte) 5, 0, false);
+ assertBigDecimalEquals("Failed on append", "12030040.5", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ fq.appendDigit((byte) 6, 0, false);
+ assertBigDecimalEquals("Failed on append", "12030040.56", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ fq.appendDigit((byte) 7, 3, false);
+ assertBigDecimalEquals("Failed on append", "12030040.560007", fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ StringBuilder expected = new StringBuilder("12030040.560007");
+ for (int i = 0; i < 10; i++) {
+ fq.appendDigit((byte) 8, 0, false);
+ expected.append("8");
+ assertBigDecimalEquals("Failed on append", expected.toString(), fq.toBigDecimal());
+ assertNull("Failed health check", fq.checkHealth());
+ }
+ }
+
+ @Test
+ public void testConvertToAccurateDouble() {
+ // based on https://github.com/google/double-conversion/issues/28
+ double[] hardDoubles = {
+ 1651087494906221570.0,
+ -5074790912492772E-327,
+ 83602530019752571E-327,
+ 2.207817077636718750000000000000,
+ 1.818351745605468750000000000000,
+ 3.941719055175781250000000000000,
+ 3.738609313964843750000000000000,
+ 3.967735290527343750000000000000,
+ 1.328025817871093750000000000000,
+ 3.920967102050781250000000000000,
+ 1.015235900878906250000000000000,
+ 1.335227966308593750000000000000,
+ 1.344520568847656250000000000000,
+ 2.879127502441406250000000000000,
+ 3.695838928222656250000000000000,
+ 1.845344543457031250000000000000,
+ 3.793952941894531250000000000000,
+ 3.211402893066406250000000000000,
+ 2.565971374511718750000000000000,
+ 0.965156555175781250000000000000,
+ 2.700004577636718750000000000000,
+ 0.767097473144531250000000000000,
+ 1.780448913574218750000000000000,
+ 2.624839782714843750000000000000,
+ 1.305290222167968750000000000000,
+ 3.834922790527343750000000000000,
+ };
+
+ double[] integerDoubles = {
+ 51423,
+ 51423e10,
+ 4.503599627370496E15,
+ 6.789512076111555E15,
+ 9.007199254740991E15,
+ 9.007199254740992E15
+ };
+
+ for (double d : hardDoubles) {
+ checkDoubleBehavior(d, true, "");
+ }
+
+ for (double d : integerDoubles) {
+ checkDoubleBehavior(d, false, "");
+ }
+
+ assertEquals("NaN check failed", Double.NaN, new FormatQuantity4(Double.NaN).toDouble());
+ assertEquals(
+ "Inf check failed",
+ Double.POSITIVE_INFINITY,
+ new FormatQuantity4(Double.POSITIVE_INFINITY).toDouble());
+ assertEquals(
+ "-Inf check failed",
+ Double.NEGATIVE_INFINITY,
+ new FormatQuantity4(Double.NEGATIVE_INFINITY).toDouble());
+
+ // Generate random doubles
+ String alert = "UNEXPECTED FAILURE: PLEASE REPORT THIS MESSAGE TO THE ICU TEAM: ";
+ for (int i = 0; i < 1000000; i++) {
+ double d = Double.longBitsToDouble(ThreadLocalRandom.current().nextLong());
+ if (Double.isNaN(d) || Double.isInfinite(d)) continue;
+ checkDoubleBehavior(d, false, alert);
+ }
+ }
+
+ private static void checkDoubleBehavior(double d, boolean explicitRequired, String alert) {
+ FormatQuantity4 fq = new FormatQuantity4(d);
+ if (explicitRequired)
+ assertTrue(alert + "Should be using approximate double", !fq.explicitExactDouble);
+ assertEquals(alert + "Initial construction from hard double", d, fq.toDouble());
+ fq.roundToInfinity();
+ if (explicitRequired)
+ assertTrue(alert + "Should not be using approximate double", fq.explicitExactDouble);
+ assertDoubleEquals(alert + "After conversion to exact BCD (double)", d, fq.toDouble());
+ assertBigDecimalEquals(
+ alert + "After conversion to exact BCD (BigDecimal)",
+ new BigDecimal(Double.toString(d)),
+ fq.toBigDecimal());
+ }
+
+ @Test
+ public void testUseApproximateDoubleWhenAble() {
+ Object[][] cases = {
+ {1.2345678, 1, MATH_CONTEXT_HALF_EVEN, false},
+ {1.2345678, 7, MATH_CONTEXT_HALF_EVEN, false},
+ {1.2345678, 12, MATH_CONTEXT_HALF_EVEN, false},
+ {1.2345678, 13, MATH_CONTEXT_HALF_EVEN, true},
+ {1.235, 1, MATH_CONTEXT_HALF_EVEN, false},
+ {1.235, 2, MATH_CONTEXT_HALF_EVEN, true},
+ {1.235, 3, MATH_CONTEXT_HALF_EVEN, false},
+ {1.000000000000001, 0, MATH_CONTEXT_HALF_EVEN, false},
+ {1.000000000000001, 0, MATH_CONTEXT_CEILING, true},
+ {1.235, 1, MATH_CONTEXT_CEILING, false},
+ {1.235, 2, MATH_CONTEXT_CEILING, false},
+ {1.235, 3, MATH_CONTEXT_CEILING, true}
+ };
+
+ for (Object[] cas : cases) {
+ double d = (Double) cas[0];
+ int maxFrac = (Integer) cas[1];
+ MathContext mc = (MathContext) cas[2];
+ boolean usesExact = (Boolean) cas[3];
+
+ FormatQuantity4 fq = new FormatQuantity4(d);
+ assertTrue("Should be using approximate double", !fq.explicitExactDouble);
+ fq.roundToMagnitude(-maxFrac, mc);
+ assertEquals(
+ "Using approximate double after rounding: " + d + " maxFrac=" + maxFrac + " " + mc,
+ usesExact,
+ fq.explicitExactDouble);
+ }
+ }
+
+ static void assertDoubleEquals(String message, double d1, double d2) {
+ boolean equal = (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6);
+ handleAssert(equal, message, d1, d2, null, false);
+ }
+
+ static void assertBigDecimalEquals(String message, String d1, BigDecimal d2) {
+ assertBigDecimalEquals(message, new BigDecimal(d1), d2);
+ }
+
+ static void assertBigDecimalEquals(String message, BigDecimal d1, BigDecimal d2) {
+ boolean equal = d1.compareTo(d2) == 0;
+ handleAssert(equal, message, d1, d2, null, false);
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.text.FieldPosition;
+import java.text.Format.Field;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.text.NumberFormat;
+
+/** @author sffc */
+public class NumberStringBuilderTest {
+ private static final String[] EXAMPLE_STRINGS = {
+ "",
+ "xyz",
+ "The quick brown fox jumps over the lazy dog",
+ "😁",
+ "mixed 😇 and ASCII",
+ "with combining characters like 🇦🇧🇨🇩"
+ };
+
+ @Test
+ public void testInsertAppendCharSequence() {
+
+ StringBuilder sb1 = new StringBuilder();
+ NumberStringBuilder sb2 = new NumberStringBuilder();
+ for (String str : EXAMPLE_STRINGS) {
+ NumberStringBuilder sb3 = new NumberStringBuilder();
+ sb1.append(str);
+ sb2.append(str, null);
+ sb3.append(str, null);
+ assertCharSequenceEquals(sb1, sb2);
+ assertCharSequenceEquals(sb3, str);
+
+ StringBuilder sb4 = new StringBuilder();
+ NumberStringBuilder sb5 = new NumberStringBuilder();
+ sb4.append("😇");
+ sb4.append(str);
+ sb4.append("xx");
+ sb5.append("😇xx", null);
+ sb5.insert(2, str, null);
+ assertCharSequenceEquals(sb4, sb5);
+
+ int start = Math.min(1, str.length());
+ int end = Math.min(10, str.length());
+ sb4.insert(3, str, start, end);
+ sb5.insert(3, str, start, end, null);
+ assertCharSequenceEquals(sb4, sb5);
+
+ sb4.append(str.toCharArray());
+ sb5.append(str.toCharArray(), null);
+ assertCharSequenceEquals(sb4, sb5);
+
+ sb4.insert(4, str.toCharArray());
+ sb5.insert(4, str.toCharArray(), null);
+ assertCharSequenceEquals(sb4, sb5);
+ }
+ }
+
+ @Test
+ public void testInsertAppendCodePoint() {
+ int[] cases = {0, 1, 60, 127, 128, 0x7fff, 0x8000, 0xffff, 0x10000, 0x1f000, 0x10ffff};
+
+ StringBuilder sb1 = new StringBuilder();
+ NumberStringBuilder sb2 = new NumberStringBuilder();
+ for (int cas : cases) {
+ NumberStringBuilder sb3 = new NumberStringBuilder();
+ sb1.appendCodePoint(cas);
+ sb2.appendCodePoint(cas, null);
+ sb3.appendCodePoint(cas, null);
+ assertCharSequenceEquals(sb1, sb2);
+ assertEquals(Character.codePointAt(sb3, 0), cas);
+
+ StringBuilder sb4 = new StringBuilder();
+ NumberStringBuilder sb5 = new NumberStringBuilder();
+ sb4.append("😇");
+ sb4.appendCodePoint(cas); // Java StringBuilder has no insertCodePoint()
+ sb4.append("xx");
+ sb5.append("😇xx", null);
+ sb5.insertCodePoint(2, cas, null);
+ assertCharSequenceEquals(sb4, sb5);
+ }
+ }
+
+ @Test
+ public void testFields() {
+ for (String str : EXAMPLE_STRINGS) {
+ NumberStringBuilder sb = new NumberStringBuilder();
+ sb.append(str, null);
+ sb.append(str, NumberFormat.Field.CURRENCY);
+ Field[] fields = sb.toFieldArray();
+ assertEquals(str.length() * 2, fields.length);
+ for (int i = 0; i < str.length(); i++) {
+ assertEquals(null, fields[i]);
+ assertEquals(NumberFormat.Field.CURRENCY, fields[i + str.length()]);
+ }
+
+ // Very basic FieldPosition test. More robust tests happen in NumberFormatTest.
+ // Let NumberFormatTest also take care of AttributedCharacterIterator material.
+ FieldPosition fp = new FieldPosition(NumberFormat.Field.CURRENCY);
+ sb.populateFieldPosition(fp, 0);
+ assertEquals(str.length(), fp.getBeginIndex());
+ assertEquals(str.length() * 2, fp.getEndIndex());
+
+ if (str.length() > 0) {
+ sb.insertCodePoint(2, 100, NumberFormat.Field.INTEGER);
+ fields = sb.toFieldArray();
+ assertEquals(str.length() * 2 + 1, fields.length);
+ assertEquals(fields[2], NumberFormat.Field.INTEGER);
+ }
+
+ sb.append(sb.clone());
+ sb.append(sb.toCharArray(), sb.toFieldArray());
+ int numNull = 0;
+ int numCurr = 0;
+ int numInt = 0;
+ Field[] oldFields = fields;
+ fields = sb.toFieldArray();
+ for (int i = 0; i < sb.length(); i++) {
+ assertEquals(oldFields[i % oldFields.length], fields[i]);
+ if (fields[i] == null) numNull++;
+ else if (fields[i] == NumberFormat.Field.CURRENCY) numCurr++;
+ else if (fields[i] == NumberFormat.Field.INTEGER) numInt++;
+ else throw new AssertionError("Encountered unknown field in " + str);
+ }
+ assertEquals(str.length() * 4, numNull);
+ assertEquals(numNull, numCurr);
+ assertEquals(str.length() > 0 ? 4 : 0, numInt);
+
+ NumberStringBuilder sb2 = new NumberStringBuilder();
+ sb2.append(sb);
+ assertTrue(sb.contentEquals(sb2));
+ assertTrue(sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray()));
+
+ sb2.insertCodePoint(0, 50, NumberFormat.Field.FRACTION);
+ assertTrue(!sb.contentEquals(sb2));
+ assertTrue(!sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray()));
+ }
+ }
+
+ @Test
+ public void testUnlimitedCapacity() {
+ NumberStringBuilder builder = new NumberStringBuilder();
+ // The builder should never fail upon repeated appends.
+ for (int i = 0; i < 1000; i++) {
+ assertEquals(builder.length(), i);
+ builder.appendCodePoint('x', null);
+ assertEquals(builder.length(), i + 1);
+ }
+ }
+
+ private static void assertCharSequenceEquals(CharSequence a, CharSequence b) {
+ assertEquals(a.toString(), b.toString());
+
+ assertEquals(a.length(), b.length());
+ for (int i = 0; i < a.length(); i++) {
+ assertEquals(a.charAt(i), b.charAt(i));
+ }
+
+ int start = Math.min(2, a.length());
+ int end = Math.min(12, a.length());
+ if (start != end) {
+ assertCharSequenceEquals(a.subSequence(start, end), b.subSequence(start, end));
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.ULocale;
+
+/** @author sffc */
+public class PatternStringTest {
+
+ @Test
+ public void testLocalized() {
+ DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
+ symbols.setDecimalSeparatorString("a");
+ symbols.setPercentString("b");
+ symbols.setMinusSignString(".");
+ symbols.setPlusSignString("'");
+
+ String standard = "+-abcb''a''#,##0.0%'a%'";
+ String localized = "’.'ab'c'b''a'''#,##0a0b'a%'";
+ String toStandard = "+-'ab'c'b''a'''#,##0.0%'a%'";
+
+ assertEquals(localized, PatternString.convertLocalized(standard, symbols, true));
+ assertEquals(toStandard, PatternString.convertLocalized(localized, symbols, false));
+ }
+
+ @Test
+ public void testToPatternSimple() {
+ String[][] cases = {
+ {"#", "0"},
+ {"0", "0"},
+ {"#0", "0"},
+ {"###", "0"},
+ {"0.##", "0.##"},
+ {"0.00", "0.00"},
+ {"0.00#", "0.00#"},
+ {"#E0", "#E0"},
+ {"0E0", "0E0"},
+ {"#00E00", "#00E00"},
+ {"#,##0", "#,##0"},
+ {"#,##0E0", "#,##0E0"},
+ {"#;#", "0;0"},
+ {"#;-#", "0"}, // ignore a negative prefix pattern of '-' since that is the default
+ {"**##0", "**##0"},
+ {"*'x'##0", "*x##0"},
+ {"a''b0", "a''b0"},
+ {"*''##0", "*''##0"},
+ {"*📺##0", "*'📺'##0"},
+ {"*'நி'##0", "*'நி'##0"},
+ };
+
+ for (String[] cas : cases) {
+ String input = cas[0];
+ String output = cas[1];
+
+ Properties properties = PatternString.parseToProperties(input);
+ String actual = PatternString.propertiesToString(properties);
+ assertEquals(
+ "Failed on input pattern '" + input + "', properties " + properties, output, actual);
+ }
+ }
+
+ @Test
+ public void testToPatternWithProperties() {
+ Object[][] cases = {
+ {new Properties().setPositivePrefix("abc"), "abc#"},
+ {new Properties().setPositiveSuffix("abc"), "#abc"},
+ {new Properties().setPositivePrefixPattern("abc"), "abc#"},
+ {new Properties().setPositiveSuffixPattern("abc"), "#abc"},
+ {new Properties().setNegativePrefix("abc"), "#;abc#"},
+ {new Properties().setNegativeSuffix("abc"), "#;#abc"},
+ {new Properties().setNegativePrefixPattern("abc"), "#;abc#"},
+ {new Properties().setNegativeSuffixPattern("abc"), "#;#abc"},
+ {new Properties().setPositivePrefix("+"), "'+'#"},
+ {new Properties().setPositivePrefixPattern("+"), "+#"},
+ {new Properties().setPositivePrefix("+'"), "'+'''#"},
+ {new Properties().setPositivePrefix("'+"), "'''+'#"},
+ {new Properties().setPositivePrefix("'"), "''#"},
+ {new Properties().setPositivePrefixPattern("+''"), "+''#"},
+ };
+
+ for (Object[] cas : cases) {
+ Properties input = (Properties) cas[0];
+ String output = (String) cas[1];
+
+ String actual = PatternString.propertiesToString(input);
+ assertEquals("Failed on input properties " + input, output, actual);
+ }
+ }
+
+ @Test
+ public void testExceptionOnInvalid() {
+ String[] invalidPatterns = {"#.#.#", "0#", "0#.", ".#0", "0#.#0", "@0", "0@"};
+
+ for (String pattern : invalidPatterns) {
+ try {
+ PatternString.parseToProperties(pattern);
+ fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern);
+ } catch (IllegalArgumentException e) {
+ }
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+
+import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
+import com.ibm.icu.impl.number.Parse.ParseMode;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+public class PropertiesTest {
+
+ @Test
+ public void testBasicEquals() {
+ Properties p1 = new Properties();
+ Properties p2 = new Properties();
+ assertEquals(p1, p2);
+
+ p1.setPositivePrefix("abc");
+ assertNotEquals(p1, p2);
+ p2.setPositivePrefix("xyz");
+ assertNotEquals(p1, p2);
+ p1.setPositivePrefix("xyz");
+ assertEquals(p1, p2);
+ }
+
+ @Test
+ public void testFieldCoverage() {
+ Properties p0 = new Properties();
+ Properties p1 = new Properties();
+ Properties p2 = new Properties();
+ Properties p3 = new Properties();
+ Properties p4 = new Properties();
+
+ Set<Integer> hashCodes = new HashSet<Integer>();
+ Field[] fields = Properties.class.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+
+ // Check for getters and setters
+ String fieldNamePascalCase =
+ Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
+ String getterName = "get" + fieldNamePascalCase;
+ String setterName = "set" + fieldNamePascalCase;
+ Method getter, setter;
+ try {
+ getter = Properties.class.getMethod(getterName);
+ assertEquals(
+ "Getter does not return correct type", field.getType(), getter.getReturnType());
+ } catch (NoSuchMethodException e) {
+ fail("Could not find method " + getterName + " for field " + field);
+ continue;
+ } catch (SecurityException e) {
+ fail("Could not access method " + getterName + " for field " + field);
+ continue;
+ }
+ try {
+ setter = Properties.class.getMethod(setterName, field.getType());
+ assertEquals(
+ "Method " + setterName + " does not return correct type",
+ Properties.class,
+ setter.getReturnType());
+ } catch (NoSuchMethodException e) {
+ fail("Could not find method " + setterName + " for field " + field);
+ continue;
+ } catch (SecurityException e) {
+ fail("Could not access method " + setterName + " for field " + field);
+ continue;
+ }
+
+ // Check for parameter name equality.
+ // The parameter name is not always available, depending on compiler settings.
+ Parameter param = setter.getParameters()[0];
+ if (!param.getName().subSequence(0, 3).equals("arg")) {
+ assertEquals("Parameter name should equal field name", field.getName(), param.getName());
+ }
+
+ try {
+ // Check for default value (should be null for objects)
+ if (field.getType() != Integer.TYPE && field.getType() != Boolean.TYPE) {
+ Object default0 = getter.invoke(p0);
+ assertEquals("Field " + field + " has non-null default value:", null, default0);
+ }
+
+ // Check for getter, equals, and hash code behavior
+ Object val0 = getSampleValueForType(field.getType(), 0);
+ Object val1 = getSampleValueForType(field.getType(), 1);
+ Object val2 = getSampleValueForType(field.getType(), 2);
+ assertNotEquals(val0, val1);
+ setter.invoke(p1, val0);
+ setter.invoke(p2, val0);
+ assertEquals(p1, p2);
+ assertEquals(p1.hashCode(), p2.hashCode());
+ assertEquals(getter.invoke(p1), getter.invoke(p2));
+ assertEquals(getter.invoke(p1), val0);
+ assertNotEquals(getter.invoke(p1), val1);
+ hashCodes.add(p1.hashCode());
+ setter.invoke(p1, val1);
+ assertNotEquals("Field " + field + " is missing from equals()", p1, p2);
+ assertNotEquals(getter.invoke(p1), getter.invoke(p2));
+ assertNotEquals(getter.invoke(p1), val0);
+ assertEquals(getter.invoke(p1), val1);
+ setter.invoke(p1, val0);
+ assertEquals("Field " + field + " setter might have side effects", p1, p2);
+ assertEquals(p1.hashCode(), p2.hashCode());
+ assertEquals(getter.invoke(p1), getter.invoke(p2));
+ setter.invoke(p1, val1);
+ setter.invoke(p2, val1);
+ assertEquals(p1, p2);
+ assertEquals(p1.hashCode(), p2.hashCode());
+ assertEquals(getter.invoke(p1), getter.invoke(p2));
+ setter.invoke(p1, val2);
+ setter.invoke(p1, val1);
+ assertEquals("Field " + field + " setter might have side effects", p1, p2);
+ assertEquals(p1.hashCode(), p2.hashCode());
+ assertEquals(getter.invoke(p1), getter.invoke(p2));
+ hashCodes.add(p1.hashCode());
+
+ // Check for clone behavior
+ Properties copy = p1.clone();
+ assertEquals("Field " + field + " did not get copied in clone", p1, copy);
+ assertEquals(p1.hashCode(), copy.hashCode());
+ assertEquals(getter.invoke(p1), getter.invoke(copy));
+
+ // Check for copyFrom behavior
+ setter.invoke(p1, val0);
+ assertNotEquals(p1, p2);
+ assertNotEquals(getter.invoke(p1), getter.invoke(p2));
+ p2.copyFrom(p1);
+ assertEquals("Field " + field + " is missing from copyFrom()", p1, p2);
+ assertEquals(p1.hashCode(), p2.hashCode());
+ assertEquals(getter.invoke(p1), getter.invoke(p2));
+
+ // Load values into p3 and p4 for clear() behavior test
+ setter.invoke(p3, getSampleValueForType(field.getType(), 3));
+ hashCodes.add(p3.hashCode());
+ setter.invoke(p4, getSampleValueForType(field.getType(), 4));
+ hashCodes.add(p4.hashCode());
+ } catch (IllegalAccessException e) {
+ fail("Could not access method for field " + field);
+ } catch (IllegalArgumentException e) {
+ fail("Could call method for field " + field);
+ } catch (InvocationTargetException e) {
+ fail("Could invoke method on target for field " + field);
+ }
+ }
+
+ // Check for clear() behavior
+ assertNotEquals(p3, p4);
+ p3.clear();
+ p4.clear();
+ assertEquals("A field is missing from the clear() function", p3, p4);
+
+ // A good hashCode() implementation should produce very few collisions. We added at most
+ // 4*fields.length codes to the set. We'll say the implementation is good if we had at least
+ // fields.length unique values.
+ // TODO: Should the requirement be stronger than this?
+ assertTrue(
+ "Too many hash code collisions: " + hashCodes.size() + " out of " + (fields.length * 4),
+ hashCodes.size() >= fields.length);
+ }
+
+ /**
+ * Creates a valid sample instance of the given type. Used to simulate getters and setters.
+ *
+ * @param type The type to generate.
+ * @param seed An integer seed, guaranteed to be positive. The same seed should generate two
+ * instances that are equal. A different seed should in general generate two instances that
+ * are not equal; this might not always be possible, such as with booleans or enums where
+ * there are limited possible values.
+ * @return An instance of the specified type.
+ */
+ Object getSampleValueForType(Class<?> type, int seed) {
+ if (type == Integer.TYPE) {
+ return seed * 1000001;
+
+ } else if (type == Boolean.TYPE) {
+ return (seed % 2) == 0;
+
+ } else if (type == BigDecimal.class) {
+ if (seed == 0) return null;
+ return new BigDecimal(seed * 1000002);
+
+ } else if (type == String.class) {
+ if (seed == 0) return null;
+ return BigInteger.valueOf(seed * 1000003).toString(32);
+
+ } else if (type == CompactStyle.class) {
+ if (seed == 0) return null;
+ CompactStyle[] values = CompactStyle.values();
+ return values[seed % values.length];
+
+ } else if (type == Currency.class) {
+ if (seed == 0) return null;
+ Object[] currencies = Currency.getAvailableCurrencies().toArray();
+ return currencies[seed % currencies.length];
+
+ } else if (type == CurrencyPluralInfo.class) {
+ if (seed == 0) return null;
+ ULocale[] locales = ULocale.getAvailableLocales();
+ return CurrencyPluralInfo.getInstance(locales[seed % locales.length]);
+
+ } else if (type == CurrencyStyle.class) {
+ if (seed == 0) return null;
+ CurrencyStyle[] values = CurrencyStyle.values();
+ return values[seed % values.length];
+
+ } else if (type == CurrencyUsage.class) {
+ if (seed == 0) return null;
+ CurrencyUsage[] values = CurrencyUsage.values();
+ return values[seed % values.length];
+
+ } else if (type == FormatWidth.class) {
+ if (seed == 0) return null;
+ FormatWidth[] values = FormatWidth.values();
+ return values[seed % values.length];
+
+ } else if (type == MathContext.class) {
+ if (seed == 0) return null;
+ RoundingMode[] modes = RoundingMode.values();
+ return new MathContext(seed, modes[seed % modes.length]);
+
+ } else if (type == MeasureUnit.class) {
+ if (seed == 0) return null;
+ Object[] units = MeasureUnit.getAvailable().toArray();
+ return units[seed % units.length];
+
+ } else if (type == PadPosition.class) {
+ if (seed == 0) return null;
+ PadPosition[] values = PadPosition.values();
+ return values[seed % values.length];
+
+ } else if (type == ParseMode.class) {
+ if (seed == 0) return null;
+ ParseMode[] values = ParseMode.values();
+ return values[seed % values.length];
+
+ } else if (type == RoundingMode.class) {
+ if (seed == 0) return null;
+ RoundingMode[] values = RoundingMode.values();
+ return values[seed % values.length];
+
+ } else if (type == SignificantDigitsMode.class) {
+ if (seed == 0) return null;
+ SignificantDigitsMode[] values = SignificantDigitsMode.values();
+ return values[seed % values.length];
+
+ } else {
+ fail("Don't know how to handle type " + type + ". Please add it to getSampleValueForType().");
+ return null;
+ }
+ }
+
+ @Test
+ public void TestBasicSerializationRoundTrip() throws IOException, ClassNotFoundException {
+ Properties props0 = new Properties();
+
+ // Write values to some of the fields
+ PatternString.parseToExistingProperties("A-**####,#00.00#b¤", props0);
+
+ // Write to byte stream
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(props0);
+ oos.flush();
+ baos.close();
+ byte[] bytes = baos.toByteArray();
+
+ // Read from byte stream
+ ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
+ Object obj = ois.readObject();
+ ois.close();
+ Properties props1 = (Properties) obj;
+
+ // Test equality
+ assertEquals("Did not round-trip through serialization", props0, props1);
+ }
+
+ /** Handler for serialization compatibility test suite. */
+ public static class PropertiesHandler implements SerializableTestUtility.Handler {
+
+ @Override
+ public Object[] getTestObjects() {
+ return new Object[] {
+ new Properties(),
+ PatternString.parseToProperties("x#,##0.00%"),
+ new Properties().setCompactStyle(CompactStyle.LONG).setMinimumExponentDigits(2)
+ };
+ }
+
+ @Override
+ public boolean hasSameBehavior(Object a, Object b) {
+ return a.equals(b);
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
+
+public class RounderTest {
+
+ @Test
+ public void testSignificantDigitsRounder() {
+ Object[][][][] cases = {
+ {
+ {{1, -1}, {0, 2}, {2, 4}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
+ {
+ {0.0, "0.0", "0.0", "0"},
+ {0.054321, "0.05432", "0.05", "0.054"},
+ {0.54321, "0.5432", "0.54", "0.54"},
+ {1.0, "1.0", "1.0", "1"},
+ {5.4321, "5.432", "5.43", "5.43"},
+ {10.0, "10", "10", "10"},
+ {11.0, "11", "11", "11"},
+ {100.0, "100", "100", "100"},
+ {100.23, "100.2", "100.2", "100.2"},
+ {543210.0, "543200", "543200", "543200"},
+ }
+ },
+ {
+ {{1, -1}, {0, 0}, {2, -1}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
+ {
+ {0.0, "0.0", "0", "0"},
+ {0.054321, "0.054321", "0", "0.054"},
+ {0.54321, "0.54321", "1", "0.54"},
+ {1.0, "1.0", "1", "1"},
+ {5.4321, "5.4321", "5", "5.4"},
+ {10.0, "10", "10", "10"},
+ {11.0, "11", "11", "11"},
+ {100.0, "100", "100", "100"},
+ {100.23, "100.23", "100", "100"},
+ {543210.0, "543210", "543210", "543210"},
+ }
+ },
+ {
+ {{0, 2}, {1, 2}, {3, 3}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
+ {
+ {0.0, ".000", ".00", ".0"},
+ {0.054321, ".0543", ".05", ".0543"},
+ {0.54321, ".543", ".54", ".543"},
+ {1.0, "1.00", "1.00", "1.0"},
+ {5.4321, "5.43", "5.43", "5.43"},
+ {10.0, "10.0", "10.0", "10.0"},
+ {11.0, "11.0", "11.0", "11.0"},
+ {100.0, "00.0", "00.0", "00.0"},
+ {100.23, "00.2", "00.2", "00.2"},
+ {543210.0, "10.0", "10.0", "10.0"}
+ }
+ }
+ };
+
+ int caseNumber = 0;
+ for (Object[][][] cas : cases) {
+ int minInt = (Integer) cas[0][0][0];
+ int maxInt = (Integer) cas[0][0][1];
+ int minFrac = (Integer) cas[0][1][0];
+ int maxFrac = (Integer) cas[0][1][1];
+ int minSig = (Integer) cas[0][2][0];
+ int maxSig = (Integer) cas[0][2][1];
+
+ Properties properties = new Properties();
+ FormatQuantity4 fq = new FormatQuantity4();
+ properties.setMinimumIntegerDigits(minInt);
+ properties.setMaximumIntegerDigits(maxInt);
+ properties.setMinimumFractionDigits(minFrac);
+ properties.setMaximumFractionDigits(maxFrac);
+ properties.setMinimumSignificantDigits(minSig);
+ properties.setMaximumSignificantDigits(maxSig);
+
+ int runNumber = 0;
+ for (Object[] run : cas[1]) {
+ double input = (Double) run[0];
+ String expected1 = (String) run[1];
+ String expected2 = (String) run[2];
+ String expected3 = (String) run[3];
+
+ properties.setSignificantDigitsMode(SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION);
+ fq.setToDouble(input);
+ SignificantDigitsRounder.getInstance(properties).apply(fq);
+ assertEquals(
+ "Case " + caseNumber + ", run " + runNumber + ", mode 0: " + fq,
+ expected1,
+ formatQuantityToString(fq));
+
+ properties.setSignificantDigitsMode(SignificantDigitsMode.RESPECT_MAXIMUM_FRACTION);
+ fq.setToDouble(input);
+ SignificantDigitsRounder.getInstance(properties).apply(fq);
+ assertEquals(
+ "Case " + caseNumber + ", run " + runNumber + ", mode 1: " + fq,
+ expected2,
+ formatQuantityToString(fq));
+
+ properties.setSignificantDigitsMode(SignificantDigitsMode.ENSURE_MINIMUM_SIGNIFICANT);
+ fq.setToDouble(input);
+ SignificantDigitsRounder.getInstance(properties).apply(fq);
+ assertEquals(
+ "Case " + caseNumber + ", run " + runNumber + ", mode 2: " + fq,
+ expected3,
+ formatQuantityToString(fq));
+
+ runNumber++;
+ }
+
+ caseNumber++;
+ }
+ }
+
+ private String formatQuantityToString(FormatQuantity fq) {
+ StringBuilder sb = new StringBuilder();
+ int udm = fq.getUpperDisplayMagnitude();
+ int ldm = fq.getLowerDisplayMagnitude();
+ if (udm == -1) sb.append('.');
+ for (int m = udm; m >= ldm; m--) {
+ sb.append(fq.getDigit(m));
+ if (m == 0 && m > ldm) sb.append('.');
+ }
+ return sb.toString();
+ }
+}
NumberFormat format_b = (NumberFormat) b;
double number = 1234.56;
- return format_a.format(number).equals(format_b.format(number));
+ String result_a = format_a.format(number);
+ String result_b = format_b.format(number);
+ boolean equal = result_a.equals(result_b);
+ if (!equal) {
+ System.out.println(format_a+" "+format_b);
+ System.out.println(result_a+" "+result_b);
+ }
+ return equal;
}
}
char chars_a[] = getCharSymbols(dfs_a);
char chars_b[] = getCharSymbols(dfs_b);
- return SerializableTestUtility.compareStrings(strings_a, strings_b) && SerializableTestUtility.compareChars(chars_a, chars_b);
+ // Spot-check char-to-string conversion (ICU 58)
+ String percent_a1 = Character.toString(dfs_a.getPercent());
+ String percent_a2 = dfs_a.getPercentString();
+ String percent_b1 = Character.toString(dfs_b.getPercent());
+ String percent_b2 = dfs_b.getPercentString();
+
+ return SerializableTestUtility.compareStrings(strings_a, strings_b)
+ && SerializableTestUtility.compareChars(chars_a, chars_b)
+ && percent_a1.equals(percent_b1)
+ && percent_a2.equals(percent_b2)
+ && percent_a1.equals(percent_a2);
}
}
import com.ibm.icu.dev.test.format.MeasureUnitTest;
import com.ibm.icu.dev.test.format.PluralRulesTest;
+import com.ibm.icu.dev.test.numbers.PropertiesTest;
import com.ibm.icu.impl.JavaTimeZone;
import com.ibm.icu.impl.OlsonTimeZone;
import com.ibm.icu.impl.TimeZoneAdapter;
map.put("com.ibm.icu.util.MeasureUnit", new MeasureUnitTest.MeasureUnitHandler());
map.put("com.ibm.icu.util.TimeUnit", new MeasureUnitTest.MeasureUnitHandler());
map.put("com.ibm.icu.text.MeasureFormat", new MeasureUnitTest.MeasureFormatHandler());
+ map.put("com.ibm.icu.impl.number.Properties", new PropertiesTest.PropertiesHandler());
map.put("com.ibm.icu.util.ICUException", new ICUExceptionHandler());
map.put("com.ibm.icu.util.ICUUncheckedIOException", new ICUUncheckedIOExceptionHandler());
return;
}
+ if (className.equals("com.ibm.icu.text.DecimalFormat_ICU58")) {
+ // Do not test the legacy DecimalFormat class in ICU 59
+ return;
+ }
+
if (c.isEnum() || !serializable.isAssignableFrom(c)) {
//System.out.println("@@@ Skipping: " + className);
return;
*/
package com.ibm.icu.dev.test.util;
+import java.util.Arrays;
import java.util.Iterator;
import org.junit.Test;
private static final Integer FRI = new Integer(6);
private static final Integer SAT = new Integer(7);
+ private static final Integer SUP1 = new Integer(8);
+ private static final Integer SUP2 = new Integer(9);
+ private static final Integer SUP3 = new Integer(10);
+ private static final Integer SUP4 = new Integer(11);
+
private static final Integer FOO = new Integer(-1);
private static final Integer BAR = new Integer(-2);
-
+
private static final Object[][] TESTDATA = {
{"Sunday", SUN},
{"Monday", MON},
{"W", WED},
{"T", THU},
{"F", FRI},
- {"S", SAT}
+ {"S", SAT},
+ {"L📺", SUP1}, // L, 0xD83D, 0xDCFA
+ {"L📺1", SUP2}, // L, 0xD83D, 0xDCFA, 1
+ {"L📻", SUP3}, // L, 0xD83D, 0xDCFB
+ {"L🃏", SUP4}, // L, 0xD83C, 0xDCCF
};
private static final Object[][] TESTCASES = {
{"TEST", new Object[]{TUE, THU}, new Object[]{TUE, THU}},
{"SUN", new Object[]{SUN, SAT}, SUN},
{"super", null, SUN},
- {"NO", null, null}
+ {"NO", null, null},
+ {"L📺", SUP1, SUP1},
+ {"l📺", null, SUP1},
+ };
+
+ private static final Object[][] TESTCASES_PARSE = {
+ {
+ "Sunday",
+ new Object[]{
+ new Object[]{SAT,SUN}, new Object[]{SAT,SUN}, // matches on "S"
+ null, null, // matches on "Su"
+ SUN, SUN, // matches on "Sun"
+ null, null, // matches on "Sund"
+ null, null, // matches on "Sunda"
+ SUN, SUN, // matches on "Sunday"
+ }
+ },
+ {
+ "sunday",
+ new Object[]{
+ null, new Object[]{SAT,SUN}, // matches on "s"
+ null, null, // matches on "su"
+ null, SUN, // matches on "sun"
+ null, null, // matches on "sund"
+ null, null, // matches on "sunda"
+ null, SUN, // matches on "sunday"
+ }
+ },
+ {
+ "MMM",
+ new Object[]{
+ MON, MON, // matches on "M"
+ // no more matches in data
+ }
+ },
+ {
+ "BBB",
+ new Object[]{
+ // no matches in data
+ }
+ },
+ {
+ "l📺12",
+ new Object[]{
+ null, null, // matches on "L"
+ null, SUP1, // matches on "L📺"
+ null, SUP2, // matches on "L📺1"
+ // no more matches in data
+ }
+ },
+ {
+ "L📻",
+ new Object[] {
+ null, null, // matches on "L"
+ SUP3, SUP3, // matches on "L📻"
+ }
+ },
+ {
+ "L🃏",
+ new Object[] {
+ null, null, // matches on "L"
+ SUP4, SUP4, // matches on "L🃏"
+ }
+ }
};
@Test
logln("Test for get(String)");
for (int i = 0; i < TESTCASES.length; i++) {
itr = map.get((String)TESTCASES[i][0]);
- checkResult(itr, TESTCASES[i][1]);
+ checkResult("get(String) case " + i, itr, TESTCASES[i][1]);
}
logln("Test for get(String, int)");
}
textBuf.append(TESTCASES[i][0]);
itr = map.get(textBuf.toString(), i);
- checkResult(itr, TESTCASES[i][1]);
+ checkResult("get(String, int) case " + i, itr, TESTCASES[i][1]);
+ }
+
+ logln("Test for ParseState");
+ for (int i = 0; i < TESTCASES_PARSE.length; i++) {
+ String test = (String) TESTCASES_PARSE[i][0];
+ Object[] expecteds = (Object[]) TESTCASES_PARSE[i][1];
+ checkParse(map, test, expecteds, true);
}
// Add duplicated entry
// Make sure the all entries are returned
itr = map.get("Sunday");
- checkResult(itr, new Object[]{FOO, SUN});
+ checkResult("Get Sunday", itr, new Object[]{FOO, SUN});
}
@Test
logln("Test for get(String)");
for (int i = 0; i < TESTCASES.length; i++) {
itr = map.get((String)TESTCASES[i][0]);
- checkResult(itr, TESTCASES[i][2]);
+ checkResult("get(String) case " + i, itr, TESTCASES[i][2]);
}
-
+
logln("Test for get(String, int)");
StringBuffer textBuf = new StringBuffer();
for (int i = 0; i < TESTCASES.length; i++) {
}
textBuf.append(TESTCASES[i][0]);
itr = map.get(textBuf.toString(), i);
- checkResult(itr, TESTCASES[i][2]);
+ checkResult("get(String, int) case " + i, itr, TESTCASES[i][2]);
+ }
+
+ logln("Test for ParseState");
+ for (int i = 0; i < TESTCASES_PARSE.length; i++) {
+ String test = (String) TESTCASES_PARSE[i][0];
+ Object[] expecteds = (Object[]) TESTCASES_PARSE[i][1];
+ checkParse(map, test, expecteds, false);
}
// Add duplicated entry
// Make sure the all entries are returned
itr = map.get("Sunday");
- checkResult(itr, new Object[]{SUN, FOO, BAR});
+ checkResult("Get Sunday", itr, new Object[]{SUN, FOO, BAR});
+ }
+
+ private void checkParse(TextTrieMap map, String text, Object[] rawExpecteds, boolean caseSensitive) {
+ // rawExpecteds has even-valued indices for case sensitive and odd-valued indicies for case insensitive
+ // Get out only the values that we want.
+ Object[] expecteds = null;
+ for (int i=rawExpecteds.length/2-1; i>=0; i--) {
+ int j = i*2+(caseSensitive?0:1);
+ if (rawExpecteds[j] != null) {
+ if (expecteds == null) {
+ expecteds = new Object[i+1];
+ }
+ expecteds[i] = rawExpecteds[j];
+ }
+ }
+ if (expecteds == null) {
+ expecteds = new Object[0];
+ }
+
+ TextTrieMap.ParseState state = null;
+ for (int charOffset=0, cpOffset=0; charOffset < text.length(); cpOffset++) {
+ int cp = Character.codePointAt(text, charOffset);
+ if (state == null) {
+ state = map.openParseState(cp);
+ }
+ if (state == null) {
+ assertEquals("Expected matches, but no matches are available", 0, expecteds.length);
+ break;
+ }
+ state.accept(cp);
+ if (cpOffset < expecteds.length - 1) {
+ assertFalse(
+ "In middle of parse sequence, but atEnd() is true: '" + text + "' offset " + charOffset,
+ state.atEnd());
+ } else if (cpOffset == expecteds.length) {
+ // Note: it possible for atEnd() to be either true or false at expecteds.length - 1;
+ // if true, we are at the end of the input string; if false, there is still input string
+ // left to be consumed, but we don't know if there are remaining matches.
+ assertTrue(
+ "At end of parse sequence, but atEnd() is false: '" + text + "' offset " + charOffset,
+ state.atEnd());
+ break;
+ }
+ Object expected = expecteds[cpOffset];
+ Iterator actual = state.getCurrentMatches();
+ checkResult("ParseState '" + text + "' offset " + charOffset, actual, expected);
+ charOffset += Character.charCount(cp);
+ }
}
private boolean eql(Object o1, Object o2) {
return o1.equals(o2);
}
- private void checkResult(Iterator itr, Object expected) {
+ private void checkResult(String memo, Iterator itr, Object expected) {
if (itr == null) {
if (expected != null) {
- errln("FAIL: Empty results - Expected: " + expected);
+ String expectedStr = (expected instanceof Object[])
+ ? Arrays.toString((Object[]) expected)
+ : expected.toString();
+ errln("FAIL: Empty results: " + memo + ": Expected: " + expectedStr);
}
return;
}