diff --git a/.gitignore b/.gitignore index dd3e842640..161d9ff828 100644 --- a/.gitignore +++ b/.gitignore @@ -897,6 +897,7 @@ icu4j/.project icu4j/build-local.properties icu4j/demos/out icu4j/doc +icu4j/eclipse icu4j/eclipse-build/out icu4j/lib/*.jar icu4j/main/classes/charset/out diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/TextTrieMap.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/TextTrieMap.java index 6688d4719f..2576ff18d1 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/TextTrieMap.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/TextTrieMap.java @@ -105,6 +105,85 @@ public class TextTrieMap { } } + /** + * 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 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 { private boolean _ignoreCase; private CharSequence _text; @@ -234,6 +313,21 @@ public class TextTrieMap { _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 values() { if (_values == null) { return null; @@ -272,6 +366,37 @@ public class TextTrieMap { return match; } + public class StepResult { + public Node node; + public int offset; + } + public void takeStep(char ch, int offset, StepResult result) { + assert offset <= charCount(); + if (offset == charCount()) { + // Go to a child node + for (int i=0; _children != null && i < _children.size(); i++) { + Node child = _children.get(i); + if (ch < child._text[0]) break; + if (ch == child._text[0]) { + // Found a matching child node + result.node = child; + result.offset = 1; + return; + } + } + // No matching children; fall through + } else if (_text[offset] == ch) { + // Return to this node; increase offset + result.node = this; + result.offset = offset + 1; + return; + } + // No matches + result.node = null; + result.offset = -1; + return; + } + private void add(char[] text, int offset, V value) { if (text.length == offset) { _values = addValue(_values, value); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java new file mode 100644 index 0000000000..992028dbe8 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java @@ -0,0 +1,524 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.NumberFormat.Field; + +/** + * Performs manipulations on affix patterns: the prefix and suffix strings associated with a decimal + * format pattern. For example: + * + * + * + * + * + * + * + *
Affix PatternExample Unescaped (Formatted) String
abcabc
ab-ab−
ab'-'ab-
ab''ab'
+ * + * To manually iterate over tokens in a literal string, use the following pattern, which is designed + * to be efficient. + * + *
+ * 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;
+ *   }
+ * }
+ * 
+ */ +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. + * + *

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. + * + *

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 TYPE_ + * constants, such as {@link #TYPE_MINUS_SIGN}. If greater than or equal to zero, a literal + * code point. + */ + public static int getTypeOrCp(long tag) { + int type = getType(tag); + return (type == 0) ? getCodePoint(tag) : -type; + } + + private static long makeTag(int offset, int type, int state, int cp) { + long tag = 0L; + tag |= offset; + tag |= (-(long) type) << 32; + tag |= ((long) state) << 36; + tag |= ((long) cp) << 40; + return tag; + } + + static int getOffset(long tag) { + return (int) (tag & 0xffffffff); + } + + static int getType(long tag) { + return (int) ((tag >>> 32) & 0xf); + } + + static int getState(long tag) { + return (int) ((tag >>> 36) & 0xf); + } + + static int getCodePoint(long tag) { + return (int) (tag >>> 40); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Endpoint.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Endpoint.java new file mode 100644 index 0000000000..ef603bc6b7 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Endpoint.java @@ -0,0 +1,286 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import com.ibm.icu.impl.number.Format.BeforeTargetAfterFormat; +import com.ibm.icu.impl.number.Format.SingularFormat; +import com.ibm.icu.impl.number.Format.TargetFormat; +import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier; +import com.ibm.icu.impl.number.formatters.CompactDecimalFormat; +import com.ibm.icu.impl.number.formatters.CurrencyFormat; +import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier; +import com.ibm.icu.impl.number.formatters.MeasureFormat; +import com.ibm.icu.impl.number.formatters.PaddingFormat; +import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat; +import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat; +import com.ibm.icu.impl.number.formatters.RoundingFormat; +import com.ibm.icu.impl.number.formatters.ScientificFormat; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.PluralRules; +import com.ibm.icu.util.ULocale; + +public class Endpoint { + // public static Format from(DecimalFormatSymbols symbols, Properties properties) + // throws ParseException { + // Format format = new PositiveIntegerFormat(symbols, properties); + // // TODO: integer-only format + // format = new PositiveDecimalFormat((SelfContainedFormat) format, symbols, properties); + // if (properties.useCompactDecimalFormat()) { + // format = CompactDecimalFormat.getInstance((SingularFormat) format, symbols, properties); + // } else { + // format = + // PositiveNegativeAffixFormat.getInstance((SingularFormat) format, symbols, properties); + // } + // if (properties.useRoundingInterval()) { + // format = new IntervalRoundingFormat((SingularFormat) format, properties); + // } else if (properties.useSignificantDigits()) { + // format = new SignificantDigitsFormat((SingularFormat) format, properties); + // } else if (properties.useFractionFormat()) { + // format = new RoundingFormat((SingularFormat) format, properties); + // } + // return format; + // } + + public static Format fromBTA(Properties properties) { + return fromBTA(properties, getSymbols()); + } + + public static SingularFormat fromBTA(Properties properties, Locale locale) { + return fromBTA(properties, getSymbols(locale)); + } + + public static SingularFormat fromBTA(Properties properties, ULocale uLocale) { + return fromBTA(properties, getSymbols(uLocale)); + } + + public static SingularFormat fromBTA(String pattern) { + return fromBTA(getProperties(pattern), getSymbols()); + } + + public static SingularFormat fromBTA(String pattern, Locale locale) { + return fromBTA(getProperties(pattern), getSymbols(locale)); + } + + public static SingularFormat fromBTA(String pattern, ULocale uLocale) { + return fromBTA(getProperties(pattern), getSymbols(uLocale)); + } + + public static SingularFormat fromBTA(String pattern, DecimalFormatSymbols symbols) { + return fromBTA(getProperties(pattern), symbols); + } + + public static SingularFormat fromBTA(Properties properties, DecimalFormatSymbols symbols) { + + if (symbols == null) throw new IllegalArgumentException("symbols must not be null"); + + // TODO: This fast track results in an improvement of about 10ns during formatting. See if + // there is a way to implement it more elegantly. + boolean canUseFastTrack = true; + PluralRules rules = getPluralRules(symbols.getULocale(), properties); + BeforeTargetAfterFormat format = new Format.BeforeTargetAfterFormat(rules); + TargetFormat target = new PositiveDecimalFormat(symbols, properties); + format.setTargetFormat(target); + // TODO: integer-only format? + if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) { + canUseFastTrack = false; + format.addBeforeFormat(MagnitudeMultiplier.getInstance(properties)); + } + if (BigDecimalMultiplier.useMultiplier(properties)) { + canUseFastTrack = false; + format.addBeforeFormat(BigDecimalMultiplier.getInstance(properties)); + } + if (MeasureFormat.useMeasureFormat(properties)) { + canUseFastTrack = false; + format.addBeforeFormat(MeasureFormat.getInstance(symbols, properties)); + } + if (CurrencyFormat.useCurrency(properties)) { + canUseFastTrack = false; + if (CompactDecimalFormat.useCompactDecimalFormat(properties)) { + format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties)); + } else if (ScientificFormat.useScientificNotation(properties)) { + // TODO: Should the currency rounder or scientific rounder be used in this case? + // For now, default to using the scientific rounder. + format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties)); + format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties)); + } else { + format.addBeforeFormat(CurrencyFormat.getCurrencyRounder(symbols, properties)); + format.addBeforeFormat(CurrencyFormat.getCurrencyModifier(symbols, properties)); + } + } else { + if (CompactDecimalFormat.useCompactDecimalFormat(properties)) { + canUseFastTrack = false; + format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties)); + } else if (ScientificFormat.useScientificNotation(properties)) { + canUseFastTrack = false; + format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties)); + format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties)); + } else { + format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties)); + format.addBeforeFormat(RoundingFormat.getDefaultOrNoRounder(properties)); + } + } + if (PaddingFormat.usePadding(properties)) { + canUseFastTrack = false; + format.addAfterFormat(PaddingFormat.getInstance(properties)); + } + if (canUseFastTrack) { + return new Format.PositiveNegativeRounderTargetFormat( + PositiveNegativeAffixFormat.getInstance(symbols, properties), + RoundingFormat.getDefaultOrNoRounder(properties), + target); + } else { + return format; + } + } + + public static String staticFormat(FormatQuantity input, Properties properties) { + return staticFormat(input, properties, getSymbols()); + } + + public static String staticFormat(FormatQuantity input, Properties properties, Locale locale) { + return staticFormat(input, properties, getSymbols(locale)); + } + + public static String staticFormat(FormatQuantity input, Properties properties, ULocale uLocale) { + return staticFormat(input, properties, getSymbols(uLocale)); + } + + public static String staticFormat(FormatQuantity input, String pattern) { + return staticFormat(input, getProperties(pattern), getSymbols()); + } + + public static String staticFormat(FormatQuantity input, String pattern, Locale locale) { + return staticFormat(input, getProperties(pattern), getSymbols(locale)); + } + + public static String staticFormat(FormatQuantity input, String pattern, ULocale uLocale) { + return staticFormat(input, getProperties(pattern), getSymbols(uLocale)); + } + + public static String staticFormat( + FormatQuantity input, String pattern, DecimalFormatSymbols symbols) { + return staticFormat(input, getProperties(pattern), symbols); + } + + public static String staticFormat( + FormatQuantity input, Properties properties, DecimalFormatSymbols symbols) { + PluralRules rules = null; + ModifierHolder mods = Format.threadLocalModifierHolder.get().clear(); + NumberStringBuilder sb = Format.threadLocalStringBuilder.get().clear(); + int length = 0; + + // Pre-processing + if (!input.isNaN()) { + if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) { + MagnitudeMultiplier.getInstance(properties).before(input, mods, rules); + } + if (BigDecimalMultiplier.useMultiplier(properties)) { + BigDecimalMultiplier.getInstance(properties).before(input, mods, rules); + } + if (MeasureFormat.useMeasureFormat(properties)) { + rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties); + MeasureFormat.getInstance(symbols, properties).before(input, mods, rules); + } + if (CompactDecimalFormat.useCompactDecimalFormat(properties)) { + rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties); + CompactDecimalFormat.apply(input, mods, rules, symbols, properties); + } else if (CurrencyFormat.useCurrency(properties)) { + rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties); + CurrencyFormat.getCurrencyRounder(symbols, properties).before(input, mods, rules); + CurrencyFormat.getCurrencyModifier(symbols, properties).before(input, mods, rules); + } else if (ScientificFormat.useScientificNotation(properties)) { + // TODO: Is it possible to combine significant digits with currency? + PositiveNegativeAffixFormat.getInstance(symbols, properties).before(input, mods, rules); + ScientificFormat.getInstance(symbols, properties).before(input, mods, rules); + } else { + PositiveNegativeAffixFormat.apply(input, mods, symbols, properties); + RoundingFormat.getDefaultOrNoRounder(properties).before(input, mods, rules); + } + } + + // Primary format step + length += new PositiveDecimalFormat(symbols, properties).target(input, sb, 0); + length += mods.applyStrong(sb, 0, length); + + // Post-processing + if (PaddingFormat.usePadding(properties)) { + length += PaddingFormat.getInstance(properties).after(mods, sb, 0, length); + } + length += mods.applyAll(sb, 0, length); + + return sb.toString(); + } + + private static final ThreadLocal> threadLocalSymbolsCache = + new ThreadLocal>() { + @Override + protected Map initialValue() { + return new HashMap(); + } + }; + + 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> threadLocalPropertiesCache = + new ThreadLocal>() { + @Override + protected Map initialValue() { + return new HashMap(); + } + }; + + 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> threadLocalRulesCache = + new ThreadLocal>() { + @Override + protected Map initialValue() { + return new HashMap(); + } + }; + + private static PluralRules getPluralRules(ULocale uLocale, Properties properties) { + // Backwards compatibility: CurrencyPluralInfo wraps its own copy of PluralRules + if (properties.getCurrencyPluralInfo() != null) { + return properties.getCurrencyPluralInfo().getPluralRules(); + } + + if (uLocale == null) uLocale = ULocale.getDefault(); + PluralRules rules = threadLocalRulesCache.get().get(uLocale); + if (rules == null) { + rules = PluralRules.forLocale(uLocale); + threadLocalRulesCache.get().put(uLocale, rules); + } + return rules; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Exportable.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Exportable.java new file mode 100644 index 0000000000..515667d915 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Exportable.java @@ -0,0 +1,15 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +/** + * This is a small interface I made to assist with converting from a formatter pipeline object to a + * pattern string. It allows classes to "export" themselves to a property bag, which in turn can be + * passed to {@link PatternString#propertiesToString(Properties)} to generate the pattern string. + * + *

Depending on the new API we expose, this process might not be necessary if we persist the + * property bag in the current DecimalFormat shim. + */ +public interface Exportable { + public void export(Properties properties); +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Format.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Format.java new file mode 100644 index 0000000000..681b663fe7 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Format.java @@ -0,0 +1,277 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.text.AttributedCharacterIterator; +import java.text.FieldPosition; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; + +import com.ibm.icu.text.PluralRules; + +// TODO: Get a better name for this base class. +public abstract class Format { + + protected static final ThreadLocal threadLocalStringBuilder = + new ThreadLocal() { + @Override + protected NumberStringBuilder initialValue() { + return new NumberStringBuilder(); + } + }; + + protected static final ThreadLocal threadLocalModifierHolder = + new ThreadLocal() { + @Override + protected ModifierHolder initialValue() { + return new ModifierHolder(); + } + }; + + public String format(FormatQuantity... inputs) { + // Setup + Deque inputDeque = new ArrayDeque(); + 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 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, it could be Deque where + // we control the API of Quantity + public abstract int process( + Deque inputs, + ModifierHolder outputMods, + NumberStringBuilder outputString, + int startIndex); +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity.java new file mode 100644 index 0000000000..a5d376587e --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity.java @@ -0,0 +1,180 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.math.MathContext; + +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.text.PluralRules; + +/** + * An interface representing a number to be processed by the decimal formatting pipeline. Includes + * methods for rounding, plural rules, and decimal digit extraction. + * + *

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. + * + *

Implementations of this interface are free to use any internal storage mechanism. + * + *

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. + * + *

If rounding to a power of ten, use the more efficient {@link #roundToMagnitude} instead. + * + * @param roundingInterval The increment to which to round. + * @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior + * if null. + */ + public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext); + + /** + * Rounds the number to a specified magnitude (power of ten). + * + * @param roundingMagnitude The power of ten to which to round. For example, a value of -2 will + * round to 2 decimal places. + * @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior + * if null. + */ + public void roundToMagnitude(int roundingMagnitude, MathContext mathContext); + + /** + * Rounds the number to an infinite number of decimal points. This has no effect except for + * forcing the double in {@link FormatQuantityBCD} to adopt its exact representation. + */ + public void roundToInfinity(); + + /** + * Multiply the internal value. + * + * @param multiplicand The value by which to multiply. + */ + public void multiplyBy(BigDecimal multiplicand); + + /** + * Scales the number by a power of ten. For example, if the value is currently "1234.56", calling + * this method with delta=-3 will change the value to "1.23456". + * + * @param delta The number of magnitudes of ten to change by. + */ + public void adjustMagnitude(int delta); + + /** + * @return The power of ten corresponding to the most significant nonzero digit. + * @throws ArithmeticException If the value represented is zero. + */ + public int getMagnitude() throws ArithmeticException; + + /** @return Whether the value represented by this {@link FormatQuantity} is zero. */ + public boolean isZero(); + + /** @return Whether the value represented by this {@link FormatQuantity} is less than zero. */ + public boolean isNegative(); + + /** @return Whether the value represented by this {@link FormatQuantity} is infinite. */ + @Override + public boolean isInfinite(); + + /** @return Whether the value represented by this {@link FormatQuantity} is not a number. */ + @Override + public boolean isNaN(); + + /** @return The value contained in this {@link FormatQuantity} approximated as a double. */ + public double toDouble(); + + public BigDecimal toBigDecimal(); + + public int maxRepresentableDigits(); + + // TODO: Should this method be removed, since FormatQuantity implements IFixedDecimal now? + /** + * Computes the plural form for this number based on the specified set of rules. + * + * @param rules A {@link PluralRules} object representing the set of rules. + * @return The {@link StandardPlural} according to the PluralRules. If the plural form is not in + * the set of standard plurals, {@link StandardPlural#OTHER} is returned instead. + */ + public StandardPlural getStandardPlural(PluralRules rules); + + // /** + // * @return The number of fraction digits, always in the closed interval [minFrac, maxFrac]. + // * @see #setIntegerFractionLength(int, int, int, int) + // */ + // public int fractionCount(); + // + // /** + // * @return The number of integer digits, always in the closed interval [minInt, maxInt]. + // * @see #setIntegerFractionLength(int, int, int, int) + // */ + // public int integerCount(); + // + // /** + // * @param index The index of the fraction digit relative to the decimal place, or 1 minus the + // * digit's power of ten. + // * @return The digit at the specified index. Undefined if index is greater than maxInt or less + // * than 0. + // * @see #fractionCount() + // */ + // public byte getFractionDigit(int index); + // + // /** + // * @param index The index of the integer digit relative to the decimal place, or the digit's power + // * of ten. + // * @return The digit at the specified index. Undefined if index is greater than maxInt or less + // * than 0. + // * @see #integerCount() + // */ + // public byte getIntegerDigit(int index); + + /** + * Gets the digit at the specified magnitude. For example, if the represented number is 12.3, + * getDigit(-1) returns 3, since 3 is the digit corresponding to 10^-1. + * + * @param magnitude The magnitude of the digit. + * @return The digit at the specified magnitude. + */ + public byte getDigit(int magnitude); + + /** + * Gets the largest power of ten that needs to be displayed. The value returned by this function + * will be bounded between minInt and maxInt. + * + * @return The highest-magnitude digit to be displayed. + */ + public int getUpperDisplayMagnitude(); + + /** + * Gets the smallest power of ten that needs to be displayed. The value returned by this function + * will be bounded between -minFrac and -maxFrac. + * + * @return The lowest-magnitude digit to be displayed. + */ + public int getLowerDisplayMagnitude(); + + public FormatQuantity clone(); + + public void copyFrom(FormatQuantity other); + + /** + * This method is for internal testing only and should be removed before release. + * + * @internal + */ + public long getPositionFingerprint(); +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java new file mode 100644 index 0000000000..988c71fdfa --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java @@ -0,0 +1,856 @@ +// © 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). + * + *

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. + */ + @SuppressWarnings("unused") + 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. + * + *

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; + } + + @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); + } + + 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; + } + } + } + } + + 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; + } + + 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(); + } + } + + 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(" 1000 ? "max" : lOptPos); + sb.append(":"); + sb.append(lReqPos); + sb.append(":"); + sb.append(rReqPos); + sb.append(":"); + sb.append(rOptPos < -1000 ? "min" : rOptPos); + sb.append(" "); + sb.append(fallback.toString()); + } else { + String digits = Long.toString(primary); + int iDec = digits.length() + primaryScale; + int iLP = iDec - toRange(lOptPos, -1000, 1000); + int iLB = iDec - toRange(lReqPos, -1000, 1000); + int iRB = iDec - toRange(rReqPos, -1000, 1000); + int iRP = iDec - toRange(rOptPos, -1000, 1000); + iDec = Math.max(Math.min(iDec, digits.length() + 1), -1); + iLP = Math.max(Math.min(iLP, digits.length() + 1), -1); + iLB = Math.max(Math.min(iLB, digits.length() + 1), -1); + iRB = Math.max(Math.min(iRB, digits.length() + 1), -1); + iRP = Math.max(Math.min(iRP, digits.length() + 1), -1); + + for (int i = -1; i <= digits.length() + 1; i++) { + if (i == iLP) sb.append('('); + if (i == iLB) sb.append('['); + if (i == iDec) sb.append('.'); + if (i == iRB) sb.append(']'); + if (i == iRP) sb.append(')'); + if (i >= 0 && i < digits.length()) sb.append(digits.charAt(i)); + else sb.append('\u00A0'); + } + } + sb.append(">"); + return sb.toString(); + } + + private static int toRange(int i, int lo, int hi) { + if (i < lo) { + return lo; + } else if (i > hi) { + return hi; + } else { + return i; + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity2.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity2.java new file mode 100644 index 0000000000..30931692d7 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity2.java @@ -0,0 +1,173 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public final class FormatQuantity2 extends FormatQuantityBCD { + + /** + * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map + * to one digit. For example, the number "12345" in BCD is "0x12345". + * + *

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( + "", + (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)), + lReqPos, + rReqPos, + (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)), + bcd, + scale); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity3.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity3.java new file mode 100644 index 0000000000..9e25a45a74 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity3.java @@ -0,0 +1,222 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public final class FormatQuantity3 extends FormatQuantityBCD { + + /** + * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map + * to one digit. For example, the number "12345" in BCD is "0x12345". + * + *

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( + "", + (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)), + lReqPos, + rReqPos, + (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)), + sb, + "E", + scale); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity4.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity4.java new file mode 100644 index 0000000000..335b78fce1 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity4.java @@ -0,0 +1,407 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public final class FormatQuantity4 extends FormatQuantityBCD { + + /** + * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map + * to one digit. For example, the number "12345" in BCD is "0x12345". + * + *

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( + "", + (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)), + lReqPos, + rReqPos, + (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)), + (usingBytes ? "bytes" : "long"), + sb, + "E", + scale); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantityBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantityBCD.java new file mode 100644 index 0000000000..6d5831990e --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantityBCD.java @@ -0,0 +1,900 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.text.FieldPosition; + +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.text.PluralRules; +import com.ibm.icu.text.PluralRules.Operand; +import com.ibm.icu.text.UFieldPosition; + +/** + * Represents numbers and digit display properties using Binary Coded Decimal (BCD). + * + * @implements {@link FormatQuantity} + */ +public abstract class FormatQuantityBCD implements FormatQuantity { + + /** + * The power of ten corresponding to the least significant digit in the BCD. For example, if this + * object represents the number "3.14", the BCD will be "0x314" and the scale will be -2. + * + *

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. + * + *

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. + * + *

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. + * + *

This method must always be called when bcd changes in order for assumptions to be correct in + * methods like {@link #fractionCount()}. + */ + protected abstract void compact(); +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantitySelector.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantitySelector.java new file mode 100644 index 0000000000..95a11ba804 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantitySelector.java @@ -0,0 +1,52 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** @author sffc */ +public class FormatQuantitySelector { + public static FormatQuantityBCD from(int input) { + return new FormatQuantity4(input); + } + + public static FormatQuantityBCD from(long input) { + return new FormatQuantity4(input); + } + + public static FormatQuantityBCD from(double input) { + return new FormatQuantity4(input); + } + + public static FormatQuantityBCD from(BigInteger input) { + return new FormatQuantity4(input); + } + + public static FormatQuantityBCD from(BigDecimal input) { + return new FormatQuantity4(input); + } + + public static FormatQuantityBCD from(com.ibm.icu.math.BigDecimal input) { + return from(input.toBigDecimal()); + } + + public static FormatQuantityBCD from(Number number) { + if (number instanceof Long) { + return from(number.longValue()); + } else if (number instanceof Integer) { + return from(number.intValue()); + } else if (number instanceof Double) { + return from(number.doubleValue()); + } else if (number instanceof BigInteger) { + return from((BigInteger) number); + } else if (number instanceof BigDecimal) { + return from((BigDecimal) number); + } else if (number instanceof com.ibm.icu.math.BigDecimal) { + return from((com.ibm.icu.math.BigDecimal) number); + } else { + throw new IllegalArgumentException( + "Number is of an unsupported type: " + number.getClass().getName()); + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java new file mode 100644 index 0000000000..00739fd574 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java @@ -0,0 +1,128 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier; +import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier; +import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier; +import com.ibm.icu.impl.number.modifiers.SimpleModifier; + +/** + * A Modifier is an immutable object that can be passed through the formatting pipeline until it is + * finally applied to the string builder. A Modifier usually contains a prefix and a suffix that are + * applied, but it could contain something else, like a {@link com.ibm.icu.text.SimpleFormatter} + * pattern. + * + * @see PositiveNegativeAffixModifier + * @see ConstantAffixModifier + * @see GeneralPluralModifier + * @see SimpleModifier + */ +public interface Modifier { + + /** + * Apply this Modifier to the string builder. + * + * @param output The string builder to which to apply this modifier. + * @param leftIndex The left index of the string within the builder. Equal to 0 when only one + * number is being formatted. + * @param rightIndex The right index of the string within the string builder. Equal to length-1 + * when only one number is being formatted. + * @return The number of characters (UTF-16 code units) that were added to the string builder. + */ + public int apply(NumberStringBuilder output, int leftIndex, int rightIndex); + + /** + * The number of characters that {@link #apply} would add to the string builder. + * + * @return The number of characters (UTF-16 code units) that would be added to a string builder. + */ + public int length(); + + /** + * Whether this modifier is strong. If a modifier is strong, it should always be applied + * immediately and not allowed to bubble up. With regard to padding, strong modifiers are + * considered to be on the inside of the prefix and suffix. + * + * @return Whether the modifier is strong. + */ + public boolean isStrong(); + + /** + * Gets the prefix string associated with this modifier, defined as the string that will be + * inserted at leftIndex when {@link #apply} is called. + * + * @return The prefix string. Will not be null. + */ + public String getPrefix(); + + /** + * Gets the prefix string associated with this modifier, defined as the string that will be + * inserted at rightIndex when {@link #apply} is called. + * + * @return The suffix string. Will not be null. + */ + public String getSuffix(); + + /** + * An interface for a modifier that contains both a positive and a negative form. Note that a + * class implementing {@link PositiveNegativeModifier} is not necessarily a {@link Modifier} + * itself. Rather, it returns a {@link Modifier} when {@link #getModifier} is called. + */ + public static interface PositiveNegativeModifier extends Exportable { + /** + * Converts this {@link PositiveNegativeModifier} to a {@link Modifier} given the negative sign. + * + * @param isNegative true if the negative form of this modifier should be used; false if the + * positive form should be used. + * @return A Modifier corresponding to the negative sign. + */ + public Modifier getModifier(boolean isNegative); + } + + /** + * An interface for a modifier that contains both a positive and a negative form for all six + * standard plurals. Note that a class implementing {@link PositiveNegativePluralModifier} is not + * necessarily a {@link Modifier} itself. Rather, it returns a {@link Modifier} when {@link + * #getModifier} is called. + */ + public static interface PositiveNegativePluralModifier extends Exportable { + /** + * Converts this {@link PositiveNegativePluralModifier} to a {@link Modifier} given the negative + * sign and the standard plural. + * + * @param plural The StandardPlural to use. + * @param isNegative true if the negative form of this modifier should be used; false if the + * positive form should be used. + * @return A Modifier corresponding to the negative sign. + */ + public Modifier getModifier(StandardPlural plural, boolean isNegative); + } + + /** + * An interface for a modifier that is represented internally by a prefix string and a suffix + * string. + */ + public static interface AffixModifier extends Modifier {} + + /** + * A starter implementation with defaults for some of the basic methods. + * + *

Implements {@link PositiveNegativeModifier} only so that instances of this class can be used when + * a {@link PositiveNegativeModifier} is required. + */ + public abstract static class BaseModifier extends Format.BeforeFormat + implements Modifier, PositiveNegativeModifier { + + @Override + public void before(FormatQuantity input, ModifierHolder mods) { + mods.add(this); + } + + @Override + public Modifier getModifier(boolean isNegative) { + return this; + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierHolder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierHolder.java new file mode 100644 index 0000000000..483d6fd679 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierHolder.java @@ -0,0 +1,106 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.util.ArrayDeque; + +public class ModifierHolder { + private ArrayDeque mods = new ArrayDeque(); + + // Using five separate fields instead of the ArrayDeque saves about 10ns at the expense of + // worse code. + // TODO: Decide which implementation to use. + + // private Modifier mod1 = null; + // private Modifier mod2 = null; + // private Modifier mod3 = null; + // private Modifier mod4 = null; + // private Modifier mod5 = null; + + public ModifierHolder clear() { + // mod1 = null; + // mod2 = null; + // mod3 = null; + // mod4 = null; + // mod5 = null; + mods.clear(); + return this; + } + + public void add(Modifier modifier) { + // if (mod1 == null) { + // mod1 = modifier; + // } else if (mod2 == null) { + // mod2 = modifier; + // } else if (mod3 == null) { + // mod3 = modifier; + // } else if (mod4 == null) { + // mod4 = modifier; + // } else if (mod5 == null) { + // mod5 = modifier; + // } else { + // throw new IndexOutOfBoundsException(); + // } + if (modifier != null) mods.addFirst(modifier); + } + + public Modifier peekLast() { + return mods.peekLast(); + } + + public Modifier removeLast() { + return mods.removeLast(); + } + + public int applyAll(NumberStringBuilder string, int leftIndex, int rightIndex) { + int addedLength = 0; + // if (mod5 != null) { + // addedLength += mod5.apply(string, leftIndex, rightIndex + addedLength); + // mod5 = null; + // } + // if (mod4 != null) { + // addedLength += mod4.apply(string, leftIndex, rightIndex + addedLength); + // mod4 = null; + // } + // if (mod3 != null) { + // addedLength += mod3.apply(string, leftIndex, rightIndex + addedLength); + // mod3 = null; + // } + // if (mod2 != null) { + // addedLength += mod2.apply(string, leftIndex, rightIndex + addedLength); + // mod2 = null; + // } + // if (mod1 != null) { + // addedLength += mod1.apply(string, leftIndex, rightIndex + addedLength); + // mod1 = null; + // } + while (!mods.isEmpty()) { + Modifier mod = mods.removeFirst(); + addedLength += mod.apply(string, leftIndex, rightIndex + addedLength); + } + return addedLength; + } + + public int applyStrong(NumberStringBuilder string, int leftIndex, int rightIndex) { + int addedLength = 0; + while (!mods.isEmpty() && mods.peekFirst().isStrong()) { + Modifier mod = mods.removeFirst(); + addedLength += mod.apply(string, leftIndex, rightIndex + addedLength); + } + return addedLength; + } + + public int totalLength() { + int length = 0; + // if (mod1 != null) length += mod1.length(); + // if (mod2 != null) length += mod2.length(); + // if (mod3 != null) length += mod3.length(); + // if (mod4 != null) length += mod4.length(); + // if (mod5 != null) length += mod5.length(); + for (Modifier mod : mods) { + if (mod == null) continue; + length += mod.length(); + } + return length; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java new file mode 100644 index 0000000000..4008307161 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java @@ -0,0 +1,411 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.FieldPosition; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.text.NumberFormat.Field; + +public class NumberStringBuilder implements CharSequence { + private char[] chars; + private Field[] fields; + private int zero; + private int length; + + public NumberStringBuilder() { + this(40); + } + + public NumberStringBuilder(int capacity) { + chars = new char[capacity]; + fields = new Field[capacity]; + zero = capacity / 2; + length = 0; + } + + @Override + public int length() { + return length; + } + + @Override + public char charAt(int index) { + if (index < 0 || index > length) { + throw new IndexOutOfBoundsException(); + } + return chars[zero + index]; + } + + /** + * Appends the specified codePoint to the end of the string. + * + * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise. + */ + public int appendCodePoint(int codePoint, Field field) { + return insertCodePoint(length, codePoint, field); + } + + /** + * Inserts the specified codePoint at the specified index in the string. + * + * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise. + */ + public int insertCodePoint(int index, int codePoint, Field field) { + int count = Character.charCount(codePoint); + int position = prepareForInsert(index, count); + Character.toChars(codePoint, chars, position); + fields[position] = field; + if (count == 2) fields[position + 1] = field; + return count; + } + + /** + * Appends the specified CharSequence to the end of the string. + * + * @return The number of chars added, which is the length of CharSequence. + */ + public int append(CharSequence sequence, Field field) { + return insert(length, sequence, field); + } + + /** + * Inserts the specified CharSequence at the specified index in the string. + * + * @return The number of chars added, which is the length of CharSequence. + */ + public int insert(int index, CharSequence sequence, Field field) { + if (sequence.length() == 0) { + // Nothing to insert. + return 0; + } else if (sequence.length() == 1) { + // Fast path: on a single-char string, using insertCodePoint below is 70% faster than the + // CharSequence method: 12.2 ns versus 41.9 ns for five operations on my Linux x86-64. + return insertCodePoint(index, sequence.charAt(0), field); + } else { + return insert(index, sequence, 0, sequence.length(), field); + } + } + + /** + * Inserts the specified CharSequence at the specified index in the string, reading from the + * CharSequence from start (inclusive) to end (exclusive). + * + * @return The number of chars added, which is the length of CharSequence. + */ + public int insert(int index, CharSequence sequence, int start, int end, Field field) { + int count = end - start; + int position = prepareForInsert(index, count); + for (int i = 0; i < count; i++) { + chars[position + i] = sequence.charAt(start + i); + fields[position + i] = field; + } + return count; + } + + /** + * Appends the chars in the specified char array to the end of the string, and associates them + * with the fields in the specified field array, which must have the same length as chars. + * + * @return The number of chars added, which is the length of the char array. + */ + public int append(char[] chars, Field[] fields) { + return insert(length, chars, fields); + } + + /** + * Inserts the chars in the specified char array at the specified index in the string, and + * associates them with the fields in the specified field array, which must have the same length + * as chars. + * + * @return The number of chars added, which is the length of the char array. + */ + public int insert(int index, char[] chars, Field[] fields) { + assert fields == null || chars.length == fields.length; + int count = chars.length; + if (count == 0) return 0; // nothing to insert + int position = prepareForInsert(index, count); + for (int i = 0; i < count; i++) { + this.chars[position + i] = chars[i]; + this.fields[position + i] = fields == null ? null : fields[i]; + } + return count; + } + + /** + * Appends the contents of another {@link NumberStringBuilder} to the end of this instance. + * + * @return The number of chars added, which is the length of the other {@link + * NumberStringBuilder}. + */ + public int append(NumberStringBuilder other) { + return insert(length, other); + } + + /** + * Inserts the contents of another {@link NumberStringBuilder} into this instance at the given + * index. + * + * @return The number of chars added, which is the length of the other {@link + * NumberStringBuilder}. + */ + public int insert(int index, NumberStringBuilder other) { + assert this != other; + int count = other.length; + if (count == 0) return 0; // nothing to insert + int position = prepareForInsert(index, count); + for (int i = 0; i < count; i++) { + this.chars[position + i] = other.chars[other.zero + i]; + this.fields[position + i] = other.fields[other.zero + i]; + } + return count; + } + + /** + * Shifts around existing data if necessary to make room for new characters. + * + * @param index The location in the string where the operation is to take place. + * @param count The number of chars (UTF-16 code units) to be inserted at that location. + * @return The position in the char array to insert the chars. + */ + private int prepareForInsert(int index, int count) { + if (index == 0 && zero - count >= 0) { + // Append to start + zero -= count; + length += count; + return zero; + } else if (index == length && zero + length + count < chars.length) { + // Append to end + length += count; + return zero + length - count; + } else { + // Move chars around and/or allocate more space + return prepareForInsertHelper(index, count); + } + } + + private int prepareForInsertHelper(int index, int count) { + // Keeping this code out of prepareForInsert() increases the speed of append operations. + if (length + count > chars.length) { + char[] newChars = new char[(length + count) * 2]; + Field[] newFields = new Field[(length + count) * 2]; + int newZero = newChars.length / 2 - (length + count) / 2; + System.arraycopy(chars, zero, newChars, newZero, index); + System.arraycopy(chars, zero + index, newChars, newZero + index + count, length - index); + System.arraycopy(fields, zero, newFields, newZero, index); + System.arraycopy(fields, zero + index, newFields, newZero + index + count, length - index); + chars = newChars; + fields = newFields; + zero = newZero; + length += count; + } else { + int newZero = chars.length / 2 - (length + count) / 2; + System.arraycopy(chars, zero, chars, newZero, length); + System.arraycopy(chars, newZero + index, chars, newZero + index + count, length - index); + System.arraycopy(fields, zero, fields, newZero, length); + System.arraycopy(fields, newZero + index, fields, newZero + index + count, length - index); + zero = newZero; + length += count; + } + return zero + index; + } + + @Override + public CharSequence subSequence(int start, int end) { + if (start < 0 || end > length || end < start) { + throw new IndexOutOfBoundsException(); + } + NumberStringBuilder other = this.clone(); + other.zero = zero + start; + other.length = end - start; + return other; + } + + /** + * Returns the string represented by the characters in this string builder. + * + *

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 fieldToDebugChar = new HashMap(); + + 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. + * + *

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(""); + return sb.toString(); + } + + /** @return A new array containing the contents of this string builder. */ + public char[] toCharArray() { + return Arrays.copyOfRange(chars, zero, zero + length); + } + + /** @return A new array containing the field values of this string builder. */ + public Field[] toFieldArray() { + return Arrays.copyOfRange(fields, zero, zero + length); + } + + /** + * @return Whether the contents and field values of this string builder are equal to the given + * chars and fields. + * @see #toCharArray + * @see #toFieldArray + */ + public boolean contentEquals(char[] chars, Field[] fields) { + if (chars.length != length) return false; + if (fields.length != length) return false; + for (int i = 0; i < length; i++) { + if (this.chars[zero + i] != chars[i]) return false; + if (this.fields[zero + i] != fields[i]) return false; + } + return true; + } + + /** + * @param other The instance to compare. + * @return Whether the contents of this instance is currently equal to the given instance. + */ + public boolean contentEquals(NumberStringBuilder other) { + if (length != other.length) return false; + for (int i = 0; i < length; i++) { + if (chars[zero + i] != other.chars[other.zero + i]) return false; + if (fields[zero + i] != other.fields[other.zero + i]) return false; + } + return true; + } + + /** + * Populates the given {@link FieldPosition} based on this string builder. + * + * @param fp The FieldPosition to populate. + * @param offset An offset to add to the field position index; can be zero. + */ + public void populateFieldPosition(FieldPosition fp, int offset) { + java.text.Format.Field rawField = fp.getFieldAttribute(); + + if (rawField == null) { + // Backwards compatibility: read from fp.getField() + if (fp.getField() == NumberFormat.INTEGER_FIELD) { + rawField = NumberFormat.Field.INTEGER; + } else if (fp.getField() == NumberFormat.FRACTION_FIELD) { + rawField = NumberFormat.Field.FRACTION; + } else { + // No field is set + return; + } + } + + if (!(rawField instanceof com.ibm.icu.text.NumberFormat.Field)) { + throw new IllegalArgumentException( + "You must pass an instance of com.ibm.icu.text.NumberFormat.Field as your FieldPosition attribute. You passed: " + + rawField.getClass().toString()); + } + /* com.ibm.icu.text.NumberFormat. */ Field field = (Field) rawField; + + boolean seenStart = false; + int fractionStart = -1; + for (int i = zero; i <= zero + length; i++) { + Field _field = (i < zero + length) ? fields[i] : null; + if (seenStart && field != _field) { + // Special case: GROUPING_SEPARATOR counts as an INTEGER. + if (field == NumberFormat.Field.INTEGER && _field == NumberFormat.Field.GROUPING_SEPARATOR) + continue; + fp.setEndIndex(i - zero + offset); + break; + } else if (!seenStart && field == _field) { + fp.setBeginIndex(i - zero + offset); + seenStart = true; + } + if (_field == NumberFormat.Field.INTEGER || _field == NumberFormat.Field.DECIMAL_SEPARATOR) { + fractionStart = i - zero + 1; + } + } + + // Backwards compatibility: FRACTION needs to start after INTEGER if empty + if (field == NumberFormat.Field.FRACTION && !seenStart) { + fp.setBeginIndex(fractionStart); + fp.setEndIndex(fractionStart); + } + } + + public AttributedCharacterIterator getIterator() { + AttributedString as = new AttributedString(toString()); + Field current = null; + int currentStart = -1; + for (int i = 0; i < length; i++) { + Field field = fields[i + zero]; + if (current == NumberFormat.Field.INTEGER && field == NumberFormat.Field.GROUPING_SEPARATOR) { + // Special case: GROUPING_SEPARATOR counts as an INTEGER. + as.addAttribute( + NumberFormat.Field.GROUPING_SEPARATOR, NumberFormat.Field.GROUPING_SEPARATOR, i, i + 1); + } else if (current != field) { + if (current != null) { + as.addAttribute(current, current, currentStart, i); + } + current = field; + currentStart = i; + } + } + if (current != null) { + as.addAttribute(current, current, currentStart, length); + } + return as.getIterator(); + } + + @Override + public NumberStringBuilder clone() { + NumberStringBuilder other = new NumberStringBuilder(chars.length); + other.zero = zero; + other.length = length; + System.arraycopy(chars, zero, other.chars, zero, length); + System.arraycopy(fields, zero, other.fields, zero, length); + return other; + } + + public NumberStringBuilder clear() { + zero = chars.length / 2; + length = 0; + return this; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PNAffixGenerator.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PNAffixGenerator.java new file mode 100644 index 0000000000..bb9af1c7d1 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PNAffixGenerator.java @@ -0,0 +1,296 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import com.ibm.icu.impl.number.Modifier.AffixModifier; +import com.ibm.icu.impl.number.formatters.CompactDecimalFormat; +import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat; +import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat.IProperties; +import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier; +import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.NumberFormat.Field; + +/** + * A class to convert from a bag of prefix/suffix properties into a positive and negative {@link + * Modifier}. This is a standard implementation used by {@link PositiveNegativeAffixFormat}, {@link + * CompactDecimalFormat}, {@link Parse}, and others. + * + *

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. + * + *

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. + * + *

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 threadLocalInstance = + new ThreadLocal() { + @Override + protected PNAffixGenerator initialValue() { + return new PNAffixGenerator(); + } + }; + + public static PNAffixGenerator getThreadLocalInstance() { + return threadLocalInstance.get(); + } + + // These instances are used internally and cached to avoid object creation. The resultInstance + // also serves as a 1-element cache to avoid creating objects when subsequent calls have + // identical prefixes and suffixes. This happens, for example, when consuming CDF data. + private Result resultInstance = new Result(); + private NumberStringBuilder sb1 = new NumberStringBuilder(); + private NumberStringBuilder sb2 = new NumberStringBuilder(); + private NumberStringBuilder sb3 = new NumberStringBuilder(); + private NumberStringBuilder sb4 = new NumberStringBuilder(); + + /** + * Generates modifiers using default currency symbols. + * + * @param symbols The symbols to interpolate for minus, plus, percent, permille, and currency. + * @param properties The bag of properties to convert. + * @return The positive and negative {@link Modifier}. + */ + public Result getModifiers( + DecimalFormatSymbols symbols, PositiveNegativeAffixFormat.IProperties properties) { + // If this method is used, the user doesn't care about currencies. Default the currency symbols + // to the information we can get from the DecimalFormatSymbols instance. + return getModifiers( + symbols, + symbols.getCurrencySymbol(), + symbols.getInternationalCurrencySymbol(), + symbols.getCurrencySymbol(), + properties); + } + + /** + * Generates modifiers using the specified currency symbol for all three lengths of currency + * placeholders in the pattern string. + * + * @param symbols The symbols to interpolate for minus, plus, percent, and permille. + * @param currencySymbol The currency symbol. + * @param properties The bag of properties to convert. + * @return The positive and negative {@link Modifier}. + */ + public Result getModifiers( + DecimalFormatSymbols symbols, + String currencySymbol, + PositiveNegativeAffixFormat.IProperties properties) { + // If this method is used, the user doesn't cares about currencies but doesn't care about + // supporting all three sizes of currency placeholders. Use the one provided string for all + // three sizes of placeholders. + return getModifiers(symbols, currencySymbol, currencySymbol, currencySymbol, properties); + } + + /** + * Generates modifiers using the three specified strings to replace the three lengths of currency + * placeholders: "¤", "¤¤", and "¤¤¤". + * + * @param symbols The symbols to interpolate for minus, plus, percent, and permille. + * @param curr1 The string to replace "¤". + * @param curr2 The string to replace "¤¤". + * @param curr3 The string to replace "¤¤¤". + * @param properties The bag of properties to convert. + * @return The positive and negative {@link Modifier}. + */ + public Result getModifiers( + DecimalFormatSymbols symbols, + String curr1, + String curr2, + String curr3, + PositiveNegativeAffixFormat.IProperties properties) { + + // Use a different code path for handling affixes with "always show plus sign" + if (properties.getPlusSignAlwaysShown()) { + return getModifiersWithPlusSign(symbols, curr1, curr2, curr3, properties); + } + + CharSequence ppp = properties.getPositivePrefixPattern(); + CharSequence psp = properties.getPositiveSuffixPattern(); + CharSequence npp = properties.getNegativePrefixPattern(); + CharSequence nsp = properties.getNegativeSuffixPattern(); + + // Set sb1/sb2 to the positive prefix/suffix. + sb1.clear(); + sb2.clear(); + AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1); + AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2); + setPositiveResult(sb1, sb2, properties); + + // Set sb1/sb2 to the negative prefix/suffix. + if (npp == null && nsp == null) { + // Negative prefix defaults to positive prefix prepended with the minus sign. + // Negative suffix defaults to positive suffix. + sb1.insert(0, symbols.getMinusSignString(), Field.SIGN); + } else { + sb1.clear(); + sb2.clear(); + AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1); + AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2); + } + setNegativeResult(sb1, sb2, properties); + + return resultInstance; + } + + private Result getModifiersWithPlusSign( + DecimalFormatSymbols symbols, + String curr1, + String curr2, + String curr3, + IProperties properties) { + + CharSequence ppp = properties.getPositivePrefixPattern(); + CharSequence psp = properties.getPositiveSuffixPattern(); + CharSequence npp = properties.getNegativePrefixPattern(); + CharSequence nsp = properties.getNegativeSuffixPattern(); + + // There are three cases, listed below with their expected outcomes. + // TODO: Should we handle the cases when the positive subpattern has a '+' already? + // + // 1) No negative subpattern. + // Positive => Positive subpattern prepended with '+' + // Negative => Positive subpattern prepended with '-' + // 2) Negative subpattern does not have '-'. + // Positive => Positive subpattern prepended with '+' + // Negative => Negative subpattern + // 3) Negative subpattern has '-'. + // Positive => Negative subpattern with '+' substituted for '-' + // Negative => Negative subpattern + + if (npp != null || nsp != null) { + // Case 2 or Case 3 + sb1.clear(); + sb2.clear(); + sb3.clear(); + sb4.clear(); + AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1); + AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2); + AffixPatternUtils.unescape( + npp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb3); + AffixPatternUtils.unescape( + nsp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb4); + if (!charSequenceEquals(sb1, sb3) || !charSequenceEquals(sb2, sb4)) { + // Case 3. The plus sign substitution was successful. + setPositiveResult(sb3, sb4, properties); + setNegativeResult(sb1, sb2, properties); + return resultInstance; + } else { + // Case 2. There was no minus sign. Set the negative result and fall through. + setNegativeResult(sb1, sb2, properties); + } + } + + // Case 1 or 2. Set sb1/sb2 to the positive prefix/suffix. + sb1.clear(); + sb2.clear(); + AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1); + AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2); + + if (npp == null && nsp == null) { + // Case 1. Compute the negative result from the positive subpattern. + sb3.clear(); + sb3.append(symbols.getMinusSignString(), Field.SIGN); + sb3.append(sb1); + setNegativeResult(sb3, sb2, properties); + } + + // Case 1 or 2. Prepend a '+' sign to the positive prefix. + sb1.insert(0, symbols.getPlusSignString(), Field.SIGN); + setPositiveResult(sb1, sb2, properties); + + return resultInstance; + } + + private void setPositiveResult( + NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) { + if (properties.getPositivePrefix() != null || properties.getPositiveSuffix() != null) { + // Override with custom affixes + String _prefix = properties.getPositivePrefix(); + String _suffix = properties.getPositiveSuffix(); + if (_prefix == null) _prefix = ""; + if (_suffix == null) _suffix = ""; + if (_prefix.length() == 0 && _suffix.length() == 0) { + resultInstance.positive = ConstantAffixModifier.EMPTY; + return; + } + if (resultInstance.positive != null + && (resultInstance.positive instanceof ConstantAffixModifier) + && ((ConstantAffixModifier) resultInstance.positive).contentEquals(_prefix, _suffix)) { + // Use the cached modifier + return; + } + resultInstance.positive = + new ConstantAffixModifier(_prefix, _suffix, null, false); + } else { + // Use pattern affixes + if (prefix.length() == 0 && suffix.length() == 0) { + resultInstance.positive = ConstantAffixModifier.EMPTY; + return; + } + if (resultInstance.positive != null + && (resultInstance.positive instanceof ConstantMultiFieldModifier) + && ((ConstantMultiFieldModifier) resultInstance.positive).contentEquals(prefix, suffix)) { + // Use the cached modifier + return; + } + resultInstance.positive = new ConstantMultiFieldModifier(prefix, suffix, false); + } + } + + private void setNegativeResult( + NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) { + if (properties.getNegativePrefix() != null || properties.getNegativeSuffix() != null) { + // Override with custom affixes + String _prefix = properties.getNegativePrefix(); + String _suffix = properties.getNegativeSuffix(); + if (_prefix == null) _prefix = ""; + if (_suffix == null) _suffix = ""; + if (_prefix.length() == 0 && _suffix.length() == 0) { + resultInstance.negative = ConstantAffixModifier.EMPTY; + return; + } + if (resultInstance.negative != null + && (resultInstance.negative instanceof ConstantAffixModifier) + && ((ConstantAffixModifier) resultInstance.negative).contentEquals(_prefix, _suffix)) { + // Use the cached modifier + return; + } + resultInstance.negative = + new ConstantAffixModifier(_prefix, _suffix, null, false); + } else { + // Use pattern affixes + if (prefix.length() == 0 && suffix.length() == 0) { + resultInstance.negative = ConstantAffixModifier.EMPTY; + return; + } + if (resultInstance.negative != null + && (resultInstance.negative instanceof ConstantMultiFieldModifier) + && ((ConstantMultiFieldModifier) resultInstance.negative).contentEquals(prefix, suffix)) { + // Use the cached modifier + return; + } + resultInstance.negative = new ConstantMultiFieldModifier(prefix, suffix, false); + } + } + + /** A null-safe equals method for CharSequences. */ + private static boolean charSequenceEquals(CharSequence a, CharSequence b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.length() != b.length()) return false; + for (int i = 0; i < a.length(); i++) { + if (a.charAt(i) != b.charAt(i)) return false; + } + return true; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java new file mode 100644 index 0000000000..5b45b47863 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java @@ -0,0 +1,2110 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.TextTrieMap; +import com.ibm.icu.impl.number.Parse.ParseMode; +import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier; +import com.ibm.icu.impl.number.formatters.CurrencyFormat; +import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier; +import com.ibm.icu.impl.number.formatters.PaddingFormat; +import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat; +import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat; +import com.ibm.icu.lang.UCharacter; +import com.ibm.icu.text.CurrencyPluralInfo; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.text.UnicodeSet; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Currency.CurrencyStringInfo; +import com.ibm.icu.util.CurrencyAmount; +import com.ibm.icu.util.ULocale; + +/** + * A parser designed to convert an arbitrary human-generated string to its best representation as a + * number: a long, a BigInteger, or a BigDecimal. + * + *

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: + * + *

    + *
  • 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". + *
  • 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.) + *
  • 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. + *
  • Leading grouping separators are not allowed, as in ",123". + *
  • 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. + *
  • The set of characters that can be interpreted as a decimal or grouping separator is + * smaller. + *
  • If currency parsing is enabled, 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. + *
+ */ + STRICT, + + /** + * Fast mode should be used in applications that don't require prefixes and suffixes to match. + * + *

In addition to ignoring prefixes and suffixes, fast mode performs the following + * optimizations: + * + *

    + *
  • 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. + *
  • 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. + *
+ */ + 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 parse(), 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. + * + *

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.ParseState currentCurrencyTrieState; + // For multi-code-point digits: + TextTrieMap.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. + * + *

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(""); + 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 digitTrie; + Set affixHolders = new HashSet(); + + 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. + * + *

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 set = new HashSet(); + + private static final ConcurrentHashMap currencyAffixPatterns = + new ConcurrentHashMap(); + + 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 threadLocalProperties = + new ThreadLocal() { + @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 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 trieMap = new TextTrieMap(false); + for (int i = 0; i < 10; i++) { + trieMap.put(digitStrings[i], (byte) i); + } + return trieMap; + } + + protected static final ThreadLocal threadLocalParseState = + new ThreadLocal() { + @Override + protected ParserState initialValue() { + return new ParserState(); + } + }; + + protected static final ThreadLocal threadLocalParsePosition = + new ThreadLocal() { + @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. + * + *

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; + // TODO(sffc): 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; + // TODO(sffc): 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); + // TODO(sffc): 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); + // TODO(sffc): 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); + // TODO(sffc): 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); + // TODO(sffc): 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 cp is whitespace (as determined by the unicode set {@link #UNISET_WHITESPACE}), + * copies item to the new list in state and sets its state name to + * nextName. + * + * @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 cp is a bidi control character (as determined by the unicode set {@link + * #UNISET_BIDI}), copies item to the new list in state and sets its + * state name to nextName. + * + * @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 cp is a padding character (as determined by {@link ParserState#paddingCp}), + * copies item to the new list in state and sets its state name to + * nextName. + * + * @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 cp is a digit character (as determined by either {@link UCharacter#digit} or + * {@link ParserState#digitCps}), copies item to the new list in state + * and sets its state name to one determined by type. Also copies the digit into a + * field in the new item determined by type. + * + *

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 cp is a sign (as determined by the unicode sets {@link #UNISET_PLUS} and {@link + * #UNISET_MINUS}), copies item to the new list in state. 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 cp is a grouping separator (as determined by the unicode set {@link + * #UNISET_GROUPING}), copies item to the new list in state and loops + * back to the same state. Also accepts if cp 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 cp is a decimal (as determined by the unicode set {@link #UNISET_DECIMAL}), + * copies item to the new list in state and goes to {@link + * StateName#AFTER_FRACTION_DIGIT}. Also accepts if cp 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. + * + *

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 returnTo1 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 state. + * + *

If cp is equal to any known ISO code or long name, copies item to + * the new list in state and sets its ISO code to the corresponding currency. + * + *

If cp is the first code point of any ISO code or long name having more them one + * code point in length, copies item to the new list in state 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.ParseState trie1 = + Currency.openParseState(uloc, cp, Currency.LONG_NAME); + TextTrieMap.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 cp is the next code point of any currency, copies item to the new + * list in state along with an instance of {@link TextTrieMap.ParseState} for + * tracking the following code points. + * + *

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.ParseState trieState) { + if (trieState == null) return 0L; + trieState.accept(cp); + long added = 0L; + Iterator 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.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.ParseState trieState) { + if (trieState == null) return 0L; + trieState.accept(cp); + long added = 0L; + Iterator currentMatches = trieState.getCurrentMatches(); + if (currentMatches != null) { + // Match on current code point + byte digit = currentMatches.next(); + StateItem next = state.getNext().copyFrom(item, returnTo1, -1); + next.returnTo1 = null; + next.appendDigit(digit, type); + added |= 1L << state.lastInsertedIndex(); + } + if (!trieState.atEnd()) { + // Prepare for matches on future code points + StateItem next = state.getNext().copyFrom(item, StateName.INSIDE_DIGIT, -1); + next.returnTo1 = returnTo1; + next.currentDigitTrieState = trieState; + next.currentDigitType = type; + added |= 1L << state.lastInsertedIndex(); + } + return added; + } + + /** + * Checks whether the two given code points are equal after applying case mapping as requested in + * the ParserState. + * + * @see #acceptString + * @see #acceptAffixPattern + */ + private static boolean codePointEquals(int cp1, int cp2, ParserState state) { + if (!state.caseSensitive) { + cp1 = UCharacter.foldCase(cp1, true); + cp2 = UCharacter.foldCase(cp2, true); + } + return cp1 == cp2; + } + + /** + * Checks whether the given code point is "ignorable" and should be skipped. BiDi characters are + * always ignorable, and whitespace is ignorable in lenient mode. + * + * @param cp The code point to test. Returns false if cp is negative. + * @param state The current {@link ParserState}, used for determining strict mode. + * @return true if cp is bidi or whitespace in lenient mode; false otherwise. + */ + private static boolean isIgnorable(int cp, ParserState state) { + if (cp < 0) return false; + if (UNISET_BIDI.contains(cp)) return true; + return state.mode == ParseMode.LENIENT && UNISET_WHITESPACE.contains(cp); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java new file mode 100644 index 0000000000..ad5c2872a5 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java @@ -0,0 +1,855 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; + +import com.ibm.icu.impl.number.formatters.PaddingFormat; +import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition; +import com.ibm.icu.text.DecimalFormatSymbols; + +/** + * Handles parsing and creation of the compact pattern string representation of a decimal format. + */ +public class PatternString { + + /** + * Parses a pattern string into a new property bag. + * + * @param pattern The pattern string, like "#,##0.00" + * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding + * increment) when parsing the pattern. This may be desirable if a custom rounding mode, such + * as CurrencyUsage, is to be used instead. + * @return A property bag object. + * @throws IllegalArgumentException If there is a syntax error in the pattern string. + */ + public static Properties parseToProperties(String pattern, boolean ignoreRounding) { + Properties properties = new Properties(); + LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding); + return properties; + } + + public static Properties parseToProperties(String pattern) { + return parseToProperties(pattern, false); + } + + /** + * Parses a pattern string into an existing property bag. All properties that can be encoded into + * a pattern string will be overwritten with either their default value or with the value coming + * from the pattern string. Properties that cannot be encoded into a pattern string, such as + * rounding mode, are not modified. + * + * @param pattern The pattern string, like "#,##0.00" + * @param properties The property bag object to overwrite. + * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding + * increment) when parsing the pattern. This may be desirable if a custom rounding mode, such + * as CurrencyUsage, is to be used instead. + * @throws IllegalArgumentException If there was a syntax error in the pattern string. + */ + public static void parseToExistingProperties( + String pattern, Properties properties, boolean ignoreRounding) { + LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding); + } + + public static void parseToExistingProperties(String pattern, Properties properties) { + parseToExistingProperties(pattern, properties, false); + } + + /** + * Creates a pattern string from a property bag. + * + *

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 fr-FR, the period in the + * pattern "0.000" means "decimal" in standard notation (as it does in every other locale), but it + * means "grouping" in localized notation. + * + * @param input The pattern to convert. + * @param symbols The symbols corresponding to the localized pattern. + * @param toLocalized true to convert from standard to localized notation; false to convert from + * localized to standard notation. + * @return The pattern expressed in the other notation. + * @deprecated ICU 59 This method is provided for backwards compatibility and should not be used + * in any new code. + */ + @Deprecated + public static String convertLocalized( + CharSequence input, DecimalFormatSymbols symbols, boolean toLocalized) { + if (input == null) return null; + + /// This is not the prettiest function in the world, but it gets the job done. /// + + // Construct a table of code points to be converted between localized and standard. + int[][] table = new int[6][2]; + int standIdx = toLocalized ? 0 : 1; + int localIdx = toLocalized ? 1 : 0; + table[0][standIdx] = '%'; + table[0][localIdx] = symbols.getPercent(); + table[1][standIdx] = '‰'; + table[1][localIdx] = symbols.getPerMill(); + table[2][standIdx] = '.'; + table[2][localIdx] = symbols.getDecimalSeparator(); + table[3][standIdx] = ','; + table[3][localIdx] = symbols.getGroupingSeparator(); + table[4][standIdx] = '-'; + table[4][localIdx] = symbols.getMinusSign(); + table[5][standIdx] = '+'; + table[5][localIdx] = symbols.getPlusSign(); + + // Special case: localIdx characters are NOT allowed to be quotes, like in de_CH. + // Use '’' instead. + for (int i = 0; i < table.length; i++) { + if (table[i][localIdx] == '\'') { + table[i][localIdx] = '’'; + } + } + + // Iterate through the string and convert + int offset = 0; + int state = 0; + StringBuilder result = new StringBuilder(); + for (; offset < input.length(); ) { + int cp = Character.codePointAt(input, offset); + int cpToAppend = cp; + + if (state == 1 || state == 3 || state == 4) { + // Inside user-specified quote + if (cp == '\'') { + if (state == 1) { + state = 0; + } else if (state == 3) { + state = 2; + cpToAppend = -1; + } else { + state = 2; + } + } + } else { + // Base state or inside special character quote + if (cp == '\'') { + if (state == 2 && offset + 1 < input.length()) { + int nextCp = Character.codePointAt(input, offset + 1); + if (nextCp == '\'') { + // escaped quote + state = 4; + } else { + // begin user-specified quote sequence + // we are already in a quote sequence, so omit the opening quote + state = 3; + cpToAppend = -1; + } + } else { + state = 1; + } + } else { + boolean needsSpecialQuote = false; + for (int i = 0; i < table.length; i++) { + if (table[i][0] == cp) { + cpToAppend = table[i][1]; + needsSpecialQuote = false; // in case an earlier translation triggered it + break; + } else if (table[i][1] == cp) { + needsSpecialQuote = true; + } + } + if (state == 0 && needsSpecialQuote) { + state = 2; + result.appendCodePoint('\''); + } else if (state == 2 && !needsSpecialQuote) { + state = 0; + result.appendCodePoint('\''); + } + } + } + if (cpToAppend != -1) { + result.appendCodePoint(cpToAppend); + } + offset += Character.charCount(cp); + } + if (state == 2) { + result.appendCodePoint('\''); + } + return result.toString(); + } + + /** Implements a recursive descent parser for decimal format patterns. */ + static class LdmlDecimalPatternParser { + + /** + * An internal, intermediate data structure used for storing parse results before they are + * finalized into a DecimalFormatPattern.Builder. + */ + private static class PatternParseResult { + SubpatternParseResult positive = new SubpatternParseResult(); + SubpatternParseResult negative = null; + + /** Finalizes the temporary data stored in the PatternParseResult to the Builder. */ + void saveToProperties(Properties properties, boolean ignoreRounding) { + // Translate from PatternState to Properties. + // Note that most data from "negative" is ignored per the specification of DecimalFormat. + + // Grouping settings + if (positive.groupingSizes[1] != -1) { + properties.setGroupingSize(positive.groupingSizes[0]); + } else { + properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE); + } + if (positive.groupingSizes[2] != -1) { + properties.setSecondaryGroupingSize(positive.groupingSizes[1]); + } else { + properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE); + } + + // For backwards compatibility, require that the pattern emit at least one min digit. + int minInt, minFrac; + if (positive.totalIntegerDigits == 0 && positive.maximumFractionDigits > 0) { + // patterns like ".##" + minInt = 0; + minFrac = Math.max(1, positive.minimumFractionDigits); + } else if (positive.minimumIntegerDigits == 0 && positive.minimumFractionDigits == 0) { + // patterns like "#.##" + minInt = 1; + minFrac = 0; + } else { + minInt = positive.minimumIntegerDigits; + minFrac = positive.minimumFractionDigits; + } + + // Rounding settings + // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage + if (positive.minimumSignificantDigits > 0) { + properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS); + properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS); + properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT); + properties.setMinimumSignificantDigits(positive.minimumSignificantDigits); + properties.setMaximumSignificantDigits(positive.maximumSignificantDigits); + } else if (!positive.rounding.isZero()) { + if (!ignoreRounding) { + properties.setMinimumFractionDigits(minFrac); + properties.setMaximumFractionDigits(positive.maximumFractionDigits); + properties.setRoundingIncrement(positive.rounding.toBigDecimal()); + } else { + properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS); + properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS); + properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT); + } + properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS); + properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS); + } else { + if (!ignoreRounding) { + properties.setMinimumFractionDigits(minFrac); + properties.setMaximumFractionDigits(positive.maximumFractionDigits); + properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT); + } else { + properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS); + properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS); + properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT); + } + properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS); + properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS); + } + + // If the pattern ends with a '.' then force the decimal point. + if (positive.hasDecimal && positive.maximumFractionDigits == 0) { + properties.setDecimalSeparatorAlwaysShown(true); + } else { + properties.setDecimalSeparatorAlwaysShown(false); + } + + // Scientific notation settings + if (positive.exponentDigits > 0) { + properties.setExponentSignAlwaysShown(positive.exponentShowPlusSign); + properties.setMinimumExponentDigits(positive.exponentDigits); + if (positive.minimumSignificantDigits == 0) { + // patterns without '@' can define max integer digits, used for engineering notation + properties.setMinimumIntegerDigits(positive.minimumIntegerDigits); + properties.setMaximumIntegerDigits(positive.totalIntegerDigits); + } else { + // patterns with '@' cannot define max integer digits + properties.setMinimumIntegerDigits(1); + properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS); + } + } else { + properties.setExponentSignAlwaysShown(Properties.DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN); + properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS); + properties.setMinimumIntegerDigits(minInt); + properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS); + } + + // Padding settings + if (positive.padding.length() > 0) { + // The width of the positive prefix and suffix templates are included in the padding + int paddingWidth = + positive.paddingWidth + + AffixPatternUtils.unescapedLength(positive.prefix) + + AffixPatternUtils.unescapedLength(positive.suffix); + properties.setFormatWidth(paddingWidth); + if (positive.padding.length() == 1) { + properties.setPadString(positive.padding.toString()); + } else if (positive.padding.length() == 2) { + if (positive.padding.charAt(0) == '\'') { + properties.setPadString("'"); + } else { + properties.setPadString(positive.padding.toString()); + } + } else { + properties.setPadString( + positive.padding.subSequence(1, positive.padding.length() - 1).toString()); + } + assert positive.paddingLocation != null; + properties.setPadPosition(positive.paddingLocation); + } else { + properties.setFormatWidth(Properties.DEFAULT_FORMAT_WIDTH); + properties.setPadString(Properties.DEFAULT_PAD_STRING); + properties.setPadPosition(Properties.DEFAULT_PAD_POSITION); + } + + // Set the affixes + // Always call the setter, even if the prefixes are empty, especially in the case of the + // negative prefix pattern, to prevent default values from overriding the pattern. + properties.setPositivePrefixPattern(positive.prefix.toString()); + properties.setPositiveSuffixPattern(positive.suffix.toString()); + if (negative != null) { + properties.setNegativePrefixPattern(negative.prefix.toString()); + properties.setNegativeSuffixPattern(negative.suffix.toString()); + } else { + properties.setNegativePrefixPattern(null); + properties.setNegativeSuffixPattern(null); + } + + // Set the magnitude multiplier + if (positive.hasPercentSign) { + properties.setMagnitudeMultiplier(2); + } else if (positive.hasPerMilleSign) { + properties.setMagnitudeMultiplier(3); + } else { + properties.setMagnitudeMultiplier(Properties.DEFAULT_MAGNITUDE_MULTIPLIER); + } + } + } + + private static class SubpatternParseResult { + int[] groupingSizes = new int[] {0, -1, -1}; + int minimumIntegerDigits = 0; + int totalIntegerDigits = 0; + int minimumFractionDigits = 0; + int maximumFractionDigits = 0; + int minimumSignificantDigits = 0; + int maximumSignificantDigits = 0; + boolean hasDecimal = false; + int paddingWidth = 0; + PadPosition paddingLocation = null; + FormatQuantity4 rounding = new FormatQuantity4(); + boolean exponentShowPlusSign = false; + int exponentDigits = 0; + boolean hasPercentSign = false; + boolean hasPerMilleSign = false; + boolean hasCurrencySign = false; + + StringBuilder padding = new StringBuilder(); + StringBuilder prefix = new StringBuilder(); + StringBuilder suffix = new StringBuilder(); + } + + /** An internal class used for tracking the cursor during parsing of a pattern string. */ + private static class ParserState { + final String pattern; + int offset; + + ParserState(String pattern) { + this.pattern = pattern; + this.offset = 0; + } + + int peek() { + if (offset == pattern.length()) { + return -1; + } else { + return pattern.codePointAt(offset); + } + } + + int next() { + int codePoint = peek(); + offset += Character.charCount(codePoint); + return codePoint; + } + + IllegalArgumentException toParseException(String message) { + StringBuilder sb = new StringBuilder(); + sb.append("Unexpected character in decimal format pattern: '"); + sb.append(pattern); + sb.append("': "); + sb.append(message); + sb.append(": "); + if (peek() == -1) { + sb.append("EOL"); + } else { + sb.append("'"); + sb.append(Character.toChars(peek())); + sb.append("'"); + } + return new IllegalArgumentException(sb.toString()); + } + } + + static void parse(String pattern, Properties properties, boolean ignoreRounding) { + if (pattern == null || pattern.length() == 0) { + // Backwards compatibility requires that we reset to the default values. + // TODO: Only overwrite the properties that "saveToProperties" normally touches? + properties.clear(); + return; + } + + // TODO: Use whitespace characters from PatternProps + // TODO: Use thread locals here. + ParserState state = new ParserState(pattern); + PatternParseResult result = new PatternParseResult(); + consumePattern(state, result); + result.saveToProperties(properties, ignoreRounding); + } + + private static void consumePattern(ParserState state, PatternParseResult result) { + // pattern := subpattern (';' subpattern)? + consumeSubpattern(state, result.positive); + if (state.peek() == ';') { + state.next(); // consume the ';' + result.negative = new SubpatternParseResult(); + consumeSubpattern(state, result.negative); + } + if (state.peek() != -1) { + throw state.toParseException("pattern"); + } + } + + private static void consumeSubpattern(ParserState state, SubpatternParseResult result) { + // subpattern := literals? number exponent? literals? + consumePadding(state, result, PadPosition.BEFORE_PREFIX); + consumeAffix(state, result, result.prefix); + consumePadding(state, result, PadPosition.AFTER_PREFIX); + consumeFormat(state, result); + consumeExponent(state, result); + consumePadding(state, result, PadPosition.BEFORE_SUFFIX); + consumeAffix(state, result, result.suffix); + consumePadding(state, result, PadPosition.AFTER_SUFFIX); + } + + private static void consumePadding( + ParserState state, SubpatternParseResult result, PadPosition paddingLocation) { + if (state.peek() != '*') { + return; + } + result.paddingLocation = paddingLocation; + state.next(); // consume the '*' + consumeLiteral(state, result.padding); + } + + private static void consumeAffix( + ParserState state, SubpatternParseResult result, StringBuilder destination) { + // literals := { literal } + while (true) { + switch (state.peek()) { + case '#': + case '@': + case ';': + case '*': + case '.': + case ',': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case -1: + // Characters that cannot appear unquoted in a literal + return; + + case '%': + result.hasPercentSign = true; + break; + + case '‰': + result.hasPerMilleSign = true; + break; + + case '¤': + result.hasCurrencySign = true; + break; + } + consumeLiteral(state, destination); + } + } + + private static void consumeLiteral(ParserState state, StringBuilder destination) { + if (state.peek() == -1) { + throw state.toParseException("expected unquoted literal but found end of string"); + } else if (state.peek() == '\'') { + destination.appendCodePoint(state.next()); // consume the starting quote + while (state.peek() != '\'') { + if (state.peek() == -1) { + throw state.toParseException("expected quoted literal but found end of string"); + } else { + destination.appendCodePoint(state.next()); // consume a quoted character + } + } + destination.appendCodePoint(state.next()); // consume the ending quote + } else { + // consume a non-quoted literal character + destination.appendCodePoint(state.next()); + } + } + + private static void consumeFormat(ParserState state, SubpatternParseResult result) { + consumeIntegerFormat(state, result); + if (state.peek() == '.') { + state.next(); // consume the decimal point + result.hasDecimal = true; + result.paddingWidth += 1; + consumeFractionFormat(state, result); + } + } + + private static void consumeIntegerFormat(ParserState state, SubpatternParseResult result) { + boolean seenSignificantDigitMarker = false; + boolean seenDigit = false; + + while (true) { + switch (state.peek()) { + case ',': + result.paddingWidth += 1; + result.groupingSizes[2] = result.groupingSizes[1]; + result.groupingSizes[1] = result.groupingSizes[0]; + result.groupingSizes[0] = 0; + break; + + case '#': + if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point"); + result.paddingWidth += 1; + result.groupingSizes[0] += 1; + result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1); + // no change to result.minimumIntegerDigits + // no change to result.minimumSignificantDigits + result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0); + result.rounding.appendDigit((byte) 0, 0, true); + break; + + case '@': + seenSignificantDigitMarker = true; + if (seenDigit) throw state.toParseException("Can't mix @ and 0 in pattern"); + result.paddingWidth += 1; + result.groupingSizes[0] += 1; + result.totalIntegerDigits += 1; + // no change to result.minimumIntegerDigits + result.minimumSignificantDigits += 1; + result.maximumSignificantDigits += 1; + result.rounding.appendDigit((byte) 0, 0, true); + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + seenDigit = true; + if (seenSignificantDigitMarker) + throw state.toParseException("Can't mix @ and 0 in pattern"); + // TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt + result.paddingWidth += 1; + result.groupingSizes[0] += 1; + result.totalIntegerDigits += 1; + result.minimumIntegerDigits += 1; + // no change to result.minimumSignificantDigits + result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0); + result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true); + break; + + default: + return; + } + state.next(); // consume the symbol + } + } + + private static void consumeFractionFormat(ParserState state, SubpatternParseResult result) { + int zeroCounter = 0; + boolean seenHash = false; + while (true) { + switch (state.peek()) { + case '#': + seenHash = true; + result.paddingWidth += 1; + // no change to result.minimumFractionDigits + result.maximumFractionDigits += 1; + zeroCounter++; + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (seenHash) throw state.toParseException("0 cannot follow # after decimal point"); + result.paddingWidth += 1; + result.minimumFractionDigits += 1; + result.maximumFractionDigits += 1; + if (state.peek() == '0') { + zeroCounter++; + } else { + result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false); + zeroCounter = 0; + } + break; + + default: + return; + } + state.next(); // consume the symbol + } + } + + private static void consumeExponent(ParserState state, SubpatternParseResult result) { + if (state.peek() != 'E') { + return; + } + state.next(); // consume the E + result.paddingWidth++; + if (state.peek() == '+') { + state.next(); // consume the + + result.exponentShowPlusSign = true; + result.paddingWidth++; + } + while (state.peek() == '0') { + state.next(); // consume the 0 + result.exponentDigits += 1; + result.paddingWidth++; + } + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java new file mode 100644 index 0000000000..9abc90c2fd --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java @@ -0,0 +1,994 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.ArrayList; + +import com.ibm.icu.impl.number.Parse.ParseMode; +import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier; +import com.ibm.icu.impl.number.formatters.CompactDecimalFormat; +import com.ibm.icu.impl.number.formatters.CurrencyFormat; +import com.ibm.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle; +import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier; +import com.ibm.icu.impl.number.formatters.MeasureFormat; +import com.ibm.icu.impl.number.formatters.PaddingFormat; +import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition; +import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat; +import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat; +import com.ibm.icu.impl.number.formatters.ScientificFormat; +import com.ibm.icu.impl.number.rounders.IncrementRounder; +import com.ibm.icu.impl.number.rounders.MagnitudeRounder; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode; +import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; +import com.ibm.icu.text.CurrencyPluralInfo; +import com.ibm.icu.text.MeasureFormat.FormatWidth; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Currency.CurrencyUsage; +import com.ibm.icu.util.MeasureUnit; + +public class Properties + implements Cloneable, + Serializable, + PositiveDecimalFormat.IProperties, + PositiveNegativeAffixFormat.IProperties, + MagnitudeMultiplier.IProperties, + ScientificFormat.IProperties, + MeasureFormat.IProperties, + CompactDecimalFormat.IProperties, + PaddingFormat.IProperties, + BigDecimalMultiplier.IProperties, + CurrencyFormat.IProperties, + Parse.IProperties, + IncrementRounder.IProperties, + MagnitudeRounder.IProperties, + SignificantDigitsRounder.IProperties { + + private static final Properties DEFAULT = new Properties(); + + /** Auto-generated. */ + private static final long serialVersionUID = 4095518955889349243L; + + // The setters in this class should NOT have any side-effects or perform any validation. It is + // up to the consumer of the property bag to deal with property validation. + + // The fields are all marked "transient" because custom serialization is being used. + + /*--------------------------------------------------------------------------------------------+/ + /| IMPORTANT! |/ + /| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(), |/ + /| and in #_hashCode(). |/ + /| |/ + /| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(), |/ + /| or #equals(), but it will NOT catch if you forget to add it to #hashCode(). |/ + /+--------------------------------------------------------------------------------------------*/ + + private transient CompactStyle compactStyle; + private transient Currency currency; + private transient CurrencyPluralInfo currencyPluralInfo; + private transient CurrencyStyle currencyStyle; + private transient CurrencyUsage currencyUsage; + private transient boolean decimalPatternMatchRequired; + private transient boolean decimalSeparatorAlwaysShown; + private transient boolean exponentSignAlwaysShown; + private transient int formatWidth; + private transient int groupingSize; + private transient int magnitudeMultiplier; + private transient MathContext mathContext; + private transient int maximumFractionDigits; + private transient int maximumIntegerDigits; + private transient int maximumSignificantDigits; + private transient FormatWidth measureFormatWidth; + private transient MeasureUnit measureUnit; + private transient int minimumExponentDigits; + private transient int minimumFractionDigits; + private transient int minimumGroupingDigits; + private transient int minimumIntegerDigits; + private transient int minimumSignificantDigits; + private transient BigDecimal multiplier; + private transient String negativePrefix; + private transient String negativePrefixPattern; + private transient String negativeSuffix; + private transient String negativeSuffixPattern; + private transient PadPosition padPosition; + private transient String padString; + private transient boolean parseCaseSensitive; + private transient boolean parseNoExponent; + private transient boolean parseIntegerOnly; + private transient ParseMode parseMode; + private transient boolean parseToBigDecimal; + private transient boolean plusSignAlwaysShown; + private transient String positivePrefix; + private transient String positivePrefixPattern; + private transient String positiveSuffix; + private transient String positiveSuffixPattern; + private transient BigDecimal roundingIncrement; + private transient RoundingMode roundingMode; + private transient int secondaryGroupingSize; + private transient SignificantDigitsMode significantDigitsMode; + + /*--------------------------------------------------------------------------------------------+/ + /| IMPORTANT! |/ + /| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(), |/ + /| and in #_hashCode(). |/ + /| |/ + /| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(), |/ + /| or #equals(), but it will NOT catch if you forget to add it to #hashCode(). |/ + /+--------------------------------------------------------------------------------------------*/ + + public Properties() { + clear(); + } + + private Properties _clear() { + compactStyle = DEFAULT_COMPACT_STYLE; + currency = DEFAULT_CURRENCY; + currencyPluralInfo = DEFAULT_CURRENCY_PLURAL_INFO; + currencyStyle = DEFAULT_CURRENCY_STYLE; + currencyUsage = DEFAULT_CURRENCY_USAGE; + decimalPatternMatchRequired = DEFAULT_DECIMAL_PATTERN_MATCH_REQUIRED; + decimalSeparatorAlwaysShown = DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN; + exponentSignAlwaysShown = DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN; + formatWidth = DEFAULT_FORMAT_WIDTH; + groupingSize = DEFAULT_GROUPING_SIZE; + magnitudeMultiplier = DEFAULT_MAGNITUDE_MULTIPLIER; + mathContext = DEFAULT_MATH_CONTEXT; + maximumFractionDigits = DEFAULT_MAXIMUM_FRACTION_DIGITS; + maximumIntegerDigits = DEFAULT_MAXIMUM_INTEGER_DIGITS; + maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS; + measureFormatWidth = DEFAULT_MEASURE_FORMAT_WIDTH; + measureUnit = DEFAULT_MEASURE_UNIT; + minimumExponentDigits = DEFAULT_MINIMUM_EXPONENT_DIGITS; + minimumFractionDigits = DEFAULT_MINIMUM_FRACTION_DIGITS; + minimumGroupingDigits = DEFAULT_MINIMUM_GROUPING_DIGITS; + minimumIntegerDigits = DEFAULT_MINIMUM_INTEGER_DIGITS; + minimumSignificantDigits = DEFAULT_MINIMUM_SIGNIFICANT_DIGITS; + multiplier = DEFAULT_MULTIPLIER; + negativePrefix = DEFAULT_NEGATIVE_PREFIX; + negativePrefixPattern = DEFAULT_NEGATIVE_PREFIX_PATTERN; + negativeSuffix = DEFAULT_NEGATIVE_SUFFIX; + negativeSuffixPattern = DEFAULT_NEGATIVE_SUFFIX_PATTERN; + padPosition = DEFAULT_PAD_POSITION; + padString = DEFAULT_PAD_STRING; + parseCaseSensitive = DEFAULT_PARSE_CASE_SENSITIVE; + parseIntegerOnly = DEFAULT_PARSE_INTEGER_ONLY; + parseMode = DEFAULT_PARSE_MODE; + parseNoExponent = DEFAULT_PARSE_NO_EXPONENT; + parseToBigDecimal = DEFAULT_PARSE_TO_BIG_DECIMAL; + plusSignAlwaysShown = DEFAULT_PLUS_SIGN_ALWAYS_SHOWN; + positivePrefix = DEFAULT_POSITIVE_PREFIX; + positivePrefixPattern = DEFAULT_POSITIVE_PREFIX_PATTERN; + positiveSuffix = DEFAULT_POSITIVE_SUFFIX; + positiveSuffixPattern = DEFAULT_POSITIVE_SUFFIX_PATTERN; + roundingIncrement = DEFAULT_ROUNDING_INCREMENT; + roundingMode = DEFAULT_ROUNDING_MODE; + secondaryGroupingSize = DEFAULT_SECONDARY_GROUPING_SIZE; + significantDigitsMode = DEFAULT_SIGNIFICANT_DIGITS_MODE; + return this; + } + + private Properties _copyFrom(Properties other) { + compactStyle = other.compactStyle; + currency = other.currency; + currencyPluralInfo = other.currencyPluralInfo; + currencyStyle = other.currencyStyle; + currencyUsage = other.currencyUsage; + decimalPatternMatchRequired = other.decimalPatternMatchRequired; + decimalSeparatorAlwaysShown = other.decimalSeparatorAlwaysShown; + exponentSignAlwaysShown = other.exponentSignAlwaysShown; + formatWidth = other.formatWidth; + groupingSize = other.groupingSize; + magnitudeMultiplier = other.magnitudeMultiplier; + mathContext = other.mathContext; + maximumFractionDigits = other.maximumFractionDigits; + maximumIntegerDigits = other.maximumIntegerDigits; + maximumSignificantDigits = other.maximumSignificantDigits; + measureFormatWidth = other.measureFormatWidth; + measureUnit = other.measureUnit; + minimumExponentDigits = other.minimumExponentDigits; + minimumFractionDigits = other.minimumFractionDigits; + minimumGroupingDigits = other.minimumGroupingDigits; + minimumIntegerDigits = other.minimumIntegerDigits; + minimumSignificantDigits = other.minimumSignificantDigits; + multiplier = other.multiplier; + negativePrefix = other.negativePrefix; + negativePrefixPattern = other.negativePrefixPattern; + negativeSuffix = other.negativeSuffix; + negativeSuffixPattern = other.negativeSuffixPattern; + padPosition = other.padPosition; + padString = other.padString; + parseCaseSensitive = other.parseCaseSensitive; + parseIntegerOnly = other.parseIntegerOnly; + parseMode = other.parseMode; + parseNoExponent = other.parseNoExponent; + parseToBigDecimal = other.parseToBigDecimal; + plusSignAlwaysShown = other.plusSignAlwaysShown; + positivePrefix = other.positivePrefix; + positivePrefixPattern = other.positivePrefixPattern; + positiveSuffix = other.positiveSuffix; + positiveSuffixPattern = other.positiveSuffixPattern; + roundingIncrement = other.roundingIncrement; + roundingMode = other.roundingMode; + secondaryGroupingSize = other.secondaryGroupingSize; + significantDigitsMode = other.significantDigitsMode; + return this; + } + + private boolean _equals(Properties other) { + boolean eq = true; + eq = eq && _equalsHelper(compactStyle, other.compactStyle); + eq = eq && _equalsHelper(currency, other.currency); + eq = eq && _equalsHelper(currencyPluralInfo, other.currencyPluralInfo); + eq = eq && _equalsHelper(currencyStyle, other.currencyStyle); + eq = eq && _equalsHelper(currencyUsage, other.currencyUsage); + eq = eq && _equalsHelper(decimalPatternMatchRequired, other.decimalPatternMatchRequired); + eq = eq && _equalsHelper(decimalSeparatorAlwaysShown, other.decimalSeparatorAlwaysShown); + eq = eq && _equalsHelper(exponentSignAlwaysShown, other.exponentSignAlwaysShown); + eq = eq && _equalsHelper(formatWidth, other.formatWidth); + eq = eq && _equalsHelper(groupingSize, other.groupingSize); + eq = eq && _equalsHelper(magnitudeMultiplier, other.magnitudeMultiplier); + eq = eq && _equalsHelper(mathContext, other.mathContext); + eq = eq && _equalsHelper(maximumFractionDigits, other.maximumFractionDigits); + eq = eq && _equalsHelper(maximumIntegerDigits, other.maximumIntegerDigits); + eq = eq && _equalsHelper(maximumSignificantDigits, other.maximumSignificantDigits); + eq = eq && _equalsHelper(measureFormatWidth, other.measureFormatWidth); + eq = eq && _equalsHelper(measureUnit, other.measureUnit); + eq = eq && _equalsHelper(minimumExponentDigits, other.minimumExponentDigits); + eq = eq && _equalsHelper(minimumFractionDigits, other.minimumFractionDigits); + eq = eq && _equalsHelper(minimumGroupingDigits, other.minimumGroupingDigits); + eq = eq && _equalsHelper(minimumIntegerDigits, other.minimumIntegerDigits); + eq = eq && _equalsHelper(minimumSignificantDigits, other.minimumSignificantDigits); + eq = eq && _equalsHelper(multiplier, other.multiplier); + eq = eq && _equalsHelper(negativePrefix, other.negativePrefix); + eq = eq && _equalsHelper(negativePrefixPattern, other.negativePrefixPattern); + eq = eq && _equalsHelper(negativeSuffix, other.negativeSuffix); + eq = eq && _equalsHelper(negativeSuffixPattern, other.negativeSuffixPattern); + eq = eq && _equalsHelper(padPosition, other.padPosition); + eq = eq && _equalsHelper(padString, other.padString); + eq = eq && _equalsHelper(parseCaseSensitive, other.parseCaseSensitive); + eq = eq && _equalsHelper(parseIntegerOnly, other.parseIntegerOnly); + eq = eq && _equalsHelper(parseMode, other.parseMode); + eq = eq && _equalsHelper(parseNoExponent, other.parseNoExponent); + eq = eq && _equalsHelper(parseToBigDecimal, other.parseToBigDecimal); + eq = eq && _equalsHelper(plusSignAlwaysShown, other.plusSignAlwaysShown); + eq = eq && _equalsHelper(positivePrefix, other.positivePrefix); + eq = eq && _equalsHelper(positivePrefixPattern, other.positivePrefixPattern); + eq = eq && _equalsHelper(positiveSuffix, other.positiveSuffix); + eq = eq && _equalsHelper(positiveSuffixPattern, other.positiveSuffixPattern); + eq = eq && _equalsHelper(roundingIncrement, other.roundingIncrement); + eq = eq && _equalsHelper(roundingMode, other.roundingMode); + eq = eq && _equalsHelper(secondaryGroupingSize, other.secondaryGroupingSize); + eq = eq && _equalsHelper(significantDigitsMode, other.significantDigitsMode); + return eq; + } + + private boolean _equalsHelper(boolean mine, boolean theirs) { + return mine == theirs; + } + + private boolean _equalsHelper(int mine, int theirs) { + return mine == theirs; + } + + private boolean _equalsHelper(Object mine, Object theirs) { + if (mine == theirs) return true; + if (mine == null) return false; + return mine.equals(theirs); + } + + private int _hashCode() { + int hashCode = 0; + hashCode ^= _hashCodeHelper(compactStyle); + hashCode ^= _hashCodeHelper(currency); + hashCode ^= _hashCodeHelper(currencyPluralInfo); + hashCode ^= _hashCodeHelper(currencyStyle); + hashCode ^= _hashCodeHelper(currencyUsage); + hashCode ^= _hashCodeHelper(decimalPatternMatchRequired); + hashCode ^= _hashCodeHelper(decimalSeparatorAlwaysShown); + hashCode ^= _hashCodeHelper(exponentSignAlwaysShown); + hashCode ^= _hashCodeHelper(formatWidth); + hashCode ^= _hashCodeHelper(groupingSize); + hashCode ^= _hashCodeHelper(magnitudeMultiplier); + hashCode ^= _hashCodeHelper(mathContext); + hashCode ^= _hashCodeHelper(maximumFractionDigits); + hashCode ^= _hashCodeHelper(maximumIntegerDigits); + hashCode ^= _hashCodeHelper(maximumSignificantDigits); + hashCode ^= _hashCodeHelper(measureFormatWidth); + hashCode ^= _hashCodeHelper(measureUnit); + hashCode ^= _hashCodeHelper(minimumExponentDigits); + hashCode ^= _hashCodeHelper(minimumFractionDigits); + hashCode ^= _hashCodeHelper(minimumGroupingDigits); + hashCode ^= _hashCodeHelper(minimumIntegerDigits); + hashCode ^= _hashCodeHelper(minimumSignificantDigits); + hashCode ^= _hashCodeHelper(multiplier); + hashCode ^= _hashCodeHelper(negativePrefix); + hashCode ^= _hashCodeHelper(negativePrefixPattern); + hashCode ^= _hashCodeHelper(negativeSuffix); + hashCode ^= _hashCodeHelper(negativeSuffixPattern); + hashCode ^= _hashCodeHelper(padPosition); + hashCode ^= _hashCodeHelper(padString); + hashCode ^= _hashCodeHelper(parseCaseSensitive); + hashCode ^= _hashCodeHelper(parseIntegerOnly); + hashCode ^= _hashCodeHelper(parseMode); + hashCode ^= _hashCodeHelper(parseNoExponent); + hashCode ^= _hashCodeHelper(parseToBigDecimal); + hashCode ^= _hashCodeHelper(plusSignAlwaysShown); + hashCode ^= _hashCodeHelper(positivePrefix); + hashCode ^= _hashCodeHelper(positivePrefixPattern); + hashCode ^= _hashCodeHelper(positiveSuffix); + hashCode ^= _hashCodeHelper(positiveSuffixPattern); + hashCode ^= _hashCodeHelper(roundingIncrement); + hashCode ^= _hashCodeHelper(roundingMode); + hashCode ^= _hashCodeHelper(secondaryGroupingSize); + hashCode ^= _hashCodeHelper(significantDigitsMode); + return hashCode; + } + + private int _hashCodeHelper(boolean value) { + return value ? 1 : 0; + } + + private int _hashCodeHelper(int value) { + return value * 13; + } + + private int _hashCodeHelper(Object value) { + if (value == null) return 0; + return value.hashCode(); + } + + public Properties clear() { + return _clear(); + } + + /** Creates and returns a shallow copy of the property bag. */ + @Override + public Properties clone() { + // super.clone() returns a shallow copy. + try { + return (Properties) super.clone(); + } catch (CloneNotSupportedException e) { + // Should never happen since super is Object + throw new UnsupportedOperationException(e); + } + } + + /** + * Shallow-copies the properties from the given property bag into this property bag. + * + * @param other The property bag from which to copy and which will not be modified. + * @return The current property bag (the one modified by this operation), for chaining. + */ + public Properties copyFrom(Properties other) { + return _copyFrom(other); + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (this == other) return true; + if (!(other instanceof Properties)) return false; + return _equals((Properties) other); + } + + @Override + public CompactStyle getCompactStyle() { + return compactStyle; + } + + @Override + public Currency getCurrency() { + return currency; + } + + /// BEGIN GETTERS/SETTERS /// + + @Override + @Deprecated + public CurrencyPluralInfo getCurrencyPluralInfo() { + return currencyPluralInfo; + } + + @Override + public CurrencyStyle getCurrencyStyle() { + return currencyStyle; + } + + @Override + public CurrencyUsage getCurrencyUsage() { + return currencyUsage; + } + + @Override + public boolean getDecimalPatternMatchRequired() { + return decimalPatternMatchRequired; + } + + @Override + public boolean getDecimalSeparatorAlwaysShown() { + return decimalSeparatorAlwaysShown; + } + + @Override + public boolean getExponentSignAlwaysShown() { + return exponentSignAlwaysShown; + } + + @Override + public int getFormatWidth() { + return formatWidth; + } + + @Override + public int getGroupingSize() { + return groupingSize; + } + + @Override + public int getMagnitudeMultiplier() { + return magnitudeMultiplier; + } + + @Override + public MathContext getMathContext() { + return mathContext; + } + + @Override + public int getMaximumFractionDigits() { + return maximumFractionDigits; + } + + @Override + public int getMaximumIntegerDigits() { + return maximumIntegerDigits; + } + + @Override + public int getMaximumSignificantDigits() { + return maximumSignificantDigits; + } + + @Override + public FormatWidth getMeasureFormatWidth() { + return measureFormatWidth; + } + + @Override + public MeasureUnit getMeasureUnit() { + return measureUnit; + } + + @Override + public int getMinimumExponentDigits() { + return minimumExponentDigits; + } + + @Override + public int getMinimumFractionDigits() { + return minimumFractionDigits; + } + + @Override + public int getMinimumGroupingDigits() { + return minimumGroupingDigits; + } + + @Override + public int getMinimumIntegerDigits() { + return minimumIntegerDigits; + } + + @Override + public int getMinimumSignificantDigits() { + return minimumSignificantDigits; + } + + @Override + public BigDecimal getMultiplier() { + return multiplier; + } + + @Override + public String getNegativePrefix() { + return negativePrefix; + } + + @Override + public String getNegativePrefixPattern() { + return negativePrefixPattern; + } + + @Override + public String getNegativeSuffix() { + return negativeSuffix; + } + + @Override + public String getNegativeSuffixPattern() { + return negativeSuffixPattern; + } + + @Override + public PadPosition getPadPosition() { + return padPosition; + } + + @Override + public String getPadString() { + return padString; + } + + @Override + public boolean getParseCaseSensitive() { + return parseCaseSensitive; + } + + @Override + public boolean getParseIntegerOnly() { + return parseIntegerOnly; + } + + @Override + public ParseMode getParseMode() { + return parseMode; + } + + @Override + public boolean getParseNoExponent() { + return parseNoExponent; + } + + @Override + public boolean getParseToBigDecimal() { + return parseToBigDecimal; + } + + @Override + public boolean getPlusSignAlwaysShown() { + return plusSignAlwaysShown; + } + + @Override + public String getPositivePrefix() { + return positivePrefix; + } + + @Override + public String getPositivePrefixPattern() { + return positivePrefixPattern; + } + + @Override + public String getPositiveSuffix() { + return positiveSuffix; + } + + @Override + public String getPositiveSuffixPattern() { + return positiveSuffixPattern; + } + + @Override + public BigDecimal getRoundingIncrement() { + return roundingIncrement; + } + + @Override + public RoundingMode getRoundingMode() { + return roundingMode; + } + + @Override + public int getSecondaryGroupingSize() { + return secondaryGroupingSize; + } + + @Override + public SignificantDigitsMode getSignificantDigitsMode() { + return significantDigitsMode; + } + + @Override + public int hashCode() { + return _hashCode(); + } + + /** Custom serialization: re-create object from serialized properties. */ + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + + // Initialize to empty + clear(); + + // Extra int for possible future use + ois.readInt(); + + // 1) How many fields were serialized? + int count = ois.readInt(); + + // 2) Read each field by its name and value + for (int i=0; i"); + 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 fieldsToSerialize = new ArrayList(); + ArrayList valuesToSerialize = new ArrayList(); + Field[] fields = Properties.class.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + try { + Object myValue = field.get(this); + if (myValue == null) { + // All *Object* values default to null; no need to serialize. + continue; + } + Object defaultValue = field.get(DEFAULT); + if (!myValue.equals(defaultValue)) { + fieldsToSerialize.add(field); + valuesToSerialize.add(myValue); + } + } catch (IllegalArgumentException e) { + // Should not happen + throw new AssertionError(e); + } catch (IllegalAccessException e) { + // Should not happen + throw new AssertionError(e); + } + } + + // 1) How many fields are to be serialized? + int count = fieldsToSerialize.size(); + oos.writeInt(count); + + // 2) Write each field with its name and value + for (int i = 0; i < count; i++) { + Field field = fieldsToSerialize.get(i); + Object value = valuesToSerialize.get(i); + oos.writeObject(field.getName()); + oos.writeObject(value); + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java new file mode 100644 index 0000000000..cd2f076bf1 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java @@ -0,0 +1,265 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.MathContext; +import java.math.RoundingMode; + +import com.ibm.icu.impl.number.formatters.CompactDecimalFormat; +import com.ibm.icu.impl.number.formatters.ScientificFormat; + +/** + * The base class for a Rounder used by ICU Decimal Format. + * + *

A Rounder must implement the method {@link #apply}. An implementation must: + * + *

    + *
  1. Either have the code applyDefaults(input); 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. + *
  2. Call one of {@link FormatQuantity#roundToIncrement}, {@link + * FormatQuantity#roundToMagnitude}, or {@link FormatQuantity#roundToInfinity} on the input. + *
+ * + *

In order to be used by {@link CompactDecimalFormat} and {@link ScientificFormat}, among + * others, your rounder must be stable upon decreasing 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 en-US. + * + * @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 + * en-US. 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 en-US. + * + * @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 + * en-US 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}. + * + *

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 threadLocalProperties = + new ThreadLocal() { + @Override + protected Properties initialValue() { + return new Properties(); + } + }; + + /** + * Gets a thread-local property bag that can be used to deliver properties to a constructor. + * Rounders themselves are guaranteed to not internally use a copy of this property bag. + * + * @return A clean, thread-local property bag. + */ + public static Properties getThreadLocalProperties() { + return threadLocalProperties.get().clear(); + } + + @Override + public void before(FormatQuantity input, ModifierHolder mods) { + apply(input); + } + + @Override + public void export(Properties properties) { + properties.setMathContext(mathContext); + properties.setRoundingMode(mathContext.getRoundingMode()); + properties.setMinimumFractionDigits(minFrac); + properties.setMinimumIntegerDigits(minInt); + properties.setMaximumFractionDigits(maxFrac); + properties.setMaximumIntegerDigits(maxInt); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/RoundingUtils.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/RoundingUtils.java new file mode 100644 index 0000000000..3994eb2573 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/RoundingUtils.java @@ -0,0 +1,165 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +import com.ibm.icu.impl.number.Rounder.IBasicRoundingProperties; + +/** @author sffc */ +public class RoundingUtils { + + public static final int SECTION_LOWER = 1; + public static final int SECTION_MIDPOINT = 2; + public static final int SECTION_UPPER = 3; + + /** + * Converts a rounding mode and metadata about the quantity being rounded to a boolean determining + * whether the value should be rounded toward infinity or toward zero. + * + *

The parameters are of type int because benchmarks on an x86-64 processor against OpenJDK + * showed that ints were demonstrably faster than enums in switch statements. + * + * @param isEven Whether the digit immediately before the rounding magnitude is even. + * @param isNegative Whether the quantity is negative. + * @param section Whether the part of the quantity to the right of the rounding magnitude is + * exactly halfway between two digits, whether it is in the lower part (closer to zero), or + * whether it is in the upper part (closer to infinity). See {@link #SECTION_LOWER}, {@link + * #SECTION_MIDPOINT}, and {@link #SECTION_UPPER}. + * @param roundingMode The integer version of the {@link RoundingMode}, which you can get via + * {@link RoundingMode#ordinal}. + * @param reference A reference object to be used when throwing an ArithmeticException. + * @return true if the number should be rounded toward zero; false if it should be rounded toward + * infinity. + */ + public static boolean getRoundingDirection( + boolean isEven, boolean isNegative, int section, int roundingMode, Object reference) { + switch (roundingMode) { + case BigDecimal.ROUND_UP: + // round away from zero + return false; + + case BigDecimal.ROUND_DOWN: + // round toward zero + return true; + + case BigDecimal.ROUND_CEILING: + // round toward positive infinity + return isNegative; + + case BigDecimal.ROUND_FLOOR: + // round toward negative infinity + return !isNegative; + + case BigDecimal.ROUND_HALF_UP: + switch (section) { + case SECTION_MIDPOINT: + return false; + case SECTION_LOWER: + return true; + case SECTION_UPPER: + return false; + } + break; + + case BigDecimal.ROUND_HALF_DOWN: + switch (section) { + case SECTION_MIDPOINT: + return true; + case SECTION_LOWER: + return true; + case SECTION_UPPER: + return false; + } + break; + + case BigDecimal.ROUND_HALF_EVEN: + switch (section) { + case SECTION_MIDPOINT: + return isEven; + case SECTION_LOWER: + return true; + case SECTION_UPPER: + return false; + } + break; + } + + // Rounding mode UNNECESSARY + throw new ArithmeticException("Rounding is required on " + reference.toString()); + } + + /** + * Gets whether the given rounding mode's rounding boundary is at the midpoint. The rounding + * boundary is the point at which a number switches from being rounded down to being rounded up. + * For example, with rounding mode HALF_EVEN, HALF_UP, or HALF_DOWN, the rounding boundary is at + * the midpoint, and this function would return true. However, for UP, DOWN, CEILING, and FLOOR, + * the rounding boundary is at the "edge", and this function would return false. + * + * @param roundingMode The integer version of the {@link RoundingMode}. + * @return true if rounding mode is HALF_EVEN, HALF_UP, or HALF_DOWN; false otherwise. + */ + public static boolean roundsAtMidpoint(int roundingMode) { + switch (roundingMode) { + case BigDecimal.ROUND_UP: + case BigDecimal.ROUND_DOWN: + case BigDecimal.ROUND_CEILING: + case BigDecimal.ROUND_FLOOR: + return false; + + default: + return true; + } + } + + private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED = + new MathContext[RoundingMode.values().length]; + + private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS = + new MathContext[RoundingMode.values().length]; + + static { + for (int i = 0; i < MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS.length; i++) { + MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[i] = new MathContext(0, RoundingMode.valueOf(i)); + MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS[i] = new MathContext(16); + } + } + + /** + * Gets the user-specified math context out of the property bag. If there is none, falls back to a + * math context with unlimited precision and the user-specified rounding mode, which defaults to + * HALF_EVEN (the IEEE 754R default). + * + * @param properties The property bag. + * @return A {@link MathContext}. Never null. + */ + public static MathContext getMathContextOrUnlimited(IBasicRoundingProperties properties) { + MathContext mathContext = properties.getMathContext(); + if (mathContext == null) { + RoundingMode roundingMode = properties.getRoundingMode(); + if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN; + mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[roundingMode.ordinal()]; + } + return mathContext; + } + + /** + * Gets the user-specified math context out of the property bag. If there is none, falls back to a + * math context with 16 digits of precision (the 64-bit IEEE 754R default) and the user-specified + * rounding mode, which defaults to HALF_EVEN (the IEEE 754R default). + * + * @param properties The property bag. + * @return A {@link MathContext}. Never null. + */ + public static MathContext getMathContextOr16Digits(IBasicRoundingProperties properties) { + MathContext mathContext = properties.getMathContext(); + if (mathContext == null) { + RoundingMode roundingMode = properties.getRoundingMode(); + if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN; + mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS[roundingMode.ordinal()]; + } + return mathContext; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/demo.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/demo.java new file mode 100644 index 0000000000..8a91de7333 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/demo.java @@ -0,0 +1,123 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.List; + +import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition; +import com.ibm.icu.impl.number.formatters.RangeFormat; +import com.ibm.icu.impl.number.modifiers.SimpleModifier; +import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.util.MeasureUnit; + +public class demo { + + public static void main(String[] args) throws ParseException { + SimpleModifier.testFormatAsPrefixSuffix(); + + System.out.println(new FormatQuantity1(3.14159)); + System.out.println(new FormatQuantity1(3.14159, true)); + System.out.println(new FormatQuantity2(3.14159)); + + System.out.println( + PatternString.propertiesToString(PatternString.parseToProperties("+**##,##,#00.05#%"))); + + ParsePosition ppos = new ParsePosition(0); + System.out.println( + Parse.parse( + "dd123", + ppos, + new Properties().setPositivePrefix("dd").setNegativePrefix("ddd"), + DecimalFormatSymbols.getInstance())); + System.out.println(ppos); + + List formats = new ArrayList(); + + Properties properties = new Properties(); + Format ndf = Endpoint.fromBTA(properties); + formats.add(ndf); + + properties = + new Properties() + .setMinimumSignificantDigits(3) + .setMaximumSignificantDigits(3) + .setCompactStyle(CompactStyle.LONG); + Format cdf = Endpoint.fromBTA(properties); + formats.add(cdf); + + properties = + new Properties().setFormatWidth(10).setPadPosition(PadPosition.AFTER_PREFIX); + Format pdf = Endpoint.fromBTA(properties); + formats.add(pdf); + + properties = + new Properties() + .setMinimumExponentDigits(1) + .setMaximumIntegerDigits(3) + .setMaximumFractionDigits(1); + Format exf = Endpoint.fromBTA(properties); + formats.add(exf); + + properties = new Properties().setRoundingIncrement(new BigDecimal("0.5")); + Format rif = Endpoint.fromBTA(properties); + formats.add(rif); + + properties = new Properties().setMeasureUnit(MeasureUnit.HECTARE); + Format muf = Endpoint.fromBTA(properties); + formats.add(muf); + + properties = + new Properties().setMeasureUnit(MeasureUnit.HECTARE).setCompactStyle(CompactStyle.LONG); + Format cmf = Endpoint.fromBTA(properties); + formats.add(cmf); + + properties = PatternString.parseToProperties("#,##0.00 \u00a4"); + Format ptf = Endpoint.fromBTA(properties); + formats.add(ptf); + + RangeFormat rf = new RangeFormat(cdf, cdf, " to "); + System.out.println(rf.format(new FormatQuantity2(1234), new FormatQuantity2(2345))); + + String[] cases = { + "1.0", + "2.01", + "1234.56", + "3000.0", + // "512.0000000000017", + // "4096.000000000001", + // "4096.000000000004", + // "4096.000000000005", + // "4096.000000000006", + // "4096.000000000007", + "0.00026418", + "0.01789261", + "468160.0", + "999000.0", + "999900.0", + "999990.0", + "0.0", + "12345678901.0", + // "789000000000000000000000.0", + // "789123123567853156372158.0", + "-5193.48", + }; + + for (String str : cases) { + System.out.println("----------"); + System.out.println(str); + System.out.println(" NDF: " + ndf.format(new FormatQuantity2(Double.parseDouble(str)))); + System.out.println(" CDF: " + cdf.format(new FormatQuantity2(Double.parseDouble(str)))); + System.out.println(" PWD: " + pdf.format(new FormatQuantity2(Double.parseDouble(str)))); + System.out.println(" EXF: " + exf.format(new FormatQuantity2(Double.parseDouble(str)))); + System.out.println(" RIF: " + rif.format(new FormatQuantity2(Double.parseDouble(str)))); + System.out.println(" MUF: " + muf.format(new FormatQuantity2(Double.parseDouble(str)))); + System.out.println(" CMF: " + cmf.format(new FormatQuantity2(Double.parseDouble(str)))); + System.out.println(" PTF: " + ptf.format(new FormatQuantity2(Double.parseDouble(str)))); + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/BigDecimalMultiplier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/BigDecimalMultiplier.java new file mode 100644 index 0000000000..a6a008a2cc --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/BigDecimalMultiplier.java @@ -0,0 +1,57 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import java.math.BigDecimal; + +import com.ibm.icu.impl.number.Format.BeforeFormat; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.Properties; + +public class BigDecimalMultiplier extends BeforeFormat { + public static interface IProperties { + + static BigDecimal DEFAULT_MULTIPLIER = null; + + /** @see #setMultiplier */ + public BigDecimal getMultiplier(); + + /** + * Multiply all numbers by this amount before formatting. + * + * @param multiplier The amount to multiply by. + * @return The property bag, for chaining. + * @see MagnitudeMultiplier + */ + public IProperties setMultiplier(BigDecimal multiplier); + } + + public static boolean useMultiplier(IProperties properties) { + return properties.getMultiplier() != IProperties.DEFAULT_MULTIPLIER; + } + + private final BigDecimal multiplier; + + public static BigDecimalMultiplier getInstance(IProperties properties) { + if (properties.getMultiplier() == null) { + throw new IllegalArgumentException("The multiplier must be present for MultiplierFormat"); + } + // TODO: Intelligently fall back to a MagnitudeMultiplier if the multiplier is a power of ten? + return new BigDecimalMultiplier(properties); + } + + private BigDecimalMultiplier(IProperties properties) { + this.multiplier = properties.getMultiplier(); + } + + @Override + public void before(FormatQuantity input, ModifierHolder mods) { + input.multiplyBy(multiplier); + } + + @Override + public void export(Properties properties) { + properties.setMultiplier(multiplier); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java new file mode 100644 index 0000000000..d3d8aef114 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java @@ -0,0 +1,449 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import java.util.HashMap; +import java.util.Map; + +import com.ibm.icu.impl.ICUData; +import com.ibm.icu.impl.ICUResourceBundle; +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.UResource; +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.Modifier; +import com.ibm.icu.impl.number.Modifier.PositiveNegativeModifier; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.PNAffixGenerator; +import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.impl.number.Rounder; +import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier; +import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode; +import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.text.NumberingSystem; +import com.ibm.icu.text.PluralRules; +import com.ibm.icu.util.ULocale; +import com.ibm.icu.util.UResourceBundle; + +public class CompactDecimalFormat extends Format.BeforeFormat { + public static interface IProperties + extends RoundingFormat.IProperties, CurrencyFormat.ICurrencyProperties { + + static CompactStyle DEFAULT_COMPACT_STYLE = null; + + /** @see #setCompactStyle */ + public CompactStyle getCompactStyle(); + + /** + * Use compact decimal formatting with the specified {@link CompactStyle}. CompactStyle.SHORT + * produces output like "10K" in locale en-US, 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> + threadLocalDataCache = + new ThreadLocal>() { + @Override + protected Map initialValue() { + return new HashMap(); + } + }; + + private static CompactDecimalData getData( + DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) { + // See if we already have a data object based on the fingerprint + CompactDecimalData data = threadLocalDataCache.get().get(fingerprint); + if (data != null) return data; + + // Make data bundle object + data = new CompactDecimalData(); + ULocale ulocale = symbols.getULocale(); + CompactDecimalDataSink sink = new CompactDecimalDataSink(data, symbols, fingerprint); + String nsName = NumberingSystem.getInstance(ulocale).getName(); + ICUResourceBundle r = + (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, ulocale); + r.getAllItemsWithFallback("NumberElements/" + nsName, sink); + if (data.isEmpty() && !nsName.equals("latn")) { + r.getAllItemsWithFallback("NumberElements/latn", sink); + } + if (sink.exception != null) { + throw sink.exception; + } + threadLocalDataCache.get().put(fingerprint, data); + return data; + } + + private static PositiveNegativeModifier getDefaultMod( + DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) { + ULocale uloc = symbols.getULocale(); + String pattern; + if (fingerprint.compactType == CompactType.CURRENCY) { + pattern = NumberFormat.getPattern(uloc, NumberFormat.CURRENCYSTYLE); + } else { + pattern = NumberFormat.getPattern(uloc, NumberFormat.NUMBERSTYLE); + } + // TODO: Clean this up; avoid the extra object creations. + // TODO: Currency may also need to override grouping settings, not just affixes. + Properties properties = PatternString.parseToProperties(pattern); + PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance(); + PNAffixGenerator.Result result = + pnag.getModifiers(symbols, fingerprint.currencySymbol, properties); + return new PositiveNegativeAffixModifier(result.positive, result.negative); + } + + private CompactDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) { + CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties); + this.rounder = getRounder(properties); + this.data = getData(symbols, fingerprint); + this.defaultMod = getDefaultMod(symbols, fingerprint); + this.style = properties.getCompactStyle(); // for exporting only + } + + @Override + public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) { + apply(input, mods, rules, rounder, data, defaultMod); + } + + @Override + protected void before(FormatQuantity input, ModifierHolder mods) { + throw new UnsupportedOperationException(); + } + + public static void apply( + FormatQuantity input, + ModifierHolder mods, + PluralRules rules, + DecimalFormatSymbols symbols, + IProperties properties) { + CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties); + Rounder rounder = getRounder(properties); + CompactDecimalData data = getData(symbols, fingerprint); + PositiveNegativeModifier defaultMod = getDefaultMod(symbols, fingerprint); + apply(input, mods, rules, rounder, data, defaultMod); + } + + private static void apply( + FormatQuantity input, + ModifierHolder mods, + PluralRules rules, + Rounder rounder, + CompactDecimalData data, + PositiveNegativeModifier defaultMod) { + + // Treat zero as if it had magnitude 0 + int magnitude; + if (input.isZero()) { + magnitude = 0; + rounder.apply(input); + } else { + int multiplier = rounder.chooseMultiplierAndApply(input, data); + magnitude = input.getMagnitude() - multiplier; + } + + StandardPlural plural = input.getStandardPlural(rules); + boolean isNegative = input.isNegative(); + Modifier mod = data.getModifier(magnitude, plural, isNegative); + if (mod == null) { + // Use the default (non-compact) modifier. + mod = defaultMod.getModifier(isNegative); + } + mods.add(mod); + } + + @Override + public void export(Properties properties) { + properties.setCompactStyle(style); + rounder.export(properties); + } + + static class CompactDecimalData implements Rounder.MultiplierGenerator { + + // A dummy object used when a "0" compact decimal entry is encountered. This is necessary + // in order to prevent falling back to root. + private static final Modifier USE_FALLBACK = new ConstantAffixModifier(); + + final Modifier[] mods; + final byte[] multipliers; + boolean isEmpty; + int largestMagnitude; + + CompactDecimalData() { + mods = new Modifier[(MAX_DIGITS + 1) * StandardPlural.COUNT * 2]; + multipliers = new byte[MAX_DIGITS + 1]; + isEmpty = true; + largestMagnitude = -1; + } + + boolean isEmpty() { + return isEmpty; + } + + @Override + public int getMultiplier(int magnitude) { + if (magnitude < 0) { + return 0; + } + if (magnitude > largestMagnitude) { + magnitude = largestMagnitude; + } + return multipliers[magnitude]; + } + + int setOrGetMultiplier(int magnitude, byte multiplier) { + if (multipliers[magnitude] != 0) { + return multipliers[magnitude]; + } + multipliers[magnitude] = multiplier; + isEmpty = false; + if (magnitude > largestMagnitude) largestMagnitude = magnitude; + return multiplier; + } + + Modifier getModifier(int magnitude, StandardPlural plural, boolean isNegative) { + if (magnitude < 0) { + return null; + } + if (magnitude > largestMagnitude) { + magnitude = largestMagnitude; + } + Modifier mod = mods[modIndex(magnitude, plural, isNegative)]; + if (mod == null && plural != StandardPlural.OTHER) { + // Fall back to "other" plural variant + mod = mods[modIndex(magnitude, StandardPlural.OTHER, isNegative)]; + } + if (mod == USE_FALLBACK) { + // Return null if USE_FALLBACK is present + mod = null; + } + return mod; + } + + public boolean has(int magnitude, StandardPlural plural) { + // Return true if USE_FALLBACK is present + return mods[modIndex(magnitude, plural, false)] != null; + } + + void setModifiers(Modifier positive, Modifier negative, int magnitude, StandardPlural plural) { + mods[modIndex(magnitude, plural, false)] = positive; + mods[modIndex(magnitude, plural, true)] = negative; + isEmpty = false; + if (magnitude > largestMagnitude) largestMagnitude = magnitude; + } + + void setNoFallback(int magnitude, StandardPlural plural) { + setModifiers(USE_FALLBACK, USE_FALLBACK, magnitude, plural); + } + + private static final int modIndex(int magnitude, StandardPlural plural, boolean isNegative) { + return magnitude * StandardPlural.COUNT * 2 + plural.ordinal() * 2 + (isNegative ? 1 : 0); + } + } + + // Should this be public or internal? + static enum CompactType { + DECIMAL, + CURRENCY + } + + static class CompactDecimalFingerprint { + // TODO: Add more stuff to the fingerprint, like the symbols used by PNAffixGenerator + final CompactStyle compactStyle; + final CompactType compactType; + final ULocale uloc; + final String currencySymbol; + + CompactDecimalFingerprint(DecimalFormatSymbols symbols, IProperties properties) { + // CompactDecimalFormat does not need to worry about the same constraints as non-compact + // currency formatting needs to consider, like the currency rounding mode and the currency + // long names with plural forms. + if (properties.getCurrency() != CurrencyFormat.ICurrencyProperties.DEFAULT_CURRENCY) { + compactType = CompactType.CURRENCY; + currencySymbol = CurrencyFormat.getCurrencySymbol(symbols, properties); + } else { + compactType = CompactType.DECIMAL; + currencySymbol = symbols.getCurrencySymbol(); // fallback; should remain unused + } + compactStyle = properties.getCompactStyle(); + uloc = symbols.getULocale(); + } + + @Override + public boolean equals(Object _other) { + if (_other == null) return false; + if (this == _other) return true; + CompactDecimalFingerprint other = (CompactDecimalFingerprint) _other; + if (compactStyle != other.compactStyle) return false; + if (compactType != other.compactType) return false; + if (currencySymbol != other.currencySymbol) { + // String comparison with null handling + if (currencySymbol == null || other.currencySymbol == null) return false; + if (!currencySymbol.equals(other.currencySymbol)) return false; + } + if (!uloc.equals(other.uloc)) return false; + return true; + } + + @Override + public int hashCode() { + int hashCode = 0; + if (compactStyle != null) hashCode ^= compactStyle.hashCode(); + if (compactType != null) hashCode ^= compactType.hashCode(); + if (uloc != null) hashCode ^= uloc.hashCode(); + if (currencySymbol != null) hashCode ^= currencySymbol.hashCode(); + return hashCode; + } + } + + private static final class CompactDecimalDataSink extends UResource.Sink { + + final CompactDecimalData data; + final DecimalFormatSymbols symbols; + final CompactStyle compactStyle; + final CompactType compactType; + final String currencySymbol; + final PNAffixGenerator pnag; + IllegalArgumentException exception; + + /* + * NumberElements{ <-- top (numbering system table) + * latn{ <-- patternsTable (one per numbering system) + * patternsLong{ <-- formatsTable (one per pattern) + * decimalFormat{ <-- powersOfTenTable (one per format) + * 1000{ <-- pluralVariantsTable (one per power of ten) + * one{"0 thousand"} <-- plural variant and template + */ + + public CompactDecimalDataSink( + CompactDecimalData data, + DecimalFormatSymbols symbols, + CompactDecimalFingerprint fingerprint) { + this.data = data; + this.symbols = symbols; + compactType = fingerprint.compactType; + currencySymbol = fingerprint.currencySymbol; + compactStyle = fingerprint.compactStyle; + pnag = PNAffixGenerator.getThreadLocalInstance(); + } + + @Override + public void put(UResource.Key key, UResource.Value value, boolean isRoot) { + UResource.Table patternsTable = value.getTable(); + for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) { + if (key.contentEquals("patternsShort") && compactStyle == CompactStyle.SHORT) { + } else if (key.contentEquals("patternsLong") && compactStyle == CompactStyle.LONG) { + } else { + continue; + } + + // traverse into the table of formats + UResource.Table formatsTable = value.getTable(); + for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) { + if (key.contentEquals("decimalFormat") && compactType == CompactType.DECIMAL) { + } else if (key.contentEquals("currencyFormat") && compactType == CompactType.CURRENCY) { + } else { + continue; + } + + // traverse into the table of powers of ten + UResource.Table powersOfTenTable = value.getTable(); + for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) { + try { + + // Assumes that the keys are always of the form "10000" where the magnitude is the + // length of the key minus one + byte magnitude = (byte) (key.length() - 1); + + // Silently ignore divisors that are too big. + if (magnitude >= MAX_DIGITS) continue; + + // Iterate over the plural variants ("one", "other", etc) + UResource.Table pluralVariantsTable = value.getTable(); + for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) { + + // Skip this magnitude/plural if we already have it from a child locale. + StandardPlural plural = StandardPlural.fromString(key.toString()); + if (data.has(magnitude, plural)) { + continue; + } + + // The value "0" means that we need to use the default pattern and not fall back + // to parent locales. Example locale where this is relevant: 'it'. + String patternString = value.toString(); + if (patternString.equals("0")) { + data.setNoFallback(magnitude, plural); + continue; + } + + // The magnitude multiplier is the difference between the magnitude and the number + // of zeros in the pattern, getMinimumIntegerDigits. + Properties properties = PatternString.parseToProperties(patternString); + byte _multiplier = (byte) -(magnitude - properties.getMinimumIntegerDigits() + 1); + if (_multiplier != data.setOrGetMultiplier(magnitude, _multiplier)) { + throw new IllegalArgumentException( + String.format( + "Different number of zeros for same power of ten in compact decimal format data for locale '%s', style '%s', type '%s'", + symbols.getULocale().toString(), + compactStyle.toString(), + compactType.toString())); + } + + PNAffixGenerator.Result result = + pnag.getModifiers(symbols, currencySymbol, properties); + data.setModifiers(result.positive, result.negative, magnitude, plural); + } + + } catch (IllegalArgumentException e) { + exception = e; + continue; + } + } + + // We want only one table of compact decimal formats, so if we get here, stop consuming. + // The data.isEmpty() check will prevent further bundles from being traversed. + return; + } + } + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java new file mode 100644 index 0000000000..b7575cc0e2 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java @@ -0,0 +1,299 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import java.math.BigDecimal; + +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.number.AffixPatternUtils; +import com.ibm.icu.impl.number.PNAffixGenerator; +import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.impl.number.Rounder; +import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier; +import com.ibm.icu.impl.number.rounders.IncrementRounder; +import com.ibm.icu.impl.number.rounders.MagnitudeRounder; +import com.ibm.icu.text.CurrencyPluralInfo; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Currency.CurrencyUsage; + +public class CurrencyFormat { + + public enum CurrencyStyle { + SYMBOL, + ISO_CODE; + } + + public static interface ICurrencyProperties { + static Currency DEFAULT_CURRENCY = null; + + /** @see #setCurrency */ + public Currency getCurrency(); + + /** + * Use the specified currency to substitute currency placeholders ('¤') in the pattern string. + * + * @param currency The currency. + * @return The property bag, for chaining. + */ + public IProperties setCurrency(Currency currency); + + static CurrencyStyle DEFAULT_CURRENCY_STYLE = null; + + /** @see #setCurrencyStyle */ + public CurrencyStyle getCurrencyStyle(); + + /** + * Use the specified {@link CurrencyStyle} to replace currency placeholders ('¤'). + * CurrencyStyle.SYMBOL will use the short currency symbol, like "$" or "€", whereas + * CurrencyStyle.ISO_CODE will use the ISO 4217 currency code, like "USD" or "EUR". + * + *

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. + * + *

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 threadLocalProperties = + new ThreadLocal() { + @Override + protected Properties initialValue() { + return new Properties(); + } + }; + + public static Rounder getCurrencyRounder(DecimalFormatSymbols symbols, IProperties properties) { + Properties cprops = threadLocalProperties.get().clear(); + populateCurrencyRounderProperties(cprops, symbols, properties); + if (cprops.getRoundingIncrement() != null) { + return IncrementRounder.getInstance(cprops); + } else { + return MagnitudeRounder.getInstance(cprops); + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MagnitudeMultiplier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MagnitudeMultiplier.java new file mode 100644 index 0000000000..6e19c7d368 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MagnitudeMultiplier.java @@ -0,0 +1,59 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.Format.BeforeFormat; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.Properties; + +public class MagnitudeMultiplier extends Format.BeforeFormat { + private static final MagnitudeMultiplier DEFAULT = new MagnitudeMultiplier(0); + + public static interface IProperties { + + static int DEFAULT_MAGNITUDE_MULTIPLIER = 0; + + /** @see #setMagnitudeMultiplier */ + public int getMagnitudeMultiplier(); + + /** + * Multiply all numbers by this power of ten before formatting. Negative multipliers reduce the + * magnitude and make numbers smaller (closer to zero). + * + * @param magnitudeMultiplier The number of powers of ten to scale. + * @return The property bag, for chaining. + * @see BigDecimalMultiplier + */ + public IProperties setMagnitudeMultiplier(int magnitudeMultiplier); + } + + public static boolean useMagnitudeMultiplier(IProperties properties) { + return properties.getMagnitudeMultiplier() != IProperties.DEFAULT_MAGNITUDE_MULTIPLIER; + } + + // Properties + final int delta; + + public static BeforeFormat getInstance(Properties properties) { + if (properties.getMagnitudeMultiplier() == 0) { + return DEFAULT; + } + return new MagnitudeMultiplier(properties.getMagnitudeMultiplier()); + } + + private MagnitudeMultiplier(int delta) { + this.delta = delta; + } + + @Override + public void before(FormatQuantity input, ModifierHolder mods) { + input.adjustMagnitude(delta); + } + + @Override + public void export(Properties properties) { + properties.setMagnitudeMultiplier(delta); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MeasureFormat.java new file mode 100644 index 0000000000..752dc0af98 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MeasureFormat.java @@ -0,0 +1,73 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier; +import com.ibm.icu.impl.number.modifiers.SimpleModifier; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.MeasureFormat.FormatWidth; +import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.ULocale; + +public class MeasureFormat { + + public static interface IProperties { + + static MeasureUnit DEFAULT_MEASURE_UNIT = null; + + /** @see #setMeasureUnit */ + public MeasureUnit getMeasureUnit(); + + /** + * Apply prefixes and suffixes for the specified {@link MeasureUnit} to the formatted number. + * + * @param measureUnit The measure unit. + * @return The property bag, for chaining. + */ + public IProperties setMeasureUnit(MeasureUnit measureUnit); + + static FormatWidth DEFAULT_MEASURE_FORMAT_WIDTH = null; + + /** @see #setMeasureFormatWidth */ + public FormatWidth getMeasureFormatWidth(); + + /** + * Use the specified {@link FormatWidth} when choosing the style of measure unit prefix/suffix. + * + *

Must be used in conjunction with {@link #setMeasureUnit}. + * + * @param measureFormatWidth The width style. Defaults to FormatWidth.WIDE. + * @return The property bag, for chaining. + */ + public IProperties setMeasureFormatWidth(FormatWidth measureFormatWidth); + } + + public static boolean useMeasureFormat(IProperties properties) { + return properties.getMeasureUnit() != IProperties.DEFAULT_MEASURE_UNIT; + } + + public static GeneralPluralModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) { + ULocale uloc = symbols.getULocale(); + MeasureUnit unit = properties.getMeasureUnit(); + FormatWidth width = properties.getMeasureFormatWidth(); + + if (unit == null) { + throw new IllegalArgumentException("A measure unit is required for MeasureFormat"); + } + if (width == null) { + width = FormatWidth.WIDE; + } + + // Temporarily, create a MeasureFormat instance for its data loading capability + // TODO: Move data loading directly into this class file + com.ibm.icu.text.MeasureFormat mf = com.ibm.icu.text.MeasureFormat.getInstance(uloc, width); + GeneralPluralModifier mod = new GeneralPluralModifier(); + for (StandardPlural plural : StandardPlural.VALUES) { + String formatString = null; + mf.getPluralFormatter(unit, width, plural.ordinal()); + mod.put(plural, new SimpleModifier(formatString, null, false)); + } + return mod; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PaddingFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PaddingFormat.java new file mode 100644 index 0000000000..5aa3c48c63 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PaddingFormat.java @@ -0,0 +1,173 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import com.ibm.icu.impl.number.Format.AfterFormat; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.impl.number.Properties; + +public class PaddingFormat implements AfterFormat { + public enum PadPosition { + BEFORE_PREFIX, + AFTER_PREFIX, + BEFORE_SUFFIX, + AFTER_SUFFIX; + + public static PadPosition fromOld(int old) { + switch (old) { + case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX: + return PadPosition.BEFORE_PREFIX; + case com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX: + return PadPosition.AFTER_PREFIX; + case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX: + return PadPosition.BEFORE_SUFFIX; + case com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX: + return PadPosition.AFTER_SUFFIX; + default: + throw new IllegalArgumentException("Don't know how to map " + old); + } + } + + public int toOld() { + switch (this) { + case BEFORE_PREFIX: + return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX; + case AFTER_PREFIX: + return com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX; + case BEFORE_SUFFIX: + return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX; + case AFTER_SUFFIX: + return com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX; + default: + return -1; // silence compiler errors + } + } + } + + public static interface IProperties { + + static int DEFAULT_FORMAT_WIDTH = 0; + + /** @see #setFormatWidth */ + public int getFormatWidth(); + + /** + * Sets the minimum width of the string output by the formatting pipeline. For example, if + * padding is enabled and paddingWidth is set to 6, formatting the number "3.14159" with the + * pattern "0.00" will result in "··3.14" if '·' is your padding string. + * + *

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. + * + *

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. + * + *

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. + * + *

Must be used in conjunction with {@link #setFormatWidth}. + * + * @param padPosition The output width. + * @return The property bag, for chaining. + * @see #setFormatWidth + */ + public IProperties setPadPosition(PadPosition padPosition); + } + + public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space + + public static boolean usePadding(IProperties properties) { + return properties.getFormatWidth() != IProperties.DEFAULT_FORMAT_WIDTH; + } + + public static AfterFormat getInstance(IProperties properties) { + return new PaddingFormat( + properties.getFormatWidth(), + properties.getPadString(), + properties.getPadPosition()); + } + + // Properties + private final int paddingWidth; + private final String paddingString; + private final PadPosition paddingLocation; + + private PaddingFormat( + int paddingWidth, String paddingString, PadPosition paddingLocation) { + this.paddingWidth = paddingWidth > 0 ? paddingWidth : 10; // TODO: Is this a sensible default? + this.paddingString = paddingString != null ? paddingString : FALLBACK_PADDING_STRING; + this.paddingLocation = + paddingLocation != null ? paddingLocation : PadPosition.BEFORE_PREFIX; + } + + @Override + public int after(ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) { + + // TODO: Count code points instead of code units? + int requiredPadding = paddingWidth - (rightIndex - leftIndex) - mods.totalLength(); + + if (requiredPadding <= 0) { + // Skip padding, but still apply modifiers to be consistent + return mods.applyAll(string, leftIndex, rightIndex); + } + + int length = 0; + if (paddingLocation == PadPosition.AFTER_PREFIX) { + length += addPadding(requiredPadding, string, leftIndex); + } else if (paddingLocation == PadPosition.BEFORE_SUFFIX) { + length += addPadding(requiredPadding, string, rightIndex); + } + length += mods.applyAll(string, leftIndex, rightIndex + length); + if (paddingLocation == PadPosition.BEFORE_PREFIX) { + length += addPadding(requiredPadding, string, leftIndex); + } else if (paddingLocation == PadPosition.AFTER_SUFFIX) { + length += addPadding(requiredPadding, string, rightIndex + length); + } + + return length; + } + + private int addPadding(int requiredPadding, NumberStringBuilder string, int index) { + for (int i = 0; i < requiredPadding; i++) { + string.insert(index, paddingString, null); + } + return paddingString.length() * requiredPadding; + } + + @Override + public void export(Properties properties) { + properties.setFormatWidth(paddingWidth); + properties.setPadString(paddingString); + properties.setPadPosition(paddingLocation); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java new file mode 100644 index 0000000000..f791ca4674 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java @@ -0,0 +1,227 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.text.NumberFormat.Field; + +public class PositiveDecimalFormat implements Format.TargetFormat { + + public static interface IProperties extends CurrencyFormat.IProperties { + + static int DEFAULT_GROUPING_SIZE = -1; + + /** @see #setGroupingSize */ + public int getGroupingSize(); + + /** + * Sets the number of digits between grouping separators. For example, the en-US 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 hi 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". + * + *

The two levels of grouping separators can be specified in the pattern string. For example, + * the hi 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 en-US. + * + * @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 en-US. Note that + * 1234567 would still be formatted as "1,234,567", not "1234,567". + * + * @param minimumGroupingDigits How many digits must appear before a grouping separator before + * enabling grouping. + * @return The property bag, for chaining. + */ + public IProperties setMinimumGroupingDigits(int minimumGroupingDigits); + } + + public static boolean useGrouping(IProperties properties) { + return properties.getGroupingSize() != IProperties.DEFAULT_GROUPING_SIZE + || properties.getSecondaryGroupingSize() != IProperties.DEFAULT_SECONDARY_GROUPING_SIZE; + } + + public static boolean allowsDecimalPoint(IProperties properties) { + return properties.getDecimalSeparatorAlwaysShown() || properties.getMaximumFractionDigits() != 0; + } + + // Properties + private final boolean alwaysShowDecimal; + private final int groupingSize; + private final int secondaryGroupingSize; + private final int minimumGroupingDigits; + + // Symbols + private final String infinityString; + private final String nanString; + private final String groupingSeparator; + private final String decimalSeparator; + private final String[] digitStrings; + private final int codePointZero; + + public PositiveDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) { + groupingSize = + (properties.getGroupingSize() < 0) + ? properties.getSecondaryGroupingSize() + : properties.getGroupingSize(); + secondaryGroupingSize = + (properties.getSecondaryGroupingSize() < 0) + ? properties.getGroupingSize() + : properties.getSecondaryGroupingSize(); + + minimumGroupingDigits = properties.getMinimumGroupingDigits(); + alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown(); + infinityString = symbols.getInfinity(); + nanString = symbols.getNaN(); + + if (CurrencyFormat.useCurrency(properties)) { + groupingSeparator = symbols.getMonetaryGroupingSeparatorString(); + decimalSeparator = symbols.getMonetaryDecimalSeparatorString(); + } else { + groupingSeparator = symbols.getGroupingSeparatorString(); + decimalSeparator = symbols.getDecimalSeparatorString(); + } + + // Check to see if we can use code points instead of strings (~15% format performance boost) + int _codePointZero = -1; + String[] _digitStrings = symbols.getDigitStringsLocal(); + for (int i = 0; i < _digitStrings.length; i++) { + int cp = Character.codePointAt(_digitStrings[i], 0); + int cc = Character.charCount(cp); + if (cc != _digitStrings[i].length()) { + _codePointZero = -1; + break; + } else if (i == 0) { + _codePointZero = cp; + } else if (cp != _codePointZero + i) { + _codePointZero = -1; + break; + } + } + if (_codePointZero != -1) { + digitStrings = null; + codePointZero = _codePointZero; + } else { + digitStrings = symbols.getDigitStrings(); // makes a copy + codePointZero = -1; + } + } + + @Override + public int target(FormatQuantity input, NumberStringBuilder string, int startIndex) { + int length = 0; + + if (input.isInfinite()) { + length += string.insert(startIndex, infinityString, NumberFormat.Field.INTEGER); + + } else if (input.isNaN()) { + length += string.insert(startIndex, nanString, NumberFormat.Field.INTEGER); + + } else { + // Add the integer digits + length += addIntegerDigits(input, string, startIndex); + + // Add the decimal point + if (input.getLowerDisplayMagnitude() < 0 || alwaysShowDecimal) { + length += string.insert(startIndex + length, decimalSeparator, NumberFormat.Field.DECIMAL_SEPARATOR); + } + + // Add the fraction digits + length += addFractionDigits(input, string, startIndex + length); + } + + return length; + } + + private int addIntegerDigits(FormatQuantity input, NumberStringBuilder string, int startIndex) { + int length = 0; + int integerCount = input.getUpperDisplayMagnitude() + 1; + for (int i = 0; i < integerCount; i++) { + // Add grouping separator + if (groupingSize > 0 && i == groupingSize && integerCount - i >= minimumGroupingDigits) { + length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR); + } else if (secondaryGroupingSize > 0 + && i > groupingSize + && (i - groupingSize) % secondaryGroupingSize == 0) { + length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR); + } + + // Get and append the next digit value + byte nextDigit = input.getDigit(i); + length += addDigit(nextDigit, string, startIndex, NumberFormat.Field.INTEGER); + } + + return length; + } + + private int addFractionDigits(FormatQuantity input, NumberStringBuilder string, int index) { + int length = 0; + int fractionCount = -input.getLowerDisplayMagnitude(); + for (int i = 0; i < fractionCount; i++) { + // Get and append the next digit value + byte nextDigit = input.getDigit(-i - 1); + length += addDigit(nextDigit, string, index + length, NumberFormat.Field.FRACTION); + } + return length; + } + + private int addDigit(byte digit, NumberStringBuilder outputString, int index, Field field) { + if (codePointZero != -1) { + return outputString.insertCodePoint(index, codePointZero + digit, field); + } else { + return outputString.insert(index, digitStrings[digit], field); + } + } + + @Override + public void export(Properties properties) { + properties.setDecimalSeparatorAlwaysShown(alwaysShowDecimal); + properties.setGroupingSize(groupingSize); + properties.setSecondaryGroupingSize(secondaryGroupingSize); + properties.setMinimumGroupingDigits(minimumGroupingDigits); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveNegativeAffixFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveNegativeAffixFormat.java new file mode 100644 index 0000000000..f71374a516 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveNegativeAffixFormat.java @@ -0,0 +1,256 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.PNAffixGenerator; +import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier; +import com.ibm.icu.text.DecimalFormatSymbols; + +/** + * The implementation of this class is a thin wrapper around {@link PNAffixGenerator}, a utility + * used by this and other classes, including {@link CompactDecimalFormat} and {@link Parse}, to + * efficiently convert from the abstract properties in the property bag to actual prefix and suffix + * strings. + */ + +/** + * This class is responsible for adding the positive/negative prefixes and suffixes from the decimal + * format pattern. Properties are set using the following methods: + * + *

    + *
  • {@link IProperties#setPositivePrefix(String)} + *
  • {@link IProperties#setPositiveSuffix(String)} + *
  • {@link IProperties#setNegativePrefix(String)} + *
  • {@link IProperties#setNegativeSuffix(String)} + *
  • {@link IProperties#setPositivePrefixPattern(String)} + *
  • {@link IProperties#setPositiveSuffixPattern(String)} + *
  • {@link IProperties#setNegativePrefixPattern(String)} + *
  • {@link IProperties#setNegativeSuffixPattern(String)} + *
+ * + * If one of the first four methods is used (those of the form setXxxYyy), the value + * will be interpreted literally. If one of the second four methods is used (those of the form + * setXxxYyyPattern), 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. + * + *

Literal characters can be used in the setXxxYyyPattern methods by using quotes; + * for example, to display a literal "%" sign, you can set the pattern '%'. To display + * a literal quote, use two quotes in a row, like ''. + * + *

If a value is set in both a setXxxYyy method and in the corresponding + * setXxxYyyPattern method, the one set in setXxxYyy takes precedence. + * + *

For more information on formatting currencies, see {@link CurrencyFormat}. + * + *

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 p, then the number 123 will be + * formatted as "p123" in the locale en-US. + * + *

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 p, then the number 123 will be + * formatted as "123p" in the locale en-US. + * + *

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 n, then the number -123 will be + * formatted as "n123" in the locale en-US. Note that if the negative prefix is left unset, + * the locale's minus sign is used. + * + *

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 n, then the number -123 will be formatted + * as "-123n" in the locale en-US. 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. + * + *

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). + * + *

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). + * + *

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). + * + *

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). + * + *

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. + * + *

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 #;#- is + * used, then formatting 123 would result in "123+" in the locale en-US. + * + *

This method should be used instead of setting the positive prefix/suffix. The + * behavior is undefined if alwaysShowPlusSign is set but the positive prefix/suffix already + * contains a plus sign. + * + * @param plusSignAlwaysShown Whether positive numbers should display a plus sign. + * @return The property bag, for chaining. + */ + public IProperties setPlusSignAlwaysShown(boolean plusSignAlwaysShown); + } + + public static PositiveNegativeAffixModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) { + PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance(); + PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties); + return new PositiveNegativeAffixModifier(result.positive, result.negative); + } + + // TODO: Investigate static interface methods (Java 8 only?) + public static void apply( + FormatQuantity input, + ModifierHolder mods, + DecimalFormatSymbols symbols, + IProperties properties) { + PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance(); + PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties); + if (input.isNegative()) { + mods.add(result.negative); + } else { + mods.add(result.positive); + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RangeFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RangeFormat.java new file mode 100644 index 0000000000..7c72e62637 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RangeFormat.java @@ -0,0 +1,58 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +// THIS CLASS IS A PROOF OF CONCEPT ONLY. +// IT REQUIRES ADDITIONAL DISCUSION ABOUT ITS DESIGN AND IMPLEMENTATION. + +package com.ibm.icu.impl.number.formatters; + +import java.util.Deque; + +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.NumberStringBuilder; + +public class RangeFormat extends Format { + // Primary settings + private final String separator; + + // Child formatters + private final Format left; + private final Format right; + + public RangeFormat(Format left, Format right, String separator) { + this.separator = separator; // TODO: This would be loaded from locale data. + this.left = left; + this.right = right; + + if (left == null || right == null) { + throw new IllegalArgumentException("Both child formatters are required for RangeFormat"); + } + } + + @Override + public int process( + Deque inputs, + ModifierHolder mods, + NumberStringBuilder string, + int startIndex) { + ModifierHolder lMods = new ModifierHolder(); + ModifierHolder rMods = new ModifierHolder(); + int lLen = left.process(inputs, lMods, string, startIndex); + int rLen = right.process(inputs, rMods, string, startIndex + lLen); + + // Bubble up any modifiers that are shared between the two sides + while (lMods.peekLast() != null && lMods.peekLast() == rMods.peekLast()) { + mods.add(lMods.removeLast()); + rMods.removeLast(); + } + + // Apply the remaining modifiers + lLen += lMods.applyAll(string, startIndex, startIndex + lLen); + rLen += rMods.applyAll(string, startIndex + lLen, startIndex + lLen + rLen); + + int sLen = string.insert(startIndex + lLen, separator, null); + + return lLen + sLen + rLen; + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RoundingFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RoundingFormat.java new file mode 100644 index 0000000000..a57caf4c06 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RoundingFormat.java @@ -0,0 +1,41 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import com.ibm.icu.impl.number.Rounder; +import com.ibm.icu.impl.number.Rounder.IBasicRoundingProperties; +import com.ibm.icu.impl.number.rounders.IncrementRounder; +import com.ibm.icu.impl.number.rounders.MagnitudeRounder; +import com.ibm.icu.impl.number.rounders.NoRounder; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder; + +// TODO: Figure out a better place to put these methods. + +public class RoundingFormat { + + public static interface IProperties + extends IBasicRoundingProperties, + IncrementRounder.IProperties, + MagnitudeRounder.IProperties, + SignificantDigitsRounder.IProperties {} + + public static Rounder getDefaultOrNoRounder(IProperties properties) { + Rounder candidate = getDefaultOrNull(properties); + if (candidate == null) { + candidate = NoRounder.getInstance(properties); + } + return candidate; + } + + public static Rounder getDefaultOrNull(IProperties properties) { + if (SignificantDigitsRounder.useSignificantDigits(properties)) { + return SignificantDigitsRounder.getInstance(properties); + } else if (IncrementRounder.useRoundingIncrement(properties)) { + return IncrementRounder.getInstance(properties); + } else if (MagnitudeRounder.useFractionFormat(properties)) { + return MagnitudeRounder.getInstance(properties); + } else { + return null; + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/ScientificFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/ScientificFormat.java new file mode 100644 index 0000000000..4f67b4e123 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/ScientificFormat.java @@ -0,0 +1,233 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.FormatQuantitySelector; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.impl.number.Rounder; +import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier; +import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier; +import com.ibm.icu.impl.number.rounders.IncrementRounder; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.NumberFormat; + +public class ScientificFormat extends Format.BeforeFormat implements Rounder.MultiplierGenerator { + + public static interface IProperties + extends RoundingFormat.IProperties, CurrencyFormat.IProperties { + + static boolean DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN = false; + + /** @see #setExponentSignAlwaysShown */ + public boolean getExponentSignAlwaysShown(); + + /** + * Sets whether to show the plus sign in the exponent part of numbers with a zero or positive + * exponent. For example, the number "1200" with the pattern "0.0E0" would be formatted as + * "1.2E+3" instead of "1.2E3" in en-US. + * + * @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 + * en-US. + * + * @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 threadLocalProperties = + new ThreadLocal() { + @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 threadLocalStringBuilder = + new ThreadLocal() { + @Override + protected StringBuilder initialValue() { + return new StringBuilder(); + } + }; + + @Override + public void before(FormatQuantity input, ModifierHolder mods) { + + // Treat zero as if it had magnitude 0 + int exponent; + if (input.isZero()) { + rounder.apply(input); + exponent = 0; + } else { + exponent = -rounder.chooseMultiplierAndApply(input, this); + } + + // Format the exponent part of the scientific format. + // Insert digits starting from the left so that append can be used. + // TODO: Use thread locals here. + FormatQuantity exponentQ = FormatQuantitySelector.from(exponent); + StringBuilder exponentSB = threadLocalStringBuilder.get(); + exponentSB.setLength(0); + exponentQ.setIntegerFractionLength(exponentDigits, Integer.MAX_VALUE, 0, 0); + for (int i = exponentQ.getUpperDisplayMagnitude(); i >= 0; i--) { + exponentSB.append(digitStrings[exponentQ.getDigit(i)]); + } + + // Add modifiers from the outside in. + mods.add( + new ConstantAffixModifier("", exponentSB.toString(), NumberFormat.Field.EXPONENT, true)); + mods.add(signMod.getModifier(exponent < 0)); + mods.add(separatorMod); + } + + @Override + public int getMultiplier(int magnitude) { + int digitsShown = ((magnitude % interval + interval) % interval) + 1; + if (digitsShown < minInt) { + digitsShown = minInt; + } else if (digitsShown > maxInt) { + digitsShown = maxInt; + } + int retval = digitsShown - magnitude - 1; + return retval; + } + + @Override + public void export(Properties properties) { + properties.setMinimumExponentDigits(exponentDigits); + properties.setExponentSignAlwaysShown(exponentShowPlusSign); + + // Set the transformed object into the property bag. This may result in a pattern string that + // uses different syntax from the original, but it will be functionally equivalent. + rounder.export(properties); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/StrongAffixFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/StrongAffixFormat.java new file mode 100644 index 0000000000..c70352d6e7 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/StrongAffixFormat.java @@ -0,0 +1,48 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.formatters; + +import java.util.Deque; + +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.impl.number.Properties; + +// TODO: This class isn't currently being used anywhere. Consider removing it. + +/** Attaches all prefixes and suffixes at this point in the render tree without bubbling up. */ +public class StrongAffixFormat extends Format implements Format.AfterFormat { + private final Format child; + + public StrongAffixFormat(Format child) { + this.child = child; + + if (child == null) { + throw new IllegalArgumentException("A child formatter is required for StrongAffixFormat"); + } + } + + @Override + public int process( + Deque inputs, + ModifierHolder mods, + NumberStringBuilder string, + int startIndex) { + int length = child.process(inputs, mods, string, startIndex); + length += mods.applyAll(string, startIndex, startIndex + length); + return length; + } + + @Override + public int after( + ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) { + return mods.applyAll(string, leftIndex, rightIndex); + } + + @Override + public void export(Properties properties) { + // Nothing to do. + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantAffixModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantAffixModifier.java new file mode 100644 index 0000000000..b133bc4872 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantAffixModifier.java @@ -0,0 +1,105 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.modifiers; + +import com.ibm.icu.impl.number.Modifier; +import com.ibm.icu.impl.number.Modifier.AffixModifier; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.text.NumberFormat.Field; + +/** The canonical implementation of {@link Modifier}, containing a prefix and suffix string. */ +public class ConstantAffixModifier extends Modifier.BaseModifier implements AffixModifier { + + // TODO: Avoid making a new instance by default if prefix and suffix are empty + public static final AffixModifier EMPTY = new ConstantAffixModifier(); + + private final String prefix; + private final String suffix; + private final Field field; + private final boolean strong; + + /** + * Constructs an instance with the given strings. + * + *

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( + "", length(), prefix, suffix); + } + + @Override + public void export(Properties properties) { + throw new UnsupportedOperationException(); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantMultiFieldModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantMultiFieldModifier.java new file mode 100644 index 0000000000..e7ed0a6123 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantMultiFieldModifier.java @@ -0,0 +1,93 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.modifiers; + +import com.ibm.icu.impl.number.Modifier; +import com.ibm.icu.impl.number.Modifier.AffixModifier; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.text.NumberFormat.Field; + +/** + * An implementation of {@link Modifier} that allows for multiple types of fields in the same + * modifier. Constructed based on the contents of two {@link NumberStringBuilder} instances (one for + * the prefix, one for the suffix). + */ +public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements AffixModifier { + + // TODO: Avoid making a new instance by default if prefix and suffix are empty + public static final ConstantMultiFieldModifier EMPTY = new ConstantMultiFieldModifier(); + + private final char[] prefixChars; + private final char[] suffixChars; + private final Field[] prefixFields; + private final Field[] suffixFields; + private final String prefix; + private final String suffix; + private final boolean strong; + + public ConstantMultiFieldModifier( + NumberStringBuilder prefix, NumberStringBuilder suffix, boolean strong) { + prefixChars = prefix.toCharArray(); + suffixChars = suffix.toCharArray(); + prefixFields = prefix.toFieldArray(); + suffixFields = suffix.toFieldArray(); + this.prefix = new String(prefixChars); + this.suffix = new String(suffixChars); + this.strong = strong; + } + + private ConstantMultiFieldModifier() { + prefixChars = new char[0]; + suffixChars = new char[0]; + prefixFields = new Field[0]; + suffixFields = new Field[0]; + prefix = ""; + suffix = ""; + strong = false; + } + + @Override + public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { + // Insert the suffix first since inserting the prefix will change the rightIndex + int length = output.insert(rightIndex, suffixChars, suffixFields); + length += output.insert(leftIndex, prefixChars, prefixFields); + return length; + } + + @Override + public int length() { + return prefixChars.length + suffixChars.length; + } + + @Override + public boolean isStrong() { + return strong; + } + + @Override + public String getPrefix() { + return prefix; + } + + @Override + public String getSuffix() { + return suffix; + } + + public boolean contentEquals(NumberStringBuilder prefix, NumberStringBuilder suffix) { + return prefix.contentEquals(prefixChars, prefixFields) + && suffix.contentEquals(suffixChars, suffixFields); + } + + @Override + public String toString() { + return String.format( + "", length(), prefix, suffix); + } + + @Override + public void export(Properties properties) { + throw new UnsupportedOperationException(); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java new file mode 100644 index 0000000000..3dfeefa405 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java @@ -0,0 +1,76 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.modifiers; + +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.Modifier; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.text.PluralRules; + +// TODO: Is it okay that this class is not completely immutable? Right now it is internal-only. +// Freezable or Builder could be used if necessary. + +/** + * A basic implementation of {@link com.ibm.icu.impl.number.Modifier.PositiveNegativePluralModifier} + * that is built on the fly using its put methods. + */ +public class GeneralPluralModifier extends Format.BeforeFormat + implements Modifier.PositiveNegativePluralModifier { + /** + * A single array for modifiers. Even elements are positive; odd elements are negative. The + * elements 2i and 2i+1 belong to the StandardPlural with ordinal i. + */ + private final Modifier[] mods; + + public GeneralPluralModifier() { + this.mods = new Modifier[StandardPlural.COUNT * 2]; + } + + /** Adds a positive/negative-agnostic modifier for the specified plural form. */ + public void put(StandardPlural plural, Modifier modifier) { + put(plural, modifier, modifier); + } + + /** Adds a positive and a negative modifier for the specified plural form. */ + public void put(StandardPlural plural, Modifier positive, Modifier negative) { + assert mods[plural.ordinal() * 2] == null; + assert mods[plural.ordinal() * 2 + 1] == null; + assert positive != null; + assert negative != null; + mods[plural.ordinal() * 2] = positive; + mods[plural.ordinal() * 2 + 1] = negative; + } + + @Override + public Modifier getModifier(StandardPlural plural, boolean isNegative) { + Modifier mod = mods[plural.ordinal() * 2 + (isNegative ? 1 : 0)]; + if (mod == null) { + mod = mods[StandardPlural.OTHER.ordinal()*2 + (isNegative ? 1 : 0)]; + } + if (mod == null) { + throw new UnsupportedOperationException(); + } + return mod; + } + + @Override + public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) { + mods.add(getModifier(input.getStandardPlural(rules), input.isNegative())); + } + + @Override + public void before(FormatQuantity input, ModifierHolder mods) { + throw new UnsupportedOperationException(); + } + + @Override + public void export(Properties properties) { + // Since we can export only one affix pair, do the one for "other". + Modifier positive = getModifier(StandardPlural.OTHER, false); + Modifier negative = getModifier(StandardPlural.OTHER, true); + PositiveNegativeAffixModifier.exportPositiveNegative(properties, positive, negative); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java new file mode 100644 index 0000000000..1384b7bbdf --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java @@ -0,0 +1,53 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.modifiers; + +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.Modifier; +import com.ibm.icu.impl.number.Modifier.AffixModifier; +import com.ibm.icu.impl.number.ModifierHolder; +import com.ibm.icu.impl.number.Properties; + +/** A class containing a positive form and a negative form of {@link ConstantAffixModifier}. */ +public class PositiveNegativeAffixModifier extends Format.BeforeFormat + implements Modifier.PositiveNegativeModifier { + private final AffixModifier positive; + private final AffixModifier negative; + + /** + * Constructs an instance using the two {@link ConstantMultiFieldModifier} classes for positive + * and negative. + * + * @param positive The positive-form Modifier. + * @param negative The negative-form Modifier. + */ + public PositiveNegativeAffixModifier(AffixModifier positive, AffixModifier negative) { + this.positive = positive; + this.negative = negative; + } + + @Override + public Modifier getModifier(boolean isNegative) { + return isNegative ? negative : positive; + } + + @Override + public void before(FormatQuantity input, ModifierHolder mods) { + Modifier mod = getModifier(input.isNegative()); + mods.add(mod); + } + + @Override + public void export(Properties properties) { + exportPositiveNegative(properties, positive, negative); + } + + /** Internal method used to export a positive and negative modifier to a property bag. */ + static void exportPositiveNegative(Properties properties, Modifier positive, Modifier negative) { + properties.setPositivePrefix(positive.getPrefix().isEmpty() ? null : positive.getPrefix()); + properties.setPositiveSuffix(positive.getSuffix().isEmpty() ? null : positive.getSuffix()); + properties.setNegativePrefix(negative.getPrefix().isEmpty() ? null : negative.getPrefix()); + properties.setNegativeSuffix(negative.getSuffix().isEmpty() ? null : negative.getSuffix()); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/SimpleModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/SimpleModifier.java new file mode 100644 index 0000000000..23a15f44aa --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/SimpleModifier.java @@ -0,0 +1,130 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.modifiers; + +import com.ibm.icu.impl.SimpleFormatterImpl; +import com.ibm.icu.impl.number.Modifier; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.text.NumberFormat.Field; + +/** + * The second primary implementation of {@link Modifier}, this one consuming a {@link + * com.ibm.icu.text.SimpleFormatter} pattern. + */ +public class SimpleModifier extends Modifier.BaseModifier { + private final String compiledPattern; + private final Field field; + private final boolean strong; + + /** Creates a modifier that uses the SimpleFormatter string formats. */ + public SimpleModifier(String compiledPattern, Field field, boolean strong) { + this.compiledPattern = (compiledPattern == null) ? "\u0001\u0000" : compiledPattern; + this.field = field; + this.strong = strong; + } + + @Override + public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { + return formatAsPrefixSuffix(compiledPattern, output, leftIndex, rightIndex, field); + } + + @Override + public int length() { + // TODO: Make a separate method for computing the length only? + return formatAsPrefixSuffix(compiledPattern, null, -1, -1, field); + } + + @Override + public boolean isStrong() { + return strong; + } + + @Override + public String getPrefix() { + // TODO: Implement this when MeasureFormat is ready. + throw new UnsupportedOperationException(); + } + + @Override + public String getSuffix() { + // TODO: Implement this when MeasureFormat is ready. + throw new UnsupportedOperationException(); + } + + /** + * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is + * because DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it + * should not depend on it. + * + *

Formats a value that is already stored inside the StringBuilder result between + * the indices startIndex and endIndex by inserting characters before + * the start index and after the end index. + * + *

This is well-defined only for patterns with exactly one argument. + * + * @param compiledPattern Compiled form of a pattern string. + * @param result The StringBuilder containing the value argument. + * @param startIndex The left index of the value within the string builder. + * @param endIndex The right index of the value within the string builder. + * @return The number of characters (UTF-16 code points) that were added to the StringBuilder. + */ + public static int formatAsPrefixSuffix( + String compiledPattern, + NumberStringBuilder result, + int startIndex, + int endIndex, + Field field) { + assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 1; + int ARG_NUM_LIMIT = 0x100; + int length = 0, offset = 2; + if (compiledPattern.charAt(1) != '\u0000') { + int prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT; + if (result != null) { + result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field); + } + length += prefixLength; + offset = 3 + prefixLength; + } + if (offset < compiledPattern.length()) { + int suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT; + if (result != null) { + result.insert( + endIndex + length, compiledPattern, offset + 1, offset + suffixLength + 1, field); + } + length += suffixLength; + } + return length; + } + + /** TODO: Move this to a test file somewhere, once we figure out what to do with the method. */ + public static void testFormatAsPrefixSuffix() { + String[] patterns = {"{0}", "X{0}Y", "XX{0}YYY", "{0}YY", "XXXX{0}"}; + Object[][] outputs = {{"", 0, 0}, {"abcde", 0, 0}, {"abcde", 2, 2}, {"abcde", 1, 3}}; + String[][] expecteds = { + {"", "XY", "XXYYY", "YY", "XXXX"}, + {"abcde", "XYabcde", "XXYYYabcde", "YYabcde", "XXXXabcde"}, + {"abcde", "abXYcde", "abXXYYYcde", "abYYcde", "abXXXXcde"}, + {"abcde", "aXbcYde", "aXXbcYYYde", "abcYYde", "aXXXXbcde"} + }; + for (int i = 0; i < patterns.length; i++) { + for (int j = 0; j < outputs.length; j++) { + String pattern = patterns[i]; + String compiledPattern = + SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, new StringBuilder(), 1, 1); + NumberStringBuilder output = new NumberStringBuilder(); + output.append((String) outputs[j][0], null); + formatAsPrefixSuffix( + compiledPattern, output, (Integer) outputs[j][1], (Integer) outputs[j][2], null); + String expected = expecteds[j][i]; + String actual = output.toString(); + assert expected.equals(actual); + } + } + } + + @Override + public void export(Properties properties) { + throw new UnsupportedOperationException(); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/IncrementRounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/IncrementRounder.java new file mode 100644 index 0000000000..01ba69ce75 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/IncrementRounder.java @@ -0,0 +1,67 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.rounders; + +import java.math.BigDecimal; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.impl.number.Rounder; + +public class IncrementRounder extends Rounder { + + public static interface IProperties extends IBasicRoundingProperties { + + static BigDecimal DEFAULT_ROUNDING_INCREMENT = null; + + /** @see #setRoundingIncrement */ + public BigDecimal getRoundingIncrement(); + + /** + * Sets the increment to which to round numbers. For example, with a rounding interval of 0.05, + * the number 11.17 would be formatted as "11.15" in locale en-US with the default + * rounding mode. + * + *

You can use either a rounding increment or significant digits, but not both at the same + * time. + * + *

The rounding increment can be specified in a pattern string. For example, the pattern + * "#,##0.05" corresponds to a rounding interval of 0.05 with 1 minimum integer digit and a + * grouping size of 3. + * + * @param roundingIncrement The interval to which to round. + * @return The property bag, for chaining. + */ + public IProperties setRoundingIncrement(BigDecimal roundingIncrement); + } + + public static boolean useRoundingIncrement(IProperties properties) { + return properties.getRoundingIncrement() != IProperties.DEFAULT_ROUNDING_INCREMENT; + } + + private final BigDecimal roundingIncrement; + + public static IncrementRounder getInstance(IProperties properties) { + return new IncrementRounder(properties); + } + + private IncrementRounder(IProperties properties) { + super(properties); + if (properties.getRoundingIncrement().compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Rounding interval must be greater than zero"); + } + roundingIncrement = properties.getRoundingIncrement(); + } + + @Override + public void apply(FormatQuantity input) { + input.roundToIncrement(roundingIncrement, mathContext); + applyDefaults(input); + } + + @Override + public void export(Properties properties) { + super.export(properties); + properties.setRoundingIncrement(roundingIncrement); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/MagnitudeRounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/MagnitudeRounder.java new file mode 100644 index 0000000000..d53f966fa0 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/MagnitudeRounder.java @@ -0,0 +1,30 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.rounders; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.Rounder; + +public class MagnitudeRounder extends Rounder { + + public static interface IProperties extends IBasicRoundingProperties {} + + public static boolean useFractionFormat(IProperties properties) { + return properties.getMinimumFractionDigits() != IProperties.DEFAULT_MINIMUM_FRACTION_DIGITS + || properties.getMaximumFractionDigits() != IProperties.DEFAULT_MAXIMUM_FRACTION_DIGITS; + } + + public static MagnitudeRounder getInstance(IBasicRoundingProperties properties) { + return new MagnitudeRounder(properties); + } + + private MagnitudeRounder(IBasicRoundingProperties properties) { + super(properties); + } + + @Override + public void apply(FormatQuantity input) { + input.roundToMagnitude(-maxFrac, mathContext); + applyDefaults(input); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/NoRounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/NoRounder.java new file mode 100644 index 0000000000..814e11e997 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/NoRounder.java @@ -0,0 +1,24 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.rounders; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.Rounder; + +/** Sets the integer and fraction length based on the properties, but does not perform rounding. */ +public final class NoRounder extends Rounder { + + public static NoRounder getInstance(IBasicRoundingProperties properties) { + return new NoRounder(properties); + } + + private NoRounder(IBasicRoundingProperties properties) { + super(properties); + } + + @Override + public void apply(FormatQuantity input) { + applyDefaults(input); + input.roundToInfinity(); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java new file mode 100644 index 0000000000..3a5554ac36 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java @@ -0,0 +1,210 @@ +// © 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 { + + 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 en-US. Note that minimum significant digits is relevant only when numbers + * have digits after the decimal point. + * + *

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". + * + *

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 + * en-US with the default rounding mode. + * + *

If both maximum significant digits and maximum integer/fraction digits are set at the same + * time, the behavior is undefined. + * + *

The number of significant digits can be specified in a pattern string using the '@' + * character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3 + * significant digits. + * + * @param maximumSignificantDigits The maximum number of significant digits to display. + * @return The property bag, for chaining. + */ + public IProperties setMaximumSignificantDigits(int maximumSignificantDigits); + + static SignificantDigitsMode DEFAULT_SIGNIFICANT_DIGITS_MODE = null; + + /** @see #setSignificantDigitsMode */ + public SignificantDigitsMode getSignificantDigitsMode(); + + /** + * Sets the strategy used when reconciling significant digits versus integer and fraction + * lengths. + * + * @param significantDigitsMode One of the options from {@link SignificantDigitsMode}. + * @return The property bag, for chaining. + */ + public IProperties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode); + } + + public static boolean useSignificantDigits(IProperties properties) { + return properties.getMinimumSignificantDigits() + != IProperties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS + || properties.getMaximumSignificantDigits() + != IProperties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS + || properties.getSignificantDigitsMode() != IProperties.DEFAULT_SIGNIFICANT_DIGITS_MODE; + } + + public static SignificantDigitsRounder getInstance(IProperties properties) { + return new SignificantDigitsRounder(properties); + } + + private final int minSig; + private final int maxSig; + private final SignificantDigitsMode mode; + + private SignificantDigitsRounder(IProperties properties) { + super(properties); + int _minSig = properties.getMinimumSignificantDigits(); + int _maxSig = properties.getMaximumSignificantDigits(); + minSig = _minSig < 1 ? 1 : _minSig > 1000 ? 1000 : _minSig; + maxSig = _maxSig < 0 ? 1000 : _maxSig < minSig ? minSig : _maxSig > 1000 ? 1000 : _maxSig; + SignificantDigitsMode _mode = properties.getSignificantDigitsMode(); + mode = _mode == null ? SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION : _mode; + } + + @Override + public void apply(FormatQuantity input) { + + int magnitude, effectiveMag, magMinSig, magMaxSig; + + if (input.isZero()) { + // Treat zero as if magnitude corresponded to the minimum number of zeros + magnitude = minInt - 1; + } else { + magnitude = input.getMagnitude(); + } + effectiveMag = Math.min(magnitude + 1, maxInt); + magMinSig = effectiveMag - minSig; + magMaxSig = effectiveMag - maxSig; + + // Step 1: pick the rounding magnitude and apply. + int roundingMagnitude; + switch (mode) { + case OVERRIDE_MAXIMUM_FRACTION: + // Always round to maxSig. + // Of the six possible orders: + // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins + // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins + // Case 3: minSig, minFrac, maxFrac, maxSig -- maxSig wins + // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins + // Case 5: minFrac, minSig, maxFrac, maxSig -- maxSig wins + // Case 6: minFrac, maxFrac, minSig, maxSig -- maxSig wins + roundingMagnitude = magMaxSig; + break; + case RESPECT_MAXIMUM_FRACTION: + // Round to the strongest of maxFrac, maxInt, and maxSig. + // Of the six possible orders: + // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins + // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins + // Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default + // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins + // Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default + // Case 6: minFrac, maxFrac, minSig, maxSig -- maxFrac wins --> differs from default + // + // Math.max() picks the rounding magnitude farthest to the left (most significant). + // Math.min() picks the rounding magnitude farthest to the right (least significant). + roundingMagnitude = Math.max(-maxFrac, magMaxSig); + break; + case ENSURE_MINIMUM_SIGNIFICANT: + // Round to the strongest of maxFrac and maxSig, and always ensure minSig. + // Of the six possible orders: + // Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins + // Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins + // Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default + // Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins + // Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default + // Case 6: minFrac, maxFrac, minSig, maxSig -- minSig wins --> differs from default + roundingMagnitude = Math.min(magMinSig, Math.max(-maxFrac, magMaxSig)); + break; + default: + throw new AssertionError(); + } + input.roundToMagnitude(roundingMagnitude, mathContext); + + // In case magnitude changed: + if (input.isZero()) { + magnitude = minInt - 1; + } else { + magnitude = input.getMagnitude(); + } + effectiveMag = Math.min(magnitude + 1, maxInt); + magMinSig = effectiveMag - minSig; + magMaxSig = effectiveMag - maxSig; + + // Step 2: pick the number of visible digits. + switch (mode) { + case OVERRIDE_MAXIMUM_FRACTION: + // Ensure minSig is always displayed. + input.setIntegerFractionLength( + minInt, maxInt, Math.max(minFrac, -magMinSig), Integer.MAX_VALUE); + break; + case RESPECT_MAXIMUM_FRACTION: + // Ensure minSig is displayed, unless doing so is in violation of maxFrac. + input.setIntegerFractionLength( + minInt, maxInt, Math.min(maxFrac, Math.max(minFrac, -magMinSig)), maxFrac); + break; + case ENSURE_MINIMUM_SIGNIFICANT: + // Follow minInt/minFrac, but ensure all digits are allowed to be visible. + input.setIntegerFractionLength(minInt, maxInt, minFrac, Integer.MAX_VALUE); + break; + } + } + + @Override + public void export(Properties properties) { + super.export(properties); + properties.setMinimumSignificantDigits(minSig); + properties.setMaximumSignificantDigits(maxSig); + properties.setSignificantDigitsMode(mode); + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java deleted file mode 100644 index 8d302a559c..0000000000 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java +++ /dev/null @@ -1,524 +0,0 @@ -// © 2016 and later: Unicode, Inc. and others. -// License & terms of use: http://www.unicode.org/copyright.html#License -/* - ******************************************************************************* - * Copyright (C) 2012-2016, International Business Machines Corporation and - * others. All Rights Reserved. - ******************************************************************************* - */ -package com.ibm.icu.text; - -import java.util.HashMap; -import java.util.Map; -import java.util.MissingResourceException; - -import com.ibm.icu.impl.ICUCache; -import com.ibm.icu.impl.ICUData; -import com.ibm.icu.impl.ICUResourceBundle; -import com.ibm.icu.impl.SimpleCache; -import com.ibm.icu.impl.UResource; -import com.ibm.icu.text.DecimalFormat.Unit; -import com.ibm.icu.util.ULocale; -import com.ibm.icu.util.UResourceBundle; - -/** - * A cache containing data by locale for {@link CompactDecimalFormat} - * - * @author Travis Keep - */ -class CompactDecimalDataCache { - - private static final String SHORT_STYLE = "short"; - private static final String LONG_STYLE = "long"; - private static final String SHORT_CURRENCY_STYLE = "shortCurrency"; - private static final String NUMBER_ELEMENTS = "NumberElements"; - private static final String PATTERNS_LONG = "patternsLong"; - private static final String PATTERNS_SHORT = "patternsShort"; - private static final String DECIMAL_FORMAT = "decimalFormat"; - private static final String CURRENCY_FORMAT = "currencyFormat"; - private static final String LATIN_NUMBERING_SYSTEM = "latn"; - - private static enum PatternsTableKey { PATTERNS_LONG, PATTERNS_SHORT }; - private static enum FormatsTableKey { DECIMAL_FORMAT, CURRENCY_FORMAT }; - - public static final String OTHER = "other"; - - /** - * We can specify prefixes or suffixes for values with up to 15 digits, - * less than 10^15. - */ - static final int MAX_DIGITS = 15; - - private final ICUCache cache = - new SimpleCache(); - - /** - * 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, divisors[4] - * 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 units; - boolean fromFallback; - - Data(long[] divisors, Map 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()), - new Data(new long[MAX_DIGITS], new HashMap()), - new Data(new long[MAX_DIGITS], new HashMap()) - ); - } - } - - /** - * 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 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 result. - * It looks at each log10 value applying the two rules. - *

- * 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. - *

- * 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. - *

- * - * @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 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 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 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 units, String variant, int base) { - DecimalFormat.Unit[] byBase = units.get(variant); - if (byBase == null) { - byBase = units.get(CompactDecimalDataCache.OTHER); - } - return byBase[base]; - } -} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java index 078f10db57..2f8c6b2fd7 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java @@ -18,44 +18,37 @@ import java.math.BigInteger; import java.text.AttributedCharacterIterator; import java.text.FieldPosition; import java.text.ParsePosition; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Pattern; -import com.ibm.icu.text.CompactDecimalDataCache.Data; -import com.ibm.icu.text.PluralRules.FixedDecimal; -import com.ibm.icu.util.Currency; -import com.ibm.icu.util.CurrencyAmount; -import com.ibm.icu.util.Output; +import com.ibm.icu.impl.number.FormatQuantity4; +import com.ibm.icu.impl.number.Properties; import com.ibm.icu.util.ULocale; /** - * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will limited real estate. - * For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will be appropriate for the given language, - * such as "1,2 Mrd." for German. - *

- * 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. - *

- * 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. - *

- * 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. - *

- * Note that important methods, like setting the number of decimals, will be moved up from DecimalFormat to - * NumberFormat. + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

Note that important methods, like setting the number of decimals, will be moved up from + * DecimalFormat to NumberFormat. * * @author markdavis * @stable ICU 49 @@ -64,513 +57,191 @@ 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 units; - private final Map currencyUnits; - private final long[] divisor; - private final long[] currencyDivisor; - private final Map 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 { /** - * Style parameter for CompactDecimalFormat. + * Short version, like "1.2T" + * * @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 - } - + SHORT, /** - * 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. + * 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 * @stable ICU 50 */ - public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) { - return new CompactDecimalFormat(locale, style); + 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(); + } - /** - * 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); - } + /** + * {@inheritDoc} + * + * @stable ICU 49 + */ + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } - /** - * The public mechanism is CompactDecimalFormat.getInstance(). - * - * @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; + /** + * {@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; + } -// DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); -// // TODO fix to use plural-dependent affixes -// Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix()); -// pluralToCurrencyAffixes = new HashMap(); -// for (String key : pluralRules.getKeywords()) { -// pluralToCurrencyAffixes.put(key, currency); -// } -// // TODO fix to get right symbol for the count + /** + * {@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; + } - finishInit(style, format.toPattern(), format.getDecimalFormatSymbols()); - } + /** + * {@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; + } - /** - * 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 pluralAffixes, Map currencyAffixes, - Collection debugCreationErrors) { + /** + * {@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; + } - 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(); - for (Entry s : currencyAffixes.entrySet()) { - String[] pair = s.getValue(); - pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1])); - } - } - finishInit(style, pattern, formatSymbols); - } + /** + * {@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; + } - 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 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} - * @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); - } +// /** +// * {@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(); +// } - private boolean mapsAreEqual( - Map lhs, Map 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 entry : lhs.entrySet()) { - DecimalFormat.Unit[] value = rhs.get(entry.getKey()); - if (value == null || !Arrays.equals(entry.getValue(), value)) { - return false; - } - } - return true; - } + /** + * Parsing is currently unsupported, and throws an UnsupportedOperationException. + * + * @stable ICU 49 + */ + @Override + public Number parse(String text, ParsePosition parsePosition) { + throw new UnsupportedOperationException(); + } - /** - * {@inheritDoc} - * @stable ICU 49 - */ - @Override - public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { - return format(number, null, toAppendTo, pos); - } + // DISALLOW Serialization, at least while draft - /** - * {@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()); - } + private void writeObject(ObjectOutputStream out) throws IOException { + throw new NotSerializableException(); + } - /** - * {@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 currencyUnit = new Output(); - 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("((?= 0) { - return UNESCAPE_QUOTE.matcher(string).replaceAll("$1"); - } - return string; - } - - private Amount toAmount(double number, Currency curr, Output 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 creationErrors, String errorMessage) { - if (creationErrors == null) { - throw new IllegalArgumentException(errorMessage); - } - creationErrors.add(errorMessage); - } - - /** - * Manufacture the unit list from arrays - */ - private Map otherPluralVariant(Map pluralCategoryToPower10ToAffix, - long[] divisor, Collection 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 result = new HashMap(); - Map seen = new HashMap(); - - String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other"); - - for (Entry 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; - } - } + private void readObject(ObjectInputStream in) throws IOException { + throw new NotSerializableException(); + } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/CurrencyPluralInfo.java b/icu4j/main/classes/core/src/com/ibm/icu/text/CurrencyPluralInfo.java index 055fafe0d0..3e89cdb3f3 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/CurrencyPluralInfo.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/CurrencyPluralInfo.java @@ -148,7 +148,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable { } /** - * Set plural rules. These are initially set in the constructor based on the locale, + * Set plural rules. These are initially set in the constructor based on the locale, * and usually do not need to be changed. * * @param ruleDescription new plural rule description @@ -162,6 +162,10 @@ public class CurrencyPluralInfo implements Cloneable, Serializable { * Set currency plural patterns. These are initially set in the constructor based on the * locale, and usually do not need to be changed. * + * The decimal digits part of the pattern cannot be specified via this method. All plural + * forms will use the same decimal pattern as set in the constructor of DecimalFormat. For + * example, you can't set "0.0" for plural "few" but "0.00" for plural "many". + * * @param pluralCount the plural count for which the currency pattern will * be overridden. * @param pattern the new currency plural pattern @@ -188,6 +192,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable { * * @stable ICU 4.2 */ + @Override public Object clone() { try { CurrencyPluralInfo other = (CurrencyPluralInfo) super.clone(); @@ -213,6 +218,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable { * * @stable ICU 4.2 */ + @Override public boolean equals(Object a) { if (a instanceof CurrencyPluralInfo) { CurrencyPluralInfo other = (CurrencyPluralInfo)a; @@ -221,17 +227,19 @@ public class CurrencyPluralInfo implements Cloneable, Serializable { } return false; } - + /** - * Mock implementation of hashCode(). This implementation always returns a constant - * value. When Java assertion is enabled, this method triggers an assertion failure. + * Override hashCode + * * @internal * @deprecated This API is ICU internal only. */ + @Override @Deprecated public int hashCode() { - assert false : "hashCode not designed"; - return 42; + return pluralCountToCurrencyUnitPattern.hashCode() + ^ pluralRules.hashCode() + ^ ulocale.hashCode(); } /** @@ -273,7 +281,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable { private void setupCurrencyPluralPattern(ULocale uloc) { pluralCountToCurrencyUnitPattern = new HashMap(); - + String numberStylePattern = NumberFormat.getPattern(uloc, NumberFormat.NUMBERSTYLE); // Split the number style pattern into pos and neg if applicable int separatorIndex = numberStylePattern.indexOf(";"); @@ -286,7 +294,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable { for (Map.Entry 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); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java index 23fc81696a..82e0cda2a5 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java @@ -1,6272 +1,2085 @@ -// © 2016 and later: Unicode, Inc. and others. +// © 2017 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.io.ObjectStreamField; import java.math.BigInteger; +import java.math.RoundingMode; import java.text.AttributedCharacterIterator; -import java.text.AttributedString; -import java.text.ChoiceFormat; import java.text.FieldPosition; -import java.text.Format; +import java.text.ParseException; 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.impl.number.AffixPatternUtils; +import com.ibm.icu.impl.number.Endpoint; +import com.ibm.icu.impl.number.Format.SingularFormat; +import com.ibm.icu.impl.number.FormatQuantity4; +import com.ibm.icu.impl.number.Parse; +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.impl.number.formatters.PositiveDecimalFormat; +import com.ibm.icu.impl.number.formatters.ScientificFormat; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode; 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.text.PluralRules.IFixedDecimal; import com.ibm.icu.util.Currency; import com.ibm.icu.util.Currency.CurrencyUsage; import com.ibm.icu.util.CurrencyAmount; import com.ibm.icu.util.ULocale; -import com.ibm.icu.util.ULocale.Category; -/** - * {@icuenhanced java.text.DecimalFormat}.{@icu _usage_} - * - * DecimalFormat 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. - * - *

To obtain a {@link NumberFormat} for a specific locale (including the default - * locale) call one of NumberFormat's factory methods such as {@link - * NumberFormat#getInstance}. Do not call the DecimalFormat constructors - * directly, unless you know what you are doing, since the {@link NumberFormat} factory - * methods may return subclasses other than DecimalFormat. If you need to - * customize the format object, do something like this: - * - *

- * NumberFormat f = NumberFormat.getInstance(loc);
- * if (f instanceof DecimalFormat) {
- *     ((DecimalFormat) f).setDecimalSeparatorAlwaysShown(true);
- * }
- * - *

Example Usage - * - * Print out a number using the localized number, currency, and percent - * format for each locale. - * - *

- * 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) {}
- *     }
- * }
- * - *

Another example use getInstance(style).
- * Print out a number using the localized number, currency, percent, - * scientific, integer, iso currency, and plural currency format for each locale. - * - *

- * 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) {}
- * }
- * - *

Patterns

- * - *

A DecimalFormat consists of a pattern and a set of - * symbols. 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. - * - *

Special Pattern Characters

- * - *

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. - * - *

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. - * - *

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. - * - *

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Symbol - * Location - * Localized? - * Meaning - *
0 - * Number - * Yes - * Digit - *
1-9 - * Number - * Yes - * '1' through '9' indicate rounding. - *
@ - * Number - * No - * Significant digit - *
# - * Number - * Yes - * Digit, zero shows as absent - *
. - * Number - * Yes - * Decimal separator or monetary decimal separator - *
- - * Number - * Yes - * Minus sign - *
, - * Number - * Yes - * Grouping separator - *
E - * Number - * Yes - * Separates mantissa and exponent in scientific notation. - * Need not be quoted in prefix or suffix. - *
+ - * Exponent - * Yes - * Prefix positive exponents with localized plus sign. - * Need not be quoted in prefix or suffix. - *
; - * Subpattern boundary - * Yes - * Separates positive and negative subpatterns - *
% - * Prefix or suffix - * Yes - * Multiply by 100 and show as percentage - *
\u2030 - * Prefix or suffix - * Yes - * Multiply by 1000 and show as per mille - *
¤ (\u00A4) - * Prefix or suffix - * No - * 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. - *
' - * Prefix or suffix - * No - * Used to quote special characters in a prefix or suffix, - * for example, "'#'#" formats 123 to - * "#123". To create a single quote - * itself, use two in a row: "# o''clock". - *
* - * Prefix or suffix boundary - * Yes - * Pad escape, precedes pad character - *
- *
- * - *

A DecimalFormat 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#)". - * - *

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. - * - *

The grouping separator 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 grouping size 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 primary grouping size, and one used for all - * others, the secondary grouping size. 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 "#,##,###,####" == "###,###,####" == - * "##,#,###,####". - * - *

Illegal patterns, such as "#.#.#" or "#.###,###", will cause - * DecimalFormat to throw an {@link IllegalArgumentException} with a message - * that describes the problem. - * - *

Pattern BNF

- * - *
- * 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
- * 
- * The first subpattern is for positive numbers. The second (optional) - * subpattern is for negative numbers. - * - *

Not indicated in the BNF syntax above: - * - *

    - * - *
  • 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. - * - *
  • 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". - * - *
  • The pad specifier padSpec may appear before the prefix, after the - * prefix, before the suffix, after the suffix, or not at all. - * - *
  • In place of '0', the digits '1' through '9' may be used to indicate a rounding - * increment. - * - *
- * - *

Parsing

- * - *

DecimalFormat parses all Unicode characters that represent decimal - * digits, as defined by {@link UCharacter#digit}. In addition, - * DecimalFormat 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. - * - *

During parsing, grouping separators are ignored. - * - *

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". - * - *

If {@link #parse(String, ParsePosition)} fails to parse a string, it returns - * null and leaves the parse position unchanged. The convenience method - * {@link #parse(String)} indicates parse failure by throwing a {@link - * java.text.ParseException}. - * - *

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, - * DecimalFormat 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). - * - *

Formatting

- * - *

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 scientific notation or significant - * digits. - * - *

  • If the number of actual integer digits exceeds the maximum integer - * digits, then only the least significant digits are shown. For example, 1997 is - * formatted as "97" if the maximum integer digits is set to 2. - * - *
  • If the number of actual integer digits is less than the minimum integer - * digits, then leading zeros are added. For example, 1997 is formatted as "01997" - * if the minimum integer digits is set to 5. - * - *
  • If the number of actual fraction digits exceeds the maximum fraction - * digits, 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. - * - *
  • If the number of actual fraction digits is less than the minimum fraction - * digits, then trailing zeros are added. For example, 0.125 is formatted as - * "0.1250" if the mimimum fraction digits is set to 4. - * - *
  • Trailing fractional zeros are not displayed if they occur j positions - * after the decimal, where j 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. - *
- * - *

Special Values - * - *

NaN is represented as a single character, typically - * \uFFFD. This character is determined by the {@link - * DecimalFormatSymbols} object. This is the only value for which the prefixes and - * suffixes are not used. - * - *

Infinity is represented as a single character, typically \u221E, - * with the positive or negative prefixes and suffixes applied. The infinity character is - * determined by the {@link DecimalFormatSymbols} object. - * - *

Scientific Notation

- * - *

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 103. The - * mantissa is typically in the half-open interval [1.0, 10.0) or sometimes [0.0, 1.0), - * but it need not be. DecimalFormat supports arbitrary mantissas. - * DecimalFormat 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". - * - *

    - * - *
  • 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, not 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 '+'.) - * - *
  • 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. - * - *
  • The maximum number of integer digits, if present, specifies the exponent grouping. - * The most common use of this is to generate engineering notation, in which the - * exponent is a multiple of three, e.g., "##0.###E0". The number 12345 is formatted - * using "##0.####E0" as "12.345E3". - * - *
  • 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. - * - *
  • 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 - * minimum integer and maximum fraction 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. - * - *
  • Exponential patterns may not contain grouping separators. - * - *
- * - *

Significant Digits

- * - * DecimalFormat 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: - * - *
- * - * - * - * - * - * - *
Pattern - * Minimum significant digits - * Maximum significant digits - * Number - * Output of format() - *
@@@ - * 3 - * 3 - * 12345 - * 12300 - *
@@@ - * 3 - * 3 - * 0.12345 - * 0.123 - *
@@## - * 2 - * 4 - * 3.14159 - * 3.142 - *
@@## - * 2 - * 4 - * 1.23004 - * 1.23 - *
- *
- * - *
    - * - *
  • Significant digit counts may be expressed using patterns that specify a minimum and - * maximum number of significant digits. These are indicated by the '@' and - * '#' characters. The minimum number of significant digits is the number of - * '@' characters. The maximum number of significant digits is the number of - * '@' characters plus the number of '#' characters following on - * the right. For example, the pattern "@@@" indicates exactly 3 significant - * digits. The pattern "@##" 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 - * "@##" formats the number 0.1203 as "0.12". - * - *
  • If a pattern uses significant digits, it may not contain a decimal separator, nor - * the '0' pattern character. Patterns such as "@00" or - * "@.###" are disallowed. - * - *
  • Any number of '#' characters may be prepended to the left of the - * leftmost '@' character. These have no effect on the minimum and maximum - * significant digits counts, but may be used to position grouping separators. For - * example, "#,#@#" indicates a minimum of one significant digits, a maximum - * of two significant digits, and a grouping size of three. - * - *
  • In order to enable significant digits formatting, use a pattern containing the - * '@' pattern character. Alternatively, call {@link - * #setSignificantDigitsUsed setSignificantDigitsUsed(true)}. - * - *
  • In order to disable significant digits formatting, use a pattern that does not - * contain the '@' pattern character. Alternatively, call {@link - * #setSignificantDigitsUsed setSignificantDigitsUsed(false)}. - * - *
  • The number of significant digits has no effect on parsing. - * - *
  • 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 getMinimumSignificantDigits() - - * 1, and a maximum fraction digit count of getMaximumSignificantDigits() - - * 1. For example, the pattern "@@###E0" is equivalent to - * "0.0###E0". - * - *
  • 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. - * - *
- * - *

Padding

- * - *

DecimalFormat 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, "$*x#,##0.00" formats - * 123 to "$xx123.00", and 1234 to "$1,234.00". - * - *

    - * - *
  • When padding is in effect, the width of the positive subpattern, including prefix - * and suffix, determines the format width. For example, in the pattern "* #0 - * o''clock", the format width is 10. - * - *
  • The width is counted in 16-bit code units (Java chars). - * - *
  • 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. - * - *
  • 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. - * - *
  • When specified in a pattern, the 16-bit char immediately following the - * pad escape is the pad character. This may be any character, including a special pattern - * character. That is, the pad escape escapes the following character. If there - * is no character after the pad escape, then the pattern is illegal. - * - *
- * - *

- * Rounding - * - *

DecimalFormat 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. - * - *

    - * - *
  • Rounding only affects the string produced by formatting. It does not affect - * parsing or change any numerical values. - * - *
  • A rounding mode 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}. - * - *
  • Some locales use rounding in their currency formats to reflect the smallest - * currency denomination. - * - *
  • In a pattern, digits '1' through '9' specify rounding, but otherwise behave - * identically to digit '0'. - * - *
- * - *

Synchronization

- * - *

DecimalFormat objects are not synchronized. Multiple threads should - * not access one formatter concurrently. - * - * @see java.text.Format - * @see NumberFormat - * @author Mark Davis - * @author Alan Liu - * @stable ICU 2.0 - */ +/** @stable ICU 2.0 */ public class DecimalFormat extends NumberFormat { - /** - * Creates a DecimalFormat using the default pattern and symbols for the default - * FORMAT locale. This is a convenient way to obtain a DecimalFormat when - * internationalization is not the main concern. - * - *

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() { - 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. + /** New serialization in ICU 59: declare different version from ICU 58. */ + private static final long serialVersionUID = 864413376551465018L; + + /** + * One non-transient field such that deserialization can determine the version of the class. This + * field has existed since the very earliest versions of DecimalFormat. + */ + @SuppressWarnings("unused") + private final int serialVersionOnStream = 5; + + //=====================================================================================// + // INSTANCE FIELDS // + //=====================================================================================// + + // Fields are package-private, so that subclasses can use them. + // properties should be final, but clone won't work if we make it final. + // All fields are transient because custom serialization is used. + + /** + * The property bag corresponding to user-specified settings and settings from the pattern string. + * In principle this should be final, but serialize and clone won't work if it is final. Does not + * need to be volatile because the reference never changes. + */ + /* final */ transient Properties properties; + + /** + * The symbols for the current locale. Volatile because threads may read and write at the same + * time. + */ + transient volatile DecimalFormatSymbols symbols; + + /** + * The pre-computed formatter object. Setters cause this to be re-computed atomically. The {@link + * #format} method uses the formatter directly without needing to synchronize. Volatile because + * threads may read and write at the same time. + */ + transient volatile SingularFormat formatter; + + /** + * The effective properties as exported from the formatter object. Volatile because threads may + * read and write at the same time. + */ + transient volatile Properties exportedProperties; + + //=====================================================================================// + // CONSTRUCTORS // + //=====================================================================================// + + /** @stable ICU 2.0 */ + public DecimalFormat() { + // Use the locale's default pattern + ULocale def = ULocale.getDefault(ULocale.Category.FORMAT); + String pattern = getPattern(def, 0); + symbols = getDefaultSymbols(); + properties = new Properties(); + exportedProperties = new Properties(); + // Regression: ignore pattern rounding information if the pattern has currency symbols. + boolean ignorePatternRounding = AffixPatternUtils.hasCurrencySymbols(pattern); + setPropertiesFromPattern(pattern, ignorePatternRounding); + refreshFormatter(); + } + + /** @stable ICU 2.0 */ + public DecimalFormat(String pattern) { + symbols = getDefaultSymbols(); + properties = new Properties(); + exportedProperties = new Properties(); + // Regression: ignore pattern rounding information if the pattern has currency symbols. + boolean ignorePatternRounding = AffixPatternUtils.hasCurrencySymbols(pattern); + setPropertiesFromPattern(pattern, ignorePatternRounding); + refreshFormatter(); + } + + /** @stable ICU 2.0 */ + public DecimalFormat(String pattern, DecimalFormatSymbols symbols) { + this.symbols = (DecimalFormatSymbols) symbols.clone(); + properties = new Properties(); + exportedProperties = new Properties(); + // Regression: ignore pattern rounding information if the pattern has currency symbols. + boolean ignorePatternRounding = AffixPatternUtils.hasCurrencySymbols(pattern); + setPropertiesFromPattern(pattern, ignorePatternRounding); + refreshFormatter(); + } + + /** @stable ICU 4.2 */ + public DecimalFormat( + String pattern, DecimalFormatSymbols symbols, CurrencyPluralInfo infoInput, int style) { + this.symbols = (DecimalFormatSymbols) symbols.clone(); + properties = new Properties(); + exportedProperties = new Properties(); + properties.setCurrencyPluralInfo(infoInput); + refreshFormatter(); + } + + /** Package-private constructor used by NumberFormat. */ + DecimalFormat(String pattern, DecimalFormatSymbols symbols, int choice) { + this.symbols = (DecimalFormatSymbols) symbols.clone(); + properties = new Properties(); + exportedProperties = new Properties(); + // If choice is a currency type, ignore the rounding information. + if (choice == CURRENCYSTYLE + || choice == ISOCURRENCYSTYLE + || choice == ACCOUNTINGCURRENCYSTYLE + || choice == CASHCURRENCYSTYLE + || choice == STANDARDCURRENCYSTYLE + || choice == PLURALCURRENCYSTYLE + || AffixPatternUtils.hasCurrencySymbols(pattern)) { + setPropertiesFromPattern(pattern, true); + } else { + setPropertiesFromPattern(pattern, false); + } + refreshFormatter(); + } + + private static DecimalFormatSymbols getDefaultSymbols() { + return DecimalFormatSymbols.getInstance(); + } + + /** + * Parses the given pattern string and overwrites the settings specified in the pattern string. + * The properties corresponding to the following setters are overwritten, either with their + * default values or with the value specified in the pattern string: + * + *

    + *
  1. {@link #setDecimalSeparatorAlwaysShown} + *
  2. {@link #setExponentSignAlwaysShown} + *
  3. {@link #setFormatWidth} + *
  4. {@link #setGroupingSize} + *
  5. {@link #setMultiplier} (percent/permille) + *
  6. {@link #setMaximumFractionDigits} + *
  7. {@link #setMaximumIntegerDigits} + *
  8. {@link #setMaximumSignificantDigits} + *
  9. {@link #setMinimumExponentDigits} + *
  10. {@link #setMinimumFractionDigits} + *
  11. {@link #setMinimumIntegerDigits} + *
  12. {@link #setMinimumSignificantDigits} + *
  13. {@link #setPadPosition} + *
  14. {@link #setPadCharacter} + *
  15. {@link #setRoundingIncrement} + *
  16. {@link #setSecondaryGroupingSize} + *
+ * + * All other settings remain untouched. + * + *

For more information on pattern strings, see UTS #35. + * + * @stable ICU 2.0 + */ + public synchronized void applyPattern(String pattern) { + setPropertiesFromPattern(pattern, false); + // Backwards compatibility: clear out user-specified prefix and suffix, + // as well as CurrencyPluralInfo. + properties.setPositivePrefix(null); + properties.setNegativePrefix(null); + properties.setPositiveSuffix(null); + properties.setNegativeSuffix(null); + properties.setCurrencyPluralInfo(null); + refreshFormatter(); + } + + /** + * Converts the given string to standard notation and then parses it using {@link #applyPattern}. + * + *

Localized notation means that instead of using generic placeholders in the pattern, you use + * the corresponding locale-specific characters instead. For example, in locale fr-FR, + * 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 localizedPattern The pattern string in localized notation. + * @stable ICU 2.0 + */ + public synchronized void applyLocalizedPattern(String localizedPattern) { + String pattern = PatternString.convertLocalized(localizedPattern, symbols, false); + applyPattern(pattern); + } + + //=====================================================================================// + // CLONE AND SERIALIZE // + //=====================================================================================// + + /** @stable ICU 2.0 */ + @Override + public Object clone() { + DecimalFormat other = (DecimalFormat) super.clone(); + other.symbols = (DecimalFormatSymbols) symbols.clone(); + other.properties = properties.clone(); + other.exportedProperties = new Properties(); + other.refreshFormatter(); + return other; + } + + /** + * Custom serialization: save property bag and symbols; the formatter object can be re-created + * from just that amount of information. + */ + private void writeObject(ObjectOutputStream oos) throws IOException { + // ICU 59 custom serialization. + // Write class metadata and serialVersionOnStream field: + oos.defaultWriteObject(); + // Extra int for possible future use: + oos.writeInt(0); + // 1) Property Bag + oos.writeObject(properties); + // 2) DecimalFormatSymbols + oos.writeObject(symbols); + } + + /** + * Custom serialization: re-create object from serialized property bag and symbols. Also supports + * reading from the legacy (pre-ICU4J 59) format and converting it to the new form. + */ + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField fieldGetter = ois.readFields(); + ObjectStreamField[] serializedFields = fieldGetter.getObjectStreamClass().getFields(); + int serialVersion = fieldGetter.get("serialVersionOnStream", -1); + + if (serialVersion > 5) { + throw new IOException( + "Cannot deserialize newer com.ibm.icu.text.DecimalFormat (v" + serialVersion + ")"); + } else if (serialVersion == 5) { + ///// ICU 59+ SERIALIZATION FORMAT ///// + // We expect this field and no other fields: + if (serializedFields.length > 1) { + throw new IOException("Too many fields when reading serial version 5"); + } + // Extra int for possible future use: + ois.readInt(); + // 1) Property Bag + properties = (Properties) ois.readObject(); + // 2) DecimalFormatSymbols + symbols = (DecimalFormatSymbols) ois.readObject(); + // Re-build transient fields + exportedProperties = new Properties(); + refreshFormatter(); + } else { + ///// LEGACY SERIALIZATION FORMAT ///// + properties = new Properties(); + // Loop through the fields. Not all fields necessarily exist in the serialization. + String pp = null, ppp = null, ps = null, psp = null; + String np = null, npp = null, ns = null, nsp = null; + for (ObjectStreamField field : serializedFields) { + String name = field.getName(); + if (name.equals("decimalSeparatorAlwaysShown")) { + setDecimalSeparatorAlwaysShown(fieldGetter.get("decimalSeparatorAlwaysShown", false)); + } else if (name.equals("exponentSignAlwaysShown")) { + setExponentSignAlwaysShown(fieldGetter.get("exponentSignAlwaysShown", false)); + } else if (name.equals("formatWidth")) { + setFormatWidth(fieldGetter.get("formatWidth", 0)); + } else if (name.equals("groupingSize")) { + setGroupingSize(fieldGetter.get("groupingSize", (byte) 3)); + } else if (name.equals("groupingSize2")) { + setSecondaryGroupingSize(fieldGetter.get("groupingSize2", (byte) 0)); + } else if (name.equals("maxSignificantDigits")) { + setMaximumSignificantDigits(fieldGetter.get("maxSignificantDigits", 6)); + } else if (name.equals("minExponentDigits")) { + setMinimumExponentDigits(fieldGetter.get("minExponentDigits", (byte) 0)); + } else if (name.equals("minSignificantDigits")) { + setMinimumSignificantDigits(fieldGetter.get("minSignificantDigits", 1)); + } else if (name.equals("multiplier")) { + setMultiplier(fieldGetter.get("multiplier", 1)); + } else if (name.equals("pad")) { + setPadCharacter(fieldGetter.get("pad", '\u0020')); + } else if (name.equals("padPosition")) { + setPadPosition(fieldGetter.get("padPosition", 0)); + } else if (name.equals("parseBigDecimal")) { + setParseBigDecimal(fieldGetter.get("parseBigDecimal", false)); + } else if (name.equals("parseRequireDecimalPoint")) { + setDecimalPatternMatchRequired(fieldGetter.get("parseRequireDecimalPoint", false)); + } else if (name.equals("roundingMode")) { + setRoundingMode(fieldGetter.get("roundingMode", 0)); + } else if (name.equals("useExponentialNotation")) { + setScientificNotation(fieldGetter.get("useExponentialNotation", false)); + } else if (name.equals("useSignificantDigits")) { + setSignificantDigitsUsed(fieldGetter.get("useSignificantDigits", false)); + } else if (name.equals("currencyPluralInfo")) { + setCurrencyPluralInfo((CurrencyPluralInfo) fieldGetter.get("currencyPluralInfo", null)); + } else if (name.equals("currencyUsage")) { + setCurrencyUsage((CurrencyUsage) fieldGetter.get("currencyUsage", null)); + } else if (name.equals("mathContext")) { + setMathContextICU((MathContext) fieldGetter.get("mathContext", null)); + } else if (name.equals("negPrefixPattern")) { + npp = (String) fieldGetter.get("negPrefixPattern", null); + } else if (name.equals("negSuffixPattern")) { + nsp = (String) fieldGetter.get("negSuffixPattern", null); + } else if (name.equals("negativePrefix")) { + np = (String) fieldGetter.get("negativePrefix", null); + } else if (name.equals("negativeSuffix")) { + ns = (String) fieldGetter.get("negativeSuffix", null); + } else if (name.equals("posPrefixPattern")) { + ppp = (String) fieldGetter.get("posPrefixPattern", null); + } else if (name.equals("posSuffixPattern")) { + psp = (String) fieldGetter.get("posSuffixPattern", null); + } else if (name.equals("positivePrefix")) { + pp = (String) fieldGetter.get("positivePrefix", null); + } else if (name.equals("positiveSuffix")) { + ps = (String) fieldGetter.get("positiveSuffix", null); + } else if (name.equals("roundingIncrement")) { + setRoundingIncrement((java.math.BigDecimal) fieldGetter.get("roundingIncrement", null)); + } else if (name.equals("symbols")) { + setDecimalFormatSymbols((DecimalFormatSymbols) fieldGetter.get("symbols", null)); } else { - expandAffixAdjustWidth(null); - } - } - - /** - * Creates a DecimalFormat from the given pattern and the symbols for the default - * FORMAT locale. This is a convenient way to obtain a DecimalFormat when - * internationalization is not the main concern. - * - *

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(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. - * - *

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(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. - * - *

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 using the setters. - * - *

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(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. - */ - DecimalFormat(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.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 Number object to represent the - * parsed value. Double objects are returned to represent non-integral - * values which cannot be stored in a BigDecimal. These are - * NaN, infinity, -infinity, and -0.0. If {@link #isParseBigDecimal()} is - * false (the default), all other values are returned as Long, - * BigInteger, or BigDecimal values, in that order of - * preference. If {@link #isParseBigDecimal()} is true, all other values are returned - * as BigDecimal 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 Number object with the parsed value or - * null 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(); - - // 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 iter = currencyPluralInfo.pluralPatternIterator(); - Set currencyUnitPatternSet = new HashSet(); - 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 decVal 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. - * - *

Examples: +123, $123, sFr123 - * @return the prefix - * @stable ICU 2.0 - */ - public String getPositivePrefix() { - return positivePrefix; - } - - /** - * Sets the positive prefix. - * - *

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. - * - *

Examples: -123, ($123) (with negative suffix), sFr-123 - * - * @return the prefix - * @stable ICU 2.0 - */ - public String getNegativePrefix() { - return negativePrefix; - } - - /** - * Sets the negative prefix. - * - *

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. - * - *

Example: 123% - * - * @return the suffix - * @stable ICU 2.0 - */ - public String getPositiveSuffix() { - return positiveSuffix; - } - - /** - * Sets the positive suffix. - * - *

Example: 123% - * @param newValue the suffix - * @stable ICU 2.0 - */ - public void setPositiveSuffix(String newValue) { - positiveSuffix = newValue; - posSuffixPattern = null; - } - - /** - * Returns the negative suffix. - * - *

Examples: -123%, ($123) (with positive suffixes) - * - * @return the suffix - * @stable ICU 2.0 - */ - public String getNegativeSuffix() { - return negativeSuffix; - } - - /** - * Sets the positive suffix. - * - *

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. - * - *

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. - * - *

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 null 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 null or - * BigDecimal(0.0) to use the default rounding increment. - * @throws IllegalArgumentException if newValue 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 null or - * BigDecimal(0.0) to use the default rounding increment. - * @throws IllegalArgumentException if newValue 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 newValue 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 BigDecimal.ROUND_UP and - * BigDecimal.ROUND_UNNECESSARY. - * @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 BigDecimal.ROUND_UP and - * BigDecimal.ROUND_UNNECESSARY. - * @exception IllegalArgumentException if roundingMode 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 format() 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 format() 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 - * format(), or zero to disable padding - * @exception IllegalArgumentException if width 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 format() is shorter - * than the format width. - * - * @return the pad position, one of PAD_BEFORE_PREFIX, - * PAD_AFTER_PREFIX, PAD_BEFORE_SUFFIX, or - * PAD_AFTER_SUFFIX. - * @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 format() is shorter - * than the format width. This has no effect unless padding is enabled. - * - * @param padPos the pad position, one of PAD_BEFORE_PREFIX, - * PAD_AFTER_PREFIX, PAD_BEFORE_SUFFIX, or - * PAD_AFTER_SUFFIX. - * @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 minExpDig < 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 getGroupingSize(). 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.)

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.) - * - *

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. - * - *

- * 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 other = (DecimalFormat) 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(); // #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 other = (DecimalFormat) 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. - *

- * Note: 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. - *

- * Note: 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. - * - *

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 - * - *

Example "#,#00.0#" -> 1,234.56 - * - *

This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2 - * fraction digits. - * - *

Example: "#,#00.0#;(#,#00.0#)" for negatives in parentheses. - * - *

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. - * - *

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 - * - *

Example "#,#00.0#" -> 1,234.56 - * - *

This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2 - * fraction digits. - * - *

Example: "#,#00.0#;(#,#00.0#)" for negatives in parantheses. - * - *

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 - * min is less than one then it is set to one. If the maximum significant - * digits count is less than min, then it is set to min. - * 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 - * max is less than one then it is set to one. If the minimum significant - * digits count is greater than max, then it is set to max. - * 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 Currency 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 Currency Usage 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 Currency Usage 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 - * serialVersionOnStream is less than 1, indicating that the stream was - * written by JDK 1.1, initialize useExponentialNotation 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 - * positivePrefix. - * - *

This pattern is expanded by the method expandAffix() to - * positivePrefix to update the latter to reflect changes in - * symbols. If this variable is null then - * positivePrefix is taken as a literal value that does not change when - * symbols changes. This variable is always null for - * DecimalFormat 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 - * positiveSuffix. This variable is analogous to - * posPrefixPattern; see that variable for further documentation. - * - * @serial - */ - // [Richard/GCL] - private String posSuffixPattern; - - /** - * The prefix pattern for negative numbers. This variable corresponds to - * negativePrefix. This variable is analogous to - * posPrefixPattern; see that variable for further documentation. - * - * @serial - */ - // [Richard/GCL] - private String negPrefixPattern; - - /** - * The suffix pattern for negative numbers. This variable corresponds to - * negativeSuffix. This variable is analogous to - * posPrefixPattern; 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 NumberFormat.groupingUsed 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 DecimalFormatSymbols 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. - * - *

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 - * useExponentialNotation is not true. - * - *

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 useExponentialNotation 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 null if rounding is not in effect, or a - * positive value if rounding is in effect. Default value null. - * - * @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 null if rounding is not in effect, or a - * positive value if rounding is in effect. Default value null. 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 - * BigDecimal rounding mode values. Default value - * BigDecimal.ROUND_HALF_EVEN. - * - * @serial - * @since AlphaWorks NumberFormat - */ - private int roundingMode = BigDecimal.ROUND_HALF_EVEN; - - /** - * Operations on BigDecimal numbers are controlled by a {@link - * MathContext} object, which provides the context (precision and other information) - * for the operation. The default MathContext settings are - * digits=0, form=PLAIN, lostDigits=false, roundingMode=ROUND_HALF_UP; - * 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 formatWidth, if - * padding is in effect. Default value ' '. - * - * @serial - * @since AlphaWorks NumberFormat - */ - private char pad = ' '; - - /** - * The position in the string at which the pad character will be - * inserted, if padding is in effect. Must have a value from - * PAD_BEFORE_PREFIX to PAD_AFTER_SUFFIX. Default value - * PAD_BEFORE_PREFIX. - * - * @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: - * - *

    - * - *
  • 0 (default): versions before JDK 1.2 - * - *
  • 1: version from JDK 1.2 and later, which includes the two new fields - * useExponentialNotation and minExponentDigits. - * - *
  • 2: version on AlphaWorks, which adds roundingMode, formatWidth, pad, - * padPosition, exponentSignAlwaysShown, roundingIncrement. - * - *
  • 3: ICU 2.2. Adds currency object. - * - *
  • 4: ICU 54. Adds currency usage(standard vs cash) - * - *
- * - * @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 attributes = new ArrayList(); - - // 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 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); - } - + // The following fields are ignored: + // "PARSE_MAX_EXPONENT" + // "currencySignCount" + // "style" + // "attributes" + // "currencyChoice" + // "formatPattern" + } + } + // Resolve affixes + if (npp == null) { + properties.setNegativePrefix(np); + } else { + properties.setNegativePrefixPattern(npp); + } + if (nsp == null) { + properties.setNegativeSuffix(ns); + } else { + properties.setNegativeSuffixPattern(nsp); + } + if (ppp == null) { + properties.setPositivePrefix(pp); + } else { + properties.setPositivePrefixPattern(ppp); + } + if (psp == null) { + properties.setPositiveSuffix(ps); + } else { + properties.setPositiveSuffixPattern(psp); + } + // Extract values from parent NumberFormat class. Have to use reflection here. + java.lang.reflect.Field getter; + try { + getter = NumberFormat.class.getDeclaredField("groupingUsed"); + getter.setAccessible(true); + setGroupingUsed((Boolean) getter.get(this)); + getter = NumberFormat.class.getDeclaredField("parseIntegerOnly"); + getter.setAccessible(true); + setParseIntegerOnly((Boolean) getter.get(this)); + getter = NumberFormat.class.getDeclaredField("maximumIntegerDigits"); + getter.setAccessible(true); + setMaximumIntegerDigits((Integer) getter.get(this)); + getter = NumberFormat.class.getDeclaredField("minimumIntegerDigits"); + getter.setAccessible(true); + setMinimumIntegerDigits((Integer) getter.get(this)); + getter = NumberFormat.class.getDeclaredField("maximumFractionDigits"); + getter.setAccessible(true); + setMaximumFractionDigits((Integer) getter.get(this)); + getter = NumberFormat.class.getDeclaredField("minimumFractionDigits"); + getter.setAccessible(true); + setMinimumFractionDigits((Integer) getter.get(this)); + getter = NumberFormat.class.getDeclaredField("currency"); + getter.setAccessible(true); + setCurrency((Currency) getter.get(this)); + getter = NumberFormat.class.getDeclaredField("parseStrict"); + getter.setAccessible(true); + setParseStrict((Boolean) getter.get(this)); + } catch (IllegalArgumentException e) { + throw new IOException(e); + } catch (IllegalAccessException e) { + throw new IOException(e); + } catch (NoSuchFieldException e) { + throw new IOException(e); + } catch (SecurityException e) { + throw new IOException(e); + } + // Finish initialization + if (symbols == null) { + symbols = getDefaultSymbols(); + } + exportedProperties = new Properties(); + refreshFormatter(); + } + } + + //=====================================================================================// + // FORMAT AND PARSE APIS // + //=====================================================================================// + + /** @stable ICU 2.0 */ + @Override + public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq, result, fieldPosition); + fq.populateUFieldPosition(fieldPosition); + return result; + } + + /** @stable ICU 2.0 */ + @Override + public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq, result, fieldPosition); + fq.populateUFieldPosition(fieldPosition); + return result; + } + + /** @stable ICU 2.0 */ + @Override + public StringBuffer format(BigInteger number, StringBuffer result, FieldPosition fieldPosition) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq, result, fieldPosition); + fq.populateUFieldPosition(fieldPosition); + return result; + } + + /** @stable ICU 2.0 */ + @Override + public StringBuffer format( + java.math.BigDecimal number, StringBuffer result, FieldPosition fieldPosition) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq, result, fieldPosition); + fq.populateUFieldPosition(fieldPosition); + return result; + } + + /** @stable ICU 2.0 */ + @Override + public StringBuffer format(BigDecimal number, StringBuffer result, FieldPosition fieldPosition) { + FormatQuantity4 fq = new FormatQuantity4(number.toBigDecimal()); + formatter.format(fq, result, fieldPosition); + fq.populateUFieldPosition(fieldPosition); + return result; + } + + /** @stable ICU 3.6 */ + @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; + } + + protected static final ThreadLocal threadLocalCurrencyProperties = + new ThreadLocal() { @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); + protected Properties initialValue() { + return new Properties(); } + }; + + @Override + public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) { + // TODO: This is ugly (although not as ugly as it was in ICU 58). + // Currency should be a free parameter, not in property bag. Fix in ICU 60. + Properties cprops = threadLocalCurrencyProperties.get(); + synchronized (this) { + cprops.copyFrom(properties); + } + cprops.setCurrency(currAmt.getCurrency()); + FormatQuantity4 fq = new FormatQuantity4(currAmt.getNumber()); + // TODO: Use a static format path here + SingularFormat fmt = Endpoint.fromBTA(cprops, symbols); + fmt.format(fq, toAppendTo, pos); + fq.populateUFieldPosition(pos); + return toAppendTo; + } + + /** @stable ICU 2.0 */ + @Override + public Number parse(String text, ParsePosition parsePosition) { + // Backwards compatibility: use currency parse mode if this is a currency instance + Number result = Parse.parse(text, parsePosition, properties, symbols); + // Backwards compatibility: return com.ibm.icu.math.BigDecimal + if (result instanceof java.math.BigDecimal) { + result = new com.ibm.icu.math.BigDecimal((java.math.BigDecimal) result); + } + return result; + } + + /** @stable ICU 49 */ + @Override + public CurrencyAmount parseCurrency(CharSequence text, ParsePosition parsePosition) { + try { + CurrencyAmount result = Parse.parseCurrency(text, parsePosition, properties, symbols); + if (result == null) return null; + Number number = result.getNumber(); + // Backwards compatibility: return com.ibm.icu.math.BigDecimal + if (number instanceof java.math.BigDecimal) { + number = new com.ibm.icu.math.BigDecimal((java.math.BigDecimal) number); + result = new CurrencyAmount(number, result.getCurrency()); + } + return result; + } catch (ParseException e) { + return null; + } + } + + //=====================================================================================// + // GETTERS AND SETTERS // + //=====================================================================================// + + /** + * Returns a copy of the decimal format symbols used by this formatter. + * + * @return desired DecimalFormatSymbols + * @see DecimalFormatSymbols + * @stable ICU 2.0 + */ + public synchronized DecimalFormatSymbols getDecimalFormatSymbols() { + return (DecimalFormatSymbols) symbols.clone(); + } + + /** + * Sets the decimal format symbols used by this formatter. The formatter uses a copy of the + * provided symbols. + * + * @param newSymbols desired DecimalFormatSymbols + * @see DecimalFormatSymbols + * @stable ICU 2.0 + */ + public synchronized void setDecimalFormatSymbols(DecimalFormatSymbols newSymbols) { + symbols = (DecimalFormatSymbols) newSymbols.clone(); + refreshFormatter(); + } + + /** + * Affixes: Gets the positive prefix string currently being used to format + * numbers. + * + *

If the affix was specified via the pattern, the string returned by this method will have + * locale symbols substituted in place of special characters according to the LDML specification. + * If the affix was specified via {@link #setPositivePrefix}, the string will be returned + * literally. + * + * @return The string being prepended to positive numbers. + * @category Affixes + * @stable ICU 2.0 + */ + public synchronized String getPositivePrefix() { + String result = exportedProperties.getPositivePrefix(); + return (result == null) ? "" : result; + } + + /** + * Affixes: Sets the string to prepend to positive numbers. For example, if you + * set the value "#", then the number 123 will be formatted as "#123" in the locale + * en-US. + * + *

Using this method overrides the affix specified via the pattern, and unlike the pattern, the + * string given to this method will be interpreted literally WITHOUT locale symbol substitutions. + * + * @param prefix The literal string to prepend to positive numbers. + * @category Affixes + * @stable ICU 2.0 + */ + public synchronized void setPositivePrefix(String prefix) { + properties.setPositivePrefix(prefix); + refreshFormatter(); + } + + /** + * Affixes: Gets the negative prefix string currently being used to format + * numbers. + * + *

If the affix was specified via the pattern, the string returned by this method will have + * locale symbols substituted in place of special characters according to the LDML specification. + * If the affix was specified via {@link #setNegativePrefix}, the string will be returned + * literally. + * + * @return The string being prepended to negative numbers. + * @category Affixes + * @stable ICU 2.0 + */ + public synchronized String getNegativePrefix() { + String result = exportedProperties.getNegativePrefix(); + return (result == null) ? "" : result; + } + + /** + * Affixes: Sets the string to prepend to negative numbers. For example, if you + * set the value "#", then the number -123 will be formatted as "#123" in the locale + * en-US (overriding the implicit default '-' in the pattern). + * + *

Using this method overrides the affix specified via the pattern, and unlike the pattern, the + * string given to this method will be interpreted literally WITHOUT locale symbol substitutions. + * + * @param suffix The literal string to prepend to negative numbers. + * @category Affixes + * @stable ICU 2.0 + */ + public synchronized void setNegativePrefix(String suffix) { + properties.setNegativePrefix(suffix); + refreshFormatter(); + } + + /** + * Affixes: Gets the positive suffix string currently being used to format + * numbers. + * + *

If the affix was specified via the pattern, the string returned by this method will have + * locale symbols substituted in place of special characters according to the LDML specification. + * If the affix was specified via {@link #setPositiveSuffix}, the string will be returned + * literally. + * + * @return The string being appended to positive numbers. + * @category Affixes + * @stable ICU 2.0 + */ + public synchronized String getPositiveSuffix() { + String result = exportedProperties.getPositiveSuffix(); + return (result == null) ? "" : result; + } + + /** + * Affixes: Sets the string to append to positive numbers. For example, if you + * set the value "#", then the number 123 will be formatted as "123#" in the locale + * en-US. + * + *

Using this method overrides the affix specified via the pattern, and unlike the pattern, the + * string given to this method will be interpreted literally WITHOUT locale symbol substitutions. + * + * @param suffix The literal string to append to positive numbers. + * @category Affixes + * @stable ICU 2.0 + */ + public synchronized void setPositiveSuffix(String suffix) { + properties.setPositiveSuffix(suffix); + refreshFormatter(); + } + + /** + * Affixes: Gets the negative suffix string currently being used to format + * numbers. + * + *

If the affix was specified via the pattern, the string returned by this method will have + * locale symbols substituted in place of special characters according to the LDML specification. + * If the affix was specified via {@link #setNegativeSuffix}, the string will be returned + * literally. + * + * @return The string being appended to negative numbers. + * @category Affixes + * @stable ICU 2.0 + */ + public synchronized String getNegativeSuffix() { + String result = exportedProperties.getNegativeSuffix(); + return (result == null) ? "" : result; + } + + /** + * Affixes: Sets the string to append to negative numbers. For example, if you + * set the value "#", then the number 123 will be formatted as "123#" in the locale + * en-US. + * + *

Using this method overrides the affix specified via the pattern, and unlike the pattern, the + * string given to this method will be interpreted literally WITHOUT locale symbol substitutions. + * + * @param suffix The literal string to append to negative numbers. + * @category Affixes + * @stable ICU 2.0 + */ + public synchronized void setNegativeSuffix(String suffix) { + properties.setNegativeSuffix(suffix); + refreshFormatter(); + } + + /** + * @return The multiplier being applied to numbers before they are formatted. + * @see #setMultiplier + * @category Multipliers + * @stable ICU 2.0 + */ + public synchronized int getMultiplier() { + if (properties.getMultiplier() != null) { + return properties.getMultiplier().intValue(); + } else { + return (int) Math.pow(10, properties.getMagnitudeMultiplier()); + } + } + + /** + * Sets a number that will be used to multiply all numbers prior to formatting. For example, when + * formatting percents, a multiplier of 100 can be used. + * + *

If a percent or permille sign is specified in the pattern, the multiplier is automatically + * set to 100 or 1000, respectively. + * + *

If the number specified here is a power of 10, a more efficient code path will be used. + * + * @param multiplier The number by which all numbers passed to {@link #format} will be multiplied. + * @throws IllegalArgumentException If the given multiplier is zero. + * @category Multipliers + * @stable ICU 2.0 + */ + public synchronized void setMultiplier(int multiplier) { + if (multiplier == 0) { + throw new IllegalArgumentException("Multiplier must be nonzero."); + } + + // Try to convert to a magnitude multiplier first + int delta = 0; + int value = multiplier; + while (multiplier != 1) { + delta++; + int temp = value / 10; + if (temp * 10 != value) { + delta = -1; + break; + } + value = temp; + } + if (delta != -1) { + properties.setMagnitudeMultiplier(delta); + } else { + properties.setMultiplier(java.math.BigDecimal.valueOf(multiplier)); + } + refreshFormatter(); + } + + /** + * @return The increment to which numbers are being rounded. + * @see #setRoundingIncrement + * @category Rounding + * @stable ICU 2.0 + */ + public synchronized java.math.BigDecimal getRoundingIncrement() { + return exportedProperties.getRoundingIncrement(); + } + + /** + * Rounding and Digit Limits: Sets an increment, or interval, to which numbers + * are rounded. For example, a rounding increment of 0.05 will cause the number 1.23 to be rounded + * to 1.25 in the default rounding mode. + * + *

The rounding increment can be specified via the pattern string: for example, the pattern + * "#,##0.05" encodes a rounding increment of 0.05. + * + *

The rounding increment is applied after any multipliers might take effect; for + * example, in scientific notation or when {@link #setMultiplier} is used. + * + *

See {@link #setMaximumFractionDigits} and {@link #setMaximumSignificantDigits} for two other + * ways of specifying rounding strategies. + * + * @param increment The increment to which numbers are to be rounded. + * @see #setRoundingMode + * @see #setMaximumFractionDigits + * @see #setMaximumSignificantDigits + * @category Rounding + * @stable ICU 2.0 + */ + public synchronized void setRoundingIncrement(java.math.BigDecimal increment) { + // Backwards compatibility: ignore rounding increment if zero, + // and instead set maximum fraction digits. + if (increment != null && increment.compareTo(java.math.BigDecimal.ZERO) == 0) { + properties.setMaximumFractionDigits(Integer.MAX_VALUE); + return; + } + + properties.setRoundingIncrement(increment); + refreshFormatter(); + } + + /** + * Rounding and Digit Limits: Overload of {@link + * #setRoundingIncrement(java.math.BigDecimal)}. + * + * @param increment The increment to which numbers are to be rounded. + * @see #setRoundingIncrement + * @category Rounding + * @stable ICU 3.6 + */ + public synchronized void setRoundingIncrement(BigDecimal increment) { + java.math.BigDecimal javaBigDecimal = (increment == null) ? null : increment.toBigDecimal(); + setRoundingIncrement(javaBigDecimal); + } + + /** + * Rounding and Digit Limits: Overload of {@link + * #setRoundingIncrement(java.math.BigDecimal)}. + * + * @param increment The increment to which numbers are to be rounded. + * @see #setRoundingIncrement + * @category Rounding + * @stable ICU 2.0 + */ + public synchronized void setRoundingIncrement(double increment) { + if (increment == 0) { + setRoundingIncrement((java.math.BigDecimal) null); + } else { + java.math.BigDecimal javaBigDecimal = java.math.BigDecimal.valueOf(increment); + setRoundingIncrement(javaBigDecimal); + } + } + + /** + * @return The rounding mode being used to round numbers. + * @see #setRoundingMode + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized int getRoundingMode() { + RoundingMode mode = exportedProperties.getRoundingMode(); + return (mode == null) ? 0 : mode.ordinal(); + } + + /** + * Rounding and Digit Limits: Sets the {@link RoundingMode} used to round + * numbers. The default rounding mode is HALF_EVEN, which rounds decimals to their closest whole + * number, and rounds to the closest even number if at the midpoint. + * + *

For more detail on rounding modes, see the ICU User + * Guide. + * + *

For backwards compatibility, the rounding mode is specified as an int argument, which can be + * from either the constants in {@link BigDecimal} or the ordinal value of {@link RoundingMode}. + * The following two calls are functionally equivalent. + * + *

+   * df.setRoundingMode(BigDecimal.ROUND_CEILING);
+   * df.setRoundingMode(RoundingMode.CEILING.ordinal());
+   * 
+ * + * @param roundingMode The integer constant rounding mode to use when formatting numbers. + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized void setRoundingMode(int roundingMode) { + properties.setRoundingMode(RoundingMode.valueOf(roundingMode)); + refreshFormatter(); + } + + /** + * @return The {@link java.math.MathContext} being used to round numbers. + * @see #setMathContext + * @category Rounding + * @stable ICU 4.2 + */ + public synchronized java.math.MathContext getMathContext() { + java.math.MathContext mathContext = exportedProperties.getMathContext(); + assert mathContext != null; + return mathContext; + } + + /** + * Rounding and Digit Limits: Sets the {@link java.math.MathContext} used to + * round numbers. A "math context" encodes both a rounding mode and a number of significant + * digits. + * + *

This method is provided for users who require their output to conform to a standard math + * context. Most users should call {@link #setRoundingMode} and/or {@link + * #setMaximumSignificantDigits} instead of this method. + * + * @param mathContext The MathContext to use when rounding numbers. + * @see java.math.MathContext + * @category Rounding + * @stable ICU 4.2 + */ + public synchronized void setMathContext(java.math.MathContext mathContext) { + properties.setMathContext(mathContext); + refreshFormatter(); + } + + // Remember the ICU math context form in order to be able to return it from the API. + // NOTE: This value is not serialized. (should it be?) + private transient int icuMathContextForm = MathContext.PLAIN; + + /** + * @return The {@link com.ibm.icu.math.MathContext} being used to round numbers. + * @see #setMathContext + * @category Rounding + * @stable ICU 4.2 + */ + public synchronized MathContext getMathContextICU() { + java.math.MathContext mathContext = getMathContext(); + return new MathContext( + mathContext.getPrecision(), + icuMathContextForm, + false, + mathContext.getRoundingMode().ordinal()); + } + + /** + * Rounding and Digit Limits: Overload of {@link #setMathContext} for {@link + * com.ibm.icu.math.MathContext}. + * + * @param mathContextICU The MathContext to use when rounding numbers. + * @see #setMathContext(java.math.MathContext) + * @category Rounding + * @stable ICU 4.2 + */ + public synchronized void setMathContextICU(MathContext mathContextICU) { + icuMathContextForm = mathContextICU.getForm(); + java.math.MathContext mathContext; + if (mathContextICU.getLostDigits()) { + // The getLostDigits() feature in ICU MathContext means "throw an ArithmeticException if + // rounding causes digits to be lost". That feature is called RoundingMode.UNNECESSARY in + // Java MathContext. + mathContext = new java.math.MathContext(mathContextICU.getDigits(), RoundingMode.UNNECESSARY); + } else { + mathContext = + new java.math.MathContext( + mathContextICU.getDigits(), RoundingMode.valueOf(mathContextICU.getRoundingMode())); + } + setMathContext(mathContext); + } + + /** + * @return The effective minimum number of digits before the decimal separator. + * @see #setMinimumIntegerDigits + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized int getMinimumIntegerDigits() { + return exportedProperties.getMinimumIntegerDigits(); + } + + /** + * Rounding and Digit Limits: Sets the minimum number of digits to display before + * the decimal separator. If the number has fewer than this many digits, the number is padded with + * zeros. + * + *

For example, if minimum integer digits is 3, the number 12.3 will be printed as "001.23". + * + *

Minimum integer and minimum and maximum fraction digits can be specified via the pattern + * string. For example, "#,#00.00#" has 2 minimum integer digits, 2 minimum fraction digits, and 3 + * maximum fraction digits. Note that it is not possible to specify maximium integer digits in the + * pattern except in scientific notation. + * + * @param value The minimum number of digits before the decimal separator. + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized void setMinimumIntegerDigits(int value) { + properties.setMinimumIntegerDigits(value); + refreshFormatter(); + } + + /** + * @return The effective maximum number of digits before the decimal separator. + * @see #setMaximumIntegerDigits + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized int getMaximumIntegerDigits() { + return exportedProperties.getMaximumIntegerDigits(); + } + + /** + * Rounding and Digit Limits: Sets the maximum number of digits to display before + * the decimal separator. If the number has more than this many digits, the number is truncated. + * + *

For example, if maximum integer digits is 3, the number 12345 will be printed as "345". + * + *

Minimum integer and minimum and maximum fraction digits can be specified via the pattern + * string. For example, "#,#00.00#" has 2 minimum integer digits, 2 minimum fraction digits, and 3 + * maximum fraction digits. Note that it is not possible to specify maximium integer digits in the + * pattern except in scientific notation. + * + * @param value The maximum number of digits before the decimal separator. + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized void setMaximumIntegerDigits(int value) { + properties.setMaximumIntegerDigits(value); + refreshFormatter(); + } + + /** + * @return The effective minimum number of integer digits after the decimal separator. + * @see #setMaximumIntegerDigits + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized int getMinimumFractionDigits() { + return exportedProperties.getMinimumFractionDigits(); + } + + /** + * Rounding and Digit Limits: Sets the minimum number of digits to display after + * the decimal separator. If the number has fewer than this many digits, the number is padded with + * zeros. + * + *

For example, if minimum fraction digits is 2, the number 123.4 will be printed as "123.40". + * + *

Minimum integer and minimum and maximum fraction digits can be specified via the pattern + * string. For example, "#,#00.00#" has 2 minimum integer digits, 2 minimum fraction digits, and 3 + * maximum fraction digits. Note that it is not possible to specify maximium integer digits in the + * pattern except in scientific notation. + * + *

See {@link #setRoundingIncrement} and {@link #setMaximumSignificantDigits} for two other + * ways of specifying rounding strategies. + * + * @param value The minimum number of integer digits after the decimal separator. + * @see #setRoundingMode + * @see #setRoundingIncrement + * @see #setMaximumSignificantDigits + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized void setMinimumFractionDigits(int value) { + properties.setMinimumFractionDigits(value); + refreshFormatter(); + } + + /** + * @return The effective maximum number of integer digits after the decimal separator. + * @see #setMaximumIntegerDigits + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized int getMaximumFractionDigits() { + return exportedProperties.getMaximumFractionDigits(); + } + + /** + * Rounding and Digit Limits: Sets the maximum number of digits to display after + * the decimal separator. If the number has more than this many digits, the number is rounded + * according to the rounding mode. + * + *

For example, if maximum fraction digits is 2, the number 123.456 will be printed as + * "123.46". + * + *

Minimum integer and minimum and maximum fraction digits can be specified via the pattern + * string. For example, "#,#00.00#" has 2 minimum integer digits, 2 minimum fraction digits, and 3 + * maximum fraction digits. Note that it is not possible to specify maximium integer digits in the + * pattern except in scientific notation. + * + * @param value The maximum number of integer digits after the decimal separator. + * @see #setRoundingMode + * @category Rounding + * @stable ICU 2.0 + */ + @Override + public synchronized void setMaximumFractionDigits(int value) { + properties.setMaximumFractionDigits(value); + refreshFormatter(); + } + + /** + * @return Whether significant digits are being used in rounding. + * @see #setSignificantDigitsUsed + * @category Rounding + * @stable ICU 3.0 + */ + public synchronized boolean areSignificantDigitsUsed() { + return SignificantDigitsRounder.useSignificantDigits(properties); + } + + /** + * Rounding and Digit Limits: Sets whether significant digits are to be used in + * rounding. + * + *

Calling df.setSignificantDigitsUsed(true) is functionally equivalent to: + * + *

+   * df.setMinimumSignificantDigits(1);
+   * df.setMaximumSignificantDigits(6);
+   * 
+ * + * @param useSignificantDigits true to enable significant digit rounding; false to disable it. + * @category Rounding + * @stable ICU 3.0 + */ + public synchronized void setSignificantDigitsUsed(boolean useSignificantDigits) { + if (useSignificantDigits) { + // These are the default values from the old implementation. + properties.setMinimumSignificantDigits(1); + properties.setMaximumSignificantDigits(6); + } else { + properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS); + properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS); + properties.setSignificantDigitsMode(null); + } + refreshFormatter(); + } + + /** + * @return The effective minimum number of significant digits displayed. + * @see #setMinimumSignificantDigits + * @category Rounding + * @stable ICU 3.0 + */ + public synchronized int getMinimumSignificantDigits() { + return exportedProperties.getMinimumSignificantDigits(); + } + + /** + * Rounding and Digit Limits: Sets the minimum number of significant digits to be + * displayed. If the number of significant digits is less than this value, the number will be + * padded with zeros as necessary. + * + *

For example, if minimum significant digits is 3 and the number is 1.2, the number will be + * printed as "1.20". + * + * @param value The minimum number of significant digits to display. + * @see #setSignificantDigitsMode + * @category Rounding + * @stable ICU 3.0 + */ + public synchronized void setMinimumSignificantDigits(int value) { + properties.setMinimumSignificantDigits(value); + refreshFormatter(); + } + + /** + * @return The effective maximum number of significant digits displayed. + * @see #setMaximumSignificantDigits + * @category Rounding + * @stable ICU 3.0 + */ + public synchronized int getMaximumSignificantDigits() { + return exportedProperties.getMaximumSignificantDigits(); + } + + /** + * Rounding and Digit Limits: Sets the maximum number of significant digits to be + * displayed. If the number of significant digits in the number exceeds this value, the number + * will be rounded according to the current rounding mode. + * + *

For example, if maximum significant digits is 3 and the number is 12345, the number will be + * printed as "12300". + * + *

See {@link #setRoundingIncrement} and {@link #setMaximumFractionDigits} for two other ways + * of specifying rounding strategies. + * + * @param value The maximum number of significant digits to display. + * @see #setRoundingMode + * @see #setRoundingIncrement + * @see #setMaximumFractionDigits + * @see #setSignificantDigitsMode + * @category Rounding + * @stable ICU 3.0 + */ + public synchronized void setMaximumSignificantDigits(int value) { + properties.setMaximumSignificantDigits(value); + refreshFormatter(); + } + + /** + * @return The current significant digits mode. + * @see #setSignificantDigitsMode + * @category Rounding + * @internal + * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. + */ + @Deprecated + public synchronized SignificantDigitsMode getSignificantDigitsMode() { + return exportedProperties.getSignificantDigitsMode(); + } + + /** + * Rounding and Digit Limits: Sets the strategy used for resolving + * minimum/maximum significant digits when minimum/maximum integer and/or fraction digits are + * specified. There are three modes: + * + *

    + *
  • Mode A: OVERRIDE_MAXIMUM_FRACTION. This is the default. Settings in maximum fraction are + * ignored. + *
  • Mode B: RESPECT_MAXIMUM_FRACTION. Round to maximum fraction even if doing so will prevent + * minimum significant from being respected. + *
  • Mode C: ENSURE_MINIMUM_SIGNIFICANT. Respect maximum fraction, but always ensure that + * minimum significant digits are shown. + *
+ * + *

The following table illustrates the difference. Below, minFrac=1, maxFrac=2, minSig=3, and + * maxSig=4: + * + *

+   *   Mode A |   Mode B |   Mode C
+   * ---------+----------+----------
+   *  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
+   * 
+ * + * @param mode The significant digits mode to use. + * @category Rounding + * @internal + * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. + */ + @Deprecated + public synchronized void setSignificantDigitsMode(SignificantDigitsMode mode) { + properties.setSignificantDigitsMode(mode); + refreshFormatter(); + } + + /** + * @return The minimum number of characters in formatted output. + * @see #setFormatWidth + * @category Padding + * @stable ICU 2.0 + */ + public synchronized int getFormatWidth() { + return exportedProperties.getFormatWidth(); + } + + /** + * Padding: 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. + * + *

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. + * + *

Padding can be specified in the pattern string using the '*' symbol. For example, the format + * "*x######0" has a format width of 7 and a pad character of 'x'. + * + *

Padding is currently counted in UTF-16 code units; see ticket #13034 for more information. + * + * @param width The minimum number of characters in the output. + * @see #setPadCharacter + * @see #setPadPosition + * @category Padding + * @stable ICU 2.0 + */ + public synchronized void setFormatWidth(int width) { + properties.setFormatWidth(width); + refreshFormatter(); + } + + /** + * @return The character used for padding. + * @see #setPadCharacter + * @category Padding + * @stable ICU 2.0 + */ + public synchronized char getPadCharacter() { + CharSequence paddingString = exportedProperties.getPadString(); + if (paddingString == null) { + return '.'; // TODO: Is this the correct behavior? + } else { + return paddingString.charAt(0); + } + } + + /** + * Padding: Sets the character used to pad numbers that are narrower than the + * width specified in {@link #setFormatWidth}. + * + *

In the pattern string, the padding character is the token that follows '*' before or after + * the prefix or suffix. + * + * @param padChar The character used for padding. + * @see #setFormatWidth + * @category Padding + * @stable ICU 2.0 + */ + public synchronized void setPadCharacter(char padChar) { + properties.setPadString(Character.toString(padChar)); + refreshFormatter(); + } + + /** + * @return The position used for padding. + * @see #setPadPosition + * @category Padding + * @stable ICU 2.0 + */ + public synchronized int getPadPosition() { + PadPosition loc = exportedProperties.getPadPosition(); + return (loc == null) ? PAD_BEFORE_PREFIX : loc.toOld(); + } + + /** + * Padding: Sets the position where to insert the pad character when narrower + * than the width specified in {@link #setFormatWidth}. For example, consider the pattern "P123S" + * with padding width 8 and padding char "*". The four positions are: + * + *

    + *
  • {@link DecimalFormat#PAD_BEFORE_PREFIX} ⇒ "***P123S" + *
  • {@link DecimalFormat#PAD_AFTER_PREFIX} ⇒ "P***123S" + *
  • {@link DecimalFormat#PAD_BEFORE_SUFFIX} ⇒ "P123***S" + *
  • {@link DecimalFormat#PAD_AFTER_SUFFIX} ⇒ "P123S***" + *
+ * + * @param padPos The position used for padding. + * @see #setFormatWidth + * @category Padding + * @stable ICU 2.0 + */ + public synchronized void setPadPosition(int padPos) { + properties.setPadPosition(PadPosition.fromOld(padPos)); + refreshFormatter(); + } + + /** + * @return Whether scientific (exponential) notation is enabled on this formatter. + * @see #setScientificNotation + * @category ScientificNotation + * @stable ICU 2.0 + */ + public synchronized boolean isScientificNotation() { + return ScientificFormat.useScientificNotation(properties); + } + + /** + * Scientific Notation: Sets whether this formatter should print in scientific + * (exponential) notation. For example, if scientific notation is enabled, the number 123000 will + * be printed as "1.23E5" in locale en-US. A locale-specific symbol is used as the + * exponent separator. + * + *

Calling df.setScientificNotation(true) is functionally equivalent to calling + * df.setMinimumExponentDigits(1). + * + * @param useScientific true to enable scientific notation; false to disable it. + * @see #setMinimumExponentDigits + * @category ScientificNotation + * @stable ICU 2.0 + */ + public synchronized void setScientificNotation(boolean useScientific) { + if (useScientific) { + properties.setMinimumExponentDigits(1); + } else { + properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS); + } + refreshFormatter(); + } + + /** + * @return The minimum number of digits printed in the exponent in scientific notation. + * @see #setMinimumExponentDigits + * @category ScientificNotation + * @stable ICU 2.0 + */ + public synchronized byte getMinimumExponentDigits() { + return (byte) exportedProperties.getMinimumExponentDigits(); + } + + /** + * Scientific Notation: Sets the minimum number of digits to be printed in the + * exponent. For example, if minimum exponent digits is 3, the number 123000 will be printed as + * "1.23E005". + * + *

This setting corresponds to the number of zeros after the 'E' in a pattern string such as + * "0.00E000". + * + * @param minExpDig The minimum number of digits in the exponent. + * @category ScientificNotation + * @stable ICU 2.0 + */ + public synchronized void setMinimumExponentDigits(byte minExpDig) { + properties.setMinimumExponentDigits(minExpDig); + refreshFormatter(); + } + + /** + * @return Whether the sign (plus or minus) is always printed in scientific notation. + * @see #setExponentSignAlwaysShown + * @category ScientificNotation + * @stable ICU 2.0 + */ + public synchronized boolean isExponentSignAlwaysShown() { + return exportedProperties.getExponentSignAlwaysShown(); + } + + /** + * Scientific Notation: Sets whether the sign (plus or minus) is always to be + * shown in the exponent in scientific notation. For example, if this setting is enabled, the + * number 123000 will be printed as "1.23E+5" in locale en-US. The number 0.0000123 will + * always be printed as "1.23E-5" in locale en-US whether or not this setting is enabled. + * + *

This setting corresponds to the '+' in a pattern such as "0.00E+0". + * + * @param expSignAlways true to always shown the sign in the exponent; false to show it for + * negatives but not positives. + * @category ScientificNotation + * @stable ICU 2.0 + */ + public synchronized void setExponentSignAlwaysShown(boolean expSignAlways) { + properties.setExponentSignAlwaysShown(expSignAlways); + refreshFormatter(); + } + + /** + * @return Whether or not grouping separators are to be printed in the output. + * @see #setGroupingUsed + * @category Separators + * @stable ICU 2.0 + */ + @Override + public synchronized boolean isGroupingUsed() { + return PositiveDecimalFormat.useGrouping(properties); + } + + /** + * Grouping: Sets whether grouping is to be used when formatting numbers. + * Grouping means whether the thousands, millions, billions, and larger powers of ten should be + * separated by a grouping separator (a comma in en-US). + * + *

For example, if grouping is enabled, 12345 will be printed as "12,345" in en-US. If + * grouping were disabled, it would instead be printed as simply "12345". + * + *

Calling df.setGroupingUsed(true) is functionally equivalent to setting grouping + * size to 3, as in df.setGroupingSize(3). + * + * @param enabled true to enable grouping separators; false to disable them. + * @see #setGroupingSize + * @see #setSecondaryGroupingSize + * @category Separators + * @stable ICU 2.0 + */ + @Override + public synchronized void setGroupingUsed(boolean enabled) { + if (enabled) { + // Set to a reasonable default value + properties.setGroupingSize(3); + } else { + properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE); + properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE); + } + refreshFormatter(); + } + + /** + * @return The primary grouping size in use. + * @see #setGroupingSize + * @category Separators + * @stable ICU 2.0 + */ + public synchronized int getGroupingSize() { + return exportedProperties.getGroupingSize(); + } + + /** + * Grouping: Sets the primary grouping size (distance between grouping + * separators) used when formatting large numbers. For most locales, this defaults to 3: the + * number of digits between the ones and thousands place, between thousands and millions, and so + * forth. + * + *

For example, with a grouping size of 3, the number 1234567 will be formatted as "1,234,567". + * + *

Grouping size can also be specified in the pattern: for example, "#,##0" corresponds to a + * grouping size of 3. + * + * @param width The grouping size to use. + * @see #setSecondaryGroupingSize + * @category Separators + * @stable ICU 2.0 + */ + public synchronized void setGroupingSize(int width) { + properties.setGroupingSize(width); + refreshFormatter(); + } + + /** + * @return The secondary grouping size in use. + * @see #setSecondaryGroupingSize + * @category Separators + * @stable ICU 2.0 + */ + public synchronized int getSecondaryGroupingSize() { + return exportedProperties.getSecondaryGroupingSize(); + } + + /** + * Grouping: Sets the secondary grouping size (distance between grouping + * separators after the first separator) used when formatting large numbers. In many south Asian + * locales, this is set to 2. + * + *

For example, with primary grouping size 3 and secondary grouping size 2, the number 1234567 + * will be formatted as "12,34,567". + * + *

Grouping size can also be specified in the pattern: for example, "#,##,##0" corresponds to a + * primary grouping size of 3 and a secondary grouping size of 2. + * + * @param width The secondary grouping size to use. + * @see #setGroupingSize + * @category Separators + * @stable ICU 2.0 + */ + public synchronized void setSecondaryGroupingSize(int width) { + properties.setSecondaryGroupingSize(width); + refreshFormatter(); + } + + /** + * @return The minimum number of digits before grouping is triggered. + * @see #setMinimumGroupingDigits + * @category Separators + * @internal + * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. + */ + @Deprecated + public synchronized int getMinimumGroupingDigits() { + return properties.getMinimumGroupingDigits(); + } + + /** + * Sets the minimum number of digits that must be before the first grouping separator in order for + * the grouping separator to be printed. For example, if minimum grouping digits is set to 2, in + * en-US, 1234 will be printed as "1234" and 12345 will be printed as "12,345". + * + * @param number The minimum number of digits before grouping is triggered. + * @category Separators + * @internal + * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. + */ + @Deprecated + public synchronized void setMinimumGroupingDigits(int number) { + properties.setMinimumGroupingDigits(number); + refreshFormatter(); + } + + /** + * @return Whether the decimal separator is shown on integers. + * @see #setDecimalSeparatorAlwaysShown + * @category Separators + * @stable ICU 2.0 + */ + public synchronized boolean isDecimalSeparatorAlwaysShown() { + return exportedProperties.getDecimalSeparatorAlwaysShown(); + } + + /** + * Separators: Sets whether the decimal separator (a period in en-US) is + * shown on integers. For example, if this setting is turned on, formatting 123 will result in + * "123." with the decimal separator. + * + *

This setting can be specified in the pattern for integer formats: "#,##0." is an example. + * + * @param value true to always show the decimal separator; false to show it only when there is a + * fraction part of the number. + * @category Separators + * @stable ICU 2.0 + */ + public synchronized void setDecimalSeparatorAlwaysShown(boolean value) { + properties.setDecimalSeparatorAlwaysShown(value); + refreshFormatter(); + } + + /** + * @return The user-specified currency. May be null. + * @see #setCurrency + * @see DecimalFormatSymbols#getCurrency + * @category Currency + * @stable ICU 2.6 + */ + @Override + public synchronized Currency getCurrency() { + return properties.getCurrency(); + } + + /** + * Sets the currency to be used when formatting numbers. The effect is twofold: + * + *

    + *
  1. Substitutions for currency symbols in the pattern string will use this currency + *
  2. The rounding mode will obey the rules for this currency (see {@link #setCurrencyUsage}) + *
+ * + * Important: Displaying the currency in the output requires that the patter + * associated with this formatter contains a currency symbol '¤'. This will be the case if the + * instance was created via {@link #getCurrencyInstance} or one of its friends. + * + * @param currency The currency to use. + * @category Currency + * @stable ICU 2.2 + */ + @Override + public synchronized void setCurrency(Currency currency) { + properties.setCurrency(currency); + // Backwards compatibility: also set the currency in the DecimalFormatSymbols + if (currency != null) { + symbols.setCurrency(currency); + String symbol = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null); + symbols.setCurrencySymbol(symbol); + } + refreshFormatter(); + } + + /** + * @return The strategy for rounding currency amounts. + * @see #setCurrencyUsage + * @category Currency + * @stable ICU 54 + */ + public synchronized CurrencyUsage getCurrencyUsage() { + // CurrencyUsage is not exported, so we have to get it from the input property bag. + // TODO: Should we export CurrencyUsage instead? + CurrencyUsage usage = properties.getCurrencyUsage(); + if (usage == null) { + usage = CurrencyUsage.STANDARD; + } + return usage; + } + + /** + * Sets the currency-dependent strategy to use when rounding numbers. There are two strategies: + * + *
    + *
  • STANDARD: When the amount displayed is intended for banking statements or electronic + * transfer. + *
  • CASH: When the amount displayed is intended to be representable in physical currency, + * like at a cash register. + *
+ * + * CASH mode is relevant in currencies that do not have tender down to the penny. For more + * information on the two rounding strategies, see UTS + * #35. If omitted, the strategy defaults to STANDARD. To override currency rounding + * altogether, use {@link #setMinimumFractionDigits} and {@link #setMaximumFractionDigits} or + * {@link #setRoundingIncrement}. + * + * @param usage The strategy to use when rounding in the current currency. + * @category Currency + * @stable ICU 54 + */ + public synchronized void setCurrencyUsage(CurrencyUsage usage) { + properties.setCurrencyUsage(usage); + refreshFormatter(); + } + + /** + * @return The current instance of CurrencyPluralInfo. + * @see #setCurrencyPluralInfo + * @category Currency + * @stable ICU 4.2 + */ + public CurrencyPluralInfo getCurrencyPluralInfo() { + // CurrencyPluralInfo also is not exported. + return properties.getCurrencyPluralInfo(); + } + + /** + * Sets a custom instance of CurrencyPluralInfo. CurrencyPluralInfo generates pattern strings for + * printing currency long names. + * + *

Most users should not call this method directly. You should instead create + * your formatter via NumberFormat.getInstance(NumberFormat.PLURALCURRENCYSTYLE). + * + * @param newInfo The CurrencyPluralInfo to use when printing currency long names. + * @category Currency + * @stable ICU 4.2 + */ + public void setCurrencyPluralInfo(CurrencyPluralInfo newInfo) { + properties.setCurrencyPluralInfo(newInfo); + refreshFormatter(); + } + + /** + * @return Whether {@link #parse} will always return a BigDecimal + * @see #setParseBigDecimal + * @category Parsing + * @stable ICU 3.6 + */ + public synchronized boolean isParseBigDecimal() { + return properties.getParseToBigDecimal(); + } + + /** + * Whether to force {@link #parse} to always return a BigDecimal. By default, {@link #parse} will + * return different data types as follows: + * + *

    + *
  1. If the number is an integer (has no fraction part), return a Long if possible, or else a + * BigInteger. + *
  2. Otherwise, return a BigDecimal. + *
+ * + * If this setting is enabled, a BigDecimal will be returned even if the number is an integer. + * + * @param value true to cause {@link #parse} to always return a BigDecimal; false to let {@link + * #parse} return different data types. + * @category Parsing + * @stable ICU 3.6 + */ + public synchronized void setParseBigDecimal(boolean value) { + properties.setParseToBigDecimal(value); + // refreshFormatter() not needed + } + + /** + * @return Always 1000, the default prior to ICU 59. + * @category Parsing + * @deprecated Setting max parse digits has no effect since ICU4J 59. + */ + @Deprecated + public int getParseMaxDigits() { + return 1000; + } + + /** + * @param maxDigits Prior to ICU 59, the maximum number of digits in the output number after + * exponential notation is applied. + * @category Parsing + * @deprecated Setting max parse digits has no effect since ICU4J 59. + */ + @Deprecated + public void setParseMaxDigits(int maxDigits) {} + + /** + * {@inheritDoc} + * + * @category Parsing + * @stable ICU 3.6 + */ + @Override + public synchronized boolean isParseStrict() { + return properties.getParseMode() == Parse.ParseMode.STRICT; + } + + /** + * {@inheritDoc} + * + * @category Parsing + * @stable ICU 3.6 + */ + @Override + public synchronized void setParseStrict(boolean parseStrict) { + Parse.ParseMode mode = parseStrict ? Parse.ParseMode.STRICT : Parse.ParseMode.LENIENT; + properties.setParseMode(mode); + // refreshFormatter() not needed + } + + /** + * @return Whether parsing should stop before encountering a decimal point and fraction part. + * @see #setParseIntegerOnly + * @category Parsing + * @stable ICU 2.0 + */ + @Override + public synchronized boolean isParseIntegerOnly() { + return properties.getParseIntegerOnly(); + } + + /** + * Parsing: Whether to ignore the fraction part of a number when parsing + * (defaults to false). If a string contains a decimal point, parsing will stop before the decimal + * point. Note that determining whether a character is a decimal point depends on the locale. + * + *

For example, in en-US, parsing the string "123.45" will return the number 123 and + * parse position 3. + * + *

This is functionally equivalent to calling {@link #setDecimalPatternMatchRequired} and a + * pattern without a decimal point. + * + * @param parseIntegerOnly true to ignore fractional parts of numbers when parsing; false to + * consume fractional parts. + * @category Parsing + * @stable ICU 2.0 + */ + @Override + public synchronized void setParseIntegerOnly(boolean parseIntegerOnly) { + properties.setParseIntegerOnly(parseIntegerOnly); + // refreshFormatter() not needed + } + + /** + * @return Whether the presence of a decimal point must match the pattern. + * @see #setDecimalPatternMatchRequired + * @category Parsing + * @stable ICU 54 + */ + public synchronized boolean isDecimalPatternMatchRequired() { + return properties.getDecimalPatternMatchRequired(); + } + + /** + * Parsing: This method is used to either require or forbid the + * presence of a decimal point in the string being parsed (disabled by default). This feature was + * designed to be an extra layer of strictness on top of strict parsing, although it can be used + * in either lenient mode or strict mode. + * + *

To require a decimal point, call this method in combination with either a pattern + * containing a decimal point or with {@link #setDecimalSeparatorAlwaysShown}. + * + *

+   * // Require a decimal point in the string being parsed:
+   * df.applyPattern("#.");
+   * df.setDecimalPatternMatchRequired(true);
+   *
+   * // Alternatively:
+   * df.setDecimalSeparatorAlwaysShown(true);
+   * df.setDecimalPatternMatchRequired(true);
+   * 
+ * + * To forbid a decimal point, call this method in combination with a pattern containing + * no decimal point. Alternatively, use {@link #setParseIntegerOnly} for the same behavior without + * depending on the contents of the pattern string. + * + *
+   * // Forbid a decimal point in the string being parsed:
+   * df.applyPattern("#");
+   * df.setDecimalPatternMatchRequired(true);
+   * 
+ * + * @param value true to either require or forbid the decimal point according to the pattern; false + * to disable this feature. + * @see #setParseIntegerOnly + * @category Parsing + * @stable ICU 54 + */ + public synchronized void setDecimalPatternMatchRequired(boolean value) { + properties.setDecimalPatternMatchRequired(value); + refreshFormatter(); + } + + /** + * @return Whether to ignore exponents when parsing. + * @see #setParseNoExponent + * @category Parsing + * @internal + * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. + */ + @Deprecated + public synchronized boolean getParseNoExponent() { + return properties.getParseNoExponent(); + } + + /** + * Specifies whether to stop parsing when an exponent separator is encountered. For example, + * parses "123E4" to 123 (with parse position 3) instead of 1230000 (with parse position 5). + * + * @param value true to prevent exponents from being parsed; false to allow them to be parsed. + * @category Parsing + * @internal + * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. + */ + @Deprecated + public synchronized void setParseNoExponent(boolean value) { + properties.setParseNoExponent(value); + refreshFormatter(); + } + + /** + * @return Whether to force case (uppercase/lowercase) to match when parsing. + * @see #setParseNoExponent + * @category Parsing + * @internal + * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. + */ + @Deprecated + public synchronized boolean getParseCaseSensitive() { + return properties.getParseCaseSensitive(); + } + + /** + * Specifies whether parsing should require cases to match in affixes, exponent separators, and + * currency codes. Case mapping is performed for each code point using {@link + * UCharacter#foldCase}. + * + * @param value true to force case (uppercase/lowercase) to match when parsing; false to ignore + * case and perform case folding. + * @category Parsing + * @internal + * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. + */ + @Deprecated + public synchronized void setParseCaseSensitive(boolean value) { + properties.setParseCaseSensitive(value); + refreshFormatter(); + } + + //=====================================================================================// + // UTILITIES // + //=====================================================================================// + + /** + * Tests for equality between this formatter and another formatter. + * + *

If two DecimalFormat instances are equal, then they will always produce the same output. + * However, the reverse is not necessarily true: if two DecimalFormat instances always produce the + * same output, they are not necessarily equal. + * + * @stable ICU 2.0 + */ + @Override + public synchronized boolean equals(Object obj) { + if (obj == null) return false; + if (obj == this) return true; + if (!(obj instanceof DecimalFormat)) return false; + DecimalFormat other = (DecimalFormat) obj; + return properties.equals(other.properties) && symbols.equals(other.symbols); + } + + /** @stable ICU 2.0 */ + @Override + public synchronized int hashCode() { + return properties.hashCode(); + } + + private static final ThreadLocal threadLocalToPatternProperties = + new ThreadLocal() { @Override - public String toString() { - return prefix + "/" + suffix; + protected Properties initialValue() { + return new Properties(); } + }; + + @Override + public synchronized String toString() { + return ""; + } + + /** + * Serializes this formatter object to a decimal format pattern string. The result of this method + * is guaranteed to be functionally equivalent to the pattern string used to create this + * instance after incorporating values from the setter methods. + * + *

For more information on decimal format pattern strings, see UTS #35. + * + *

Important: Not all properties are capable of being encoded in a pattern + * string. See a list of properties in {@link #applyPattern}. + * + * @return A decimal format pattern string. + * @stable ICU 2.0 + */ + public synchronized String toPattern() { + // Pull some properties from exportedProperties and others from properties + // to keep affix patterns intact. In particular, pull rounding properties + // so that CurrencyUsage is reflected properly. + // TODO: Consider putting this logic in PatternString.java instead. + Properties tprops = threadLocalToPatternProperties.get(); + tprops.copyFrom(properties); + if (com.ibm.icu.impl.number.formatters.CurrencyFormat.useCurrency(properties)) { + tprops.setMinimumFractionDigits(exportedProperties.getMinimumFractionDigits()); + tprops.setMaximumFractionDigits(exportedProperties.getMaximumFractionDigits()); + tprops.setRoundingIncrement(exportedProperties.getRoundingIncrement()); } + return PatternString.propertiesToString(tprops); + } - static final Unit NULL_UNIT = new Unit("", ""); + /** + * Calls {@link #toPattern} and converts the string to localized notation. For more information on + * localized notation, see {@link #applyLocalizedPattern}. + * + * @return A decimal format pattern string in localized notation. + * @stable ICU 2.0 + */ + public synchronized String toLocalizedPattern() { + String pattern = toPattern(); + return PatternString.convertLocalized(pattern, symbols, true); + } - // 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. + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public IFixedDecimal getFixedDecimal(double number) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq); + return fq; + } - 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(); - } + /** Rebuilds the formatter object from the property bag. */ + void refreshFormatter() { + if (exportedProperties == null) { + // exportedProperties is null only when the formatter is not ready yet. + // The only time when this happens is during legacy deserialization. + return; } + formatter = Endpoint.fromBTA(properties, symbols); + exportedProperties.clear(); + formatter.export(exportedProperties); + } - static final double roundingIncrementEpsilon = 0.000000001; + /** + * Updates the property bag with settings from the given pattern. + * + * @param pattern The pattern string to parse. + * @param ignoreRounding Whether to read rounding information from the string. Set to false if + * CurrencyUsage is to be used instead. + * @see PatternString#parseToExistingProperties + */ + void setPropertiesFromPattern(String pattern, boolean ignoreRounding) { + PatternString.parseToExistingProperties(pattern, properties, ignoreRounding); + } - 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; - } - } + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public synchronized void setProperties(PropertySetter func) { + func.set(properties); + refreshFormatter(); + } + + public static interface PropertySetter { + public void set(Properties props); + } + + /** + * {@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; } - -// eof diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormatSymbols.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormatSymbols.java index 0e3ec41ebe..07e812bcd2 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormatSymbols.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormatSymbols.java @@ -232,8 +232,11 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { * Returns the array of strings used as digits, in order from 0 through 9 * Package private method - doesn't create a defensively copy. * @return the array of digit strings + * @internal + * @deprecated This API is ICU internal only. */ - String[] getDigitStringsLocal() { + @Deprecated + public String[] getDigitStringsLocal() { return digitStrings; } @@ -1318,9 +1321,9 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { setMonetaryGroupingSeparatorString(numberElements[11]); setExponentMultiplicationSign(numberElements[12]); - digit = DecimalFormat.PATTERN_DIGIT; // Localized pattern character no longer in CLDR - padEscape = DecimalFormat.PATTERN_PAD_ESCAPE; - sigDigit = DecimalFormat.PATTERN_SIGNIFICANT_DIGIT; + digit = '#'; // Localized pattern character no longer in CLDR + padEscape = '*'; + sigDigit = '@'; CurrencyDisplayInfo info = CurrencyData.provider.getInstance(locale, true); @@ -1448,8 +1451,8 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { exponential = 'E'; } if (serialVersionOnStream < 2) { - padEscape = DecimalFormat.PATTERN_PAD_ESCAPE; - plusSign = DecimalFormat.PATTERN_PLUS_SIGN; + padEscape = '*'; + plusSign = '+'; exponentSeparator = String.valueOf(exponential); // Although we read the exponential field on stream to create the // exponentSeparator, we don't do the reverse, since scientific @@ -1527,7 +1530,7 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { groupingSeparatorString = String.valueOf(groupingSeparator); } if (percentString == null) { - percentString = String.valueOf(percentString); + percentString = String.valueOf(percent); } if (perMillString == null) { perMillString = String.valueOf(perMill); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat_ICU58.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat_ICU58.java new file mode 100644 index 0000000000..1476274955 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat_ICU58.java @@ -0,0 +1,6277 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +/* + ******************************************************************************* + * Copyright (C) 1996-2016, International Business Machines Corporation and + * others. All Rights Reserved. + ******************************************************************************* + */ +package com.ibm.icu.text; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.math.BigInteger; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.ChoiceFormat; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.ibm.icu.impl.ICUConfig; +import com.ibm.icu.impl.PatternProps; +import com.ibm.icu.impl.Utility; +import com.ibm.icu.lang.UCharacter; +import com.ibm.icu.math.BigDecimal; +import com.ibm.icu.math.MathContext; +import com.ibm.icu.text.PluralRules.FixedDecimal; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Currency.CurrencyUsage; +import com.ibm.icu.util.CurrencyAmount; +import com.ibm.icu.util.ULocale; +import com.ibm.icu.util.ULocale.Category; + +/** + * {@icuenhanced java.text.DecimalFormat}.{@icu _usage_} + * + * DecimalFormat 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. + * + *

To obtain a {@link NumberFormat} for a specific locale (including the default + * locale) call one of NumberFormat's factory methods such as {@link + * NumberFormat#getInstance}. Do not call the DecimalFormat constructors + * directly, unless you know what you are doing, since the {@link NumberFormat} factory + * methods may return subclasses other than DecimalFormat. If you need to + * customize the format object, do something like this: + * + *

+ * NumberFormat f = NumberFormat.getInstance(loc);
+ * if (f instanceof DecimalFormat) {
+ *     ((DecimalFormat) f).setDecimalSeparatorAlwaysShown(true);
+ * }
+ * + *

Example Usage + * + * Print out a number using the localized number, currency, and percent + * format for each locale. + * + *

+ * 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) {}
+ *     }
+ * }
+ * + *

Another example use getInstance(style).
+ * Print out a number using the localized number, currency, percent, + * scientific, integer, iso currency, and plural currency format for each locale. + * + *

+ * 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) {}
+ * }
+ * + *

Patterns

+ * + *

A DecimalFormat consists of a pattern and a set of + * symbols. 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. + * + *

Special Pattern Characters

+ * + *

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. + * + *

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. + * + *

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. + * + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Symbol + * Location + * Localized? + * Meaning + *
0 + * Number + * Yes + * Digit + *
1-9 + * Number + * Yes + * '1' through '9' indicate rounding. + *
@ + * Number + * No + * Significant digit + *
# + * Number + * Yes + * Digit, zero shows as absent + *
. + * Number + * Yes + * Decimal separator or monetary decimal separator + *
- + * Number + * Yes + * Minus sign + *
, + * Number + * Yes + * Grouping separator + *
E + * Number + * Yes + * Separates mantissa and exponent in scientific notation. + * Need not be quoted in prefix or suffix. + *
+ + * Exponent + * Yes + * Prefix positive exponents with localized plus sign. + * Need not be quoted in prefix or suffix. + *
; + * Subpattern boundary + * Yes + * Separates positive and negative subpatterns + *
% + * Prefix or suffix + * Yes + * Multiply by 100 and show as percentage + *
\u2030 + * Prefix or suffix + * Yes + * Multiply by 1000 and show as per mille + *
¤ (\u00A4) + * Prefix or suffix + * No + * 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. + *
' + * Prefix or suffix + * No + * Used to quote special characters in a prefix or suffix, + * for example, "'#'#" formats 123 to + * "#123". To create a single quote + * itself, use two in a row: "# o''clock". + *
* + * Prefix or suffix boundary + * Yes + * Pad escape, precedes pad character + *
+ *
+ * + *

A DecimalFormat 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#)". + * + *

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. + * + *

The grouping separator 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 grouping size 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 primary grouping size, and one used for all + * others, the secondary grouping size. 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 "#,##,###,####" == "###,###,####" == + * "##,#,###,####". + * + *

Illegal patterns, such as "#.#.#" or "#.###,###", will cause + * DecimalFormat to throw an {@link IllegalArgumentException} with a message + * that describes the problem. + * + *

Pattern BNF

+ * + *
+ * 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
+ * 
+ * The first subpattern is for positive numbers. The second (optional) + * subpattern is for negative numbers. + * + *

Not indicated in the BNF syntax above: + * + *

    + * + *
  • 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. + * + *
  • 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". + * + *
  • The pad specifier padSpec may appear before the prefix, after the + * prefix, before the suffix, after the suffix, or not at all. + * + *
  • In place of '0', the digits '1' through '9' may be used to indicate a rounding + * increment. + * + *
+ * + *

Parsing

+ * + *

DecimalFormat parses all Unicode characters that represent decimal + * digits, as defined by {@link UCharacter#digit}. In addition, + * DecimalFormat 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. + * + *

During parsing, grouping separators are ignored. + * + *

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". + * + *

If {@link #parse(String, ParsePosition)} fails to parse a string, it returns + * null and leaves the parse position unchanged. The convenience method + * {@link #parse(String)} indicates parse failure by throwing a {@link + * java.text.ParseException}. + * + *

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, + * DecimalFormat 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). + * + *

Formatting

+ * + *

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 scientific notation or significant + * digits. + * + *

  • If the number of actual integer digits exceeds the maximum integer + * digits, then only the least significant digits are shown. For example, 1997 is + * formatted as "97" if the maximum integer digits is set to 2. + * + *
  • If the number of actual integer digits is less than the minimum integer + * digits, then leading zeros are added. For example, 1997 is formatted as "01997" + * if the minimum integer digits is set to 5. + * + *
  • If the number of actual fraction digits exceeds the maximum fraction + * digits, 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. + * + *
  • If the number of actual fraction digits is less than the minimum fraction + * digits, then trailing zeros are added. For example, 0.125 is formatted as + * "0.1250" if the mimimum fraction digits is set to 4. + * + *
  • Trailing fractional zeros are not displayed if they occur j positions + * after the decimal, where j 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. + *
+ * + *

Special Values + * + *

NaN is represented as a single character, typically + * \uFFFD. This character is determined by the {@link + * DecimalFormatSymbols} object. This is the only value for which the prefixes and + * suffixes are not used. + * + *

Infinity is represented as a single character, typically \u221E, + * with the positive or negative prefixes and suffixes applied. The infinity character is + * determined by the {@link DecimalFormatSymbols} object. + * + *

Scientific Notation

+ * + *

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 103. The + * mantissa is typically in the half-open interval [1.0, 10.0) or sometimes [0.0, 1.0), + * but it need not be. DecimalFormat supports arbitrary mantissas. + * DecimalFormat 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". + * + *

    + * + *
  • 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, not 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 '+'.) + * + *
  • 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. + * + *
  • The maximum number of integer digits, if present, specifies the exponent grouping. + * The most common use of this is to generate engineering notation, in which the + * exponent is a multiple of three, e.g., "##0.###E0". The number 12345 is formatted + * using "##0.####E0" as "12.345E3". + * + *
  • 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. + * + *
  • 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 + * minimum integer and maximum fraction 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. + * + *
  • Exponential patterns may not contain grouping separators. + * + *
+ * + *

Significant Digits

+ * + * DecimalFormat 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: + * + *
+ * + * + * + * + * + * + *
Pattern + * Minimum significant digits + * Maximum significant digits + * Number + * Output of format() + *
@@@ + * 3 + * 3 + * 12345 + * 12300 + *
@@@ + * 3 + * 3 + * 0.12345 + * 0.123 + *
@@## + * 2 + * 4 + * 3.14159 + * 3.142 + *
@@## + * 2 + * 4 + * 1.23004 + * 1.23 + *
+ *
+ * + *
    + * + *
  • Significant digit counts may be expressed using patterns that specify a minimum and + * maximum number of significant digits. These are indicated by the '@' and + * '#' characters. The minimum number of significant digits is the number of + * '@' characters. The maximum number of significant digits is the number of + * '@' characters plus the number of '#' characters following on + * the right. For example, the pattern "@@@" indicates exactly 3 significant + * digits. The pattern "@##" 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 + * "@##" formats the number 0.1203 as "0.12". + * + *
  • If a pattern uses significant digits, it may not contain a decimal separator, nor + * the '0' pattern character. Patterns such as "@00" or + * "@.###" are disallowed. + * + *
  • Any number of '#' characters may be prepended to the left of the + * leftmost '@' character. These have no effect on the minimum and maximum + * significant digits counts, but may be used to position grouping separators. For + * example, "#,#@#" indicates a minimum of one significant digits, a maximum + * of two significant digits, and a grouping size of three. + * + *
  • In order to enable significant digits formatting, use a pattern containing the + * '@' pattern character. Alternatively, call {@link + * #setSignificantDigitsUsed setSignificantDigitsUsed(true)}. + * + *
  • In order to disable significant digits formatting, use a pattern that does not + * contain the '@' pattern character. Alternatively, call {@link + * #setSignificantDigitsUsed setSignificantDigitsUsed(false)}. + * + *
  • The number of significant digits has no effect on parsing. + * + *
  • 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 getMinimumSignificantDigits() - + * 1, and a maximum fraction digit count of getMaximumSignificantDigits() - + * 1. For example, the pattern "@@###E0" is equivalent to + * "0.0###E0". + * + *
  • 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. + * + *
+ * + *

Padding

+ * + *

DecimalFormat 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, "$*x#,##0.00" formats + * 123 to "$xx123.00", and 1234 to "$1,234.00". + * + *

    + * + *
  • When padding is in effect, the width of the positive subpattern, including prefix + * and suffix, determines the format width. For example, in the pattern "* #0 + * o''clock", the format width is 10. + * + *
  • The width is counted in 16-bit code units (Java chars). + * + *
  • 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. + * + *
  • 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. + * + *
  • When specified in a pattern, the 16-bit char immediately following the + * pad escape is the pad character. This may be any character, including a special pattern + * character. That is, the pad escape escapes the following character. If there + * is no character after the pad escape, then the pattern is illegal. + * + *
+ * + *

+ * Rounding + * + *

DecimalFormat 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. + * + *

    + * + *
  • Rounding only affects the string produced by formatting. It does not affect + * parsing or change any numerical values. + * + *
  • A rounding mode 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}. + * + *
  • Some locales use rounding in their currency formats to reflect the smallest + * currency denomination. + * + *
  • In a pattern, digits '1' through '9' specify rounding, but otherwise behave + * identically to digit '0'. + * + *
+ * + *

Synchronization

+ * + *

DecimalFormat 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 + * FORMAT locale. This is a convenient way to obtain a DecimalFormat when + * internationalization is not the main concern. + * + *

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 + * FORMAT locale. This is a convenient way to obtain a DecimalFormat when + * internationalization is not the main concern. + * + *

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. + * + *

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. + * + *

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 using the setters. + * + *

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 Number object to represent the + * parsed value. Double objects are returned to represent non-integral + * values which cannot be stored in a BigDecimal. These are + * NaN, infinity, -infinity, and -0.0. If {@link #isParseBigDecimal()} is + * false (the default), all other values are returned as Long, + * BigInteger, or BigDecimal values, in that order of + * preference. If {@link #isParseBigDecimal()} is true, all other values are returned + * as BigDecimal 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 Number object with the parsed value or + * null 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(); + + // 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 iter = currencyPluralInfo.pluralPatternIterator(); + Set currencyUnitPatternSet = new HashSet(); + 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 decVal 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. + * + *

Examples: +123, $123, sFr123 + * @return the prefix + * @stable ICU 2.0 + */ + public String getPositivePrefix() { + return positivePrefix; + } + + /** + * Sets the positive prefix. + * + *

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. + * + *

Examples: -123, ($123) (with negative suffix), sFr-123 + * + * @return the prefix + * @stable ICU 2.0 + */ + public String getNegativePrefix() { + return negativePrefix; + } + + /** + * Sets the negative prefix. + * + *

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. + * + *

Example: 123% + * + * @return the suffix + * @stable ICU 2.0 + */ + public String getPositiveSuffix() { + return positiveSuffix; + } + + /** + * Sets the positive suffix. + * + *

Example: 123% + * @param newValue the suffix + * @stable ICU 2.0 + */ + public void setPositiveSuffix(String newValue) { + positiveSuffix = newValue; + posSuffixPattern = null; + } + + /** + * Returns the negative suffix. + * + *

Examples: -123%, ($123) (with positive suffixes) + * + * @return the suffix + * @stable ICU 2.0 + */ + public String getNegativeSuffix() { + return negativeSuffix; + } + + /** + * Sets the positive suffix. + * + *

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. + * + *

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. + * + *

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 null 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 null or + * BigDecimal(0.0) to use the default rounding increment. + * @throws IllegalArgumentException if newValue 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 null or + * BigDecimal(0.0) to use the default rounding increment. + * @throws IllegalArgumentException if newValue 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 newValue 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 BigDecimal.ROUND_UP and + * BigDecimal.ROUND_UNNECESSARY. + * @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 BigDecimal.ROUND_UP and + * BigDecimal.ROUND_UNNECESSARY. + * @exception IllegalArgumentException if roundingMode 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 format() 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 format() 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 + * format(), or zero to disable padding + * @exception IllegalArgumentException if width 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 format() is shorter + * than the format width. + * + * @return the pad position, one of PAD_BEFORE_PREFIX, + * PAD_AFTER_PREFIX, PAD_BEFORE_SUFFIX, or + * PAD_AFTER_SUFFIX. + * @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 format() is shorter + * than the format width. This has no effect unless padding is enabled. + * + * @param padPos the pad position, one of PAD_BEFORE_PREFIX, + * PAD_AFTER_PREFIX, PAD_BEFORE_SUFFIX, or + * PAD_AFTER_SUFFIX. + * @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 minExpDig < 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 getGroupingSize(). 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.)

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.) + * + *

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. + * + *

+ * 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(); // #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. + *

+ * Note: 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. + *

+ * Note: 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. + * + *

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 + * + *

Example "#,#00.0#" -> 1,234.56 + * + *

This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2 + * fraction digits. + * + *

Example: "#,#00.0#;(#,#00.0#)" for negatives in parentheses. + * + *

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. + * + *

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 + * + *

Example "#,#00.0#" -> 1,234.56 + * + *

This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2 + * fraction digits. + * + *

Example: "#,#00.0#;(#,#00.0#)" for negatives in parantheses. + * + *

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 + * min is less than one then it is set to one. If the maximum significant + * digits count is less than min, then it is set to min. + * 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 + * max is less than one then it is set to one. If the minimum significant + * digits count is greater than max, then it is set to max. + * 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 Currency 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 Currency Usage 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 Currency Usage 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 + * serialVersionOnStream is less than 1, indicating that the stream was + * written by JDK 1.1, initialize useExponentialNotation 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 + * positivePrefix. + * + *

This pattern is expanded by the method expandAffix() to + * positivePrefix to update the latter to reflect changes in + * symbols. If this variable is null then + * positivePrefix is taken as a literal value that does not change when + * symbols changes. This variable is always null for + * DecimalFormat 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 + * positiveSuffix. This variable is analogous to + * posPrefixPattern; see that variable for further documentation. + * + * @serial + */ + // [Richard/GCL] + private String posSuffixPattern; + + /** + * The prefix pattern for negative numbers. This variable corresponds to + * negativePrefix. This variable is analogous to + * posPrefixPattern; see that variable for further documentation. + * + * @serial + */ + // [Richard/GCL] + private String negPrefixPattern; + + /** + * The suffix pattern for negative numbers. This variable corresponds to + * negativeSuffix. This variable is analogous to + * posPrefixPattern; 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 NumberFormat.groupingUsed 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 DecimalFormatSymbols 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. + * + *

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 + * useExponentialNotation is not true. + * + *

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 useExponentialNotation 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 null if rounding is not in effect, or a + * positive value if rounding is in effect. Default value null. + * + * @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 null if rounding is not in effect, or a + * positive value if rounding is in effect. Default value null. 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 + * BigDecimal rounding mode values. Default value + * BigDecimal.ROUND_HALF_EVEN. + * + * @serial + * @since AlphaWorks NumberFormat + */ + private int roundingMode = BigDecimal.ROUND_HALF_EVEN; + + /** + * Operations on BigDecimal numbers are controlled by a {@link + * MathContext} object, which provides the context (precision and other information) + * for the operation. The default MathContext settings are + * digits=0, form=PLAIN, lostDigits=false, roundingMode=ROUND_HALF_UP; + * 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 formatWidth, if + * padding is in effect. Default value ' '. + * + * @serial + * @since AlphaWorks NumberFormat + */ + private char pad = ' '; + + /** + * The position in the string at which the pad character will be + * inserted, if padding is in effect. Must have a value from + * PAD_BEFORE_PREFIX to PAD_AFTER_SUFFIX. Default value + * PAD_BEFORE_PREFIX. + * + * @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: + * + *

    + * + *
  • 0 (default): versions before JDK 1.2 + * + *
  • 1: version from JDK 1.2 and later, which includes the two new fields + * useExponentialNotation and minExponentDigits. + * + *
  • 2: version on AlphaWorks, which adds roundingMode, formatWidth, pad, + * padPosition, exponentSignAlwaysShown, roundingIncrement. + * + *
  • 3: ICU 2.2. Adds currency object. + * + *
  • 4: ICU 54. Adds currency usage(standard vs cash) + * + *
+ * + * @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 attributes = new ArrayList(); + + // 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 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 diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java index 6facd54e4b..ad4fa24198 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java @@ -56,13 +56,13 @@ import com.ibm.icu.util.UResourceBundle; *

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: *

  *      MeasureFormat fmtFr = MeasureFormat.getInstance(
  *              ULocale.FRENCH, FormatWidth.SHORT);
  *      Measure measure = new Measure(23, MeasureUnit.CELSIUS);
- *      
+ *
  *      // Output: 23 °C
  *      System.out.println(fmtFr.format(measure));
  *
@@ -70,29 +70,29 @@ import com.ibm.icu.util.UResourceBundle;
  *
  *      // Output: 70 °F
  *      System.out.println(fmtFr.format(measureF));
- *     
+ *
  *      MeasureFormat fmtFrFull = MeasureFormat.getInstance(
  *              ULocale.FRENCH, FormatWidth.WIDE);
  *      // Output: 70 pieds et 5,3 pouces
  *      System.out.println(fmtFrFull.formatMeasures(
  *              new Measure(70, MeasureUnit.FOOT),
  *              new Measure(5.3, MeasureUnit.INCH)));
- *              
+ *
  *      // Output: 1 pied et 1 pouce
  *      System.out.println(fmtFrFull.formatMeasures(
  *              new Measure(1, MeasureUnit.FOOT),
  *              new Measure(1, MeasureUnit.INCH)));
- *  
+ *
  *      MeasureFormat fmtFrNarrow = MeasureFormat.getInstance(
                 ULocale.FRENCH, FormatWidth.NARROW);
  *      // Output: 1′ 1″
  *      System.out.println(fmtFrNarrow.formatMeasures(
  *              new Measure(1, MeasureUnit.FOOT),
  *              new Measure(1, MeasureUnit.INCH)));
- *      
- *      
+ *
+ *
  *      MeasureFormat fmtEn = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.WIDE);
- *      
+ *
  *      // Output: 1 inch, 2 feet
  *      fmtEn.formatMeasures(
  *              new Measure(1, MeasureUnit.INCH),
@@ -105,7 +105,7 @@ import com.ibm.icu.util.UResourceBundle;
  * This class is immutable and thread-safe so long as its deprecated subclass,
  * TimeUnitFormat, is never used. TimeUnitFormat is not thread-safe, and is
  * mutable. Although this class has existing subclasses, this class does not support new
- * sub-classes.   
+ * sub-classes.
  *
  * @see com.ibm.icu.text.UFormat
  * @author Alan Liu
@@ -154,7 +154,7 @@ public class MeasureFormat extends UFormat {
 
     /**
      * Formatting width enum.
-     * 
+     *
      * @stable ICU 53
      */
     // Be sure to update MeasureUnitTest.TestSerialFormatWidthEnum
@@ -163,21 +163,21 @@ public class MeasureFormat extends UFormat {
 
         /**
          * Spell out everything.
-         * 
+         *
          * @stable ICU 53
          */
-        WIDE(ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE), 
+        WIDE(ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE),
 
         /**
          * Abbreviate when possible.
-         * 
+         *
          * @stable ICU 53
          */
-        SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE), 
+        SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE),
 
         /**
          * Brief. Use only a symbol for the unit when possible.
-         * 
+         *
          * @stable ICU 53
          */
         NARROW(ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE),
@@ -186,7 +186,7 @@ public class MeasureFormat extends UFormat {
          * Identical to NARROW except when formatMeasures is called with
          * an hour and minute; minute and second; or hour, minute, and second Measures.
          * In these cases formatMeasures formats as 5:37:23 instead of 5h, 37m, 23s.
-         * 
+         *
          * @stable ICU 53
          */
         NUMERIC(ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE);
@@ -294,16 +294,16 @@ public class MeasureFormat extends UFormat {
      * If the pos argument identifies a NumberFormat field,
      * then its indices are set to the beginning and end of the first such field
      * encountered. MeasureFormat itself does not supply any fields.
-     * 
+     *
      * Calling a
      * formatMeasures 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
@@ -327,7 +327,7 @@ public class MeasureFormat extends UFormat {
         } else if (obj instanceof Measure){
             toAppendTo.append(formatMeasure((Measure) obj, numberFormat, new StringBuilder(), fpos));
         } else {
-            throw new IllegalArgumentException(obj.toString());            
+            throw new IllegalArgumentException(obj.toString());
         }
         if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
             pos.setBeginIndex(fpos.getBeginIndex() + prevLength);
@@ -356,7 +356,7 @@ public class MeasureFormat extends UFormat {
      * and using the appropriate Number values. Typically the units should be
      * in descending order, with all but the last Measure having integer values
      * (eg, not “3.2 feet, 2 inches”).
-     * 
+     *
      * @param measures a sequence of one or more measures.
      * @return the formatted string.
      * @stable ICU 53
@@ -375,7 +375,7 @@ public class MeasureFormat extends UFormat {
      * 
Note: If the format doesn’t have enough decimals, or lowValue ≥ highValue, * the result will be a degenerate range, like “5-5 meters”. *
Currency Units are not yet supported. - * + * * @param lowValue low value in range * @param highValue high value in range * @return the formatted string. @@ -416,11 +416,11 @@ public class MeasureFormat extends UFormat { } final double lowDouble = lowNumber.doubleValue(); - String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble, + String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble, lowFpos.getCountVisibleFractionDigits(), lowFpos.getFractionDigits())); final double highDouble = highNumber.doubleValue(); - String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble, + String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble, highFpos.getCountVisibleFractionDigits(), highFpos.getFractionDigits())); final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(getLocale()); @@ -482,10 +482,10 @@ public class MeasureFormat extends UFormat { result.append(affix.substring(pos+replacement.length())); } } - + /** - * Formats a single measure per unit. - * + * Formats a single measure per unit. + * * An example of such a formatted string is "3.5 meters per second." * * @param measure the measure object. In above example, 3.5 meters. @@ -521,11 +521,11 @@ public class MeasureFormat extends UFormat { /** * Formats a sequence of measures. - * + * * If the fieldPosition argument identifies a NumberFormat field, * then its indices are set to the beginning and end of the first such field * encountered. MeasureFormat itself does not supply any fields. - * + * * @param appendTo the formatted string appended here. * @param fieldPosition Identifies a field in the formatted text. * @param measures the measures to format. @@ -612,8 +612,8 @@ public class MeasureFormat extends UFormat { } MeasureFormat rhs = (MeasureFormat) other; // A very slow but safe implementation. - return getWidth() == rhs.getWidth() - && getLocale().equals(rhs.getLocale()) + return getWidth() == rhs.getWidth() + && getLocale().equals(rhs.getLocale()) && getNumberFormat().equals(rhs.getNumberFormat()); } @@ -624,7 +624,7 @@ public class MeasureFormat extends UFormat { @Override public final int hashCode() { // A very slow but safe implementation. - return (getLocale().hashCode() * 31 + return (getLocale().hashCode() * 31 + getNumberFormat().hashCode()) * 31 + getWidth().hashCode(); } @@ -1020,7 +1020,12 @@ public class MeasureFormat extends UFormat { return pattern; } - private String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) { + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) { if (index != StandardPlural.OTHER_INDEX) { String pattern = getFormatterOrNull(unit, width, index); if (pattern != null) { @@ -1171,6 +1176,7 @@ public class MeasureFormat extends UFormat { suffix = pattern.substring(pos+3); } } + @Override public String toString() { return prefix + "; " + suffix; } @@ -1202,7 +1208,7 @@ public class MeasureFormat extends UFormat { if (fieldPositionFoundIndex == -1) { results[i] = formatMeasure(measures[i], nf, new StringBuilder(), fpos).toString(); if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) { - fieldPositionFoundIndex = i; + fieldPositionFoundIndex = i; } } else { results[i] = formatMeasure(measures[i], nf); @@ -1283,7 +1289,7 @@ public class MeasureFormat extends UFormat { // if hour-minute-second if (startIndex == 0 && endIndex == 2) { return formatNumeric( - d, + d, numericFormatters.getHourMinuteSecond(), DateFormat.Field.SECOND, hms[endIndex], @@ -1292,7 +1298,7 @@ public class MeasureFormat extends UFormat { // if minute-second if (startIndex == 1 && endIndex == 2) { return formatNumeric( - d, + d, numericFormatters.getMinuteSecond(), DateFormat.Field.SECOND, hms[endIndex], @@ -1301,7 +1307,7 @@ public class MeasureFormat extends UFormat { // if hour-minute if (startIndex == 0 && endIndex == 1) { return formatNumeric( - d, + d, numericFormatters.getHourMinute(), DateFormat.Field.MINUTE, hms[endIndex], @@ -1404,6 +1410,7 @@ public class MeasureFormat extends UFormat { public MeasureProxy() { } + @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeByte(0); // version out.writeUTF(locale.toLanguageTag()); @@ -1413,6 +1420,7 @@ public class MeasureFormat extends UFormat { out.writeObject(keyValues); } + @Override @SuppressWarnings("unchecked") public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { in.readByte(); // version. diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java index f31f5d52c0..08e5a9d270 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java @@ -38,7 +38,7 @@ import com.ibm.icu.impl.PatternProps; import com.ibm.icu.impl.Utility; import com.ibm.icu.text.MessagePattern.ArgType; import com.ibm.icu.text.MessagePattern.Part; -import com.ibm.icu.text.PluralRules.FixedDecimal; +import com.ibm.icu.text.PluralRules.IFixedDecimal; import com.ibm.icu.text.PluralRules.PluralType; import com.ibm.icu.util.ICUUncheckedIOException; import com.ibm.icu.util.ULocale; @@ -2108,7 +2108,7 @@ public class MessageFormat extends UFormat { assert context.number.doubleValue() == number; // argument number minus the offset context.numberString = context.formatter.format(context.number); if(context.formatter instanceof DecimalFormat) { - FixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number); + IFixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number); return rules.select(dec); } else { return rules.select(number); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java index 018d75f12b..74d1928a7b 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java @@ -497,8 +497,13 @@ public abstract class NumberFormat extends UFormat { } /** - * {@icu} Sets whether strict parsing is in effect. When this is true, the - * following conditions cause a parse failure (examples use the pattern "#,##0.#"):
    + * {@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.#"):
      + *
    • The presence and position of special symbols, including currency, must match the + * pattern.
      + * '123-' fails (the minus sign is expected in the prefix, not suffix)
    • *
    • Leading or doubled grouping separators
      * ',123' and '1,,234" fail
    • *
    • Groups of incorrect length when grouping is used
      @@ -1113,8 +1118,13 @@ public abstract class NumberFormat extends UFormat { /** * Returns the maximum number of digits allowed in the integer portion of a * number. The default value is 40, which subclasses can override. - * When formatting, the exact behavior when this value is exceeded is - * subclass-specific. When parsing, this has no effect. + * + * When formatting, if the number of digits exceeds this value, the highest- + * significance digits are truncated until the limit is reached, in accordance + * with UTS#35. + * + * This setting has no effect on parsing. + * * @return the maximum number of integer digits * @see #setMaximumIntegerDigits * @stable ICU 2.0 @@ -1415,10 +1425,12 @@ public abstract class NumberFormat extends UFormat { f.setDecimalSeparatorAlwaysShown(false); f.setParseIntegerOnly(true); } - if (choice == CASHCURRENCYSTYLE) { f.setCurrencyUsage(CurrencyUsage.CASH); } + if (choice == PLURALCURRENCYSTYLE) { + f.setCurrencyPluralInfo(CurrencyPluralInfo.getInstance(desiredLocale)); + } format = f; } // TODO: the actual locale of the *pattern* may differ from that @@ -1449,8 +1461,11 @@ public abstract class NumberFormat extends UFormat { * @param choice the pattern format. * @return the pattern * @stable ICU 3.2 + * @internal + * @deprecated This API is ICU internal only. */ - protected static String getPattern(ULocale forLocale, int choice) { + @Deprecated + public static String getPattern(ULocale forLocale, int choice) { /* for ISOCURRENCYSTYLE and PLURALCURRENCYSTYLE, * the pattern is the same as the pattern of CURRENCYSTYLE * but by replacing the single currency sign with @@ -1460,6 +1475,7 @@ public abstract class NumberFormat extends UFormat { switch (choice) { case NUMBERSTYLE: case INTEGERSTYLE: + case PLURALCURRENCYSTYLE: patternKey = "decimalFormat"; break; case CURRENCYSTYLE: @@ -1469,7 +1485,6 @@ public abstract class NumberFormat extends UFormat { break; case CASHCURRENCYSTYLE: case ISOCURRENCYSTYLE: - case PLURALCURRENCYSTYLE: case STANDARDCURRENCYSTYLE: patternKey = "currencyFormat"; break; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java index 0f90d00c32..c9dc1ff6dd 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java @@ -18,6 +18,8 @@ import java.util.Map; import com.ibm.icu.impl.Utility; import com.ibm.icu.text.PluralRules.FixedDecimal; +import com.ibm.icu.text.PluralRules.IFixedDecimal; +import com.ibm.icu.text.PluralRules.Operand; import com.ibm.icu.text.PluralRules.PluralType; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.ULocale.Category; @@ -554,8 +556,8 @@ public class PluralFormat extends UFormat { private final class PluralSelectorAdapter implements PluralSelector { @Override public String select(Object context, double number) { - FixedDecimal dec = (FixedDecimal) context; - assert dec.source == (dec.isNegative ? -number : number); + IFixedDecimal dec = (IFixedDecimal) context; + assert dec.getPluralOperand(Operand.n) == Math.abs(number); return pluralRules.select(dec); } } @@ -618,7 +620,7 @@ public class PluralFormat extends UFormat { } else { numberString = numberFormat.format(numberMinusOffset); } - FixedDecimal dec; + IFixedDecimal dec; if(numberFormat instanceof DecimalFormat) { dec = ((DecimalFormat) numberFormat).getFixedDecimal(numberMinusOffset); } else { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java index 970f7ca04a..90a8eda81d 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java @@ -356,7 +356,7 @@ public class PluralRules implements Serializable { private static final long serialVersionUID = 9163464945387899416L; @Override - public boolean isFulfilled(FixedDecimal n) { + public boolean isFulfilled(IFixedDecimal n) { return true; } @@ -412,11 +412,21 @@ public class PluralRules implements Serializable { */ public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE)); - private enum Operand { + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public static enum Operand { + /** The double value of the entire number. */ n, + /** The integer value, with the fraction digits truncated off. */ i, + /** All visible fraction digits as an integer, including trailing zeros. */ f, + /** Visible fraction digits, not including trailing zeros. */ t, + /** Number of visible fraction digits. */ v, w, /* deprecated */ @@ -428,7 +438,18 @@ public class PluralRules implements Serializable { * @deprecated This API is ICU internal only. */ @Deprecated - public static class FixedDecimal extends Number implements Comparable { + 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, IFixedDecimal { private static final long serialVersionUID = -4756200506571685661L; /** * @internal @@ -726,8 +747,9 @@ public class PluralRules implements Serializable { * @internal * @deprecated This API is ICU internal only. */ + @Override @Deprecated - public double get(Operand operand) { + public double getPluralOperand(Operand operand) { switch(operand) { default: return source; case i: return integerValue; @@ -881,6 +903,22 @@ public class PluralRules implements Serializable { ) throws IOException, ClassNotFoundException { throw new NotSerializableException(); } + + /* (non-Javadoc) + * @see com.ibm.icu.text.PluralRules.IFixedDecimal#isNaN() + */ + @Override + public boolean isNaN() { + return Double.isNaN(source); + } + + /* (non-Javadoc) + * @see com.ibm.icu.text.PluralRules.IFixedDecimal#isInfinite() + */ + @Override + public boolean isInfinite() { + return Double.isInfinite(source); + } } /** @@ -1106,7 +1144,7 @@ public class PluralRules implements Serializable { * Returns true if the number fulfills the constraint. * @param n the number to test, >= 0. */ - boolean isFulfilled(FixedDecimal n); + boolean isFulfilled(IFixedDecimal n); /* * Returns false if an unlimited number of values fulfills the @@ -1463,10 +1501,10 @@ public class PluralRules implements Serializable { } @Override - public boolean isFulfilled(FixedDecimal number) { - double n = number.get(operand); + public boolean isFulfilled(IFixedDecimal number) { + double n = number.getPluralOperand(operand); if ((integersOnly && (n - (long)n) != 0.0 - || operand == Operand.j && number.visibleDecimalDigitCount != 0)) { + || operand == Operand.j && number.getPluralOperand(Operand.v) != 0)) { return !inRange; } if (mod != 0) { @@ -1566,7 +1604,7 @@ public class PluralRules implements Serializable { } @Override - public boolean isFulfilled(FixedDecimal n) { + public boolean isFulfilled(IFixedDecimal n) { return a.isFulfilled(n) && b.isFulfilled(n); } @@ -1594,7 +1632,7 @@ public class PluralRules implements Serializable { } @Override - public boolean isFulfilled(FixedDecimal n) { + public boolean isFulfilled(IFixedDecimal n) { return a.isFulfilled(n) || b.isFulfilled(n); } @@ -1645,7 +1683,7 @@ public class PluralRules implements Serializable { return keyword; } - public boolean appliesTo(FixedDecimal n) { + public boolean appliesTo(IFixedDecimal n) { return constraint.isFulfilled(n); } @@ -1708,7 +1746,7 @@ public class PluralRules implements Serializable { return this; } - private Rule selectRule(FixedDecimal n) { + private Rule selectRule(IFixedDecimal n) { for (Rule rule : rules) { if (rule.appliesTo(n)) { return rule; @@ -1717,8 +1755,8 @@ public class PluralRules implements Serializable { return null; } - public String select(FixedDecimal n) { - if (Double.isInfinite(n.source) || Double.isNaN(n.source)) { + public String select(IFixedDecimal n) { + if (n.isInfinite() || n.isNaN()) { return KEYWORD_OTHER; } Rule r = selectRule(n); @@ -1780,7 +1818,7 @@ public class PluralRules implements Serializable { return null; } - public boolean select(FixedDecimal sample, String keyword) { + public boolean select(IFixedDecimal sample, String keyword) { for (Rule rule : rules) { if (rule.getKeyword().equals(keyword) && rule.appliesTo(sample)) { return true; @@ -1800,9 +1838,9 @@ public class PluralRules implements Serializable { } @SuppressWarnings("unused") - private boolean addConditional(Set toAddTo, Set others, double trial) { + private boolean addConditional(Set toAddTo, Set others, double trial) { boolean added; - FixedDecimal toAdd = new FixedDecimal(trial); + IFixedDecimal toAdd = new FixedDecimal(trial); if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) { others.add(toAdd); added = true; @@ -1969,7 +2007,7 @@ public class PluralRules implements Serializable { * @deprecated This API is ICU internal only. */ @Deprecated - public String select(FixedDecimal number) { + public String select(IFixedDecimal number) { return rules.select(number); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/RuleBasedNumberFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/RuleBasedNumberFormat.java index 9ec0ea47de..d54c16cd9c 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/RuleBasedNumberFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/RuleBasedNumberFormat.java @@ -1267,7 +1267,7 @@ public class RuleBasedNumberFormat extends NumberFormat { public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { - if (MIN_VALUE.compareTo(number) >= 0 || MAX_VALUE.compareTo(number) <= 0) { + if (MIN_VALUE.compareTo(number) > 0 || MAX_VALUE.compareTo(number) < 0) { // We're outside of our normal range that this framework can handle. // The DecimalFormat will provide more accurate results. return getDecimalFormat().format(number, toAppendTo, pos); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/ScientificNumberFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/ScientificNumberFormatter.java index 527dd7ea93..93bb25f8ce 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/ScientificNumberFormatter.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/ScientificNumberFormatter.java @@ -13,6 +13,7 @@ import java.text.AttributedCharacterIterator.Attribute; import java.text.CharacterIterator; import java.util.Map; +import com.ibm.icu.impl.number.Parse; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.util.ULocale; @@ -229,14 +230,14 @@ public final class ScientificNumberFormatter { int start = iterator.getRunStart(NumberFormat.Field.EXPONENT_SIGN); int limit = iterator.getRunLimit(NumberFormat.Field.EXPONENT_SIGN); int aChar = char32AtAndAdvance(iterator); - if (DecimalFormat.minusSigns.contains(aChar)) { + if (Parse.UNISET_MINUS.contains(aChar)) { append( iterator, copyFromOffset, start, result); result.append(SUPERSCRIPT_MINUS_SIGN); - } else if (DecimalFormat.plusSigns.contains(aChar)) { + } else if (Parse.UNISET_PLUS.contains(aChar)) { append( iterator, copyFromOffset, diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java b/icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java index 06300ca3df..8437c96884 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java @@ -674,19 +674,7 @@ public class Currency extends MeasureUnit { */ @Deprecated public static String parse(ULocale locale, String text, int type, ParsePosition pos) { - List> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale); - if (currencyTrieVec == null) { - TextTrieMap currencyNameTrie = - new TextTrieMap(true); - TextTrieMap currencySymbolTrie = - new TextTrieMap(false); - currencyTrieVec = new ArrayList>(); - currencyTrieVec.add(currencySymbolTrie); - currencyTrieVec.add(currencyNameTrie); - setupCurrencyTrieVec(locale, currencyTrieVec); - CURRENCY_NAME_CACHE.put(locale, currencyTrieVec); - } - + List> currencyTrieVec = getCurrencyTrieVec(locale); int maxLength = 0; String isoResult = null; @@ -711,6 +699,37 @@ public class Currency extends MeasureUnit { return isoResult; } + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public static TextTrieMap.ParseState openParseState( + ULocale locale, int startingCp, int type) { + List> currencyTrieVec = getCurrencyTrieVec(locale); + if (type == Currency.LONG_NAME) { + return currencyTrieVec.get(0).openParseState(startingCp); + } else { + return currencyTrieVec.get(1).openParseState(startingCp); + } + } + + private static List> getCurrencyTrieVec(ULocale locale) { + List> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale); + if (currencyTrieVec == null) { + TextTrieMap currencyNameTrie = + new TextTrieMap(true); + TextTrieMap currencySymbolTrie = + new TextTrieMap(false); + currencyTrieVec = new ArrayList>(); + currencyTrieVec.add(currencySymbolTrie); + currencyTrieVec.add(currencyNameTrie); + setupCurrencyTrieVec(locale, currencyTrieVec); + CURRENCY_NAME_CACHE.put(locale, currencyTrieVec); + } + return currencyTrieVec; + } + private static void setupCurrencyTrieVec(ULocale locale, List> trieVec) { @@ -734,7 +753,12 @@ public class Currency extends MeasureUnit { } } - private static final class CurrencyStringInfo { + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public static final class CurrencyStringInfo { private String isoCode; private String currencyString; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/Measure.java b/icu4j/main/classes/core/src/com/ibm/icu/util/Measure.java index f641fec1f2..3e90a851d6 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/Measure.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/Measure.java @@ -43,7 +43,7 @@ public class Measure { */ public Measure(Number number, MeasureUnit unit) { if (number == null || unit == null) { - throw new NullPointerException(); + throw new NullPointerException("Number and MeasureUnit must not be null"); } this.number = number; this.unit = unit; diff --git a/icu4j/main/tests/collate/collate-tests-build.launch b/icu4j/main/tests/collate/collate-tests-build.launch index 0257ecaeb7..2bbd4fd13f 100644 --- a/icu4j/main/tests/collate/collate-tests-build.launch +++ b/icu4j/main/tests/collate/collate-tests-build.launch @@ -2,17 +2,17 @@ - + - + - + diff --git a/icu4j/main/tests/core/.externalToolBuilders/copy-test-data.launch b/icu4j/main/tests/core/.externalToolBuilders/copy-test-data.launch index 84a657d3f6..d63360ff0a 100644 --- a/icu4j/main/tests/core/.externalToolBuilders/copy-test-data.launch +++ b/icu4j/main/tests/core/.externalToolBuilders/copy-test-data.launch @@ -5,18 +5,18 @@ - + - + - + diff --git a/icu4j/main/tests/core/core-tests-build.launch b/icu4j/main/tests/core/core-tests-build.launch index b9ec0cd9e6..1772dd9acd 100644 --- a/icu4j/main/tests/core/core-tests-build.launch +++ b/icu4j/main/tests/core/core-tests-build.launch @@ -2,17 +2,17 @@ - + - + - + diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt b/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt index 930b04ed34..7af6850059 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt @@ -17,8 +17,8 @@ set locale ar set pattern +0;-# begin format output breaks -6 \u200F+\u0666 JK --6 \u200F-\u0666 JK +6 \u061C+\u0666 JK +-6 \u061C-\u0666 K test basic patterns set locale fr_FR @@ -54,6 +54,75 @@ format output 12345 2345.000 72.1234 72.1234 +test patterns with no '0' symbols +set locale en_US +begin +pattern format output breaks +# 514.23 514 +# 0.23 0 +# 0 0 +# 1 1 +##.# 514.23 514.2 +##.# 0.23 0.2 +##.# 0 0 +##.# 1 1 +#.# 514.23 514.2 +#.# 0.23 0.2 +#.# 0 0 +#.# 1 1 +.# 514.23 514.2 +.# 0.23 .2 +.# 0 .0 +.# 1 1.0 +#. 514.23 514. +#. 0.23 0. +#. 0 0. +#. 1 1. +. 514.23 514. +. 0.23 0. +. 0 0. +. 1 1. + +test behavior on numbers approaching zero +set locale en +begin +pattern format output breaks +#.## 0.01 0.01 +#.## 0.001 0 +#.## 0 0 +#.00 0.01 .01 +#.00 0.001 .00 +#.00 0 .00 +0.00 0.01 0.01 +0.00 0.001 0.00 +0.00 0 0.00 + +// Not in official spec, but needed for backwards compatibility +test patterns with leading grouping separator +set locale en_US +begin +pattern format output breaks +,##0 1234.56 1,235 +'#',## 3456 #34,56 + +test patterns with valid and invalid quote marks +set locale et +begin +pattern format output breaks +'# 1 fail +''# 1 '1 +'''# 1 fail +''''# 1 ''1 +'''''# 1 fail +'-''-'# 1 -'-1 +// K doesn't know the locale symbol for et +-'-'# 1 −-1 K +'#'# 1 #1 +''#'' 1 '1' +''#- 1 '1− K +'-'#- 1 -1− K +-#'-' 1 −1- K + test int64 set locale en begin @@ -113,12 +182,36 @@ pattern format output breaks 0.05E0 12301.2 1,25E4 JK ##0.000#E0 0.17 170,0E-3 // JDK doesn't support significant digits in exponents +@@@E0 6.235 6,24E0 K @@@E0 6235 6,24E3 K @@@#E0 6200 6,20E3 K @@@#E0 6201 6,201E3 K @@@#E0 6201.7 6,202E3 K @@@#E00 6201.7 6,202E03 K @@@#E+00 6201.7 6,202E+03 K +// If no zeros are specified, significant digits is fraction length plus 1 +#.##E0 52413 5,24E4 +###.##E0 52413 52,4E3 K +#E0 52413 5,2413E4 K +0E0 52413 5E4 + +test scientific with grouping +set locale en +set pattern #,##0.000E0 +begin +format output breaks +// J throws an IllegalArgumentException when parsing the pattern. +1 1.000E0 J +11 11.00E0 J +111 111.0E0 J +// K doesn't print the grouping separator ("1111E0") +1111 1,111E0 JK +// K prints too many digits ("1.1111E4") +11111 1.111E4 JK +111111 11.11E4 JK +1111111 111.1E4 JK +11111111 1,111E4 JK +111111111 1.111E8 JK test percents set locale fr @@ -165,7 +258,7 @@ $**####,##0 1234 $***1\u00a0234 K // In J ICU adds padding as if 'EUR' is only 2 chars (2 * 0xa4) \u00a4\u00a4 **####0.00 433.0 EUR *433,00 JK // In J ICU adds padding as if 'EUR' is only 2 chars (2 * 0xa4) -\u00a4\u00a4 **#######0 433.0 EUR *433,00 JK +\u00a4\u00a4 **#######0 433.0 EUR ****433 JK test padding and currencies begin @@ -235,13 +328,16 @@ set pattern #E0 set format 299792458.0 begin minIntegerDigits maxIntegerDigits minFractionDigits maxFractionDigits output breaks +// JDK gives 2.99792458E8 (maxInt + maxFrac instead of minInt + maxFrac) +1 1000 0 5 2.99792E8 K // JDK gives .3E9 instead of unlimited precision. 0 1 0 0 2.99792458E8 K 1 1 0 0 3E8 // JDK gives E0 instead of allowing for unlimited precision -0 0 0 0 2.99792458E8 K -// JDK gives .299792E9 -0 1 0 5 2.9979E8 K +// S obeys the maximum integer digits and returns .299792458E9 +0 0 0 0 2.99792458E8 KS +// JDK and S give .299792E9 +0 1 0 5 2.9979E8 KS // JDK gives 300E6 0 3 0 0 299.792458E6 K // JDK gives 299.8E6 (maybe maxInt + maxFrac instead of minInt + maxFrac)? @@ -256,11 +352,14 @@ minIntegerDigits maxIntegerDigits minFractionDigits maxFractionDigits output bre 4 4 0 0 2998E5 0 0 1 5 .29979E9 // JDK gives E0 -0 0 1 0 2.99792458E8 K +// S obeys the maximum integer digits +0 0 1 0 2.99792458E8 KS // JDK gives .2998E9 -0 0 0 4 2.998E8 K +0 0 0 4 2.998E8 KS +// S correctly formats this as 29.979246E7. // JDK uses 8 + 6 for significant digits instead of 2 + 6 -2 8 1 6 2.9979246E8 K +// J and C return 2.9979246E8. +2 8 1 6 29.979246E7 CJK // Treat max int digits > 8 as being the same as min int digits. // This behavior is not spelled out in the specification. // JDK fails here because it tries to use 9 + 6 = 15 sig digits. @@ -290,7 +389,8 @@ set format 29979245.0 begin minIntegerDigits maxIntegerDigits minFractionDigits maxFractionDigits output breaks // JDK gives E0 -0 0 0 0 2.9979245E7 K +// S obeys the max integer digits and prints 0.299... +0 0 0 0 2.9979245E7 KS // JDK gives .3E8 0 1 0 0 2.9979245E7 K // JDK gives 2998E4. @@ -300,23 +400,27 @@ test ticket 11524 set locale en set pattern #,##0.### begin -format maxIntegerDigits output -123 1 3 -123 -2147483648 0 -12345 1 5 -12345 -2147483648 0 -5.3 1 5.3 -5.3 -2147483648 .3 +format maxIntegerDigits output breaks +123 1 3 +0 0 0 +// S ignores max integer if it is less than zero and prints "123" +123 -2147483648 0 S +12345 1 5 +12345 -2147483648 0 S +5.3 1 5.3 +5.3 -2147483648 .3 S test patterns with zero set locale en set format 0 begin -pattern output +pattern output breaks #.# 0 #. 0. .# .0 # 0 +#,##0.00 0.00 +#,###.00 .00 00.000E00 00.000E00 0.####E0 0E0 ##0.######E000 0E000 @@ -334,8 +438,8 @@ format output breaks 0.001234 0.001234 K 0.0012345 0.0012345 K 0.00123456 0.0012346 K --43 -43.0 K --43.7 -43.7 K +-43 -43.0 +-43.7 -43.7 -43.76 -43.76 K -43.762 -43.762 K -43.7626 -43.763 K @@ -360,7 +464,7 @@ output grouping breaks grouping2 minGroupingDigits 1,2345,6789 4 1,23,45,6789 4 K 2 1,23,45,6789 4 K 2 2 -123,456789 6 K 6 3 +123,456789 6 6 3 123456789 6 JK 6 4 test multiplier setters @@ -370,7 +474,7 @@ format multiplier output breaks 23 -12 -276 23 -1 -23 // ICU4J and JDK throw exception on zero multiplier. ICU4C does not. -23 0 23 JK +23 0 23 JKS 23 1 23 23 12 276 -23 12 -276 @@ -394,7 +498,7 @@ set pattern bill0 set format 1357 begin padCharacter formatWidth output breaks -* 8 bill1357 K +* 8 bill1357 * 9 *bill1357 K ^ 10 ^^bill1357 K @@ -406,7 +510,7 @@ begin output breaks useScientific 186283.00 1.86E5 K 1 -186283.00 K 0 +186283.00 0 test rounding mode setters set locale en_US @@ -423,19 +527,43 @@ format roundingMode output breaks -1.49 down -1 K 1.01 up 1.5 K 1.49 down 1 K --1.01 ceiling -1 K --1.49 floor -1.5 K +-1.01 ceiling -1 +-1.49 floor -1.5 test currency usage setters -// TODO: Find a country and currency where standard and cash differ set locale CH -set currency CHF set pattern \u00a4\u00a4 0 begin -format currencyUsage output breaks -0.37 standard CHF 0.37 K -// TODO: Get the rounding data into ICU4C and ICU4J -0.37 cash CHF 0.35 CJK +format currency currencyUsage output breaks +0.37 CHF standard CHF 0.37 K +0.37 CHF cash CHF 0.35 CK +1.234 CZK standard CZK 1.23 K +1.234 CZK cash CZK 1 + +test currency usage to pattern +set locale en +begin +currency currencyUsage toPattern breaks +// These work in J, but it prepends an extra hash sign to the pattern. +// K does not support this feature. +USD standard 0.00 JK +CHF standard 0.00 JK +CZK standard 0.00 JK +USD cash 0.00 JK +CHF cash 0.05 JK +CZK cash 0 JK + +test currency rounding +set locale en +set currency USD +begin +pattern format output breaks +# 123 123 S +// Currency rounding should always override the pattern. +// K prints the currency in ISO format for some reason. +\u00a4# 123 $123.00 K +\u00a4#.000 123 $123.00 K +\u00a4#.## 123 $123.00 K test exponent parameter setters set locale en_US @@ -445,12 +573,10 @@ begin decimalSeparatorAlwaysShown exponentSignAlwaysShown minimumExponentDigits output breaks 0 0 2 3E08 K 0 1 3 3E+008 K -// ICU DecimalFormat J does not honor decimalSeparatorAlwaysShown -// for scientific notation. But JDK DecimalFormat does honor // decimalSeparatorAlwaysShown K=JDK; C=ICU4C; J=ICU4J // See ticket 11621 -1 0 2 3.E08 JK -1 1 3 3.E+008 JK +1 0 2 3.E08 K +1 1 3 3.E+008 K 1 0 1 3.E8 0 0 1 3E8 @@ -462,7 +588,7 @@ format output breaks decimalSeparatorAlwaysShown // decimalSeparatorAlwaysShown off by default 299792458 3E8 299000000 2.99E8 -299792458 3.E8 J 1 +299792458 3.E8 1 test pad position setters set locale en_US @@ -505,7 +631,7 @@ set locale en_US set pattern [0.00];(#) begin format output breaks -Inf [\u221e] K +Inf [\u221e] -Inf (\u221e) K NaN NaN K @@ -539,36 +665,38 @@ begin locale pattern format output breaks en #0% 0.4376 44% // This next test breaks JDK. JDK doesn't multiply by 100. -// It also is now broken in ICU4J until #10368 is fixed. -fa \u0025\u00a0\u0023\u0030 0.4376 \u200e\u066a\u00a0\u06f4\u06f4 JK +fa \u0025\u00a0\u0023\u0030 0.4376 \u200e\u066a\u00a0\u06f4\u06f4 K test toPattern set locale en begin pattern toPattern breaks +// All of the "S" failures in this section are because of functionally equivalent patterns // JDK doesn't support any patterns with padding or both negative prefix and suffix // Breaks ICU4J See ticket 11671 **0,000 **0,000 JK **##0,000 **##0,000 K **###0,000 **###0,000 K -**####0,000 **#,##0,000 K +**####0,000 **#,##0,000 KS ###,000. #,000. -0,000 #0,000 +0,000 #0,000 S .00 #.00 -000 #000 -000,000 #,000,000 +000 #000 S +000,000 #,000,000 S pp#,000 pp#,000 -00.## #00.## +00.## #00.## S #,#00.025 #,#00.025 // No secondary grouping in JDK #,##,###.02500 #,##,###.02500 K pp#,000;(#) pp#,000;(#,000) K -**####,##,##0.0##;(#) **#,##,##,##0.0##;**(##,##,##0.0##) K +**####,##,##0.0##;(#) **#,##,##,##0.0##;**(##,##,##0.0##) KS // No significant digits in JDK @@### @@### K @,@#,### @,@#,### K 0.00E0 0.00E0 -@@@##E0 @@@##E0 K +// The following one works in JDK, probably because +// it just returns the same string +@@@##E0 @@@##E0 ###0.00#E0 ###0.00#E0 ##00.00#E0 ##00.00#E0 0.00E+00 0.00E+00 K @@ -589,7 +717,8 @@ parse output breaks // J requires prefix and suffix for lenient parsing, but C doesn't 5,347.25 5347.25 JK (5,347.25 -5347.25 J --5,347.25 fail +// S is successful at parsing this as -5347.25 in lenient mode +-5,347.25 fail S +3.52E4 35200 (34.8E-3) -0.0348 // JDK stops parsing at the spaces. JDK doesn't see space as a grouping separator @@ -598,7 +727,7 @@ parse output breaks // J doesn't allow trailing separators before E but C does (34,,25,E-1) -342.5 J (34 25 E-1) -342.5 JK -(34,,25 E-1) -3425 J +(34,,25 E-1) -342.5 JK // Spaces are not allowed after exponent symbol // C parses up to the E but J bails (34 25E -1) -3425 JK @@ -648,16 +777,16 @@ set locale en set pattern #,##0.0###+;#- begin parse output breaks -// C sees this as -3426, don't understand why -3426 -3426 JK +// C sees this as -3426, don't understand why. +// J and K just bail. +3426 3426 JKC 3426+ 3426 -// J bails, but JDK will parse up to the space and get 34. -// C sees -34 -34 d1+ -34 JK +// J bails; C and K see -34 +34 d1+ 34 JKC // JDK sees this as -1234 for some reason // J bails b/c of trailing separators // C parses until trailing separators, but sees -1234 -1,234,,,+ -1234 JK +1,234,,,+ 1234 JKC 1,234- -1234 // J bails because of trailing separators 1,234,- -1234 J @@ -668,55 +797,70 @@ parse output breaks test parse strict set locale en -set pattern +#,##0.0###;(#) +set pattern +#,##,##0.0###;(#) set lenient 0 +set minGroupingDigits 2 begin parse output breaks +123d5 123 +5347.25 5347.25 // separators in wrong place cause failure, no separators ok. -+5,347.25 5347.25 -(5347.25) -5347.25 -(5,347.25) -5347.25 ++65,347.25 65347.25 +(65347.25) -65347.25 +(65,347.25) -65347.25 // JDK does allow separators in the wrong place and parses as -5347.25 (53,47.25) fail K // strict requires prefix or suffix -5,347.25 fail +65,347.25 fail +3.52E4 35200 (34.8E-3) -0.0348 (3425E-1) -342.5 // Strict doesn't allow separators in sci notation. -(3,425) -3425 -// JDK allows separators in sci notation and parses as -342.5 -(3,425E-1) fail K +(63,425) -63425 +// JDK and S allow separators in sci notation and parses as -342.5 +(63,425E-1) fail KS // Both prefix and suffix needed for strict. // JDK accepts this and parses as -342.5 (3425E-1 fail K +3.52EE4 3.52 -+1,234,567.8901 1234567.8901 ++12,34,567.8901 1234567.8901 // With strict digit separators don't have to be the right type // JDK doesn't acknowledge space as a separator -+1 234 567.8901 1234567.8901 K ++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 @@ -724,7 +868,8 @@ set parseIntegerOnly 1 begin parse output breaks 35 35 -+35 fail +// S accepts leading plus signs ++35 35 CJK -35 -35 2.63 2 -39.99 -39 @@ -746,8 +891,8 @@ set pattern 0 set locale en begin parse output outputCurrency breaks -// See ticket 11735 -53.45 fail USD J +// Fixed in ticket 11735 +53.45 fail USD test parse strange prefix set locale en @@ -775,12 +920,13 @@ set negativePrefix set negativeSuffix 9N begin parse output breaks +// S is the only implementation that passes these cases. // C consumes the '9' as a digit and assumes number is negative // J and JDK bail -// 6549K 654 CJK +6549K 654 CJK // C consumes the '9' as a digit and assumes number is negative // J and JDK bail -// 6549N -654 CJK +6549N -654 CJK test really strange prefix set locale en @@ -791,6 +937,39 @@ parse output 8245 45 2845 -45 +test parse pattern with quotes +set locale en +set pattern '-'#y +begin +parse output +-45y 45 + +test parse with locale symbols +// The grouping separator in it_CH is an apostrophe +set locale it_CH +set pattern # +begin +parse output breaks +१३ 13 +१३.३१‍ 13.31 +// J and K stop parsing at the apostrophe +123'456 123456 JK +524'1.3 5241.3 JK +३'१‍ 31 JK + +test parse with European-style comma/period +set locale pt +set pattern # +begin +parse output breaks +// J and K get 123 +123.456 123456 JK +123,456 123.456 +987,654.321 987.654 +987,654 321 987.654 +// J and K get 987 +987.654,321 987654.321 JK + test select set locale sr begin @@ -811,25 +990,28 @@ NaN 0.0 other test parse currency ISO set pattern 0.00 \u00a4\u00a4;(#) \u00a4\u00a4 -set locale en_US +set locale en_GB begin parse output outputCurrency breaks -$53.45 53.45 USD +53.45 fail GBP +£53.45 53.45 GBP +$53.45 fail USD 53.45 USD 53.45 USD +53.45 GBP 53.45 GBP USD 53.45 53.45 USD J -53.45USD fail USD +53.45USD 53.45 USD CJ USD53.45 53.45 USD (7.92) USD -7.92 USD -(7.92) EUR -7.92 EUR +(7.92) GBP -7.92 GBP (7.926) USD -7.926 USD -(7.926 USD) fail USD -(USD 7.926) fail USD -USD (7.926) fail USD -USD (7.92) fail USD -(7.92)USD fail USD -USD(7.92) fail USD +(7.926 USD) -7.926 USD CJ +(USD 7.926) -7.926 USD CJ +USD (7.926) -7.926 USD CJ +USD (7.92) -7.92 USD CJ +(7.92)USD -7.92 USD CJ +USD(7.92) -7.92 USD CJ (8) USD -8 USD --8 USD fail USD +-8 USD -8 USD CJ 67 USD 67 USD 53.45$ fail USD US Dollars 53.45 53.45 USD J @@ -837,37 +1019,41 @@ US Dollars 53.45 53.45 USD J US Dollar 53.45 53.45 USD J 53.45 US Dollar 53.45 USD US Dollars53.45 53.45 USD -53.45US Dollars fail USD +53.45US Dollars 53.45 USD CJ US Dollar53.45 53.45 USD US Dollat53.45 fail USD -53.45US Dollar fail USD -US Dollars (53.45) fail USD +53.45US Dollar 53.45 USD CJ +US Dollars (53.45) -53.45 USD CJ (53.45) US Dollars -53.45 USD -US Dollar (53.45) fail USD +(53.45) Euros -53.45 EUR +US Dollar (53.45) -53.45 USD CJ (53.45) US Dollar -53.45 USD -US Dollars(53.45) fail USD -(53.45)US Dollars fail USD -US Dollar(53.45) fail USD +US Dollars(53.45) -53.45 USD CJ +(53.45)US Dollars -53.45 USD CJ +US Dollar(53.45) -53.45 USD CJ US Dollat(53.45) fail USD -(53.45)US Dollar fail USD +(53.45)US Dollar -53.45 USD CJ test parse currency ISO negative set pattern 0.00 \u00a4\u00a4;-# \u00a4\u00a4 -set locale en_US +set locale en_GB begin parse output outputCurrency breaks -$53.45 53.45 USD +53.45 fail GBP +£53.45 53.45 GBP +$53.45 fail USD 53.45 USD 53.45 USD +53.45 GBP 53.45 GBP USD 53.45 53.45 USD J -53.45USD fail USD +53.45USD 53.45 USD CJ USD53.45 53.45 USD -7.92 USD -7.92 USD --7.92 EUR -7.92 EUR +-7.92 GBP -7.92 GBP -7.926 USD -7.926 USD -USD -7.926 fail USD --7.92USD fail USD -USD-7.92 fail USD +USD -7.926 -7.926 USD CJ +-7.92USD -7.92 USD CJ +USD-7.92 -7.92 USD CJ -8 USD -8 USD 67 USD 67 USD 53.45$ fail USD @@ -876,70 +1062,75 @@ US Dollars 53.45 53.45 USD J US Dollar 53.45 53.45 USD J 53.45 US Dollar 53.45 USD US Dollars53.45 53.45 USD -53.45US Dollars fail USD +53.45US Dollars 53.45 USD CJ US Dollar53.45 53.45 USD US Dollat53.45 fail USD -53.45US Dollar fail USD +53.45US Dollar 53.45 USD CJ test parse currency long set pattern 0.00 \u00a4\u00a4\u00a4;(#) \u00a4\u00a4\u00a4 -set locale en_US +set locale en_GB begin parse output outputCurrency breaks -$53.45 53.45 USD +// J throws a NullPointerException on the first case +53.45 fail GBP +£53.45 53.45 GBP +$53.45 fail USD 53.45 USD 53.45 USD +53.45 GBP 53.45 GBP USD 53.45 53.45 USD J -// See ticket 11735 -53.45USD fail USD J +53.45USD 53.45 USD CJ USD53.45 53.45 USD (7.92) USD -7.92 USD +(7.92) GBP -7.92 GBP (7.926) USD -7.926 USD -(7.926 USD) fail USD -(USD 7.926) fail USD -USD (7.926) fail USD -USD (7.92) fail USD -(7.92)USD fail USD -USD(7.92) fail USD +(7.926 USD) -7.926 USD CJ +(USD 7.926) -7.926 USD CJ +USD (7.926) -7.926 USD CJ +USD (7.92) -7.92 USD CJ +(7.92)USD -7.92 USD CJ +USD(7.92) -7.92 USD CJ (8) USD -8 USD -// See ticket 11735 --8 USD fail USD J +-8 USD -8 USD CJ 67 USD 67 USD -// See ticket 11735 -53.45$ fail USD J +// J throws a NullPointerException on the next case +53.45$ fail USD US Dollars 53.45 53.45 USD J 53.45 US Dollars 53.45 USD US Dollar 53.45 53.45 USD J 53.45 US Dollar 53.45 USD US Dollars53.45 53.45 USD -// See ticket 11735 -53.45US Dollars fail USD J +53.45US Dollars 53.45 USD CJ US Dollar53.45 53.45 USD US Dollat53.45 fail USD -// See ticket 11735 -53.45US Dollar fail USD J +53.45US Dollar 53.45 USD CJ test parse currency short set pattern 0.00 \u00a4;(#) \u00a4 -set locale en_US +set locale en_GB begin parse output outputCurrency breaks -$53.45 53.45 USD +53.45 fail GBP +£53.45 53.45 GBP +$53.45 fail USD 53.45 USD 53.45 USD +53.45 GBP 53.45 GBP USD 53.45 53.45 USD J -53.45USD fail USD +53.45USD 53.45 USD CJ USD53.45 53.45 USD (7.92) USD -7.92 USD +(7.92) GBP -7.92 GBP (7.926) USD -7.926 USD -(7.926 USD) fail USD -(USD 7.926) fail USD -USD (7.926) fail USD -USD (7.92) fail USD -(7.92)USD fail USD -USD(7.92) fail USD +(7.926 USD) -7.926 USD CJ +(USD 7.926) -7.926 USD CJ +USD (7.926) -7.926 USD CJ +USD (7.92) -7.92 USD CJ +(7.92)USD -7.92 USD CJ +USD(7.92) -7.92 USD CJ (8) USD -8 USD --8 USD fail USD +-8 USD -8 USD CJ 67 USD 67 USD 53.45$ fail USD US Dollars 53.45 53.45 USD J @@ -947,45 +1138,51 @@ US Dollars 53.45 53.45 USD J US Dollar 53.45 53.45 USD J 53.45 US Dollar 53.45 USD US Dollars53.45 53.45 USD -53.45US Dollars fail USD +53.45US Dollars 53.45 USD CJ US Dollar53.45 53.45 USD US Dollat53.45 fail USD -53.45US Dollar fail USD +53.45US Dollar 53.45 USD CJ test parse currency short prefix set pattern \u00a40.00;(\u00a4#) -set locale en_US +set locale en_GB begin parse output outputCurrency breaks -$53.45 53.45 USD -53.45 USD fail USD +53.45 fail GBP +£53.45 53.45 GBP +$53.45 fail USD +53.45 USD 53.45 USD CJ +53.45 GBP 53.45 GBP CJ USD 53.45 53.45 USD J -53.45USD fail USD +53.45USD 53.45 USD CJ USD53.45 53.45 USD -(7.92) USD fail USD -(7.926) USD fail USD -(7.926 USD) fail USD +// S fails these because '(' is an incomplete prefix. +(7.92) USD -7.92 USD CJS +(7.92) GBP -7.92 GBP CJS +(7.926) USD -7.926 USD CJS +(7.926 USD) -7.926 USD CJS (USD 7.926) -7.926 USD J -USD (7.926) fail USD -USD (7.92) fail USD -(7.92)USD fail USD -USD(7.92) fail USD -(8) USD fail USD --8 USD fail USD -67 USD fail USD +USD (7.926) -7.926 USD CJS +USD (7.92) -7.92 USD CJS +(7.92)USD -7.92 USD CJS +USD(7.92) -7.92 USD CJS +(8) USD -8 USD CJS +-8 USD -8 USD CJ +67 USD 67 USD CJ 53.45$ fail USD US Dollars 53.45 53.45 USD J 53.45 US Dollars 53.45 USD US Dollar 53.45 53.45 USD J 53.45 US Dollar 53.45 USD US Dollars53.45 53.45 USD -53.45US Dollars fail USD +53.45US Dollars 53.45 USD CJ US Dollar53.45 53.45 USD -53.45US Dollar fail USD +53.45US Dollar 53.45 USD CJ test format foreign currency set locale fa_IR +set currency IRR begin pattern format output breaks \u00a4\u00a4\u00a4 0.00;\u00a4\u00a4\u00a4 # 1235 \u0631\u06cc\u0627\u0644 \u0627\u06cc\u0631\u0627\u0646 \u06F1\u06F2\u06F3\u06F5 K @@ -1058,6 +1255,22 @@ EUR 7.82 7.82 EUR Euro 7.82 7.82 EUR Euros 7.82 7.82 EUR +test parse currency without currency mode +// Should accept a symbol associated with the currency specified by the API, +// but should not traverse the full currency data. +set locale en_US +set pattern \u00a4#,##0.00 +begin +parse currency output breaks +$52.41 USD 52.41 +USD52.41 USD 52.41 K +\u20ac52.41 USD fail +EUR52.41 USD fail +$52.41 EUR fail +USD52.41 EUR fail +\u20ac52.41 EUR 52.41 K +EUR52.41 EUR 52.41 + test parse currency ISO strict set pattern 0.00 \u00a4\u00a4;(#) \u00a4\u00a4 set locale en_US @@ -1110,3 +1323,107 @@ begin format output breaks -0.99 -0 JK +test parse decimalPatternMatchRequired +set locale en +set decimalPatternMatchRequired 1 +begin +pattern parse output breaks +// K doesn't support this feature. +0 123 123 +0 123. fail JK +0 1.23 fail JK +0 -513 -513 +0 -513. fail JK +0 -5.13 fail JK +0.0 123 fail K +0.0 123. 123 +0.0 1.23 1.23 +0.0 -513 fail K +0.0 -513. -513 +0.0 -5.13 -5.13 + +test parse minus sign +set locale en +set pattern # +begin +parse output breaks +-123 -123 +- 123 -123 JK + -123 -123 JK + - 123 -123 JK +123- -123 JKS +123 - -123 JKS + +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 + diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/BigNumberFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/BigNumberFormatTest.java index 641dc7f315..86946deea8 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/BigNumberFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/BigNumberFormatTest.java @@ -48,11 +48,11 @@ public class BigNumberFormatTest extends TestFmwk { DecimalFormatSymbols US = new DecimalFormatSymbols(Locale.US); DecimalFormat f = new DecimalFormat("#,##,###", US); expect(f, new Long(123456789), "12,34,56,789"); - expectPat(f, "#,##,###"); + expectPat(f, "#,##,##0"); f.applyPattern("#,###"); f.setSecondaryGroupingSize(4); expect(f, new Long(123456789), "12,3456,789"); - expectPat(f, "#,####,###"); + expectPat(f, "#,####,##0"); // On Sun JDK 1.2-1.3, the hi_IN locale uses '0' for a zero digit, // but on IBM JDK 1.2-1.3, the locale uses U+0966. @@ -144,7 +144,7 @@ public class BigNumberFormatTest extends TestFmwk { fmt.setFormatWidth(16); // 12 34567890123456 - expectPat(fmt, "AA*^#,###,##0.00ZZ"); + expectPat(fmt, "AA*^#####,##0.00ZZ"); } private void expectPat(DecimalFormat fmt, String exp) { @@ -227,16 +227,16 @@ public class BigNumberFormatTest extends TestFmwk { expect(new DecimalFormat[] { new DecimalFormat("#E0", US), new DecimalFormat("##E0", US), new DecimalFormat("####E0", US), - new DecimalFormat("0E0", US), - new DecimalFormat("00E0", US), - new DecimalFormat("000E0", US), + new DecimalFormat("0E0", US), + new DecimalFormat("00E0", US), + new DecimalFormat("000E0", US), }, new Long(45678000), new String[] { "4.5678E7", "45.678E6", "4567.8E4", "5E7", - "46E6", + "46E6", "457E5", } ); @@ -285,13 +285,13 @@ public class BigNumberFormatTest extends TestFmwk { new Long(-1000000000), "(1,000,000,000.00)", }); } - + private void expect(NumberFormat fmt, Object[] data) { for (int i=0; i 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"}, - }); + // TODO(sffc): Re-write these tests for the new CompactDecimalFormat pipeline - Map currencyAffixes = new HashMap(); - currencyAffixes.put("one", new String[] {"", "$"}); - currencyAffixes.put("other", new String[] {"", "$s"}); +// @Test +// public void TestACoreCompactFormat() { +// Map 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 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"); +// } - 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 affixes, Map currencyAffixes, long[] divisors, Object[][] testItems) { - Collection 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); - } - } +// private void checkCore(Map affixes, Map currencyAffixes, long[] divisors, Object[][] testItems) { +// Collection debugCreationErrors = new LinkedHashSet(); +// CompactDecimalFormat cdf = new CompactDecimalFormat( +// "#,###.00", +// DecimalFormatSymbols.getInstance(new ULocale("fr")), +// CompactStyle.SHORT, PluralRules.createRules("one: j is 1 or f is 1"), +// divisors, affixes, currencyAffixes, +// debugCreationErrors +// ); +// if (debugCreationErrors.size() != 0) { +// for (String s : debugCreationErrors) { +// errln("Creation error: " + s); +// } +// } else { +// checkCdf("special cdf ", cdf, testItems); +// } +// } @Test public void TestDefaultSignificantDigits() { - // We are expecting two significant digits as default. + // We are expecting two significant digits for compact formats with one or two zeros, + // and rounded to the unit for compact formats with three or more zeros. CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(ULocale.ENGLISH, CompactStyle.SHORT); + assertEquals("Default significant digits", "120K", cdf.format(123456)); assertEquals("Default significant digits", "12K", cdf.format(12345)); assertEquals("Default significant digits", "1.2K", cdf.format(1234)); assertEquals("Default significant digits", "120", cdf.format(123)); @@ -432,10 +457,11 @@ public class CompactDecimalFormatTest extends TestFmwk { CompactDecimalFormat cdf = CompactDecimalFormat.getInstance( ULocale.ENGLISH, CompactStyle.LONG); BigInteger source_int = new BigInteger("31415926535897932384626433"); - assertEquals("BigInteger format wrong: ", "31,000,000,000,000 trillion", + cdf.setMaximumFractionDigits(0); + assertEquals("BigInteger format wrong: ", "31,415,926,535,898 trillion", cdf.format(source_int)); BigDecimal source_dec = new BigDecimal(source_int); - assertEquals("BigDecimal format wrong: ", "31,000,000,000,000 trillion", + assertEquals("BigDecimal format wrong: ", "31,415,926,535,898 trillion", cdf.format(source_dec)); } @@ -548,7 +574,7 @@ public class CompactDecimalFormatTest extends TestFmwk { result = cdf.format(new CurrencyAmount(43000f, Currency.getInstance("USD"))); assertEquals("CDF should correctly format 43000 with currency in 'ar'", "US$ ٤٣ ألف", result); result = cdf.format(new CurrencyAmount(-43000f, Currency.getInstance("USD"))); - assertEquals("CDF should correctly format -43000 with currency in 'ar'", "US$ ؜-٤٣ ألف", result); + assertEquals("CDF should correctly format -43000 with currency in 'ar'", "؜-US$ ٤٣ ألف", result); // Extra locale with different positive/negative formats cdf = CompactDecimalFormat.getInstance(new ULocale("fi"), CompactDecimalFormat.CompactStyle.SHORT); @@ -590,4 +616,42 @@ public class CompactDecimalFormatTest extends TestFmwk { result = cdf.format(new CurrencyAmount(123000, Currency.getInstance("EUR"))); assertEquals("CDF should correctly format 123000 with currency in 'it'", "120000 €", result); } + + @Test + public void TestBug11319() { + if (logKnownIssue("11319", "CDF does not fall back from zh-Hant-HK to zh-Hant")) { + return; + } + + CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(new ULocale("zh-Hant-HK"), CompactStyle.SHORT); + String result = cdf.format(958000000L); + assertEquals("CDF should correctly format 958 million in zh-Hant-HK", "9.6億", result); + } + + @Test + public void TestBug12975() { + ULocale locale = new ULocale("it"); + CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(locale, CompactStyle.SHORT); + String resultCdf = cdf.format(120000); + DecimalFormat df = (DecimalFormat) DecimalFormat.getInstance(locale); + String resultDefault = df.format(120000); + assertEquals("CompactDecimalFormat should use default pattern when compact pattern is unavailable", + resultDefault, resultCdf); + } + + @Test + public void TestBug11534() { + ULocale locale = new ULocale("pt_PT"); + CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(locale, CompactStyle.SHORT); + String result = cdf.format(1000); + assertEquals("pt_PT should fall back to pt", "1 mil", result); + } + + @Test + public void TestBug12181() { + ULocale loc = ULocale.ENGLISH; + CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(loc, CompactStyle.SHORT); + String s = cdf.format(-1500); + assertEquals("Should work with negative numbers", "-1.5K", s); + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTestData.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestData.java similarity index 96% rename from icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTestData.java rename to icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestData.java index ac8e1d5124..167fd0bdc5 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTestData.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestData.java @@ -43,45 +43,45 @@ import com.ibm.icu.util.ULocale; *

      * 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. - *

      + *

      * To add a new attribute, first create a public field for it. * Next, add the attribute name to the fieldOrdering array. * Finally, create a setter method for it. - * + * * @author rocketman */ -public class NumberFormatTestData { - +public class DataDrivenNumberFormatTestData { + /** * The locale. */ public ULocale locale = null; - + /** * The currency. */ public Currency currency = null; - + /** * The pattern to initialize the formatter, for example 0.00" */ public String pattern = null; - + /** * The value to format as a string. For example 1234.5 would be "1234.5" */ public String format = null; - + /** * The formatted value. */ public String output = null; - + /** * Field for arbitrary comments. */ public String comment = null; - + public Integer minIntegerDigits = null; public Integer maxIntegerDigits = null; public Integer minFractionDigits = null; @@ -117,21 +117,22 @@ public class NumberFormatTestData { public String plural = null; public Integer parseIntegerOnly = null; public Integer decimalPatternMatchRequired = null; + public Integer parseCaseSensitive = null; public Integer parseNoExponent = null; public String outputCurrency = null; - - - + + + /** * nothing or empty means that test ought to work for both C and JAVA; * "C" means test is known to fail in C. "J" means test is known to fail in JAVA. * "CJ" means test is known to fail for both languages. */ public String breaks = null; - + private static Map roundingModeMap = new HashMap(); - + static { roundingModeMap.put("ceiling", BigDecimal.ROUND_CEILING); roundingModeMap.put("floor", BigDecimal.ROUND_FLOOR); @@ -142,18 +143,18 @@ public class NumberFormatTestData { roundingModeMap.put("halfUp", BigDecimal.ROUND_HALF_UP); roundingModeMap.put("unnecessary", BigDecimal.ROUND_UNNECESSARY); } - + private static Map currencyUsageMap = new HashMap(); - + static { currencyUsageMap.put("standard", Currency.CurrencyUsage.STANDARD); currencyUsageMap.put("cash", Currency.CurrencyUsage.CASH); } - + private static Map padPositionMap = new HashMap(); - + static { // TODO: Fix so that it doesn't depend on DecimalFormat. padPositionMap.put("beforePrefix", DecimalFormat.PAD_BEFORE_PREFIX); @@ -161,10 +162,10 @@ public class NumberFormatTestData { padPositionMap.put("beforeSuffix", DecimalFormat.PAD_BEFORE_SUFFIX); padPositionMap.put("afterSuffix", DecimalFormat.PAD_AFTER_SUFFIX); } - + private static Map formatStyleMap = new HashMap(); - + static { formatStyleMap.put("decimal", NumberFormat.NUMBERSTYLE); formatStyleMap.put("currency", NumberFormat.CURRENCYSTYLE); @@ -175,7 +176,7 @@ public class NumberFormatTestData { formatStyleMap.put("currencyAccounting", NumberFormat.ACCOUNTINGCURRENCYSTYLE); formatStyleMap.put("cashCurrency", NumberFormat.CASHCURRENCYSTYLE); } - + // Add any new fields here. On test failures, fields are printed in the same order they // appear here. private static String[] fieldOrdering = { @@ -224,16 +225,16 @@ public class NumberFormatTestData { "parseNoExponent", "outputCurrency" }; - + static { HashSet set = new HashSet(); 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 fromString(Map map, String key) { T value = map.get(key); if (value == null) { @@ -241,222 +242,226 @@ public class NumberFormatTestData { } return value; } - + // start field setters. // add setter for each new field in this block. - + public void setLocale(String value) { locale = new ULocale(value); } - + public void setCurrency(String value) { currency = Currency.getInstance(value); } - + public void setPattern(String value) { pattern = value; } - + public void setFormat(String value) { format = value; } - + public void setOutput(String value) { output = value; } - + public void setComment(String value) { comment = value; } - + public void setMinIntegerDigits(String value) { minIntegerDigits = Integer.valueOf(value); } - + public void setMaxIntegerDigits(String value) { maxIntegerDigits = Integer.valueOf(value); } - + public void setMinFractionDigits(String value) { minFractionDigits = Integer.valueOf(value); } - + public void setMaxFractionDigits(String value) { maxFractionDigits = Integer.valueOf(value); } - + public void setMinGroupingDigits(String value) { minGroupingDigits = Integer.valueOf(value); } - + public void setBreaks(String value) { breaks = value; } - + public void setUseSigDigits(String value) { useSigDigits = Integer.valueOf(value); } - + public void setMinSigDigits(String value) { minSigDigits = Integer.valueOf(value); } - + public void setMaxSigDigits(String value) { maxSigDigits = Integer.valueOf(value); } - + public void setUseGrouping(String value) { useGrouping = Integer.valueOf(value); } - + public void setMultiplier(String value) { multiplier = Integer.valueOf(value); } - + public void setRoundingIncrement(String value) { roundingIncrement = Double.valueOf(value); } - + public void setFormatWidth(String value) { formatWidth = Integer.valueOf(value); } - + public void setPadCharacter(String value) { padCharacter = value; } - + public void setUseScientific(String value) { useScientific = Integer.valueOf(value); } - + public void setGrouping(String value) { grouping = Integer.valueOf(value); } - + public void setGrouping2(String value) { grouping2 = Integer.valueOf(value); } - + public void setRoundingMode(String value) { roundingMode = fromString(roundingModeMap, value); } - + public void setCurrencyUsage(String value) { currencyUsage = fromString(currencyUsageMap, value); } - + public void setMinimumExponentDigits(String value) { minimumExponentDigits = Integer.valueOf(value); } - + public void setExponentSignAlwaysShown(String value) { exponentSignAlwaysShown = Integer.valueOf(value); } - + public void setDecimalSeparatorAlwaysShown(String value) { decimalSeparatorAlwaysShown = Integer.valueOf(value); } - + public void setPadPosition(String value) { padPosition = fromString(padPositionMap, value); } - + public void setPositivePrefix(String value) { positivePrefix = value; } - + public void setPositiveSuffix(String value) { positiveSuffix = value; } - + public void setNegativePrefix(String value) { negativePrefix = value; } - + public void setNegativeSuffix(String value) { negativeSuffix = value; } - + public void setLocalizedPattern(String value) { localizedPattern = value; } - + public void setToPattern(String value) { toPattern = value; } - + public void setToLocalizedPattern(String value) { toLocalizedPattern = value; } - + public void setStyle(String value) { style = fromString(formatStyleMap, value); } - + public void setParse(String value) { parse = value; } - + public void setLenient(String value) { lenient = Integer.valueOf(value); } - + public void setPlural(String value) { plural = value; } - + public void setParseIntegerOnly(String value) { parseIntegerOnly = Integer.valueOf(value); } - + + public void setParseCaseSensitive(String value) { + parseCaseSensitive = Integer.valueOf(value); + } + public void setDecimalPatternMatchRequired(String value) { decimalPatternMatchRequired = Integer.valueOf(value); } - + public void setParseNoExponent(String value) { parseNoExponent = Integer.valueOf(value); } - + public void setOutputCurrency(String value) { outputCurrency = value; } - + // end field setters. - + // start of field clearers // Add clear methods that can be set in one test and cleared // in the next i.e the breaks field. - + public void clearBreaks() { breaks = null; } - + public void clearUseGrouping() { useGrouping = null; } - + public void clearGrouping2() { grouping2 = null; } - + public void clearGrouping() { grouping = null; } - + public void clearMinGroupingDigits() { minGroupingDigits = null; } - + public void clearUseScientific() { useScientific = null; } - + public void clearDecimalSeparatorAlwaysShown() { decimalSeparatorAlwaysShown = null; } - + // end field clearers - + public void setField(String fieldName, String valueString) throws NoSuchMethodException { Method m = getClass().getMethod( @@ -469,7 +474,7 @@ public class NumberFormatTestData { throw new RuntimeException(e); } } - + public void clearField(String fieldName) throws NoSuchMethodException { Method m = getClass().getMethod(fieldToClearer(fieldName)); @@ -481,8 +486,9 @@ public class NumberFormatTestData { throw new RuntimeException(e); } } - - public String toString() { + + @Override + public String toString() { StringBuilder result = new StringBuilder(); result.append("{"); boolean first = true; @@ -517,7 +523,7 @@ public class NumberFormatTestData { + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); } - + private static String fieldToClearer(String fieldName) { return "clear" + Character.toUpperCase(fieldName.charAt(0)) diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestUtility.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestUtility.java index 66fc0fc013..2479111082 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestUtility.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestUtility.java @@ -9,7 +9,9 @@ package com.ibm.icu.dev.test.format; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintStream; import java.util.ArrayList; import java.util.List; @@ -21,12 +23,12 @@ import com.ibm.icu.impl.Utility; * A collection of methods to run the data driven number format test suite. */ public class DataDrivenNumberFormatTestUtility { - + /** * Base class for code under test. */ public static abstract class CodeUnderTest { - + /** * Returns the ID of the code under test. This ID is used to identify * tests that are known to fail for this particular code under test. @@ -37,101 +39,107 @@ public class DataDrivenNumberFormatTestUtility { public Character Id() { return null; } - + /** * Runs a single formatting test. On success, returns null. * On failure, returns the error. This implementation just returns null. * Subclasses should override. * @param tuple contains the parameters of the format test. */ - public String format(NumberFormatTestData tuple) { + public String format(DataDrivenNumberFormatTestData tuple) { + if (tuple.output != null && tuple.output.equals("fail")) return "fail"; return null; } - + /** * Runs a single toPattern test. On success, returns null. * On failure, returns the error. This implementation just returns null. * Subclasses should override. * @param tuple contains the parameters of the format test. */ - public String toPattern(NumberFormatTestData tuple) { + public String toPattern(DataDrivenNumberFormatTestData tuple) { + if (tuple.output != null && tuple.output.equals("fail")) return "fail"; return null; } - + /** * Runs a single parse test. On success, returns null. * On failure, returns the error. This implementation just returns null. * Subclasses should override. * @param tuple contains the parameters of the format test. */ - public String parse(NumberFormatTestData tuple) { + public String parse(DataDrivenNumberFormatTestData tuple) { + if (tuple.output != null && tuple.output.equals("fail")) return "fail"; return null; } - + /** * Runs a single parse currency test. On success, returns null. * On failure, returns the error. This implementation just returns null. * Subclasses should override. * @param tuple contains the parameters of the format test. */ - public String parseCurrency(NumberFormatTestData tuple) { + public String parseCurrency(DataDrivenNumberFormatTestData tuple) { + if (tuple.output != null && tuple.output.equals("fail")) return "fail"; return null; } - + /** * Runs a single select test. On success, returns null. * On failure, returns the error. This implementation just returns null. * Subclasses should override. * @param tuple contains the parameters of the format test. */ - public String select(NumberFormatTestData tuple) { + public String select(DataDrivenNumberFormatTestData tuple) { + if (tuple.output != null && tuple.output.equals("fail")) return "fail"; return null; } } - + private static enum RunMode { SKIP_KNOWN_FAILURES, - INCLUDE_KNOWN_FAILURES + INCLUDE_KNOWN_FAILURES } - + private final CodeUnderTest codeUnderTest; private String fileLine = null; private int fileLineNumber = 0; - private String fileTestName = ""; - private NumberFormatTestData tuple = new NumberFormatTestData(); - + private String fileTestName = ""; + private DataDrivenNumberFormatTestData tuple = new DataDrivenNumberFormatTestData(); + /** * Runs all the tests in the data driven test suite against codeUnderTest. * @param fileName The name of the test file. A relative file name under * com/ibm/icu/dev/data such as "data.txt" * @param codeUnderTest the code under test */ - - static void runSuite( + + public 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 */ - static void runFormatSuiteIncludingKnownFailures( + public static void runFormatSuiteIncludingKnownFailures( String fileName, CodeUnderTest codeUnderTest) { new DataDrivenNumberFormatTestUtility(codeUnderTest) .run(fileName, RunMode.INCLUDE_KNOWN_FAILURES); } - + private DataDrivenNumberFormatTestUtility( CodeUnderTest codeUnderTest) { this.codeUnderTest = codeUnderTest; } - + private void run(String fileName, RunMode runMode) { Character codeUnderTestIdObj = codeUnderTest.Id(); char codeUnderTestId = @@ -144,7 +152,7 @@ public class DataDrivenNumberFormatTestUtility { if (fileLine != null && fileLine.charAt(0) == '\uFEFF') { fileLine = fileLine.substring(1); } - + int state = 0; List columnValues; List columnNames = null; @@ -166,7 +174,7 @@ public class DataDrivenNumberFormatTestUtility { if (state == 0) { if (fileLine.startsWith("test ")) { fileTestName = fileLine; - tuple = new NumberFormatTestData(); + tuple = new DataDrivenNumberFormatTestData(); } else if (fileLine.startsWith("set ")) { if (!setTupleField()) { return; @@ -196,19 +204,41 @@ public class DataDrivenNumberFormatTestUtility { return; } } - if (runMode == RunMode.INCLUDE_KNOWN_FAILURES - || !breaks(codeUnderTestId)) { - String errorMessage = isPass(tuple); - if (errorMessage != null) { - showError(errorMessage); + if (runMode == RunMode.INCLUDE_KNOWN_FAILURES || !breaks(codeUnderTestId)) { + String errorMessage; + Exception err = null; + boolean shouldFail = (tuple.output != null && tuple.output.equals("fail")) + ? !breaks(codeUnderTestId) + : breaks(codeUnderTestId); + try { + errorMessage = isPass(tuple); + } catch (Exception e) { + err = e; + errorMessage = "Exception: " + e + ": " + e.getCause(); + } + if (shouldFail && errorMessage == null) { + showError("Expected failure, but passed"); + } else if (!shouldFail && errorMessage != null) { + if (err != null) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(os); + err.printStackTrace(ps); + String stackTrace = os.toString(); + showError(errorMessage + " Stack trace: " + stackTrace.substring(0, 500)); + } else { + showError(errorMessage); + } } } } fileLine = null; } } catch (Exception e) { - showError(e.toString()); - e.printStackTrace(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(os); + e.printStackTrace(ps); + String stackTrace = os.toString(); + showError("MAJOR ERROR: " + e.toString() + " Stack trace: " + stackTrace.substring(0,500)); } finally { try { if (in != null) { @@ -228,7 +258,7 @@ public class DataDrivenNumberFormatTestUtility { private static boolean isSpace(char c) { return (c == 0x09 || c == 0x20 || c == 0x3000); } - + private boolean setTupleField() { List parts = splitBy(3, (char) 0x20); if (parts.size() < 3) { @@ -237,7 +267,7 @@ public class DataDrivenNumberFormatTestUtility { } return setField(parts.get(1), parts.get(2)); } - + private boolean setField(String name, String value) { try { tuple.setField(name, Utility.unescape(value)); @@ -247,7 +277,7 @@ public class DataDrivenNumberFormatTestUtility { return false; } } - + private boolean clearField(String name) { try { tuple.clearField(name); @@ -257,17 +287,17 @@ public class DataDrivenNumberFormatTestUtility { return false; } } - + private void showError(String message) { TestFmwk.errln(String.format("line %d: %s\n%s\n%s", fileLineNumber, Utility.escape(message), fileTestName,fileLine)); } - + private List splitBy(char delimiter) { return splitBy(Integer.MAX_VALUE, delimiter); } - + private List splitBy(int max, char delimiter) { - ArrayList result = new ArrayList(); + ArrayList result = new ArrayList(); int colIdx = 0; int colStart = 0; int len = fileLine.length(); @@ -282,7 +312,7 @@ public class DataDrivenNumberFormatTestUtility { } result.add(fileLine.substring(colStart, len)); return result; - } + } private boolean readLine(BufferedReader in) throws IOException { String line = in.readLine(); @@ -301,8 +331,8 @@ public class DataDrivenNumberFormatTestUtility { fileLine = idx == 0 ? "" : line; return true; } - - private String isPass(NumberFormatTestData tuple) { + + private String isPass(DataDrivenNumberFormatTestData tuple) { StringBuilder result = new StringBuilder(); if (tuple.format != null && tuple.output != null) { String errorMessage = codeUnderTest.format(tuple); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPI.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPI.java index ad0a462509..973e71e1a6 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPI.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPI.java @@ -6,11 +6,11 @@ * Corporation and others. All Rights Reserved. **/ -/** +/** * Port From: JDK 1.4b1 : java.text.Format.IntlTestDecimalFormatAPI * Source File: java/text/format/IntlTestDecimalFormatAPI.java **/ - + /* @test 1.4 98/03/06 @summary test International Decimal Format API @@ -35,18 +35,18 @@ import com.ibm.icu.text.NumberFormat; public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk { /** - * Problem 1: simply running - * decF4.setRoundingMode(java.math.BigDecimal.ROUND_HALF_UP) does not work + * Problem 1: simply running + * decF4.setRoundingMode(java.math.BigDecimal.ROUND_HALF_UP) does not work * as decF4.setRoundingIncrement(.0001) must also be run. - * Problem 2: decF4.format(8.88885) does not return 8.8889 as expected. - * You must run decF4.format(new BigDecimal(Double.valueOf(8.88885))) in + * Problem 2: decF4.format(8.88885) does not return 8.8889 as expected. + * You must run decF4.format(new BigDecimal(Double.valueOf(8.88885))) in * order for this to work as expected. - * Problem 3: There seems to be no way to set half up to be the default + * Problem 3: There seems to be no way to set half up to be the default * rounding mode. - * We solved the problem with the code at the bottom of this page however + * We solved the problem with the code at the bottom of this page however * this is not quite general purpose enough to include in icu4j. A static - * setDefaultRoundingMode function would solve the problem nicely. Also - * decimal places past 20 are not handled properly. A small ammount of work + * setDefaultRoundingMode function would solve the problem nicely. Also + * decimal places past 20 are not handled properly. A small ammount of work * would make bring this up to snuff. */ @Test @@ -55,7 +55,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk // problem 2 double number = 8.88885; String expected = "8.8889"; - + String pat = ",##0.0000"; DecimalFormat dec = new DecimalFormat(pat); dec.setRoundingMode(BigDecimal.ROUND_HALF_UP); @@ -65,7 +65,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk if (!str.equals(expected)) { errln("Fail: " + number + " x \"" + pat + "\" = \"" + str + "\", expected \"" + expected + "\""); - } + } pat = ",##0.0001"; dec = new DecimalFormat(pat); @@ -74,25 +74,25 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk if (!str.equals(expected)) { errln("Fail: " + number + " x \"" + pat + "\" = \"" + str + "\", expected \"" + expected + "\""); - } - + } + // testing 20 decimal places pat = ",##0.00000000000000000001"; dec = new DecimalFormat(pat); BigDecimal bignumber = new BigDecimal("8.888888888888888888885"); expected = "8.88888888888888888889"; - + dec.setRoundingMode(BigDecimal.ROUND_HALF_UP); - str = dec.format(bignumber); + str = dec.format(bignumber); if (!str.equals(expected)) { errln("Fail: " + bignumber + " x \"" + pat + "\" = \"" + str + "\", expected \"" + expected + "\""); - } - + } + } - /** - * This test checks various generic API methods in DecimalFormat to achieve + /** + * This test checks various generic API methods in DecimalFormat to achieve * 100% API coverage. */ @Test @@ -298,7 +298,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk DecimalFormat decfmt = new DecimalFormat(); MathContext resultICU; - MathContext comp1 = new MathContext(0, MathContext.PLAIN); + MathContext comp1 = new MathContext(0, MathContext.PLAIN, false, MathContext.ROUND_HALF_EVEN); resultICU = decfmt.getMathContextICU(); if ((comp1.getDigits() != resultICU.getDigits()) || (comp1.getForm() != resultICU.getForm()) || @@ -309,7 +309,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk " / expected: " + comp1.toString()); } - MathContext comp2 = new MathContext(5, MathContext.ENGINEERING); + MathContext comp2 = new MathContext(5, MathContext.ENGINEERING, false, MathContext.ROUND_HALF_EVEN); decfmt.setMathContextICU(comp2); resultICU = decfmt.getMathContextICU(); if ((comp2.getDigits() != resultICU.getDigits()) || @@ -344,7 +344,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk // get default rounding increment r1 = pat.getRoundingIncrement(); - // set rounding mode with zero increment. Rounding + // set rounding mode with zero increment. Rounding // increment should be set by this operation pat.setRoundingMode(BigDecimal.ROUND_UP); r2 = pat.getRoundingIncrement(); @@ -358,43 +358,43 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk } } } - + @Test public void testJB6648() { DecimalFormat df = new DecimalFormat(); df.setParseStrict(true); - + String numstr = new String(); - + String[] patterns = { "0", "00", "000", "0,000", "0.0", - "#000.0" + "#000.0" }; - + for(int i=0; i < patterns.length; i++) { df.applyPattern(patterns[i]); - numstr = df.format(5); + numstr = df.format(5); try { Number n = df.parse(numstr); logln("INFO: Parsed " + numstr + " -> " + n); } catch (ParseException pe) { errln("ERROR: Failed round trip with strict parsing."); - } + } } - + df.applyPattern(patterns[1]); - numstr = "005"; + numstr = "005"; try { Number n = df.parse(numstr); logln("INFO: Successful parse for " + numstr + " with strict parse enabled. Number is " + n); } catch (ParseException pe) { errln("ERROR: Parse Exception encountered in strict mode: numstr -> " + numstr); - } - + } + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPIC.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPIC.java index 5a8a5448f8..f0fe8ea5fc 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPIC.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPIC.java @@ -228,7 +228,7 @@ public class IntlTestDecimalFormatAPIC extends com.ibm.icu.dev.test.TestFmwk { s2 = pat.toPattern(); logln("Extracted pattern is " + s2); if (!s2.equals(p1)) { - errln("ERROR: toPattern() result did not match pattern applied"); + errln("ERROR: toPattern() result did not match pattern applied: " + p1 + " vs " + s2); } String p2 = new String("#,##0.0# FF;(#,##0.0# FF)"); @@ -237,9 +237,7 @@ public class IntlTestDecimalFormatAPIC extends com.ibm.icu.dev.test.TestFmwk { String s3; s3 = pat.toLocalizedPattern(); logln("Extracted pattern is " + s3); - if (!s3.equals(p2)) { - errln("ERROR: toLocalizedPattern() result did not match pattern applied"); - } + assertEquals("ERROR: toLocalizedPattern() result did not match pattern applied", p2, s3); // ======= Test getStaticClassID() diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatSymbolsC.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatSymbolsC.java index f8b4eaa2e3..77495f41d3 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatSymbolsC.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatSymbolsC.java @@ -7,7 +7,7 @@ ******************************************************************************* */ -/** +/** * Port From: ICU4C v1.8.1 : format : IntlTestDecimalFormatSymbols * Source File: $ICU4CRoot/source/test/intltest/tsdcfmsy.cpp **/ @@ -30,105 +30,105 @@ public class IntlTestDecimalFormatSymbolsC extends com.ibm.icu.dev.test.TestFmwk * Test the API of DecimalFormatSymbols; primarily a simple get/set set. */ @Test - public void TestSymbols() { - DecimalFormatSymbols fr = new DecimalFormatSymbols(Locale.FRENCH); + public void TestSymbols() { + DecimalFormatSymbols fr = new DecimalFormatSymbols(Locale.FRENCH); DecimalFormatSymbols en = new DecimalFormatSymbols(Locale.ENGLISH); - + if (en.equals(fr)) { errln("ERROR: English DecimalFormatSymbols equal to French"); } - + // just do some VERY basic tests to make sure that get/set work - + char zero = en.getZeroDigit(); fr.setZeroDigit(zero); if (fr.getZeroDigit() != en.getZeroDigit()) { errln("ERROR: get/set ZeroDigit failed"); } - + char group = en.getGroupingSeparator(); fr.setGroupingSeparator(group); if (fr.getGroupingSeparator() != en.getGroupingSeparator()) { errln("ERROR: get/set GroupingSeparator failed"); } - + char decimal = en.getDecimalSeparator(); fr.setDecimalSeparator(decimal); if (fr.getDecimalSeparator() != en.getDecimalSeparator()) { errln("ERROR: get/set DecimalSeparator failed"); } - + char perMill = en.getPerMill(); fr.setPerMill(perMill); if (fr.getPerMill() != en.getPerMill()) { errln("ERROR: get/set PerMill failed"); } - + char percent = en.getPercent(); fr.setPercent(percent); if (fr.getPercent() != en.getPercent()) { errln("ERROR: get/set Percent failed"); } - + char digit = en.getDigit(); fr.setDigit(digit); if (fr.getPercent() != en.getPercent()) { errln("ERROR: get/set Percent failed"); } - + char patternSeparator = en.getPatternSeparator(); fr.setPatternSeparator(patternSeparator); if (fr.getPatternSeparator() != en.getPatternSeparator()) { errln("ERROR: get/set PatternSeparator failed"); } - + String infinity = en.getInfinity(); fr.setInfinity(infinity); String infinity2 = fr.getInfinity(); if (!infinity.equals(infinity2)) { errln("ERROR: get/set Infinity failed"); } - + String nan = en.getNaN(); fr.setNaN(nan); String nan2 = fr.getNaN(); if (!nan.equals(nan2)) { errln("ERROR: get/set NaN failed"); } - + char minusSign = en.getMinusSign(); fr.setMinusSign(minusSign); if (fr.getMinusSign() != en.getMinusSign()) { errln("ERROR: get/set MinusSign failed"); } - + // char exponential = en.getExponentialSymbol(); // fr.setExponentialSymbol(exponential); // if(fr.getExponentialSymbol() != en.getExponentialSymbol()) { // errln("ERROR: get/set Exponential failed"); // } - + //DecimalFormatSymbols foo = new DecimalFormatSymbols(); //The variable is never used - + en = (DecimalFormatSymbols) fr.clone(); - + if (!en.equals(fr)) { errln("ERROR: Clone failed"); } - + DecimalFormatSymbols sym = new DecimalFormatSymbols(Locale.US); - + verify(34.5, "00.00", sym, "34.50"); sym.setDecimalSeparator('S'); verify(34.5, "00.00", sym, "34S50"); sym.setPercent('P'); verify(34.5, "00 %", sym, "3450 P"); sym.setCurrencySymbol("D"); - verify(34.5, "\u00a4##.##", sym, "D34.5"); + verify(34.5, "\u00a4##.##", sym, "D34.50"); sym.setGroupingSeparator('|'); verify(3456.5, "0,000.##", sym, "3|456S5"); } - + /** helper functions**/ public void verify(double value, String pattern, DecimalFormatSymbols sym, String expected) { DecimalFormat df = new DecimalFormat(pattern, sym); @@ -136,7 +136,7 @@ public class IntlTestDecimalFormatSymbolsC extends com.ibm.icu.dev.test.TestFmwk FieldPosition pos = new FieldPosition(-1); buffer = df.format(value, buffer, pos); if(!buffer.toString().equals(expected)){ - errln("ERROR: format failed after setSymbols()\n Expected" + + errln("ERROR: format failed after setSymbols()\n Expected" + expected + ", Got " + buffer); } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java index d4fbe45f2d..ddfd80c95c 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java @@ -30,6 +30,7 @@ import java.util.TreeMap; import org.junit.Test; import com.ibm.icu.dev.test.TestFmwk; +import com.ibm.icu.dev.test.serializable.FormatHandler; import com.ibm.icu.dev.test.serializable.SerializableTestUtility; import com.ibm.icu.impl.Pair; import com.ibm.icu.impl.Utility; @@ -2035,6 +2036,13 @@ public class MeasureUnitTest extends TestFmwk { } } + @Test + public void testBug11966() { + Locale locale = new Locale("en", "AU"); + MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE); + // Should not throw an exception. + } + // DO NOT DELETE THIS FUNCTION! It may appear as dead code, but we use this to generate code // for MeasureFormat during the release process. static Map> getUnitsToPerParts() { @@ -2528,6 +2536,8 @@ public class MeasureUnitTest extends TestFmwk { public static class MeasureFormatHandler implements SerializableTestUtility.Handler { + FormatHandler.NumberFormatHandler nfh = new FormatHandler.NumberFormatHandler(); + @Override public Object[] getTestObjects() { @@ -2547,8 +2557,7 @@ public class MeasureUnitTest extends TestFmwk { MeasureFormat b1 = (MeasureFormat) b; return a1.getLocale().equals(b1.getLocale()) && a1.getWidth().equals(b1.getWidth()) - && a1.getNumberFormat().equals(b1.getNumberFormat()) - ; + && nfh.hasSameBehavior(a1.getNumberFormat(), b1.getNumberFormat()); } } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java new file mode 100644 index 0000000000..09ccd38f07 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java @@ -0,0 +1,434 @@ +// © 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.dev.test.number.ShanesDataDrivenTestUtility; +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() { + ShanesDataDrivenTestUtility.run(); + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatRegressionTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatRegressionTest.java index 6b9aa25988..4a98339805 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatRegressionTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatRegressionTest.java @@ -7,7 +7,7 @@ ******************************************************************************* */ -/** +/** * Port From: ICU4C v1.8.1 : format : NumberFormatRegressionTest * Source File: $ICU4CRoot/source/test/intltest/numrgts.cpp **/ @@ -31,7 +31,7 @@ import com.ibm.icu.text.NumberFormat; import com.ibm.icu.util.Calendar; import com.ibm.icu.util.ULocale; -/** +/** * Performs regression test for MessageFormat **/ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { @@ -51,26 +51,26 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { errln("FAIL"); } } - + /** * DateFormat should call setIntegerParseOnly(TRUE) on adopted * NumberFormat objects. */ @Test public void TestJ691() { - + Locale loc = new Locale("fr", "CH"); - + // set up the input date string & expected output String udt = "11.10.2000"; String exp = "11.10.00"; - + // create a Calendar for this locale Calendar cal = Calendar.getInstance(loc); - + // create a NumberFormat for this locale NumberFormat nf = NumberFormat.getInstance(loc); - + // *** Here's the key: We don't want to have to do THIS: //nf.setParseIntegerOnly(true); // However with changes to fr_CH per cldrbug:9370 we have to do the following: @@ -78,10 +78,10 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { // create the DateFormat DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, loc); - + df.setCalendar(cal); df.setNumberFormat(nf); - + // set parsing to lenient & parse Date ulocdat = new Date(); df.setLenient(true); @@ -92,32 +92,32 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { } // format back to a string String outString = df.format(ulocdat); - + if (!outString.equals(exp)) { errln("FAIL: " + udt + " => " + outString); } } - + /** * Test getIntegerInstance(); */ @Test public void Test4408066() { - + NumberFormat nf1 = NumberFormat.getIntegerInstance(); NumberFormat nf2 = NumberFormat.getIntegerInstance(Locale.CHINA); - + //test isParseIntegerOnly if (!nf1.isParseIntegerOnly() || !nf2.isParseIntegerOnly()) { errln("Failed : Integer Number Format Instance should set setParseIntegerOnly(true)"); } - + //Test format { double[] data = { - -3.75, -2.5, -1.5, - -1.25, 0, 1.0, - 1.25, 1.5, 2.5, + -3.75, -2.5, -1.5, + -1.25, 0, 1.0, + 1.25, 1.5, 2.5, 3.75, 10.0, 255.5 }; String[] expected = { @@ -126,11 +126,11 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { "1", "2", "2", "4", "10", "256" }; - + for (int i = 0; i < data.length; ++i) { String result = nf1.format(data[i]); if (!result.equals(expected[i])) { - errln("Failed => Source: " + Double.toString(data[i]) + errln("Failed => Source: " + Double.toString(data[i]) + ";Formatted : " + result + ";but expectted: " + expected[i]); } @@ -139,9 +139,9 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { //Test parse, Parsing should stop at "." { String data[] = { - "-3.75", "-2.5", "-1.5", - "-1.25", "0", "1.0", - "1.25", "1.5", "2.5", + "-3.75", "-2.5", "-1.5", + "-1.25", "0", "1.0", + "1.25", "1.5", "2.5", "3.75", "10.0", "255.5" }; long[] expected = { @@ -150,7 +150,7 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { 1, 1, 2, 3, 10, 255 }; - + for (int i = 0; i < data.length; ++i) { Number n = null; try { @@ -162,31 +162,27 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { errln("Failed: Integer Number Format should parse string to Long/Integer"); } if (n.longValue() != expected[i]) { - errln("Failed=> Source: " + data[i] + errln("Failed=> Source: " + data[i] + ";result : " + n.toString() + ";expected :" + Long.toString(expected[i])); } } } } - + //Test New serialized DecimalFormat(2.0) read old serialized forms of DecimalFormat(1.3.1.1) @Test public void TestSerialization() throws IOException{ byte[][] contents = NumberFormatSerialTestData.getContent(); double data = 1234.56; String[] expected = { - "1,234.56", "$1,234.56", "123,456%", "1.23456E3"}; + "1,234.56", "$1,234.56", "1.23456E3", "1,234.56"}; for (int i = 0; i < 4; ++i) { ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(contents[i])); try { NumberFormat format = (NumberFormat) ois.readObject(); String result = format.format(data); - if (result.equals(expected[i])) { - logln("OK: Deserialized bogus NumberFormat(new version read old version)"); - } else { - errln("FAIL: the test data formats are not euqal"); - } + assertEquals("Deserialization new version should read old version", expected[i], result); } catch (Exception e) { warnln("FAIL: " + e.getMessage()); } @@ -284,7 +280,7 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { try { Number n = nfmt.parse(data[i]); if (expected[i] != n.doubleValue()) { - errln("Failed: Parsed result for " + data[i] + ": " + errln("Failed: Parsed result for " + data[i] + ": " + n.doubleValue() + " / expected: " + expected[i]); } } catch (ParseException pe) { @@ -295,8 +291,8 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { @Test public void TestSurrogatesParsing() { // Test parsing of numbers that use digits from the supplemental planes. final String[] data = { - "1\ud801\udca2,3\ud801\udca45.67", // - "\ud801\udca1\ud801\udca2,\ud801\udca3\ud801\udca4\ud801\udca5.\ud801\udca6\ud801\udca7\ud801\udca8", // + "1\ud801\udca2,3\ud801\udca45.67", // + "\ud801\udca1\ud801\udca2,\ud801\udca3\ud801\udca4\ud801\udca5.\ud801\udca6\ud801\udca7\ud801\udca8", // "\ud835\udfd2.\ud835\udfd7E-\ud835\udfd1", "\ud835\udfd3.8E-0\ud835\udfd0" }; @@ -313,7 +309,7 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { try { Number n = nfmt.parse(data[i]); if (expected[i] != n.doubleValue()) { - errln("Failed: Parsed result for " + data[i] + ": " + errln("Failed: Parsed result for " + data[i] + ": " + n.doubleValue() + " / expected: " + expected[i]); } } catch (ParseException pe) { @@ -324,7 +320,7 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { void checkNBSPPatternRtNum(String testcase, NumberFormat nf, double myNumber) { String myString = nf.format(myNumber); - + double aNumber; try { aNumber = nf.parse(myString).doubleValue(); @@ -349,19 +345,19 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { public void TestNBSPInPattern() { NumberFormat nf = null; String testcase; - - + + testcase="ar_AE UNUM_CURRENCY"; nf = NumberFormat.getCurrencyInstance(new ULocale("ar_AE")); checkNBSPPatternRT(testcase, nf); - // if we don't have CLDR 1.6 data, bring out the problem anyways - + // if we don't have CLDR 1.6 data, bring out the problem anyways + String SPECIAL_PATTERN = "\u00A4\u00A4'\u062f.\u0625.\u200f\u00a0'###0.00"; testcase = "ar_AE special pattern: " + SPECIAL_PATTERN; nf = new DecimalFormat(); ((DecimalFormat)nf).applyPattern(SPECIAL_PATTERN); checkNBSPPatternRT(testcase, nf); - + } /* @@ -385,4 +381,14 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk { errln("FAIL: Parsed result: " + num + " - expected: " + val); } } + + @Test + public void TestAffixesNoCurrency() { + ULocale locale = new ULocale("en"); + DecimalFormat nf = (DecimalFormat) NumberFormat.getInstance(locale, NumberFormat.PLURALCURRENCYSTYLE); + assertEquals( + "Positive suffix should contain the single currency sign when no currency is set", + " \u00A4", + nf.getPositiveSuffix()); + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatSerialTestData.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatSerialTestData.java index 95680965dc..6461cc2321 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatSerialTestData.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatSerialTestData.java @@ -10,13 +10,9 @@ package com.ibm.icu.dev.test.format; public class NumberFormatSerialTestData { - //get Content - public static byte[][] getContent() { - return content; - } //NumberFormat.getInstance(Locale.US) - static byte[] generalInstance = new byte[]{ + static byte[] generalInstance() { return 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, @@ -85,10 +81,10 @@ public class NumberFormatSerialTestData { 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.getCurrencyInstance(Locale.US) - static byte[] currencyInstance = new byte[]{ + static byte[] currencyInstance () { return 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, @@ -158,82 +154,10 @@ public class NumberFormatSerialTestData { 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.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[]{ + static byte[] scientificInstance() { return 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, @@ -302,7 +226,295 @@ public class NumberFormatSerialTestData { 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, - }; + };}; - final static byte[][] content = {generalInstance, currencyInstance, percentInstance, scientificInstance}; + static byte[] icu58Latest() { return 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, 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() { return 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, 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 + };}; + + static byte[][] getContent() { + return new byte[][] { + generalInstance(), + currencyInstance(), + scientificInstance(), + icu58Latest(), + newFromPattern() + }; + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java index 897846b2be..9c940fafc7 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java @@ -14,8 +14,13 @@ package com.ibm.icu.dev.test.format; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.math.BigInteger; +import java.math.RoundingMode; import java.text.AttributedCharacterIterator; import java.text.FieldPosition; import java.text.Format; @@ -36,6 +41,7 @@ import com.ibm.icu.impl.ICUConfig; import com.ibm.icu.impl.LocaleUtility; import com.ibm.icu.impl.data.ResourceReader; import com.ibm.icu.impl.data.TokenIterator; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode; import com.ibm.icu.math.BigDecimal; import com.ibm.icu.math.MathContext; import com.ibm.icu.text.CompactDecimalFormat; @@ -49,408 +55,12 @@ import com.ibm.icu.text.NumberFormat.SimpleNumberFormatFactory; import com.ibm.icu.text.NumberingSystem; import com.ibm.icu.text.RuleBasedNumberFormat; import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Currency.CurrencyUsage; import com.ibm.icu.util.CurrencyAmount; import com.ibm.icu.util.ULocale; public class NumberFormatTest extends TestFmwk { - private static ULocale EN = new ULocale("en"); - - private static Number toNumber(String s) { - if (s.equals("NaN")) { - return Double.NaN; - } else if (s.equals("-Inf")) { - return Double.NEGATIVE_INFINITY; - } else if (s.equals("Inf")) { - return Double.POSITIVE_INFINITY; - } - return new BigDecimal(s); - } - - - private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU = - new DataDrivenNumberFormatTestUtility.CodeUnderTest() { - @Override - public Character Id() { return 'J'; } - - @Override - public String format(NumberFormatTestData tuple) { - DecimalFormat fmt = newDecimalFormat(tuple); - String actual = fmt.format(toNumber(tuple.format)); - String expected = tuple.output; - if (!expected.equals(actual)) { - return "Expected " + expected + ", got " + actual; - } - return null; - } - - @Override - public String toPattern(NumberFormatTestData tuple) { - DecimalFormat fmt = newDecimalFormat(tuple); - StringBuilder result = new StringBuilder(); - if (tuple.toPattern != null) { - String expected = tuple.toPattern; - String actual = fmt.toPattern(); - if (!expected.equals(actual)) { - result.append("Expected toPattern=" + expected + ", got " + actual); - } - } - if (tuple.toLocalizedPattern != null) { - String expected = tuple.toLocalizedPattern; - String actual = fmt.toLocalizedPattern(); - if (!expected.equals(actual)) { - result.append("Expected toLocalizedPattern=" + expected + ", got " + actual); - } - } - return result.length() == 0 ? null : result.toString(); - } - - @Override - public String parse(NumberFormatTestData tuple) { - DecimalFormat fmt = newDecimalFormat(tuple); - ParsePosition ppos = new ParsePosition(0); - Number actual = fmt.parse(tuple.parse, ppos); - if (ppos.getIndex() == 0) { - if (!tuple.output.equals("fail")) { - return "Parse error expected."; - } - return null; - } - if (tuple.output.equals("fail")) { - return "Parse succeeded: "+actual+", but was expected to fail."; - } - Number expected = toNumber(tuple.output); - // number types cannot be compared, this is the best we can do. - if (expected.doubleValue() != (actual.doubleValue())) { - return "Expected: " + expected + ", got: " + actual; - } - return null; - } - - @Override - public String parseCurrency(NumberFormatTestData tuple) { - DecimalFormat fmt = newDecimalFormat(tuple); - ParsePosition ppos = new ParsePosition(0); - CurrencyAmount currAmt = fmt.parseCurrency(tuple.parse, ppos); - if (ppos.getIndex() == 0) { - if (!tuple.output.equals("fail")) { - return "Parse error expected."; - } - return null; - } - if (tuple.output.equals("fail")) { - return "Parse succeeded: "+currAmt+", but was expected to fail."; - } - Number expected = toNumber(tuple.output); - Number actual = currAmt.getNumber(); - // number types cannot be compared, this is the best we can do. - if (expected.doubleValue() != (actual.doubleValue())) { - return "Expected: " + expected + ", got: " + actual; - } - - if (!tuple.outputCurrency.equals(currAmt.getCurrency().toString())) { - return "Expected currency: " + tuple.outputCurrency + ", got: " + currAmt.getCurrency(); - } - return null; - } - - /** - * @param tuple - * @return - */ - private DecimalFormat newDecimalFormat(NumberFormatTestData tuple) { - - DecimalFormat fmt = new DecimalFormat( - tuple.pattern == null ? "0" : tuple.pattern, - new DecimalFormatSymbols(tuple.locale == null ? EN : tuple.locale)); - adjustDecimalFormat(tuple, fmt); - return fmt; - } - /** - * @param tuple - * @param fmt - */ - private void adjustDecimalFormat(NumberFormatTestData tuple, DecimalFormat fmt) { - if (tuple.minIntegerDigits != null) { - fmt.setMinimumIntegerDigits(tuple.minIntegerDigits); - } - if (tuple.maxIntegerDigits != null) { - fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits); - } - if (tuple.minFractionDigits != null) { - fmt.setMinimumFractionDigits(tuple.minFractionDigits); - } - if (tuple.maxFractionDigits != null) { - fmt.setMaximumFractionDigits(tuple.maxFractionDigits); - } - if (tuple.currency != null) { - fmt.setCurrency(tuple.currency); - } - if (tuple.minGroupingDigits != null) { - // Oops we don't support this. - } - if (tuple.useSigDigits != null) { - fmt.setSignificantDigitsUsed( - tuple.useSigDigits != 0); - } - if (tuple.minSigDigits != null) { - fmt.setMinimumSignificantDigits(tuple.minSigDigits); - } - if (tuple.maxSigDigits != null) { - fmt.setMaximumSignificantDigits(tuple.maxSigDigits); - } - if (tuple.useGrouping != null) { - fmt.setGroupingUsed(tuple.useGrouping != 0); - } - if (tuple.multiplier != null) { - fmt.setMultiplier(tuple.multiplier); - } - if (tuple.roundingIncrement != null) { - fmt.setRoundingIncrement(tuple.roundingIncrement.doubleValue()); - } - if (tuple.formatWidth != null) { - fmt.setFormatWidth(tuple.formatWidth); - } - if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) { - fmt.setPadCharacter(tuple.padCharacter.charAt(0)); - } - if (tuple.useScientific != null) { - fmt.setScientificNotation(tuple.useScientific != 0); - } - if (tuple.grouping != null) { - fmt.setGroupingSize(tuple.grouping); - } - if (tuple.grouping2 != null) { - fmt.setSecondaryGroupingSize(tuple.grouping2); - } - if (tuple.roundingMode != null) { - fmt.setRoundingMode(tuple.roundingMode); - } - if (tuple.currencyUsage != null) { - fmt.setCurrencyUsage(tuple.currencyUsage); - } if (tuple.minimumExponentDigits != null) { - fmt.setMinimumExponentDigits( - tuple.minimumExponentDigits.byteValue()); - } - if (tuple.exponentSignAlwaysShown != null) { - fmt.setExponentSignAlwaysShown( - tuple.exponentSignAlwaysShown != 0); - } - if (tuple.decimalSeparatorAlwaysShown != null) { - fmt.setDecimalSeparatorAlwaysShown( - tuple.decimalSeparatorAlwaysShown != 0); - } - if (tuple.padPosition != null) { - fmt.setPadPosition(tuple.padPosition); - } - if (tuple.positivePrefix != null) { - fmt.setPositivePrefix(tuple.positivePrefix); - } - if (tuple.positiveSuffix != null) { - fmt.setPositiveSuffix(tuple.positiveSuffix); - } - if (tuple.negativePrefix != null) { - fmt.setNegativePrefix(tuple.negativePrefix); - } - if (tuple.negativeSuffix != null) { - fmt.setNegativeSuffix(tuple.negativeSuffix); - } - if (tuple.localizedPattern != null) { - fmt.applyLocalizedPattern(tuple.localizedPattern); - } - int lenient = tuple.lenient == null ? 1 : tuple.lenient.intValue(); - fmt.setParseStrict(lenient == 0); - if (tuple.parseIntegerOnly != null) { - fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0); - } - if (tuple.decimalPatternMatchRequired != null) { - fmt.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0); - } - if (tuple.parseNoExponent != null) { - // Oops, not supported for now - } - } - }; - - - private DataDrivenNumberFormatTestUtility.CodeUnderTest JDK = - new DataDrivenNumberFormatTestUtility.CodeUnderTest() { - @Override - public Character Id() { return 'K'; } - - @Override - public String format(NumberFormatTestData tuple) { - java.text.DecimalFormat fmt = newDecimalFormat(tuple); - String actual = fmt.format(toNumber(tuple.format)); - String expected = tuple.output; - if (!expected.equals(actual)) { - return "Expected " + expected + ", got " + actual; - } - return null; - } - - @Override - public String toPattern(NumberFormatTestData tuple) { - java.text.DecimalFormat fmt = newDecimalFormat(tuple); - StringBuilder result = new StringBuilder(); - if (tuple.toPattern != null) { - String expected = tuple.toPattern; - String actual = fmt.toPattern(); - if (!expected.equals(actual)) { - result.append("Expected toPattern=" + expected + ", got " + actual); - } - } - if (tuple.toLocalizedPattern != null) { - String expected = tuple.toLocalizedPattern; - String actual = fmt.toLocalizedPattern(); - if (!expected.equals(actual)) { - result.append("Expected toLocalizedPattern=" + expected + ", got " + actual); - } - } - return result.length() == 0 ? null : result.toString(); - } - - @Override - public String parse(NumberFormatTestData tuple) { - java.text.DecimalFormat fmt = newDecimalFormat(tuple); - ParsePosition ppos = new ParsePosition(0); - Number actual = fmt.parse(tuple.parse, ppos); - if (ppos.getIndex() == 0) { - if (!tuple.output.equals("fail")) { - return "Parse error expected."; - } - return null; - } - if (tuple.output.equals("fail")) { - return "Parse succeeded: "+actual+", but was expected to fail."; - } - Number expected = toNumber(tuple.output); - // number types cannot be compared, this is the best we can do. - if (expected.doubleValue() != actual.doubleValue()) { - return "Expected: " + expected + ", got: " + actual; - } - return null; - } - - - - /** - * @param tuple - * @return - */ - private java.text.DecimalFormat newDecimalFormat(NumberFormatTestData tuple) { - java.text.DecimalFormat fmt = new java.text.DecimalFormat( - tuple.pattern == null ? "0" : tuple.pattern, - new java.text.DecimalFormatSymbols( - (tuple.locale == null ? EN : tuple.locale).toLocale())); - adjustDecimalFormat(tuple, fmt); - return fmt; - } - - /** - * @param tuple - * @param fmt - */ - private void adjustDecimalFormat(NumberFormatTestData tuple, java.text.DecimalFormat fmt) { - if (tuple.minIntegerDigits != null) { - fmt.setMinimumIntegerDigits(tuple.minIntegerDigits); - } - if (tuple.maxIntegerDigits != null) { - fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits); - } - if (tuple.minFractionDigits != null) { - fmt.setMinimumFractionDigits(tuple.minFractionDigits); - } - if (tuple.maxFractionDigits != null) { - fmt.setMaximumFractionDigits(tuple.maxFractionDigits); - } - if (tuple.currency != null) { - fmt.setCurrency(java.util.Currency.getInstance(tuple.currency.toString())); - } - if (tuple.minGroupingDigits != null) { - // Oops we don't support this. - } - if (tuple.useSigDigits != null) { - // Oops we don't support this - } - if (tuple.minSigDigits != null) { - // Oops we don't support this - } - if (tuple.maxSigDigits != null) { - // Oops we don't support this - } - if (tuple.useGrouping != null) { - fmt.setGroupingUsed(tuple.useGrouping != 0); - } - if (tuple.multiplier != null) { - fmt.setMultiplier(tuple.multiplier); - } - if (tuple.roundingIncrement != null) { - // Not supported - } - if (tuple.formatWidth != null) { - // Not supported - } - if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) { - // Not supported - } - if (tuple.useScientific != null) { - // Not supported - } - if (tuple.grouping != null) { - fmt.setGroupingSize(tuple.grouping); - } - if (tuple.grouping2 != null) { - // Not supported - } - if (tuple.roundingMode != null) { - // Not supported - } - if (tuple.currencyUsage != null) { - // Not supported - } - if (tuple.minimumExponentDigits != null) { - // Not supported - } - if (tuple.exponentSignAlwaysShown != null) { - // Not supported - } - if (tuple.decimalSeparatorAlwaysShown != null) { - fmt.setDecimalSeparatorAlwaysShown( - tuple.decimalSeparatorAlwaysShown != 0); - } - if (tuple.padPosition != null) { - // Not supported - } - if (tuple.positivePrefix != null) { - fmt.setPositivePrefix(tuple.positivePrefix); - } - if (tuple.positiveSuffix != null) { - fmt.setPositiveSuffix(tuple.positiveSuffix); - } - if (tuple.negativePrefix != null) { - fmt.setNegativePrefix(tuple.negativePrefix); - } - if (tuple.negativeSuffix != null) { - fmt.setNegativeSuffix(tuple.negativeSuffix); - } - if (tuple.localizedPattern != null) { - fmt.applyLocalizedPattern(tuple.localizedPattern); - } - - // lenient parsing not supported by JDK - if (tuple.parseIntegerOnly != null) { - fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0); - } - if (tuple.decimalPatternMatchRequired != null) { - // Oops, not supported - } - if (tuple.parseNoExponent != null) { - // Oops, not supported for now - } - } - }; - @Test public void TestRoundingScientific10542() { DecimalFormat format = @@ -610,7 +220,7 @@ public class NumberFormatTest extends TestFmwk { DecimalFormatSymbols sym = new DecimalFormatSymbols(Locale.US); final String pat[] = { "#.#", "#.", ".#", "#" }; int pat_length = pat.length; - final String newpat[] = { "#0.#", "#0.", "#.0", "#" }; + final String newpat[] = { "0.#", "0.", "#.0", "0" }; final String num[] = { "0", "0.", ".0", "0" }; for (int i=0; i maximumIntegerDigits)" is true int[][] cases = { { -1, 0 }, { 0, 1 }, { 1, 0 }, { 2, 0 }, { 2, 1 }, { 10, 0 } }; - int[] expectedMax = { 0, 1, 1, 2, 2, 10 }; + int[] expectedMax = { 1, 1, 1, 2, 2, 10 }; if (cases.length != expectedMax.length) { errln("Can't continue test case method TestSetMinimumIntegerDigits " + "since the test case arrays are unequal."); } else { for (int i = 0; i < cases.length; i++) { - nf.setMaximumIntegerDigits(cases[i][1]); nf.setMinimumIntegerDigits(cases[i][0]); + nf.setMaximumIntegerDigits(cases[i][1]); if (nf.getMaximumIntegerDigits() != expectedMax[i]) { errln("NumberFormat.setMinimumIntegerDigits(int newValue " - + "did not return an expected result for parameter " + cases[i][1] + " and " + cases[i][0] + + "did not return an expected result for parameter " + cases[i][0] + " and " + cases[i][1] + " and expected " + expectedMax[i] + " but got " + nf.getMaximumIntegerDigits()); } } @@ -3875,6 +3578,10 @@ public class NumberFormatTest extends TestFmwk { } } + /* + * This feature had to do with a limitation in DigitList.java that no longer exists in the + * new implementation. + * @Test public void TestParseMaxDigits() { DecimalFormat fmt = new DecimalFormat(); @@ -3883,7 +3590,7 @@ public class NumberFormatTest extends TestFmwk { fmt.setParseMaxDigits(-1); - /* Default value is 1000 */ + // Default value is 1000 if (fmt.getParseMaxDigits() != 1000) { errln("Fail valid value checking in setParseMaxDigits."); } @@ -3902,6 +3609,7 @@ public class NumberFormatTest extends TestFmwk { } } + */ private static class FormatCharItrTestThread implements Runnable { private final NumberFormat fmt; @@ -4392,13 +4100,27 @@ public class NumberFormatTest extends TestFmwk { } } + @Test + public void TestCurrencyWithMinMaxFractionDigits() { + DecimalFormat df = new DecimalFormat(); + df.applyPattern("¤#,##0.00"); + df.setCurrency(Currency.getInstance("USD")); + assertEquals("Basic currency format fails", "$1.23", df.format(1.234)); + df.setMaximumFractionDigits(4); + assertEquals("Currency with max fraction == 4", "$1.234", df.format(1.234)); + df.setMinimumFractionDigits(4); + assertEquals("Currency with min fraction == 4", "$1.2340", df.format(1.234)); + } + @Test public void TestParseRequiredDecimalPoint() { String[] testPattern = { "00.####", "00.0", "00" }; String value2Parse = "99"; + String value2ParseWithDecimal = "99.9"; double parseValue = 99; + double parseValueWithDecimal = 99.9; DecimalFormat parser = new DecimalFormat(); double result; boolean hasDecimalPoint; @@ -4414,6 +4136,13 @@ public class NumberFormatTest extends TestFmwk { TestFmwk.errln("Parsing " + value2Parse + " should have succeeded with " + testPattern[i] + " and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired()); } + try { + result = parser.parse(value2ParseWithDecimal).doubleValue(); + assertEquals("wrong parsed value", parseValueWithDecimal, result); + } catch (ParseException e) { + TestFmwk.errln("Parsing " + value2ParseWithDecimal + " should have succeeded with " + testPattern[i] + + " and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired()); + } parser.setDecimalPatternMatchRequired(true); try { @@ -4425,31 +4154,23 @@ public class NumberFormatTest extends TestFmwk { } catch (ParseException e) { // OK, should fail } + try { + result = parser.parse(value2ParseWithDecimal).doubleValue(); + if(!hasDecimalPoint){ + TestFmwk.errln("Parsing " + value2ParseWithDecimal + " should NOT have succeeded with " + testPattern[i] + + " and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired()); + } + } catch (ParseException e) { + // OK, should fail + } } - } - - //TODO(junit): investigate - @Test - public void TestDataDrivenICU() { - DataDrivenNumberFormatTestUtility.runSuite( - "numberformattestspecification.txt", ICU); - } - - //TODO(junit): investigate - @Test - public void TestDataDrivenJDK() { - DataDrivenNumberFormatTestUtility.runSuite( - "numberformattestspecification.txt", JDK); - } - - @Test public void TestCurrFmtNegSameAsPositive() { DecimalFormatSymbols decfmtsym = DecimalFormatSymbols.getInstance(Locale.US); decfmtsym.setMinusSign('\u200B'); // ZERO WIDTH SPACE, in ICU4J cannot set to empty string - DecimalFormat decfmt = new DecimalFormat("\u00A4#,##0.00;\u00A4#,##0.00", decfmtsym); + DecimalFormat decfmt = new DecimalFormat("\u00A4#,##0.00;-\u00A4#,##0.00", decfmtsym); String currFmtResult = decfmt.format(-100.0); if (!currFmtResult.equals("\u200B$100.00")) { errln("decfmt.toPattern results wrong, expected \u200B$100.00, got " + currFmtResult); @@ -4458,7 +4179,7 @@ public class NumberFormatTest extends TestFmwk { @Test public void TestNumberFormatTestDataToString() { - new NumberFormatTestData().toString(); + new DataDrivenNumberFormatTestData().toString(); } // Testing for Issue 11805. @@ -4603,9 +4324,7 @@ public class NumberFormatTest extends TestFmwk { result.get(i).value); } } - // TODO: restore when #11914 is fixed. - // assertTrue("Comparing vector results for " + formattedOutput, - // expected.containsAll(result)); + assertTrue("Comparing vector results for " + formattedOutput, expected.containsAll(result)); } // Testing for Issue 11914, missing FieldPositions for some field types. @@ -4905,30 +4624,504 @@ public class NumberFormatTest extends TestFmwk { public void TestStringSymbols() { DecimalFormatSymbols symbols = new DecimalFormatSymbols(ULocale.US); + // Attempt digits with multiple code points. String[] customDigits = {"(0)", "(1)", "(2)", "(3)", "(4)", "(5)", "(6)", "(7)", "(8)", "(9)"}; symbols.setDigitStrings(customDigits); + 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)"); + + // 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)"); - DecimalFormat fmt = new DecimalFormat("#,##0.0#", symbols); - - expect2(fmt, 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 " + f + "; want 9.02"); @@ -1100,17 +1105,17 @@ public class NumberRegressionTests extends TestFmwk { * * JDK 1.1.6 Bug, did NOT occur in 1.1.5 * Possibly related to bug 4125885. - * + * * This class demonstrates a regression in version 1.1.6 * of DecimalFormat class. - * + * * 1.1.6 Results * Value 1.2 Format #.00 Result '01.20' !!!wrong * Value 1.2 Format 0.00 Result '001.20' !!!wrong * Value 1.2 Format 00.00 Result '0001.20' !!!wrong * Value 1.2 Format #0.0# Result '1.2' * Value 1.2 Format #0.00 Result '001.20' !!!wrong - * + * * 1.1.5 Results * Value 1.2 Format #.00 Result '1.20' * Value 1.2 Format 0.00 Result '1.20' @@ -1204,12 +1209,12 @@ public class NumberRegressionTests extends TestFmwk { String out = nf.format(pi); String pat = nf.toPattern(); double val = nf.parse(out).doubleValue(); - + nf.applyPattern(pat); String out2 = nf.format(pi); String pat2 = nf.toPattern(); double val2 = nf.parse(out2).doubleValue(); - + if (!pat.equals(pat2)) errln("Fail with \"" + PATS[i] + "\": Patterns should concur, \"" + pat + "\" vs. \"" + pat2 + "\""); @@ -1246,7 +1251,9 @@ public class NumberRegressionTests extends TestFmwk { logln("Applying pattern \"" + pattern + "\""); sdf.applyPattern(pattern); int minIntDig = sdf.getMinimumIntegerDigits(); - if (minIntDig != 0) { + // In ICU 58 and older, this case returned 0. + // Now it returns 1 instead, since the pattern parser enforces at least 1 min digit. + if (minIntDig != 1) { errln("Test failed"); errln(" Minimum integer digits : " + minIntDig); errln(" new pattern: " + sdf.toPattern()); @@ -1295,9 +1302,7 @@ public class NumberRegressionTests extends TestFmwk { e.printStackTrace(); } logln("The string " + s + " parsed as " + n); - if (n.doubleValue() != dbl) { - errln("Round trip failure"); - } + assertEquals("Round trip failure", dbl, n.doubleValue()); } /** @@ -1327,7 +1332,7 @@ public class NumberRegressionTests extends TestFmwk { @Test public void Test4167494() throws Exception { NumberFormat fmt = NumberFormat.getInstance(Locale.US); - + double a = Double.MAX_VALUE; String s = fmt.format(a); double b = fmt.parse(s).doubleValue(); @@ -1379,15 +1384,15 @@ public class NumberRegressionTests extends TestFmwk { @Test public void Test4176114() { String[] DATA = { - "00", "#00", - "000", "#000", // No grouping - "#000", "#000", // No grouping + "00", "00", + "000", "000", // No grouping + "#000", "000", // No grouping "#,##0", "#,##0", "#,000", "#,000", - "0,000", "#0,000", - "00,000", "#00,000", - "000,000", "#,000,000", - "0,000,000,000,000.0000", "#0,000,000,000,000.0000", // Reported + "0,000", "0,000", + "00,000", "00,000", + "000,000", "000,000", + "0,000,000,000,000.0000", "0,000,000,000,000.0000", // Reported }; for (int i=0; i max. - // Numberformat should catch this and throw an exception. - for (int i = 0; i < offsets.length; ++i) { - bytes[offsets[i]] = (byte)(4 - i); - } - - { - ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); - try { - NumberFormat format = (NumberFormat) ois.readObject(); - logln("format: " + format.format(1234.56)); //fix "The variable is never used" - errln("FAIL: Deserialized bogus NumberFormat with minXDigits > maxXDigits"); - } catch (InvalidObjectException e) { - logln("Ok: " + e.getMessage()); - } - } - - // Set values so they are too high, but min <= max - // Format should pass the min <= max test, and DecimalFormat should reset to current maximum - // (for compatibility with versions streamed out before the maximums were imposed). - for (int i = 0; i < offsets.length; ++i) { - bytes[offsets[i]] = 4; - } - - { - ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); - NumberFormat format = (NumberFormat) ois.readObject(); - //For compatibility with previous version - if ((format.getMaximumIntegerDigits() != 309) - || format.getMaximumFractionDigits() != 340) { - errln("FAIL: Deserialized bogus NumberFormat with values out of range," + - " intMin: " + format.getMinimumIntegerDigits() + - " intMax: " + format.getMaximumIntegerDigits() + - " fracMin: " + format.getMinimumFractionDigits() + - " fracMax: " + format.getMaximumFractionDigits()); - } else { - logln("Ok: Digit count out of range"); - } - } - } - - /** * Some DecimalFormatSymbols changes are not picked up by DecimalFormat. * This includes the minus sign, currency symbol, international currency @@ -1553,7 +1466,7 @@ public class NumberRegressionTests extends TestFmwk { fmt.getPositiveSuffix() + ", exp ^"); } sym.setPercent('%'); - + fmt.applyPattern("#\u2030"); sym.setPerMill('^'); fmt.setDecimalFormatSymbols(sym); @@ -1599,7 +1512,7 @@ public class NumberRegressionTests extends TestFmwk { System.out.println("\n Test skipped for release 2.2"); return; } - + // Since the pattern logic has changed, make sure that patterns round // trip properly. Test stream in/out integrity too. Locale[] avail = NumberFormat.getAvailableLocales(); @@ -1622,11 +1535,12 @@ public class NumberRegressionTests extends TestFmwk { break; } DecimalFormat df = (DecimalFormat) nf; - + // Test toPattern/applyPattern round trip String pat = df.toPattern(); DecimalFormatSymbols symb = new DecimalFormatSymbols(avail[i]); DecimalFormat f2 = new DecimalFormat(pat, symb); + f2.setCurrency(df.getCurrency()); // Currency does not travel with the pattern string if (!df.equals(f2)) { errln("FAIL: " + avail[i] + " #" + j + " -> \"" + pat + "\" -> \"" + f2.toPattern() + '"'); @@ -1636,24 +1550,32 @@ public class NumberRegressionTests extends TestFmwk { pat = df.toLocalizedPattern(); try{ f2.applyLocalizedPattern(pat); - + String s1 = f2.format(123456); String s2 = df.format(123456); if(!s1.equals(s2)){ errln("FAIL: " + avail[i] + " #" + j + " -> localized \"" + s2 + "\" -> \"" + s2 + '"'+ " in locale "+df.getLocale(ULocale.ACTUAL_LOCALE)); - + } - if (!df.equals(f2)) { - errln("FAIL: " + avail[i] + " #" + j + " -> localized \"" + pat + - "\" -> \"" + f2.toLocalizedPattern() + '"'+ " in locale "+df.getLocale(ULocale.ACTUAL_LOCALE)); - errln("s1: "+s1+" s2: "+s2); - } - + + // Equality of formatter objects is NOT guaranteed across toLocalizedPattern/applyLocalizedPattern. + // However, equality of relevant properties is guaranteed. + assertEquals("Localized FAIL on posPrefix", df.getPositivePrefix(), f2.getPositivePrefix()); + assertEquals("Localized FAIL on posSuffix", df.getPositiveSuffix(), f2.getPositiveSuffix()); + assertEquals("Localized FAIL on negPrefix", df.getNegativePrefix(), f2.getNegativePrefix()); + assertEquals("Localized FAIL on negSuffix", df.getNegativeSuffix(), f2.getNegativeSuffix()); + assertEquals("Localized FAIL on groupingSize", df.getGroupingSize(), f2.getGroupingSize()); + assertEquals("Localized FAIL on secondaryGroupingSize", df.getSecondaryGroupingSize(), f2.getSecondaryGroupingSize()); + assertEquals("Localized FAIL on minFrac", df.getMinimumFractionDigits(), f2.getMinimumFractionDigits()); + assertEquals("Localized FAIL on maxFrac", df.getMaximumFractionDigits(), f2.getMaximumFractionDigits()); + assertEquals("Localized FAIL on minInt", df.getMinimumIntegerDigits(), f2.getMinimumIntegerDigits()); + assertEquals("Localized FAIL on maxInt", df.getMaximumIntegerDigits(), f2.getMaximumIntegerDigits()); + }catch(IllegalArgumentException ex){ - errln(ex.getMessage()+" for locale "+ df.getLocale(ULocale.ACTUAL_LOCALE)); + throw new AssertionError("For locale " + avail[i], ex); } - + // Test writeObject/readObject round trip ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -1682,7 +1604,7 @@ public class NumberRegressionTests extends TestFmwk { sym = new DecimalFormatSymbols(Locale.US); for (int j=0; j 0 != DATA[i] > 0) { errln("\"" + str + "\" parse(x " + fmt.getMultiplier() + ") => " + n); @@ -1735,15 +1657,15 @@ public class NumberRegressionTests extends TestFmwk { new Double(1.006), "1.01", }; NumberFormat fmt = NumberFormat.getInstance(Locale.US); - fmt.setMaximumFractionDigits(2); + fmt.setMaximumFractionDigits(2); for (int i=0; i", curr, AffixPatternUtils.hasCurrencySymbols(input)); + assertEquals("Length on <" + input + ">", length, AffixPatternUtils.unescapedLength(input)); + + sb.clear(); + AffixPatternUtils.unescape(input, symbols, "$", "XXX", "long name", "−", sb); + assertEquals("Output on <" + input + ">", output, sb.toString()); + } + } + + @Test + public void testInvalid() { + String[] invalidExamples = {"'", "x'", "'x", "'x''", "''x'"}; + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("en_US")); + NumberStringBuilder sb = new NumberStringBuilder(); + + for (String str : invalidExamples) { + try { + AffixPatternUtils.hasCurrencySymbols(str); + fail("No exception was thrown on an invalid string"); + } catch (IllegalArgumentException e) { + // OK + } + try { + AffixPatternUtils.unescapedLength(str); + fail("No exception was thrown on an invalid string"); + } catch (IllegalArgumentException e) { + // OK + } + try { + AffixPatternUtils.unescape(str, symbols, "$", "XXX", "long name", "−", sb); + fail("No exception was thrown on an invalid string"); + } catch (IllegalArgumentException e) { + // OK + } + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/FormatQuantityTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/FormatQuantityTest.java new file mode 100644 index 0000000000..9b491ca80e --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/FormatQuantityTest.java @@ -0,0 +1,475 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.number; + +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 formats = new ArrayList(); + + 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 formats, int mode) { + if (mode == 2) { + assertEquals("Double is not valid", Double.toString(Double.parseDouble(str)), str); + } + + List qs = new ArrayList(); + 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 formats) { + for (Format format : formats) { + FormatQuantity q0 = rq0.clone(); + FormatQuantity q1 = rq1.clone(); + String s1 = format.format(q0); + String s2 = format.format(q1); + assertEquals("Different output from formatter (" + q0 + ", " + q1 + ")", s1, s2); + } + } + + private static void testFormatQuantityBehavior(FormatQuantity rq0, FormatQuantity rq1) { + FormatQuantity q0 = rq0.clone(); + FormatQuantity q1 = rq1.clone(); + + assertEquals("Different sign (" + q0 + ", " + q1 + ")", q0.isNegative(), q1.isNegative()); + + assertEquals( + "Different fingerprint (" + q0 + ", " + q1 + ")", + q0.getPositionFingerprint(), + q1.getPositionFingerprint()); + + assertEquals( + "Different upper range of digits (" + q0 + ", " + q1 + ")", + q0.getUpperDisplayMagnitude(), + q1.getUpperDisplayMagnitude()); + + assertDoubleEquals( + "Different double values (" + q0 + ", " + q1 + ")", q0.toDouble(), q1.toDouble()); + + assertBigDecimalEquals( + "Different BigDecimal values (" + q0 + ", " + q1 + ")", + q0.toBigDecimal(), + q1.toBigDecimal()); + + int equalityDigits = Math.min(q0.maxRepresentableDigits(), q1.maxRepresentableDigits()); + for (int m = q0.getUpperDisplayMagnitude(), i = 0; + m >= Math.min(q0.getLowerDisplayMagnitude(), q1.getLowerDisplayMagnitude()) + && i < equalityDigits; + m--, i++) { + assertEquals( + "Different digit at magnitude " + m + " (" + q0 + ", " + q1 + ")", + q0.getDigit(m), + q1.getDigit(m)); + } + + if (rq0 instanceof FormatQuantity4) { + String message = ((FormatQuantity4) rq0).checkHealth(); + if (message != null) errln(message); + } + if (rq1 instanceof FormatQuantity4) { + String message = ((FormatQuantity4) rq1).checkHealth(); + if (message != null) errln(message); + } + } + + @Test + public void testSwitchStorage() { + FormatQuantity4 fq = new FormatQuantity4(); + + fq.setToLong(1234123412341234L); + assertFalse("Should not be using byte array", fq.usingBytes()); + assertBigDecimalEquals("Failed on initialize", "1234123412341234", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + // Long -> Bytes + fq.appendDigit((byte) 5, 0, true); + assertTrue("Should be using byte array", fq.usingBytes()); + assertBigDecimalEquals("Failed on multiply", "12341234123412345", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + // Bytes -> Long + fq.roundToMagnitude(5, MATH_CONTEXT_HALF_EVEN); + assertFalse("Should not be using byte array", fq.usingBytes()); + assertBigDecimalEquals("Failed on round", "12341234123400000", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + } + + @Test + public void testAppend() { + FormatQuantity4 fq = new FormatQuantity4(); + fq.appendDigit((byte) 1, 0, true); + assertBigDecimalEquals("Failed on append", "1.", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + fq.appendDigit((byte) 2, 0, true); + assertBigDecimalEquals("Failed on append", "12.", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + fq.appendDigit((byte) 3, 1, true); + assertBigDecimalEquals("Failed on append", "1203.", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + fq.appendDigit((byte) 0, 1, true); + assertBigDecimalEquals("Failed on append", "120300.", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + fq.appendDigit((byte) 4, 0, true); + assertBigDecimalEquals("Failed on append", "1203004.", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + fq.appendDigit((byte) 0, 0, true); + assertBigDecimalEquals("Failed on append", "12030040.", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + fq.appendDigit((byte) 5, 0, false); + assertBigDecimalEquals("Failed on append", "12030040.5", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + fq.appendDigit((byte) 6, 0, false); + assertBigDecimalEquals("Failed on append", "12030040.56", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + fq.appendDigit((byte) 7, 3, false); + assertBigDecimalEquals("Failed on append", "12030040.560007", fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + StringBuilder expected = new StringBuilder("12030040.560007"); + for (int i = 0; i < 10; i++) { + fq.appendDigit((byte) 8, 0, false); + expected.append("8"); + assertBigDecimalEquals("Failed on append", expected.toString(), fq.toBigDecimal()); + assertNull("Failed health check", fq.checkHealth()); + } + } + + @Test + public void testConvertToAccurateDouble() { + // based on https://github.com/google/double-conversion/issues/28 + double[] hardDoubles = { + 1651087494906221570.0, + -5074790912492772E-327, + 83602530019752571E-327, + 2.207817077636718750000000000000, + 1.818351745605468750000000000000, + 3.941719055175781250000000000000, + 3.738609313964843750000000000000, + 3.967735290527343750000000000000, + 1.328025817871093750000000000000, + 3.920967102050781250000000000000, + 1.015235900878906250000000000000, + 1.335227966308593750000000000000, + 1.344520568847656250000000000000, + 2.879127502441406250000000000000, + 3.695838928222656250000000000000, + 1.845344543457031250000000000000, + 3.793952941894531250000000000000, + 3.211402893066406250000000000000, + 2.565971374511718750000000000000, + 0.965156555175781250000000000000, + 2.700004577636718750000000000000, + 0.767097473144531250000000000000, + 1.780448913574218750000000000000, + 2.624839782714843750000000000000, + 1.305290222167968750000000000000, + 3.834922790527343750000000000000, + }; + + double[] integerDoubles = { + 51423, + 51423e10, + 4.503599627370496E15, + 6.789512076111555E15, + 9.007199254740991E15, + 9.007199254740992E15 + }; + + for (double d : hardDoubles) { + checkDoubleBehavior(d, true, ""); + } + + for (double d : integerDoubles) { + checkDoubleBehavior(d, false, ""); + } + + assertEquals("NaN check failed", Double.NaN, new FormatQuantity4(Double.NaN).toDouble()); + assertEquals( + "Inf check failed", + Double.POSITIVE_INFINITY, + new FormatQuantity4(Double.POSITIVE_INFINITY).toDouble()); + assertEquals( + "-Inf check failed", + Double.NEGATIVE_INFINITY, + new FormatQuantity4(Double.NEGATIVE_INFINITY).toDouble()); + + // Generate random doubles + String alert = "UNEXPECTED FAILURE: PLEASE REPORT THIS MESSAGE TO THE ICU TEAM: "; + for (int i = 0; i < 1000000; i++) { + double d = Double.longBitsToDouble(ThreadLocalRandom.current().nextLong()); + if (Double.isNaN(d) || Double.isInfinite(d)) continue; + checkDoubleBehavior(d, false, alert); + } + } + + private static void checkDoubleBehavior(double d, boolean explicitRequired, String alert) { + FormatQuantity4 fq = new FormatQuantity4(d); + if (explicitRequired) + assertTrue(alert + "Should be using approximate double", !fq.explicitExactDouble); + assertEquals(alert + "Initial construction from hard double", d, fq.toDouble()); + fq.roundToInfinity(); + if (explicitRequired) + assertTrue(alert + "Should not be using approximate double", fq.explicitExactDouble); + assertDoubleEquals(alert + "After conversion to exact BCD (double)", d, fq.toDouble()); + assertBigDecimalEquals( + alert + "After conversion to exact BCD (BigDecimal)", + new BigDecimal(Double.toString(d)), + fq.toBigDecimal()); + } + + @Test + public void testUseApproximateDoubleWhenAble() { + Object[][] cases = { + {1.2345678, 1, MATH_CONTEXT_HALF_EVEN, false}, + {1.2345678, 7, MATH_CONTEXT_HALF_EVEN, false}, + {1.2345678, 12, MATH_CONTEXT_HALF_EVEN, false}, + {1.2345678, 13, MATH_CONTEXT_HALF_EVEN, true}, + {1.235, 1, MATH_CONTEXT_HALF_EVEN, false}, + {1.235, 2, MATH_CONTEXT_HALF_EVEN, true}, + {1.235, 3, MATH_CONTEXT_HALF_EVEN, false}, + {1.000000000000001, 0, MATH_CONTEXT_HALF_EVEN, false}, + {1.000000000000001, 0, MATH_CONTEXT_CEILING, true}, + {1.235, 1, MATH_CONTEXT_CEILING, false}, + {1.235, 2, MATH_CONTEXT_CEILING, false}, + {1.235, 3, MATH_CONTEXT_CEILING, true} + }; + + for (Object[] cas : cases) { + double d = (Double) cas[0]; + int maxFrac = (Integer) cas[1]; + MathContext mc = (MathContext) cas[2]; + boolean usesExact = (Boolean) cas[3]; + + FormatQuantity4 fq = new FormatQuantity4(d); + assertTrue("Should be using approximate double", !fq.explicitExactDouble); + fq.roundToMagnitude(-maxFrac, mc); + assertEquals( + "Using approximate double after rounding: " + d + " maxFrac=" + maxFrac + " " + mc, + usesExact, + fq.explicitExactDouble); + } + } + + static void assertDoubleEquals(String message, double d1, double d2) { + boolean equal = (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6); + handleAssert(equal, message, d1, d2, null, false); + } + + static void assertBigDecimalEquals(String message, String d1, BigDecimal d2) { + assertBigDecimalEquals(message, new BigDecimal(d1), d2); + } + + static void assertBigDecimalEquals(String message, BigDecimal d1, BigDecimal d2) { + boolean equal = d1.compareTo(d2) == 0; + handleAssert(equal, message, d1, d2, null, false); + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java new file mode 100644 index 0000000000..2482865e9b --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java @@ -0,0 +1,171 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.number; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.text.FieldPosition; +import java.text.Format.Field; + +import org.junit.Test; + +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.text.NumberFormat; + +/** @author sffc */ +public class NumberStringBuilderTest { + private static final String[] EXAMPLE_STRINGS = { + "", + "xyz", + "The quick brown fox jumps over the lazy dog", + "😁", + "mixed 😇 and ASCII", + "with combining characters like 🇦🇧🇨🇩" + }; + + @Test + public void testInsertAppendCharSequence() { + + StringBuilder sb1 = new StringBuilder(); + NumberStringBuilder sb2 = new NumberStringBuilder(); + for (String str : EXAMPLE_STRINGS) { + NumberStringBuilder sb3 = new NumberStringBuilder(); + sb1.append(str); + sb2.append(str, null); + sb3.append(str, null); + assertCharSequenceEquals(sb1, sb2); + assertCharSequenceEquals(sb3, str); + + StringBuilder sb4 = new StringBuilder(); + NumberStringBuilder sb5 = new NumberStringBuilder(); + sb4.append("😇"); + sb4.append(str); + sb4.append("xx"); + sb5.append("😇xx", null); + sb5.insert(2, str, null); + assertCharSequenceEquals(sb4, sb5); + + int start = Math.min(1, str.length()); + int end = Math.min(10, str.length()); + sb4.insert(3, str, start, end); + sb5.insert(3, str, start, end, null); + assertCharSequenceEquals(sb4, sb5); + + sb4.append(str.toCharArray()); + sb5.append(str.toCharArray(), null); + assertCharSequenceEquals(sb4, sb5); + + sb4.insert(4, str.toCharArray()); + sb5.insert(4, str.toCharArray(), null); + assertCharSequenceEquals(sb4, sb5); + } + } + + @Test + public void testInsertAppendCodePoint() { + int[] cases = {0, 1, 60, 127, 128, 0x7fff, 0x8000, 0xffff, 0x10000, 0x1f000, 0x10ffff}; + + StringBuilder sb1 = new StringBuilder(); + NumberStringBuilder sb2 = new NumberStringBuilder(); + for (int cas : cases) { + NumberStringBuilder sb3 = new NumberStringBuilder(); + sb1.appendCodePoint(cas); + sb2.appendCodePoint(cas, null); + sb3.appendCodePoint(cas, null); + assertCharSequenceEquals(sb1, sb2); + assertEquals(Character.codePointAt(sb3, 0), cas); + + StringBuilder sb4 = new StringBuilder(); + NumberStringBuilder sb5 = new NumberStringBuilder(); + sb4.append("😇"); + sb4.appendCodePoint(cas); // Java StringBuilder has no insertCodePoint() + sb4.append("xx"); + sb5.append("😇xx", null); + sb5.insertCodePoint(2, cas, null); + assertCharSequenceEquals(sb4, sb5); + } + } + + @Test + public void testFields() { + for (String str : EXAMPLE_STRINGS) { + NumberStringBuilder sb = new NumberStringBuilder(); + sb.append(str, null); + sb.append(str, NumberFormat.Field.CURRENCY); + Field[] fields = sb.toFieldArray(); + assertEquals(str.length() * 2, fields.length); + for (int i = 0; i < str.length(); i++) { + assertEquals(null, fields[i]); + assertEquals(NumberFormat.Field.CURRENCY, fields[i + str.length()]); + } + + // Very basic FieldPosition test. More robust tests happen in NumberFormatTest. + // Let NumberFormatTest also take care of AttributedCharacterIterator material. + FieldPosition fp = new FieldPosition(NumberFormat.Field.CURRENCY); + sb.populateFieldPosition(fp, 0); + assertEquals(str.length(), fp.getBeginIndex()); + assertEquals(str.length() * 2, fp.getEndIndex()); + + if (str.length() > 0) { + sb.insertCodePoint(2, 100, NumberFormat.Field.INTEGER); + fields = sb.toFieldArray(); + assertEquals(str.length() * 2 + 1, fields.length); + assertEquals(fields[2], NumberFormat.Field.INTEGER); + } + + sb.append(sb.clone()); + sb.append(sb.toCharArray(), sb.toFieldArray()); + int numNull = 0; + int numCurr = 0; + int numInt = 0; + Field[] oldFields = fields; + fields = sb.toFieldArray(); + for (int i = 0; i < sb.length(); i++) { + assertEquals(oldFields[i % oldFields.length], fields[i]); + if (fields[i] == null) numNull++; + else if (fields[i] == NumberFormat.Field.CURRENCY) numCurr++; + else if (fields[i] == NumberFormat.Field.INTEGER) numInt++; + else throw new AssertionError("Encountered unknown field in " + str); + } + assertEquals(str.length() * 4, numNull); + assertEquals(numNull, numCurr); + assertEquals(str.length() > 0 ? 4 : 0, numInt); + + NumberStringBuilder sb2 = new NumberStringBuilder(); + sb2.append(sb); + assertTrue(sb.contentEquals(sb2)); + assertTrue(sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray())); + + sb2.insertCodePoint(0, 50, NumberFormat.Field.FRACTION); + assertTrue(!sb.contentEquals(sb2)); + assertTrue(!sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray())); + } + } + + @Test + public void testUnlimitedCapacity() { + NumberStringBuilder builder = new NumberStringBuilder(); + // The builder should never fail upon repeated appends. + for (int i = 0; i < 1000; i++) { + assertEquals(builder.length(), i); + builder.appendCodePoint('x', null); + assertEquals(builder.length(), i + 1); + } + } + + private static void assertCharSequenceEquals(CharSequence a, CharSequence b) { + assertEquals(a.toString(), b.toString()); + + assertEquals(a.length(), b.length()); + for (int i = 0; i < a.length(); i++) { + assertEquals(a.charAt(i), b.charAt(i)); + } + + int start = Math.min(2, a.length()); + int end = Math.min(12, a.length()); + if (start != end) { + assertCharSequenceEquals(a.subSequence(start, end), b.subSequence(start, end)); + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java new file mode 100644 index 0000000000..5c4f770b2c --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java @@ -0,0 +1,110 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.number; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.util.ULocale; + +/** @author sffc */ +public class PatternStringTest { + + @Test + public void testLocalized() { + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH); + symbols.setDecimalSeparatorString("a"); + symbols.setPercentString("b"); + symbols.setMinusSignString("."); + symbols.setPlusSignString("'"); + + String standard = "+-abcb''a''#,##0.0%'a%'"; + String localized = "’.'ab'c'b''a'''#,##0a0b'a%'"; + String toStandard = "+-'ab'c'b''a'''#,##0.0%'a%'"; + + assertEquals(localized, PatternString.convertLocalized(standard, symbols, true)); + assertEquals(toStandard, PatternString.convertLocalized(localized, symbols, false)); + } + + @Test + public void testToPatternSimple() { + String[][] cases = { + {"#", "0"}, + {"0", "0"}, + {"#0", "0"}, + {"###", "0"}, + {"0.##", "0.##"}, + {"0.00", "0.00"}, + {"0.00#", "0.00#"}, + {"#E0", "#E0"}, + {"0E0", "0E0"}, + {"#00E00", "#00E00"}, + {"#,##0", "#,##0"}, + {"#,##0E0", "#,##0E0"}, + {"#;#", "0;0"}, + {"#;-#", "0"}, // ignore a negative prefix pattern of '-' since that is the default + {"**##0", "**##0"}, + {"*'x'##0", "*x##0"}, + {"a''b0", "a''b0"}, + {"*''##0", "*''##0"}, + {"*📺##0", "*'📺'##0"}, + {"*'நி'##0", "*'நி'##0"}, + }; + + for (String[] cas : cases) { + String input = cas[0]; + String output = cas[1]; + + Properties properties = PatternString.parseToProperties(input); + String actual = PatternString.propertiesToString(properties); + assertEquals( + "Failed on input pattern '" + input + "', properties " + properties, output, actual); + } + } + + @Test + public void testToPatternWithProperties() { + Object[][] cases = { + {new Properties().setPositivePrefix("abc"), "abc#"}, + {new Properties().setPositiveSuffix("abc"), "#abc"}, + {new Properties().setPositivePrefixPattern("abc"), "abc#"}, + {new Properties().setPositiveSuffixPattern("abc"), "#abc"}, + {new Properties().setNegativePrefix("abc"), "#;abc#"}, + {new Properties().setNegativeSuffix("abc"), "#;#abc"}, + {new Properties().setNegativePrefixPattern("abc"), "#;abc#"}, + {new Properties().setNegativeSuffixPattern("abc"), "#;#abc"}, + {new Properties().setPositivePrefix("+"), "'+'#"}, + {new Properties().setPositivePrefixPattern("+"), "+#"}, + {new Properties().setPositivePrefix("+'"), "'+'''#"}, + {new Properties().setPositivePrefix("'+"), "'''+'#"}, + {new Properties().setPositivePrefix("'"), "''#"}, + {new Properties().setPositivePrefixPattern("+''"), "+''#"}, + }; + + for (Object[] cas : cases) { + Properties input = (Properties) cas[0]; + String output = (String) cas[1]; + + String actual = PatternString.propertiesToString(input); + assertEquals("Failed on input properties " + input, output, actual); + } + } + + @Test + public void testExceptionOnInvalid() { + String[] invalidPatterns = {"#.#.#", "0#", "0#.", ".#0", "0#.#0", "@0", "0@"}; + + for (String pattern : invalidPatterns) { + try { + PatternString.parseToProperties(pattern); + fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern); + } catch (IllegalArgumentException e) { + } + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PropertiesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PropertiesTest.java new file mode 100644 index 0000000000..7fbc00573c --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PropertiesTest.java @@ -0,0 +1,331 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.number; + +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 hashCodes = new HashSet(); + Field[] fields = Properties.class.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + + // Check for getters and setters + String fieldNamePascalCase = + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1); + String getterName = "get" + fieldNamePascalCase; + String setterName = "set" + fieldNamePascalCase; + Method getter, setter; + try { + getter = Properties.class.getMethod(getterName); + assertEquals( + "Getter does not return correct type", field.getType(), getter.getReturnType()); + } catch (NoSuchMethodException e) { + fail("Could not find method " + getterName + " for field " + field); + continue; + } catch (SecurityException e) { + fail("Could not access method " + getterName + " for field " + field); + continue; + } + try { + setter = Properties.class.getMethod(setterName, field.getType()); + assertEquals( + "Method " + setterName + " does not return correct type", + Properties.class, + setter.getReturnType()); + } catch (NoSuchMethodException e) { + fail("Could not find method " + setterName + " for field " + field); + continue; + } catch (SecurityException e) { + fail("Could not access method " + setterName + " for field " + field); + continue; + } + + // Check for parameter name equality. + // The parameter name is not always available, depending on compiler settings. + Parameter param = setter.getParameters()[0]; + if (!param.getName().subSequence(0, 3).equals("arg")) { + assertEquals("Parameter name should equal field name", field.getName(), param.getName()); + } + + try { + // Check for default value (should be null for objects) + if (field.getType() != Integer.TYPE && field.getType() != Boolean.TYPE) { + Object default0 = getter.invoke(p0); + assertEquals("Field " + field + " has non-null default value:", null, default0); + } + + // Check for getter, equals, and hash code behavior + Object val0 = getSampleValueForType(field.getType(), 0); + Object val1 = getSampleValueForType(field.getType(), 1); + Object val2 = getSampleValueForType(field.getType(), 2); + assertNotEquals(val0, val1); + setter.invoke(p1, val0); + setter.invoke(p2, val0); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + assertEquals(getter.invoke(p1), getter.invoke(p2)); + assertEquals(getter.invoke(p1), val0); + assertNotEquals(getter.invoke(p1), val1); + hashCodes.add(p1.hashCode()); + setter.invoke(p1, val1); + assertNotEquals("Field " + field + " is missing from equals()", p1, p2); + assertNotEquals(getter.invoke(p1), getter.invoke(p2)); + assertNotEquals(getter.invoke(p1), val0); + assertEquals(getter.invoke(p1), val1); + setter.invoke(p1, val0); + assertEquals("Field " + field + " setter might have side effects", p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + assertEquals(getter.invoke(p1), getter.invoke(p2)); + setter.invoke(p1, val1); + setter.invoke(p2, val1); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + assertEquals(getter.invoke(p1), getter.invoke(p2)); + setter.invoke(p1, val2); + setter.invoke(p1, val1); + assertEquals("Field " + field + " setter might have side effects", p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + assertEquals(getter.invoke(p1), getter.invoke(p2)); + hashCodes.add(p1.hashCode()); + + // Check for clone behavior + Properties copy = p1.clone(); + assertEquals("Field " + field + " did not get copied in clone", p1, copy); + assertEquals(p1.hashCode(), copy.hashCode()); + assertEquals(getter.invoke(p1), getter.invoke(copy)); + + // Check for copyFrom behavior + setter.invoke(p1, val0); + assertNotEquals(p1, p2); + assertNotEquals(getter.invoke(p1), getter.invoke(p2)); + p2.copyFrom(p1); + assertEquals("Field " + field + " is missing from copyFrom()", p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + assertEquals(getter.invoke(p1), getter.invoke(p2)); + + // Load values into p3 and p4 for clear() behavior test + setter.invoke(p3, getSampleValueForType(field.getType(), 3)); + hashCodes.add(p3.hashCode()); + setter.invoke(p4, getSampleValueForType(field.getType(), 4)); + hashCodes.add(p4.hashCode()); + } catch (IllegalAccessException e) { + fail("Could not access method for field " + field); + } catch (IllegalArgumentException e) { + fail("Could call method for field " + field); + } catch (InvocationTargetException e) { + fail("Could invoke method on target for field " + field); + } + } + + // Check for clear() behavior + assertNotEquals(p3, p4); + p3.clear(); + p4.clear(); + assertEquals("A field is missing from the clear() function", p3, p4); + + // A good hashCode() implementation should produce very few collisions. We added at most + // 4*fields.length codes to the set. We'll say the implementation is good if we had at least + // fields.length unique values. + // TODO: Should the requirement be stronger than this? + assertTrue( + "Too many hash code collisions: " + hashCodes.size() + " out of " + (fields.length * 4), + hashCodes.size() >= fields.length); + } + + /** + * Creates a valid sample instance of the given type. Used to simulate getters and setters. + * + * @param type The type to generate. + * @param seed An integer seed, guaranteed to be positive. The same seed should generate two + * instances that are equal. A different seed should in general generate two instances that + * are not equal; this might not always be possible, such as with booleans or enums where + * there are limited possible values. + * @return An instance of the specified type. + */ + Object getSampleValueForType(Class type, int seed) { + if (type == Integer.TYPE) { + return seed * 1000001; + + } else if (type == Boolean.TYPE) { + return (seed % 2) == 0; + + } else if (type == BigDecimal.class) { + if (seed == 0) return null; + return new BigDecimal(seed * 1000002); + + } else if (type == String.class) { + if (seed == 0) return null; + return BigInteger.valueOf(seed * 1000003).toString(32); + + } else if (type == CompactStyle.class) { + if (seed == 0) return null; + CompactStyle[] values = CompactStyle.values(); + return values[seed % values.length]; + + } else if (type == Currency.class) { + if (seed == 0) return null; + Object[] currencies = Currency.getAvailableCurrencies().toArray(); + return currencies[seed % currencies.length]; + + } else if (type == CurrencyPluralInfo.class) { + if (seed == 0) return null; + ULocale[] locales = ULocale.getAvailableLocales(); + return CurrencyPluralInfo.getInstance(locales[seed % locales.length]); + + } else if (type == CurrencyStyle.class) { + if (seed == 0) return null; + CurrencyStyle[] values = CurrencyStyle.values(); + return values[seed % values.length]; + + } else if (type == CurrencyUsage.class) { + if (seed == 0) return null; + CurrencyUsage[] values = CurrencyUsage.values(); + return values[seed % values.length]; + + } else if (type == FormatWidth.class) { + if (seed == 0) return null; + FormatWidth[] values = FormatWidth.values(); + return values[seed % values.length]; + + } else if (type == MathContext.class) { + if (seed == 0) return null; + RoundingMode[] modes = RoundingMode.values(); + return new MathContext(seed, modes[seed % modes.length]); + + } else if (type == MeasureUnit.class) { + if (seed == 0) return null; + Object[] units = MeasureUnit.getAvailable().toArray(); + return units[seed % units.length]; + + } else if (type == PadPosition.class) { + if (seed == 0) return null; + PadPosition[] values = PadPosition.values(); + return values[seed % values.length]; + + } else if (type == ParseMode.class) { + if (seed == 0) return null; + ParseMode[] values = ParseMode.values(); + return values[seed % values.length]; + + } else if (type == RoundingMode.class) { + if (seed == 0) return null; + RoundingMode[] values = RoundingMode.values(); + return values[seed % values.length]; + + } else if (type == SignificantDigitsMode.class) { + if (seed == 0) return null; + SignificantDigitsMode[] values = SignificantDigitsMode.values(); + return values[seed % values.length]; + + } else { + fail("Don't know how to handle type " + type + ". Please add it to getSampleValueForType()."); + return null; + } + } + + @Test + public void TestBasicSerializationRoundTrip() throws IOException, ClassNotFoundException { + Properties props0 = new Properties(); + + // Write values to some of the fields + PatternString.parseToExistingProperties("A-**####,#00.00#b¤", props0); + + // Write to byte stream + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(props0); + oos.flush(); + baos.close(); + byte[] bytes = baos.toByteArray(); + + // Read from byte stream + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + Object obj = ois.readObject(); + ois.close(); + Properties props1 = (Properties) obj; + + // Test equality + assertEquals("Did not round-trip through serialization", props0, props1); + } + + /** Handler for serialization compatibility test suite. */ + public static class PropertiesHandler implements SerializableTestUtility.Handler { + + @Override + public Object[] getTestObjects() { + return new Object[] { + new Properties(), + PatternString.parseToProperties("x#,##0.00%"), + new Properties().setCompactStyle(CompactStyle.LONG).setMinimumExponentDigits(2) + }; + } + + @Override + public boolean hasSameBehavior(Object a, Object b) { + return a.equals(b); + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/RounderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/RounderTest.java new file mode 100644 index 0000000000..19d5f53773 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/RounderTest.java @@ -0,0 +1,134 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.number; + +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(); + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ShanesDataDrivenTestUtility.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ShanesDataDrivenTestUtility.java new file mode 100644 index 0000000000..c286d5fdf7 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ShanesDataDrivenTestUtility.java @@ -0,0 +1,345 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.number; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.ParseException; +import java.text.ParsePosition; + +import com.ibm.icu.dev.test.format.DataDrivenNumberFormatTestData; +import com.ibm.icu.dev.test.format.DataDrivenNumberFormatTestUtility; +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 ShanesDataDrivenTestUtility extends CodeUnderTest { + static final String dataPath = + "../../../icu4j-core-tests/src/com/ibm/icu/dev/data/numberformattestspecification.txt"; + + public static void run() { + CodeUnderTest tester = new ShanesDataDrivenTestUtility(); + 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); + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java index f79d4fceaa..ea291facc0 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java @@ -1106,7 +1106,14 @@ public class FormatHandler NumberFormat format_b = (NumberFormat) b; double number = 1234.56; - return format_a.format(number).equals(format_b.format(number)); + String result_a = format_a.format(number); + String result_b = format_b.format(number); + boolean equal = result_a.equals(result_b); + if (!equal) { + System.out.println(format_a+" "+format_b); + System.out.println(result_a+" "+result_b); + } + return equal; } } @@ -1710,7 +1717,17 @@ public class FormatHandler char chars_a[] = getCharSymbols(dfs_a); char chars_b[] = getCharSymbols(dfs_b); - return SerializableTestUtility.compareStrings(strings_a, strings_b) && SerializableTestUtility.compareChars(chars_a, chars_b); + // Spot-check char-to-string conversion (ICU 58) + String percent_a1 = Character.toString(dfs_a.getPercent()); + String percent_a2 = dfs_a.getPercentString(); + String percent_b1 = Character.toString(dfs_b.getPercent()); + String percent_b2 = dfs_b.getPercentString(); + + return SerializableTestUtility.compareStrings(strings_a, strings_b) + && SerializableTestUtility.compareChars(chars_a, chars_b) + && percent_a1.equals(percent_b1) + && percent_a2.equals(percent_b2) + && percent_a1.equals(percent_a2); } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java index 99660b0c05..a813e2af43 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java @@ -30,6 +30,7 @@ import java.util.Locale; import com.ibm.icu.dev.test.format.MeasureUnitTest; import com.ibm.icu.dev.test.format.PluralRulesTest; +import com.ibm.icu.dev.test.number.PropertiesTest; import com.ibm.icu.impl.JavaTimeZone; import com.ibm.icu.impl.OlsonTimeZone; import com.ibm.icu.impl.TimeZoneAdapter; @@ -827,6 +828,7 @@ public class SerializableTestUtility { map.put("com.ibm.icu.util.MeasureUnit", new MeasureUnitTest.MeasureUnitHandler()); map.put("com.ibm.icu.util.TimeUnit", new MeasureUnitTest.MeasureUnitHandler()); map.put("com.ibm.icu.text.MeasureFormat", new MeasureUnitTest.MeasureFormatHandler()); + map.put("com.ibm.icu.impl.number.Properties", new PropertiesTest.PropertiesHandler()); map.put("com.ibm.icu.util.ICUException", new ICUExceptionHandler()); map.put("com.ibm.icu.util.ICUUncheckedIOException", new ICUUncheckedIOExceptionHandler()); @@ -925,6 +927,11 @@ public class SerializableTestUtility { return; } + if (className.equals("com.ibm.icu.text.DecimalFormat_ICU58")) { + // Do not test the legacy DecimalFormat class in ICU 59 + return; + } + if (c.isEnum() || !serializable.isAssignableFrom(c)) { //System.out.println("@@@ Skipping: " + className); return; diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TextTrieMapTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TextTrieMapTest.java index 39e5a9d28e..d95812a66b 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TextTrieMapTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TextTrieMapTest.java @@ -8,6 +8,7 @@ */ package com.ibm.icu.dev.test.util; +import java.util.Arrays; import java.util.Iterator; import org.junit.Test; @@ -25,9 +26,14 @@ public class TextTrieMapTest extends TestFmwk { private static final Integer FRI = new Integer(6); private static final Integer SAT = new Integer(7); + private static final Integer SUP1 = new Integer(8); + private static final Integer SUP2 = new Integer(9); + private static final Integer SUP3 = new Integer(10); + private static final Integer SUP4 = new Integer(11); + private static final Integer FOO = new Integer(-1); private static final Integer BAR = new Integer(-2); - + private static final Object[][] TESTDATA = { {"Sunday", SUN}, {"Monday", MON}, @@ -49,7 +55,11 @@ public class TextTrieMapTest extends TestFmwk { {"W", WED}, {"T", THU}, {"F", FRI}, - {"S", SAT} + {"S", SAT}, + {"L📺", SUP1}, // L, 0xD83D, 0xDCFA + {"L📺1", SUP2}, // L, 0xD83D, 0xDCFA, 1 + {"L📻", SUP3}, // L, 0xD83D, 0xDCFB + {"L🃏", SUP4}, // L, 0xD83C, 0xDCCF }; private static final Object[][] TESTCASES = { @@ -62,7 +72,70 @@ public class TextTrieMapTest extends TestFmwk { {"TEST", new Object[]{TUE, THU}, new Object[]{TUE, THU}}, {"SUN", new Object[]{SUN, SAT}, SUN}, {"super", null, SUN}, - {"NO", null, null} + {"NO", null, null}, + {"L📺", SUP1, SUP1}, + {"l📺", null, SUP1}, + }; + + private static final Object[][] TESTCASES_PARSE = { + { + "Sunday", + new Object[]{ + new Object[]{SAT,SUN}, new Object[]{SAT,SUN}, // matches on "S" + null, null, // matches on "Su" + SUN, SUN, // matches on "Sun" + null, null, // matches on "Sund" + null, null, // matches on "Sunda" + SUN, SUN, // matches on "Sunday" + } + }, + { + "sunday", + new Object[]{ + null, new Object[]{SAT,SUN}, // matches on "s" + null, null, // matches on "su" + null, SUN, // matches on "sun" + null, null, // matches on "sund" + null, null, // matches on "sunda" + null, SUN, // matches on "sunday" + } + }, + { + "MMM", + new Object[]{ + MON, MON, // matches on "M" + // no more matches in data + } + }, + { + "BBB", + new Object[]{ + // no matches in data + } + }, + { + "l📺12", + new Object[]{ + null, null, // matches on "L" + null, SUP1, // matches on "L📺" + null, SUP2, // matches on "L📺1" + // no more matches in data + } + }, + { + "L📻", + new Object[] { + null, null, // matches on "L" + SUP3, SUP3, // matches on "L📻" + } + }, + { + "L🃏", + new Object[] { + null, null, // matches on "L" + SUP4, SUP4, // matches on "L🃏" + } + } }; @Test @@ -76,7 +149,7 @@ public class TextTrieMapTest extends TestFmwk { logln("Test for get(String)"); for (int i = 0; i < TESTCASES.length; i++) { itr = map.get((String)TESTCASES[i][0]); - checkResult(itr, TESTCASES[i][1]); + checkResult("get(String) case " + i, itr, TESTCASES[i][1]); } logln("Test for get(String, int)"); @@ -88,7 +161,14 @@ public class TextTrieMapTest extends TestFmwk { } textBuf.append(TESTCASES[i][0]); itr = map.get(textBuf.toString(), i); - checkResult(itr, TESTCASES[i][1]); + checkResult("get(String, int) case " + i, itr, TESTCASES[i][1]); + } + + logln("Test for ParseState"); + for (int i = 0; i < TESTCASES_PARSE.length; i++) { + String test = (String) TESTCASES_PARSE[i][0]; + Object[] expecteds = (Object[]) TESTCASES_PARSE[i][1]; + checkParse(map, test, expecteds, true); } // Add duplicated entry @@ -98,7 +178,7 @@ public class TextTrieMapTest extends TestFmwk { // Make sure the all entries are returned itr = map.get("Sunday"); - checkResult(itr, new Object[]{FOO, SUN}); + checkResult("Get Sunday", itr, new Object[]{FOO, SUN}); } @Test @@ -112,9 +192,9 @@ public class TextTrieMapTest extends TestFmwk { logln("Test for get(String)"); for (int i = 0; i < TESTCASES.length; i++) { itr = map.get((String)TESTCASES[i][0]); - checkResult(itr, TESTCASES[i][2]); + checkResult("get(String) case " + i, itr, TESTCASES[i][2]); } - + logln("Test for get(String, int)"); StringBuffer textBuf = new StringBuffer(); for (int i = 0; i < TESTCASES.length; i++) { @@ -124,7 +204,14 @@ public class TextTrieMapTest extends TestFmwk { } textBuf.append(TESTCASES[i][0]); itr = map.get(textBuf.toString(), i); - checkResult(itr, TESTCASES[i][2]); + checkResult("get(String, int) case " + i, itr, TESTCASES[i][2]); + } + + logln("Test for ParseState"); + for (int i = 0; i < TESTCASES_PARSE.length; i++) { + String test = (String) TESTCASES_PARSE[i][0]; + Object[] expecteds = (Object[]) TESTCASES_PARSE[i][1]; + checkParse(map, test, expecteds, false); } // Add duplicated entry @@ -134,7 +221,55 @@ public class TextTrieMapTest extends TestFmwk { // Make sure the all entries are returned itr = map.get("Sunday"); - checkResult(itr, new Object[]{SUN, FOO, BAR}); + checkResult("Get Sunday", itr, new Object[]{SUN, FOO, BAR}); + } + + private void checkParse(TextTrieMap map, String text, Object[] rawExpecteds, boolean caseSensitive) { + // rawExpecteds has even-valued indices for case sensitive and odd-valued indicies for case insensitive + // Get out only the values that we want. + Object[] expecteds = null; + for (int i=rawExpecteds.length/2-1; i>=0; i--) { + int j = i*2+(caseSensitive?0:1); + if (rawExpecteds[j] != null) { + if (expecteds == null) { + expecteds = new Object[i+1]; + } + expecteds[i] = rawExpecteds[j]; + } + } + if (expecteds == null) { + expecteds = new Object[0]; + } + + TextTrieMap.ParseState state = null; + for (int charOffset=0, cpOffset=0; charOffset < text.length(); cpOffset++) { + int cp = Character.codePointAt(text, charOffset); + if (state == null) { + state = map.openParseState(cp); + } + if (state == null) { + assertEquals("Expected matches, but no matches are available", 0, expecteds.length); + break; + } + state.accept(cp); + if (cpOffset < expecteds.length - 1) { + assertFalse( + "In middle of parse sequence, but atEnd() is true: '" + text + "' offset " + charOffset, + state.atEnd()); + } else if (cpOffset == expecteds.length) { + // Note: it possible for atEnd() to be either true or false at expecteds.length - 1; + // if true, we are at the end of the input string; if false, there is still input string + // left to be consumed, but we don't know if there are remaining matches. + assertTrue( + "At end of parse sequence, but atEnd() is false: '" + text + "' offset " + charOffset, + state.atEnd()); + break; + } + Object expected = expecteds[cpOffset]; + Iterator actual = state.getCurrentMatches(); + checkResult("ParseState '" + text + "' offset " + charOffset, actual, expected); + charOffset += Character.charCount(cp); + } } private boolean eql(Object o1, Object o2) { @@ -147,10 +282,13 @@ public class TextTrieMapTest extends TestFmwk { return o1.equals(o2); } - private void checkResult(Iterator itr, Object expected) { + private void checkResult(String memo, Iterator itr, Object expected) { if (itr == null) { if (expected != null) { - errln("FAIL: Empty results - Expected: " + expected); + String expectedStr = (expected instanceof Object[]) + ? Arrays.toString((Object[]) expected) + : expected.toString(); + errln("FAIL: Empty results: " + memo + ": Expected: " + expectedStr); } return; } diff --git a/icu4j/main/tests/translit/translit-tests-build.launch b/icu4j/main/tests/translit/translit-tests-build.launch index 3bc7e4db1f..d3a3019d93 100644 --- a/icu4j/main/tests/translit/translit-tests-build.launch +++ b/icu4j/main/tests/translit/translit-tests-build.launch @@ -2,17 +2,17 @@ - + - + - +