From 7849d4bd064214c52df989f68f791f381cfe601e Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Fri, 17 Mar 2017 23:54:23 +0000 Subject: [PATCH] ICU-7467 Merging branch to trunk 4 X-SVN-Rev: 39866 --- .../src/com/ibm/icu/impl/TextTrieMap.java | 125 + .../icu/impl/number/AffixPatternUtils.java | 524 ++ .../src/com/ibm/icu/impl/number/Endpoint.java | 286 + .../com/ibm/icu/impl/number/Exportable.java | 15 + .../src/com/ibm/icu/impl/number/Format.java | 277 + .../ibm/icu/impl/number/FormatQuantity.java | 180 + .../ibm/icu/impl/number/FormatQuantity1.java | 860 +++ .../ibm/icu/impl/number/FormatQuantity2.java | 173 + .../ibm/icu/impl/number/FormatQuantity3.java | 222 + .../ibm/icu/impl/number/FormatQuantity4.java | 407 ++ .../icu/impl/number/FormatQuantityBCD.java | 900 +++ .../impl/number/FormatQuantitySelector.java | 52 + .../src/com/ibm/icu/impl/number/Modifier.java | 128 + .../ibm/icu/impl/number/ModifierHolder.java | 106 + .../icu/impl/number/NumberStringBuilder.java | 411 ++ .../ibm/icu/impl/number/PNAffixGenerator.java | 296 + .../src/com/ibm/icu/impl/number/Parse.java | 2110 ++++++ .../ibm/icu/impl/number/PatternString.java | 855 +++ .../com/ibm/icu/impl/number/Properties.java | 994 +++ .../src/com/ibm/icu/impl/number/Rounder.java | 265 + .../ibm/icu/impl/number/RoundingUtils.java | 165 + .../src/com/ibm/icu/impl/number/demo.java | 123 + .../formatters/BigDecimalMultiplier.java | 57 + .../formatters/CompactDecimalFormat.java | 449 ++ .../number/formatters/CurrencyFormat.java | 299 + .../formatters/MagnitudeMultiplier.java | 59 + .../impl/number/formatters/MeasureFormat.java | 73 + .../impl/number/formatters/PaddingFormat.java | 173 + .../formatters/PositiveDecimalFormat.java | 227 + .../PositiveNegativeAffixFormat.java | 256 + .../impl/number/formatters/RangeFormat.java | 58 + .../number/formatters/RoundingFormat.java | 41 + .../number/formatters/ScientificFormat.java | 233 + .../number/formatters/StrongAffixFormat.java | 48 + .../modifiers/ConstantAffixModifier.java | 105 + .../modifiers/ConstantMultiFieldModifier.java | 93 + .../modifiers/GeneralPluralModifier.java | 76 + .../PositiveNegativeAffixModifier.java | 53 + .../impl/number/modifiers/SimpleModifier.java | 130 + .../number/rounders/IncrementRounder.java | 67 + .../number/rounders/MagnitudeRounder.java | 30 + .../icu/impl/number/rounders/NoRounder.java | 24 + .../rounders/SignificantDigitsRounder.java | 223 + .../ibm/icu/text/CompactDecimalDataCache.java | 524 -- .../ibm/icu/text/CompactDecimalFormat.java | 739 +- .../com/ibm/icu/text/CurrencyPluralInfo.java | 24 +- .../ibm/icu/text/DecimalFormatSymbols.java | 17 +- .../com/ibm/icu/text/DecimalFormat_ICU58.java | 6277 +++++++++++++++++ .../src/com/ibm/icu/text/MeasureFormat.java | 82 +- .../src/com/ibm/icu/text/MessageFormat.java | 4 +- .../src/com/ibm/icu/text/NumberFormat.java | 29 +- .../src/com/ibm/icu/text/PluralFormat.java | 8 +- .../src/com/ibm/icu/text/PluralRules.java | 74 +- .../ibm/icu/text/RuleBasedNumberFormat.java | 2 +- .../icu/text/ScientificNumberFormatter.java | 5 +- .../core/src/com/ibm/icu/util/Currency.java | 52 +- .../core/src/com/ibm/icu/util/Measure.java | 2 +- .../data/numberformattestspecification.txt | 617 +- .../dev/test/format/BigNumberFormatTest.java | 20 +- .../test/format/CompactDecimalFormatTest.java | 244 +- ...va => DataDrivenNumberFormatTestData.java} | 176 +- .../DataDrivenNumberFormatTestUtility.java | 116 +- .../test/format/IntlTestDecimalFormatAPI.java | 68 +- .../format/IntlTestDecimalFormatAPIC.java | 6 +- .../format/IntlTestDecimalFormatSymbolsC.java | 48 +- .../icu/dev/test/format/MeasureUnitTest.java | 13 +- .../format/NumberFormatDataDrivenTest.java | 433 ++ .../format/NumberFormatRegressionTest.java | 90 +- .../format/NumberFormatSerialTestData.java | 362 +- .../icu/dev/test/format/NumberFormatTest.java | 1163 +-- .../dev/test/format/NumberFormatTestCases.txt | 9 +- .../test/format/NumberRegressionTests.java | 250 +- .../test/format/ShanesDataDrivenTester.java | 343 + .../dev/test/format/TestMessageFormat.java | 5 +- .../test/numbers/AffixPatternUtilsTest.java | 126 + .../dev/test/numbers/FormatQuantityTest.java | 475 ++ .../test/numbers/NumberStringBuilderTest.java | 171 + .../dev/test/numbers/PatternStringTest.java | 110 + .../icu/dev/test/numbers/PropertiesTest.java | 331 + .../ibm/icu/dev/test/numbers/RounderTest.java | 134 + .../dev/test/serializable/FormatHandler.java | 21 +- .../serializable/SerializableTestUtility.java | 7 + .../icu/dev/test/util/TextTrieMapTest.java | 162 +- 83 files changed, 23204 insertions(+), 2383 deletions(-) create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/Endpoint.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/Exportable.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/Format.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity2.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity3.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity4.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantityBCD.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantitySelector.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierHolder.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/PNAffixGenerator.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/RoundingUtils.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/demo.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/BigDecimalMultiplier.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MagnitudeMultiplier.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MeasureFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PaddingFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveNegativeAffixFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RangeFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RoundingFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/ScientificFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/StrongAffixFormat.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantAffixModifier.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantMultiFieldModifier.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/SimpleModifier.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/IncrementRounder.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/MagnitudeRounder.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/NoRounder.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java delete mode 100644 icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat_ICU58.java rename icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/{NumberFormatTestData.java => DataDrivenNumberFormatTestData.java} (96%) create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ShanesDataDrivenTester.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/AffixPatternUtilsTest.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/FormatQuantityTest.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/NumberStringBuilderTest.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PatternStringTest.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PropertiesTest.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/RounderTest.java 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 6688d4719f1..2576ff18d1c 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 00000000000..992028dbe87 --- /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 00000000000..ef603bc6b72 --- /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 00000000000..515667d9156 --- /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 00000000000..681b663fe76 --- /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 00000000000..a5d376587e0 --- /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 00000000000..f9cc8e7f165 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java @@ -0,0 +1,860 @@ +// © 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. + */ + 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; + } + + /** @return */ + @Override + public StandardPlural getStandardPlural(PluralRules rules) { + if (rules == null) { + // Fail gracefully if the user didn't provide a PluralRules + return StandardPlural.OTHER; + } else { + // TODO: Avoid converting to a double for the sake of PluralRules + String ruleString = rules.select(toDouble()); + return StandardPlural.orOtherFromString(ruleString); + } + } + + @Override + public double getPluralOperand(Operand operand) { + // TODO: This is a temporary hack. + return new PluralRules.FixedDecimal(toDouble()).getPluralOperand(operand); + } + + /** @return */ + public boolean hasNextFraction() { + if (rReqPos < 0) { + // We are in the required zone. + return true; + } else if (rOptPos >= 0) { + // We are in the forbidden zone. + return false; + } else { + // We are in the optional zone. + if (primary == -1) { + return fallback.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) > 0; + } else { + if (primaryScale <= -19) { + // The number is a fraction so small that it consists of only fraction digits. + return primary > 0; + } else if (primaryScale < 0) { + // Check if we have a fraction part. + long factor = POWERS_OF_TEN[0 - primaryScale]; + return ((primary % factor) != 0); + } else { + // The lowest digit in the long has magnitude greater than -1. + return false; + } + } + } + } + + /** @return */ + public byte nextFraction() { + byte returnValue; + if (primary == -1) { + BigDecimal temp = fallback.multiply(BigDecimal.TEN); + returnValue = temp.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue(); + fallback = fallback.setScale(0, RoundingMode.FLOOR).add(temp.remainder(BigDecimal.ONE)); + } else { + if (primaryScale <= -20) { + // The number is a fraction so small that it has no first fraction digit. + primaryScale += 1; + returnValue = 0; + } else if (primaryScale < 0) { + // Extract the fraction digit out of the middle of the long. + long factor = POWERS_OF_TEN[0 - primaryScale - 1]; + long temp1 = primary / factor; + long temp2 = primary % factor; + returnValue = (byte) (temp1 % 10); // not necessarily nonzero + primary = ((temp1 / 10) * factor) + temp2; + primaryScale += 1; + if (temp1 != 0) { + primaryPrecision -= 1; + } + } else { + // The lowest digit in the long has magnitude greater than -1. + returnValue = 0; + } + } + + // Update digit brackets + if (lOptPos < 0) { + lOptPos += 1; + } + if (lReqPos < 0) { + lReqPos += 1; + } + if (rReqPos < 0) { + rReqPos += 1; + } + if (rOptPos < 0) { + rOptPos += 1; + } + + assert returnValue >= 0; + return returnValue; + } + + /** @return */ + public boolean hasNextInteger() { + if (lReqPos > 0) { + // We are in the required zone. + return true; + } else if (lOptPos <= 0) { + // We are in the forbidden zone. + return false; + } else { + // We are in the optional zone. + if (primary == -1) { + return fallback.setScale(0, RoundingMode.FLOOR).compareTo(BigDecimal.ZERO) > 0; + } else { + if (primaryScale < -18) { + // The number is a fraction so small that it has no integer part. + return false; + } else if (primaryScale < 0) { + // Check if we have an integer part. + long factor = POWERS_OF_TEN[0 - primaryScale]; + return ((primary % factor) != primary); // equivalent: ((primary / 10) != 0) + } else { + // The lowest digit in the long has magnitude of at least 0. + return primary != 0; + } + } + } + } + + private int integerCount() { + int digitsRemaining; + if (primary == -1) { + digitsRemaining = precisionBigDecimal(fallback) + scaleBigDecimal(fallback); + } else { + digitsRemaining = primaryPrecision + primaryScale; + } + return Math.min(Math.max(digitsRemaining, lReqPos), lOptPos); + } + + private int fractionCount() { + // TODO: This is temporary. + FormatQuantity1 copy = (FormatQuantity1) this.clone(); + int fractionCount = 0; + while (copy.hasNextFraction()) { + copy.nextFraction(); + fractionCount++; + } + return fractionCount; + } + + @Override + public int getUpperDisplayMagnitude() { + return integerCount() - 1; + } + + @Override + public int getLowerDisplayMagnitude() { + return -fractionCount(); + } + + // @Override + // public byte getIntegerDigit(int index) { + // return getDigitPos(index); + // } + // + // @Override + // public byte getFractionDigit(int index) { + // return getDigitPos(-index - 1); + // } + + @Override + public byte getDigit(int magnitude) { + // TODO: This is temporary. + FormatQuantity1 copy = (FormatQuantity1) this.clone(); + if (magnitude < 0) { + for (int p = -1; p > magnitude; p--) { + copy.nextFraction(); + } + return copy.nextFraction(); + } else { + for (int p = 0; p < magnitude; p++) { + copy.nextInteger(); + } + return copy.nextInteger(); + } + } + + /** @return */ + public byte nextInteger() { + byte returnValue; + if (primary == -1) { + returnValue = fallback.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue(); + BigDecimal temp = fallback.divide(BigDecimal.TEN).setScale(0, RoundingMode.FLOOR); + fallback = fallback.remainder(BigDecimal.ONE).add(temp); + } else { + if (primaryScale < -18) { + // The number is a fraction so small that it has no integer part. + returnValue = 0; + } else if (primaryScale < 0) { + // Extract the integer digit out of the middle of the long. In many ways, this is the heart + // of the digit iterator algorithm. + long factor = POWERS_OF_TEN[0 - primaryScale]; + if ((primary % factor) != primary) { // equivalent: ((primary / 10) != 0) + returnValue = (byte) ((primary / factor) % 10); + long temp = (primary / 10); + primary = temp - (temp % factor) + (primary % factor); + primaryPrecision -= 1; + } else { + returnValue = 0; + } + } else if (primaryScale == 0) { + // Fast-path for primaryScale == 0 (otherwise equivalent to previous step). + if (primary != 0) { + returnValue = (byte) (primary % 10); + primary /= 10; + primaryPrecision -= 1; + } else { + returnValue = 0; + } + } else { + // The lowest digit in the long has magnitude greater than 0. + primaryScale -= 1; + returnValue = 0; + } + } + + // Update digit brackets + if (lOptPos > 0) { + lOptPos -= 1; + } + if (lReqPos > 0) { + lReqPos -= 1; + } + if (rReqPos > 0) { + rReqPos -= 1; + } + if (rOptPos > 0) { + rOptPos -= 1; + } + + assert returnValue >= 0; + return returnValue; + } + + /** + * Helper method to compute the precision of a BigDecimal by our definition of precision, which is + * that the number zero gets precision zero. + * + * @param decimal The BigDecimal whose precision to compute. + * @return The precision by our definition. + */ + private static int precisionBigDecimal(BigDecimal decimal) { + if (decimal.compareTo(BigDecimal.ZERO) == 0) { + return 0; + } else { + return decimal.precision(); + } + } + + /** + * Helper method to compute the scale of a BigDecimal by our definition of scale, which is that + * deeper fractions result in negative scales as opposed to positive scales. + * + * @param decimal The BigDecimal whose scale to compute. + * @return The scale by our definition. + */ + private static int scaleBigDecimal(BigDecimal decimal) { + return -decimal.scale(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(" 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 00000000000..30931692d7f --- /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 00000000000..9e25a45a74f --- /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 00000000000..335b78fce17 --- /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 00000000000..6d5831990ee --- /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 00000000000..95a11ba8041 --- /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 00000000000..00739fd5743 --- /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 00000000000..483d6fd6792 --- /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 00000000000..4008307161c --- /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 00000000000..bb9af1c7d1c --- /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 00000000000..110171ffe4c --- /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; + acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false); + if (state.length > 0 && mode == ParseMode.FAST) break; + if (parseCurrency) { + acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item); + } + } + break; + + case AFTER_FRACTION_DIGIT: + // We encountered a decimal point + acceptFractionDigit(cp, StateName.AFTER_FRACTION_DIGIT, state, item); + if (state.length > 0 && mode == ParseMode.FAST) break; + acceptBidi(cp, StateName.AFTER_FRACTION_DIGIT, state, item); + if (state.length > 0 && mode == ParseMode.FAST) break; + acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item); + if (state.length > 0 && mode == ParseMode.FAST) break; + if (!ignoreExponent) { + acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item); + if (state.length > 0 && mode == ParseMode.FAST) break; + } + if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) { + acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item); + } + if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) { + acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item); + if (state.length > 0 && mode == ParseMode.FAST) break; + acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false); + if (state.length > 0 && mode == ParseMode.FAST) break; + if (parseCurrency) { + acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item); + } + } + break; + + case AFTER_EXPONENT_SEPARATOR: + acceptBidi(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item); + acceptMinusOrPlusSign(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item, true); + acceptExponentDigit(cp, StateName.AFTER_EXPONENT_DIGIT, state, item); + break; + + case AFTER_EXPONENT_DIGIT: + acceptBidi(cp, StateName.AFTER_EXPONENT_DIGIT, state, item); + acceptPadding(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item); + acceptExponentDigit(cp, StateName.AFTER_EXPONENT_DIGIT, state, item); + if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) { + acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item); + } + if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) { + acceptWhitespace(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item); + acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false); + if (parseCurrency) { + acceptCurrency(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item); + } + } + break; + + case BEFORE_SUFFIX: + // Accept whitespace, suffixes, and exponent separators + acceptBidi(cp, StateName.BEFORE_SUFFIX, state, item); + acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item); + if (!ignoreExponent) { + acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item); + } + if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) { + acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item); + } + if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) { + acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item); + acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false); + if (parseCurrency) { + acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item); + } + } + break; + + case BEFORE_SUFFIX_SEEN_EXPONENT: + // Accept whitespace and suffixes but not exponent separators + acceptBidi(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item); + acceptPadding(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item); + if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) { + acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item); + } + if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) { + acceptWhitespace(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item); + acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item, false); + if (parseCurrency) { + acceptCurrency(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item); + } + } + break; + + case AFTER_SUFFIX: + if ((mode == ParseMode.LENIENT || mode == ParseMode.FAST) && parseCurrency) { + // Continue traversing in case there is a currency symbol to consume + acceptBidi(cp, StateName.AFTER_SUFFIX, state, item); + acceptPadding(cp, StateName.AFTER_SUFFIX, state, item); + acceptWhitespace(cp, StateName.AFTER_SUFFIX, state, item); + acceptMinusOrPlusSign(cp, StateName.AFTER_SUFFIX, state, item, false); + if (parseCurrency) { + acceptCurrency(cp, StateName.AFTER_SUFFIX, state, item); + } + } + // Otherwise, do not accept any more characters. + break; + + case INSIDE_CURRENCY: + acceptCurrencyOffset(cp, state, item); + break; + + case INSIDE_DIGIT: + acceptDigitTrieOffset(cp, state, item); + break; + + case INSIDE_STRING: + acceptStringOffset(cp, state, item); + // Accept arbitrary bidi in the middle of strings. + if (state.length == 0 && UNISET_BIDI.contains(cp)) { + state.getNext().copyFrom(item, item.name, cp); + } + break; + + case INSIDE_AFFIX_PATTERN: + acceptAffixPatternOffset(cp, state, item); + // Accept arbitrary bidi and whitespace (if lenient) in the middle of affixes. + if (state.length == 0 && isIgnorable(cp, state)) { + state.getNext().copyFrom(item, item.name, cp); + } + break; + } + } + + if (state.length == 0) { + // No parse paths continue past this point. We have found the longest parsable string + // from the input. Restore previous state without the offset and break. + state.swapBack(); + break; + } + + offset += Character.charCount(cp); + } + + // Post-processing + if (state.length == 0) { + if (DEBUGGING) { + System.out.println("No matches found"); + System.out.println("- - - - - - - - - -"); + } + return null; + } else { + + // Loop through the candidates. "continue" skips a candidate as invalid. + StateItem best = null; + outer: + for (int i = 0; i < state.length; i++) { + StateItem item = state.items[i]; + + if (DEBUGGING) { + System.out.println(":end " + item); + } + + // Check that at least one digit was read. + if (!item.hasNumber()) { + if (DEBUGGING) System.out.println("-> rejected due to no number value"); + continue; + } + + if (mode == ParseMode.STRICT) { + // Perform extra checks for strict mode. + // We require that the affixes match. + boolean sawPrefix = item.sawPrefix || (item.affix != null && item.affix.p.isEmpty()); + boolean sawSuffix = item.sawSuffix || (item.affix != null && item.affix.s.isEmpty()); + boolean hasEmptyAffix = + state.affixHolders.contains(AffixHolder.EMPTY_POSITIVE) + || state.affixHolders.contains(AffixHolder.EMPTY_NEGATIVE); + if (sawPrefix && sawSuffix) { + // OK + } else if (!sawPrefix && !sawSuffix && hasEmptyAffix) { + // OK + } else { + // Has a prefix or suffix that doesn't match + if (DEBUGGING) System.out.println("-> rejected due to mismatched prefix/suffix"); + continue; + } + + // Check that grouping sizes are valid. + int grouping1 = properties.getGroupingSize(); + int grouping2 = properties.getSecondaryGroupingSize(); + grouping1 = grouping1 > 0 ? grouping1 : grouping2; + grouping2 = grouping2 > 0 ? grouping2 : grouping1; + long groupingWidths = item.groupingWidths; + int numGroupingRegions = 16 - Long.numberOfLeadingZeros(groupingWidths) / 4; + // If the last grouping is zero, accept strings like "1," but reject string like "1,.23" + // Strip off multiple last-groupings to handle cases like "123,," or "123 " + while (numGroupingRegions > 1 && (groupingWidths & 0xf) == 0) { + if (item.sawDecimalPoint) { + if (DEBUGGING) System.out.println("-> rejected due to decimal point after grouping"); + continue outer; + } else { + groupingWidths >>>= 4; + numGroupingRegions--; + } + } + if (grouping1 < 0) { + // OK (no grouping data available) + } else if (numGroupingRegions <= 1) { + // OK (no grouping digits) + } else if ((groupingWidths & 0xf) != grouping1) { + // First grouping size is invalid + if (DEBUGGING) System.out.println("-> rejected due to first grouping violation"); + continue; + } else if (((groupingWidths >>> ((numGroupingRegions - 1) * 4)) & 0xf) > grouping2) { + // String like "1234,567" where the highest grouping is too large + if (DEBUGGING) System.out.println("-> rejected due to final grouping violation"); + continue; + } else { + for (int j = 1; j < numGroupingRegions - 1; j++) { + if (((groupingWidths >>> (j * 4)) & 0xf) != grouping2) { + // A grouping size somewhere in the middle is invalid + if (DEBUGGING) System.out.println("-> rejected due to inner grouping violation"); + continue outer; + } + } + } + } + + // Optionally require that the presence of a decimal point matches the pattern. + if (properties.getDecimalPatternMatchRequired() + && item.sawDecimalPoint != PositiveDecimalFormat.allowsDecimalPoint(properties)) { + if (DEBUGGING) System.out.println("-> rejected due to decimal point violation"); + continue; + } + + // When parsing currencies, require that a currency symbol was found. + if (parseCurrency && !item.sawCurrency) { + if (DEBUGGING) System.out.println("-> rejected due to lack of currency"); + continue; + } + + // If we get here, then this candidate is acceptable. + // Use the earliest candidate in the list, or the one with the highest score. + if (best == null) { + best = item; + } else if (item.score > best.score) { + best = item; + } + } + + if (DEBUGGING) { + System.out.println("- - - - - - - - - -"); + } + + if (best != null) { + ppos.setIndex(offset - best.trailingCount); + return best; + } else { + ppos.setErrorIndex(offset); + return null; + } + } + } + + /** + * If 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 00000000000..ad5c2872a5d --- /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 00000000000..9abc90c2fd7 --- /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 00000000000..1e090ccd712 --- /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#roundToInterval}, {@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 00000000000..3994eb25737 --- /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 00000000000..8a91de73331 --- /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 00000000000..a6a008a2ccc --- /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 00000000000..d3d8aef1147 --- /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 00000000000..b7575cc0e2f --- /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 00000000000..6e19c7d3684 --- /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 00000000000..752dc0af988 --- /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 00000000000..5aa3c48c63f --- /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 00000000000..f791ca46748 --- /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 00000000000..f71374a516d --- /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 00000000000..7c72e626372 --- /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 00000000000..a57caf4c06e --- /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 00000000000..4f67b4e1234 --- /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 00000000000..c70352d6e7a --- /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 00000000000..b133bc48723 --- /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 00000000000..e7ed0a61234 --- /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 00000000000..3dfeefa4059 --- /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 00000000000..1384b7bbdf0 --- /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 00000000000..23a15f44aac --- /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 00000000000..01ba69ce75b --- /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 00000000000..d53f966fa09 --- /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 00000000000..814e11e997f --- /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 00000000000..e11bf463b8b --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java @@ -0,0 +1,223 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.rounders; + +import java.math.RoundingMode; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.impl.number.Rounder; + +public class SignificantDigitsRounder extends Rounder { + + /** + * Sets whether the minimum significant digits should override the maximum integer and fraction + * digits. This affects both display and rounding. Default is true. + * + *

For example, if this option is enabled, formatting the number 4.567 with 3 min/max + * significant digits against the pattern "0.0" (1 min/max fraction digits) will result in "4.57" + * in locale en-US with the default rounding mode. If this option is disabled, the max + * fraction digits take priority instead, and the output will be "4.6". + * + * @param significantDigitsOverride true to ensure that the minimum significant digits are always + * shown; false to ensure that the maximum integer and fraction digits are obeyed. + * @return The property bag, for chaining. + */ + public static enum SignificantDigitsMode { + OVERRIDE_MAXIMUM_FRACTION, + RESPECT_MAXIMUM_FRACTION, + ENSURE_MINIMUM_SIGNIFICANT + }; + + public static interface IProperties extends IBasicRoundingProperties { + + static int DEFAULT_MINIMUM_SIGNIFICANT_DIGITS = -1; + + /** @see #setMinimumSignificantDigits */ + public int getMinimumSignificantDigits(); + + /** + * Sets the minimum number of significant digits to display. If, after rounding to the number of + * significant digits specified by {@link #setMaximumSignificantDigits}, the number of remaining + * significant digits is less than the minimum, the number will be padded with zeros. For + * example, if minimum significant digits is 3, the number 5.8 will be formatted as "5.80" in + * locale 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 8d302a559c9..00000000000 --- 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 078f10db57a..1d034b1901c 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,559 +18,228 @@ 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 */ 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 { - /** - * Short version, like "1.2T" - * @stable ICU 50 - */ - SHORT, - /** - * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not. - * @stable ICU 50 - */ - LONG - } - - /** - * Create a CompactDecimalFormat appropriate for a locale. The result may - * be affected by the number system in the locale, such as ar-u-nu-latn. - * - * @param locale the desired locale - * @param style the compact style - * @stable ICU 50 - */ - public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) { - return new CompactDecimalFormat(locale, style); - } - + /** + * Style parameter for CompactDecimalFormat. + * + * @stable ICU 50 + */ + public enum CompactStyle { /** - * Create a CompactDecimalFormat appropriate for a locale. The result may - * be affected by the number system in the locale, such as ar-u-nu-latn. + * Short version, like "1.2T" * - * @param locale the desired locale - * @param style the compact style * @stable ICU 50 */ - public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) { - return new CompactDecimalFormat(ULocale.forLocale(locale), style); - } - + SHORT, /** - * The public mechanism is CompactDecimalFormat.getInstance(). + * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not. * - * @param locale - * the desired locale - * @param style - * the compact style - */ - CompactDecimalFormat(ULocale locale, CompactStyle style) { - this.pluralRules = PluralRules.forLocale(locale); - DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(locale); - CompactDecimalDataCache.Data data = getData(locale, style); - CompactDecimalDataCache.Data currencyData = getCurrencyData(locale); - this.units = data.units; - this.divisor = data.divisors; - this.currencyUnits = currencyData.units; - this.currencyDivisor = currencyData.divisors; - this.style = style; - pluralToCurrencyAffixes = null; - -// DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); -// // TODO fix to use plural-dependent affixes -// Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix()); -// pluralToCurrencyAffixes = new HashMap(); -// for (String key : pluralRules.getKeywords()) { -// pluralToCurrencyAffixes.put(key, currency); -// } -// // TODO fix to get right symbol for the count - - finishInit(style, format.toPattern(), format.getDecimalFormatSymbols()); - } - - /** - * Create a short number "from scratch". Intended for internal use. The prefix, suffix, and divisor arrays are - * parallel, and provide the information for each power of 10. When formatting a value, the correct power of 10 is - * found, then the value is divided by the divisor, and the prefix and suffix are set (using - * setPositivePrefix/Suffix). - * - * @param pattern - * A number format pattern. Note that the prefix and suffix are discarded, and the decimals are - * overridden by default. - * @param formatSymbols - * Decimal format symbols, typically from a locale. - * @param style - * compact style. - * @param divisor - * An array of prefix values, one for each power of 10 from 0 to 14 - * @param pluralAffixes - * A map from plural categories to affixes. - * @param currencyAffixes - * A map from plural categories to currency affixes. - * @param debugCreationErrors - * A collection of strings for debugging. If null on input, then any errors found will be added to that - * collection instead of throwing exceptions. - * @internal - * @deprecated This API is ICU internal only. - */ - @Deprecated - public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols, - CompactStyle style, PluralRules pluralRules, - long[] divisor, Map pluralAffixes, Map currencyAffixes, - Collection debugCreationErrors) { - - this.pluralRules = pluralRules; - this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors); - this.currencyUnits = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors); - if (!pluralRules.getKeywords().equals(this.units.keySet())) { - debugCreationErrors.add("Missmatch in pluralCategories, should be: " + pluralRules.getKeywords() + ", was actually " + this.units.keySet()); - } - this.divisor = divisor.clone(); - this.currencyDivisor = divisor.clone(); - if (currencyAffixes == null) { - pluralToCurrencyAffixes = null; - } else { - pluralToCurrencyAffixes = new HashMap(); - for (Entry s : currencyAffixes.entrySet()) { - String[] pair = s.getValue(); - pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1])); - } - } - finishInit(style, pattern, formatSymbols); - } - - private void finishInit(CompactStyle style, String pattern, DecimalFormatSymbols formatSymbols) { - applyPattern(pattern); - setDecimalFormatSymbols(formatSymbols); - setMaximumSignificantDigits(2); // default significant digits - setSignificantDigitsUsed(true); - if (style == CompactStyle.SHORT) { - setGroupingUsed(false); - } - setCurrency(null); - } - - /** - * {@inheritDoc} - * @stable ICU 49 - */ - @Override - public boolean equals(Object obj) { - if (obj == null) - return false; - if (!super.equals(obj)) - return false; // super does class check - CompactDecimalFormat other = (CompactDecimalFormat) obj; - return mapsAreEqual(units, other.units) - && Arrays.equals(divisor, other.divisor) - && (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes - || pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes)) - && pluralRules.equals(other.pluralRules); - } - - private boolean mapsAreEqual( - Map 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; - } - - /** - * {@inheritDoc} - * @stable ICU 49 - */ - @Override - public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { - return format(number, null, toAppendTo, pos); - } - - /** - * {@inheritDoc} * @stable ICU 50 */ - @Override - public AttributedCharacterIterator formatToCharacterIterator(Object obj) { - if (!(obj instanceof Number)) { - throw new IllegalArgumentException(); - } - Number number = (Number) obj; - Amount amount = toAmount(number.doubleValue(), null, null); - return super.formatToCharacterIterator(amount.getQty(), amount.getUnit()); - } - - /** - * {@inheritDoc} - * @stable ICU 49 - */ - @Override - public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { - return format((double) number, toAppendTo, pos); - } - - /** - * {@inheritDoc} - * @stable ICU 49 - */ - @Override - public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) { - return format(number.doubleValue(), toAppendTo, pos); - } - - /** - * {@inheritDoc} - * @stable ICU 49 - */ - @Override - public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { - return format(number.doubleValue(), toAppendTo, pos); - } - - /** - * {@inheritDoc} - * @stable ICU 49 - */ - @Override - public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { - return format(number.doubleValue(), toAppendTo, pos); - } - /** - * {@inheritDoc} - * @internal ICU 57 technology preview - * @deprecated This API might change or be removed in a future release. - */ - @Override - @Deprecated - public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) { - return format(currAmt.getNumber().doubleValue(), currAmt.getCurrency(), toAppendTo, pos); - } - - /** - * Parsing is currently unsupported, and throws an UnsupportedOperationException. - * @stable ICU 49 - */ - @Override - public Number parse(String text, ParsePosition parsePosition) { - throw new UnsupportedOperationException(); - } - - // DISALLOW Serialization, at least while draft - - private void writeObject(ObjectOutputStream out) throws IOException { - throw new NotSerializableException(); - } - - private void readObject(ObjectInputStream in) throws IOException { - throw new NotSerializableException(); - } - - /* INTERNALS */ - private StringBuffer format(double number, Currency curr, StringBuffer toAppendTo, FieldPosition pos) { - if (curr != null && style == CompactStyle.LONG) { - throw new UnsupportedOperationException("CompactDecimalFormat does not support LONG style for currency."); - } - - // Compute the scaled amount, prefix, and suffix appropriate for the number's magnitude. - Output 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; - } - } + LONG + } + + /** + * Create a CompactDecimalFormat appropriate for a locale. The result may be affected by the + * number system in the locale, such as ar-u-nu-latn. + * + * @param locale the desired locale + * @param style the compact style + * @stable ICU 50 + */ + public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) { + return new CompactDecimalFormat(locale, style); + } + + /** + * Create a CompactDecimalFormat appropriate for a locale. The result may be affected by the + * number system in the locale, such as ar-u-nu-latn. + * + * @param locale the desired locale + * @param style the compact style + * @stable ICU 50 + */ + public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) { + return new CompactDecimalFormat(ULocale.forLocale(locale), style); + } + + /** + * The public mechanism is CompactDecimalFormat.getInstance(). + * + * @param locale the desired locale + * @param style the compact style + */ + CompactDecimalFormat(ULocale locale, CompactStyle style) { + // Use the locale's default pattern + String pattern = getPattern(locale, 0); + symbols = DecimalFormatSymbols.getInstance(locale); + properties = new Properties(); + properties.setCompactStyle(style); + exportedProperties = new Properties(); + setPropertiesFromPattern(pattern, true); + if (style == CompactStyle.SHORT) { + // TODO: This was setGroupingUsed(false) in ICU 58. Is it okay that I changed it for ICU 59? + properties.setMinimumGroupingDigits(2); + } + refreshFormatter(); + } + + /** + * {@inheritDoc} + * + * @stable ICU 49 + */ + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + /** + * {@inheritDoc} + * + * @stable ICU 49 + */ + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq, toAppendTo, pos); + fq.populateUFieldPosition(pos); + return toAppendTo; + } + + /** + * {@inheritDoc} + * + * @stable ICU 50 + */ + @Override + public AttributedCharacterIterator formatToCharacterIterator(Object obj) { + if (!(obj instanceof Number)) throw new IllegalArgumentException(); + Number number = (Number) obj; + FormatQuantity4 fq = new FormatQuantity4(number); + AttributedCharacterIterator result = formatter.formatToCharacterIterator(fq); + return result; + } + + /** + * {@inheritDoc} + * + * @stable ICU 49 + */ + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq, toAppendTo, pos); + fq.populateUFieldPosition(pos); + return toAppendTo; + } + + /** + * {@inheritDoc} + * + * @stable ICU 49 + */ + @Override + public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq, toAppendTo, pos); + fq.populateUFieldPosition(pos); + return toAppendTo; + } + + /** + * {@inheritDoc} + * + * @stable ICU 49 + */ + @Override + public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { + FormatQuantity4 fq = new FormatQuantity4(number); + formatter.format(fq, toAppendTo, pos); + fq.populateUFieldPosition(pos); + return toAppendTo; + } + + /** + * {@inheritDoc} + * + * @stable ICU 49 + */ + @Override + public StringBuffer format( + com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { + FormatQuantity4 fq = new FormatQuantity4(number.toBigDecimal()); + formatter.format(fq, toAppendTo, pos); + fq.populateUFieldPosition(pos); + return toAppendTo; + } + +// /** +// * {@inheritDoc} +// * +// * @internal ICU 57 technology preview +// * @deprecated This API might change or be removed in a future release. +// */ +// @Override +// @Deprecated +// public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) { +// // TODO(sffc) +// throw new UnsupportedOperationException(); +// } + + /** + * Parsing is currently unsupported, and throws an UnsupportedOperationException. + * + * @stable ICU 49 + */ + @Override + public Number parse(String text, ParsePosition parsePosition) { + throw new UnsupportedOperationException(); + } + + // DISALLOW Serialization, at least while draft + + private void writeObject(ObjectOutputStream out) throws IOException { + throw new NotSerializableException(); + } + + private void readObject(ObjectInputStream in) throws IOException { + throw new NotSerializableException(); + } } 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 055fafe0d0b..3e89cdb3f3b 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/DecimalFormatSymbols.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormatSymbols.java index 0e3ec41ebe9..07e812bcd2a 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 00000000000..14762749559 --- /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 6facd54e4bd..ad4fa241989 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 f31f5d52c02..08e5a9d2700 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 018d75f12b8..74d1928a7bb 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 0f90d00c320..c9dc1ff6ddc 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 970f7ca04aa..90a8eda81dd 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 9ec0ea47de4..d54c16cd9c9 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 527dd7ea934..93bb25f8cea 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 06300ca3dfd..8437c96884c 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 f641fec1f27..3e90a851d67 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/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt b/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt index 930b04ed34e..e1a51ef5b8d 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 JK +123 - -123 JK + +test parse case sensitive +set locale en +set lenient 1 +set pattern Aa# +begin +parse parseCaseSensitive output breaks +Aa1.23 1 1.23 +Aa1.23 0 1.23 +AA1.23 1 fail +// J and K do not support case-insensitive parsing for prefix/suffix. +// J supports it for the exponent separator, but not K. +AA1.23 0 1.23 JK +aa1.23 1 fail +aa1.23 0 1.23 JK +Aa1.23E3 1 1230 +Aa1.23E3 0 1230 +Aa1.23e3 1 1.23 J +Aa1.23e3 0 1230 K +NaN 1 NaN K +NaN 0 NaN K +nan 1 fail +nan 0 NaN JK + +test parse infinity and scientific notation overflow +set locale en +begin +parse output breaks +NaN NaN K +// JDK returns zero +1E999999999999999 Inf K +-1E999999999999999 -Inf K +1E-99999999999999 0.0 +// Note: The test suite code doesn't properly check for 0.0 vs. -0.0 +-1E-99999999999999 -0.0 +1E2147483648 Inf K +1E2147483647 Inf K +1E2147483646 1E2147483646 +1E-2147483649 0 +1E-2147483648 0 +// S returns zero here +1E-2147483647 1E-2147483647 S +1E-2147483646 1E-2147483646 + +test format push limits +set locale en +set minFractionDigits 2 +set roundingMode halfDown +begin +maxFractionDigits format output breaks +100 987654321987654321 987654321987654321.00 +100 987654321.987654321 987654321.987654321 +100 9999999999999.9950000000001 9999999999999.9950000000001 +2 9999999999999.9950000000001 10000000000000.00 +2 9999999.99499999 9999999.99 +// K doesn't support halfDowm rounding mode? +2 9999999.995 9999999.99 K +2 9999999.99500001 10000000.00 +100 56565656565656565656565656565656565656565656565656565656565656 56565656565656565656565656565656565656565656565656565656565656.00 +100 454545454545454545454545454545.454545454545454545454545454545 454545454545454545454545454545.454545454545454545454545454545 +100 0.0000000000000000000123 0.0000000000000000000123 +100 -78787878787878787878787878787878 -78787878787878787878787878787878.00 +100 -8989898989898989898989.8989898989898989 -8989898989898989898989.8989898989898989 + +test ticket 11230 +set locale en +set pattern ### +begin +parse output breaks +// K and J return null; S returns 99 + 9 9 9 JKS +// K and J return null + 9 999 9999 JK + 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 641dc7f315a..86946deea8a 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"}, - }); - - 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"); - } - - 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); - } - } + // TODO(sffc): Re-write these tests for the new CompactDecimalFormat pipeline + +// @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"); +// } + +// 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 ac8e1d51245..167fd0bdc51 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 66fc0fc013f..70a1fd49747 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,86 +39,92 @@ 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( 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 @@ -126,12 +134,12 @@ public class DataDrivenNumberFormatTestUtility { 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 ad0a462509e..973e71e1a65 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 5a8a5448f80..f0fe8ea5fcb 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 f8b4eaa2e39..77495f41d34 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 d4fbe45f2df..ddfd80c95ca 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 00000000000..6532e7eddca --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java @@ -0,0 +1,433 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.format; + +import java.math.BigDecimal; +import java.text.ParsePosition; + +import org.junit.Test; + +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.DecimalFormat_ICU58; +import com.ibm.icu.util.CurrencyAmount; +import com.ibm.icu.util.ULocale; + +public class NumberFormatDataDrivenTest { + + private static ULocale EN = new ULocale("en"); + + private static Number toNumber(String s) { + if (s.equals("NaN")) { + return Double.NaN; + } else if (s.equals("-Inf")) { + return Double.NEGATIVE_INFINITY; + } else if (s.equals("Inf")) { + return Double.POSITIVE_INFINITY; + } + return new BigDecimal(s); + } + + private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU58 = + new DataDrivenNumberFormatTestUtility.CodeUnderTest() { + @Override + public Character Id() { + return 'J'; + } + + @Override + public String format(DataDrivenNumberFormatTestData tuple) { + DecimalFormat_ICU58 fmt = createDecimalFormat(tuple); + String actual = fmt.format(toNumber(tuple.format)); + String expected = tuple.output; + if (!expected.equals(actual)) { + return "Expected " + expected + ", got " + actual; + } + return null; + } + + @Override + public String toPattern(DataDrivenNumberFormatTestData tuple) { + DecimalFormat_ICU58 fmt = createDecimalFormat(tuple); + StringBuilder result = new StringBuilder(); + if (tuple.toPattern != null) { + String expected = tuple.toPattern; + String actual = fmt.toPattern(); + if (!expected.equals(actual)) { + result.append("Expected toPattern=" + expected + ", got " + actual); + } + } + if (tuple.toLocalizedPattern != null) { + String expected = tuple.toLocalizedPattern; + String actual = fmt.toLocalizedPattern(); + if (!expected.equals(actual)) { + result.append("Expected toLocalizedPattern=" + expected + ", got " + actual); + } + } + return result.length() == 0 ? null : result.toString(); + } + + @Override + public String parse(DataDrivenNumberFormatTestData tuple) { + DecimalFormat_ICU58 fmt = createDecimalFormat(tuple); + ParsePosition ppos = new ParsePosition(0); + Number actual = fmt.parse(tuple.parse, ppos); + if (ppos.getIndex() == 0) { + return "Parse failed; got " + actual + ", but expected " + tuple.output; + } + if (tuple.output.equals("fail")) { + return null; + } + Number expected = toNumber(tuple.output); + // number types cannot be compared, this is the best we can do. + if (expected.doubleValue() != actual.doubleValue() + && !Double.isNaN(expected.doubleValue()) + && !Double.isNaN(expected.doubleValue())) { + return "Expected: " + expected + ", got: " + actual; + } + return null; + } + + @Override + public String parseCurrency(DataDrivenNumberFormatTestData tuple) { + DecimalFormat_ICU58 fmt = createDecimalFormat(tuple); + ParsePosition ppos = new ParsePosition(0); + CurrencyAmount currAmt = fmt.parseCurrency(tuple.parse, ppos); + if (ppos.getIndex() == 0) { + return "Parse failed; got " + currAmt + ", but expected " + tuple.output; + } + if (tuple.output.equals("fail")) { + return null; + } + Number expected = toNumber(tuple.output); + Number actual = currAmt.getNumber(); + // number types cannot be compared, this is the best we can do. + if (expected.doubleValue() != actual.doubleValue() + && !Double.isNaN(expected.doubleValue()) + && !Double.isNaN(expected.doubleValue())) { + return "Expected: " + expected + ", got: " + actual; + } + + if (!tuple.outputCurrency.equals(currAmt.getCurrency().toString())) { + return "Expected currency: " + tuple.outputCurrency + ", got: " + currAmt.getCurrency(); + } + return null; + } + + /** + * @param tuple + * @return + */ + private DecimalFormat_ICU58 createDecimalFormat(DataDrivenNumberFormatTestData tuple) { + + DecimalFormat_ICU58 fmt = + new DecimalFormat_ICU58( + tuple.pattern == null ? "0" : tuple.pattern, + new DecimalFormatSymbols(tuple.locale == null ? EN : tuple.locale)); + adjustDecimalFormat(tuple, fmt); + return fmt; + } + /** + * @param tuple + * @param fmt + */ + private void adjustDecimalFormat( + DataDrivenNumberFormatTestData tuple, DecimalFormat_ICU58 fmt) { + if (tuple.minIntegerDigits != null) { + fmt.setMinimumIntegerDigits(tuple.minIntegerDigits); + } + if (tuple.maxIntegerDigits != null) { + fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits); + } + if (tuple.minFractionDigits != null) { + fmt.setMinimumFractionDigits(tuple.minFractionDigits); + } + if (tuple.maxFractionDigits != null) { + fmt.setMaximumFractionDigits(tuple.maxFractionDigits); + } + if (tuple.currency != null) { + fmt.setCurrency(tuple.currency); + } + if (tuple.minGroupingDigits != null) { + // Oops we don't support this. + } + if (tuple.useSigDigits != null) { + fmt.setSignificantDigitsUsed(tuple.useSigDigits != 0); + } + if (tuple.minSigDigits != null) { + fmt.setMinimumSignificantDigits(tuple.minSigDigits); + } + if (tuple.maxSigDigits != null) { + fmt.setMaximumSignificantDigits(tuple.maxSigDigits); + } + if (tuple.useGrouping != null) { + fmt.setGroupingUsed(tuple.useGrouping != 0); + } + if (tuple.multiplier != null) { + fmt.setMultiplier(tuple.multiplier); + } + if (tuple.roundingIncrement != null) { + fmt.setRoundingIncrement(tuple.roundingIncrement.doubleValue()); + } + if (tuple.formatWidth != null) { + fmt.setFormatWidth(tuple.formatWidth); + } + if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) { + fmt.setPadCharacter(tuple.padCharacter.charAt(0)); + } + if (tuple.useScientific != null) { + fmt.setScientificNotation(tuple.useScientific != 0); + } + if (tuple.grouping != null) { + fmt.setGroupingSize(tuple.grouping); + } + if (tuple.grouping2 != null) { + fmt.setSecondaryGroupingSize(tuple.grouping2); + } + if (tuple.roundingMode != null) { + fmt.setRoundingMode(tuple.roundingMode); + } + if (tuple.currencyUsage != null) { + fmt.setCurrencyUsage(tuple.currencyUsage); + } + if (tuple.minimumExponentDigits != null) { + fmt.setMinimumExponentDigits(tuple.minimumExponentDigits.byteValue()); + } + if (tuple.exponentSignAlwaysShown != null) { + fmt.setExponentSignAlwaysShown(tuple.exponentSignAlwaysShown != 0); + } + if (tuple.decimalSeparatorAlwaysShown != null) { + fmt.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0); + } + if (tuple.padPosition != null) { + fmt.setPadPosition(tuple.padPosition); + } + if (tuple.positivePrefix != null) { + fmt.setPositivePrefix(tuple.positivePrefix); + } + if (tuple.positiveSuffix != null) { + fmt.setPositiveSuffix(tuple.positiveSuffix); + } + if (tuple.negativePrefix != null) { + fmt.setNegativePrefix(tuple.negativePrefix); + } + if (tuple.negativeSuffix != null) { + fmt.setNegativeSuffix(tuple.negativeSuffix); + } + if (tuple.localizedPattern != null) { + fmt.applyLocalizedPattern(tuple.localizedPattern); + } + int lenient = tuple.lenient == null ? 1 : tuple.lenient.intValue(); + fmt.setParseStrict(lenient == 0); + if (tuple.parseIntegerOnly != null) { + fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0); + } + if (tuple.parseCaseSensitive != null) { + // Not supported. + } + if (tuple.decimalPatternMatchRequired != null) { + fmt.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0); + } + if (tuple.parseNoExponent != null) { + // Oops, not supported for now + } + } + }; + + private DataDrivenNumberFormatTestUtility.CodeUnderTest JDK = + new DataDrivenNumberFormatTestUtility.CodeUnderTest() { + @Override + public Character Id() { + return 'K'; + } + + @Override + public String format(DataDrivenNumberFormatTestData tuple) { + java.text.DecimalFormat fmt = createDecimalFormat(tuple); + String actual = fmt.format(toNumber(tuple.format)); + String expected = tuple.output; + if (!expected.equals(actual)) { + return "Expected " + expected + ", got " + actual; + } + return null; + } + + @Override + public String toPattern(DataDrivenNumberFormatTestData tuple) { + java.text.DecimalFormat fmt = createDecimalFormat(tuple); + StringBuilder result = new StringBuilder(); + if (tuple.toPattern != null) { + String expected = tuple.toPattern; + String actual = fmt.toPattern(); + if (!expected.equals(actual)) { + result.append("Expected toPattern=" + expected + ", got " + actual); + } + } + if (tuple.toLocalizedPattern != null) { + String expected = tuple.toLocalizedPattern; + String actual = fmt.toLocalizedPattern(); + if (!expected.equals(actual)) { + result.append("Expected toLocalizedPattern=" + expected + ", got " + actual); + } + } + return result.length() == 0 ? null : result.toString(); + } + + @Override + public String parse(DataDrivenNumberFormatTestData tuple) { + java.text.DecimalFormat fmt = createDecimalFormat(tuple); + ParsePosition ppos = new ParsePosition(0); + Number actual = fmt.parse(tuple.parse, ppos); + if (ppos.getIndex() == 0) { + return "Parse failed; got " + actual + ", but expected " + tuple.output; + } + if (tuple.output.equals("fail")) { + return null; + } + Number expected = toNumber(tuple.output); + // number types cannot be compared, this is the best we can do. + if (expected.doubleValue() != actual.doubleValue() + && !Double.isNaN(expected.doubleValue()) + && !Double.isNaN(expected.doubleValue())) { + return "Expected: " + expected + ", got: " + actual; + } + return null; + } + + /** + * @param tuple + * @return + */ + private java.text.DecimalFormat createDecimalFormat(DataDrivenNumberFormatTestData tuple) { + java.text.DecimalFormat fmt = + new java.text.DecimalFormat( + tuple.pattern == null ? "0" : tuple.pattern, + new java.text.DecimalFormatSymbols( + (tuple.locale == null ? EN : tuple.locale).toLocale())); + adjustDecimalFormat(tuple, fmt); + return fmt; + } + + /** + * @param tuple + * @param fmt + */ + private void adjustDecimalFormat( + DataDrivenNumberFormatTestData tuple, java.text.DecimalFormat fmt) { + if (tuple.minIntegerDigits != null) { + fmt.setMinimumIntegerDigits(tuple.minIntegerDigits); + } + if (tuple.maxIntegerDigits != null) { + fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits); + } + if (tuple.minFractionDigits != null) { + fmt.setMinimumFractionDigits(tuple.minFractionDigits); + } + if (tuple.maxFractionDigits != null) { + fmt.setMaximumFractionDigits(tuple.maxFractionDigits); + } + if (tuple.currency != null) { + fmt.setCurrency(java.util.Currency.getInstance(tuple.currency.toString())); + } + if (tuple.minGroupingDigits != null) { + // Oops we don't support this. + } + if (tuple.useSigDigits != null) { + // Oops we don't support this + } + if (tuple.minSigDigits != null) { + // Oops we don't support this + } + if (tuple.maxSigDigits != null) { + // Oops we don't support this + } + if (tuple.useGrouping != null) { + fmt.setGroupingUsed(tuple.useGrouping != 0); + } + if (tuple.multiplier != null) { + fmt.setMultiplier(tuple.multiplier); + } + if (tuple.roundingIncrement != null) { + // Not supported + } + if (tuple.formatWidth != null) { + // Not supported + } + if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) { + // Not supported + } + if (tuple.useScientific != null) { + // Not supported + } + if (tuple.grouping != null) { + fmt.setGroupingSize(tuple.grouping); + } + if (tuple.grouping2 != null) { + // Not supported + } + if (tuple.roundingMode != null) { + // Not supported + } + if (tuple.currencyUsage != null) { + // Not supported + } + if (tuple.minimumExponentDigits != null) { + // Not supported + } + if (tuple.exponentSignAlwaysShown != null) { + // Not supported + } + if (tuple.decimalSeparatorAlwaysShown != null) { + fmt.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0); + } + if (tuple.padPosition != null) { + // Not supported + } + if (tuple.positivePrefix != null) { + fmt.setPositivePrefix(tuple.positivePrefix); + } + if (tuple.positiveSuffix != null) { + fmt.setPositiveSuffix(tuple.positiveSuffix); + } + if (tuple.negativePrefix != null) { + fmt.setNegativePrefix(tuple.negativePrefix); + } + if (tuple.negativeSuffix != null) { + fmt.setNegativeSuffix(tuple.negativeSuffix); + } + if (tuple.localizedPattern != null) { + fmt.applyLocalizedPattern(tuple.localizedPattern); + } + + // lenient parsing not supported by JDK + if (tuple.parseIntegerOnly != null) { + fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0); + } + if (tuple.parseCaseSensitive != null) { + // Not supported. + } + if (tuple.decimalPatternMatchRequired != null) { + // Oops, not supported + } + if (tuple.parseNoExponent != null) { + // Oops, not supported for now + } + } + }; + + @Test + public void TestDataDrivenICU58() { + DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures( + "numberformattestspecification.txt", ICU58); + } + + @Test + public void TestDataDrivenJDK() { + DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures( + "numberformattestspecification.txt", JDK); + } + + @Test + public void TestDataDrivenShane() { + ShanesDataDrivenTester.run(); + } +} 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 6b9aa25988b..4a983398051 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 95680965dc8..ff180aab78c 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 @@ -12,7 +12,7 @@ package com.ibm.icu.dev.test.format; public class NumberFormatSerialTestData { //get Content public static byte[][] getContent() { - return content; + return content; } //NumberFormat.getInstance(Locale.US) @@ -160,78 +160,6 @@ public class NumberFormatSerialTestData { }; - //NumberFormat.getPercentInstance(Locale.US) - static byte[] percentInstance = new byte[]{ - -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, - 116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, - 3, 98, -40, 114, 48, 58, 2, 0, 22, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83, - 101, 112, 97, 114, 97, 116, 111, 114, 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 90, - 0, 23, 101, 120, 112, 111, 110, 101, 110, 116, 83, 105, 103, 110, 65, 108, 119, 97, 121, 115, - 83, 104, 111, 119, 110, 73, 0, 11, 102, 111, 114, 109, 97, 116, 87, 105, 100, 116, 104, 66, - 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 83, 105, 122, 101, 66, 0, 13, 103, 114, 111, - 117, 112, 105, 110, 103, 83, 105, 122, 101, 50, 66, 0, 17, 109, 105, 110, 69, 120, 112, 111, - 110, 101, 110, 116, 68, 105, 103, 105, 116, 115, 73, 0, 10, 109, 117, 108, 116, 105, 112, 108, - 105, 101, 114, 67, 0, 3, 112, 97, 100, 73, 0, 11, 112, 97, 100, 80, 111, 115, 105, 116, - 105, 111, 110, 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 73, 0, - 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, - 97, 109, 90, 0, 22, 117, 115, 101, 69, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 78, - 111, 116, 97, 116, 105, 111, 110, 76, 0, 16, 110, 101, 103, 80, 114, 101, 102, 105, 120, 80, - 97, 116, 116, 101, 114, 110, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, - 83, 116, 114, 105, 110, 103, 59, 76, 0, 16, 110, 101, 103, 83, 117, 102, 102, 105, 120, 80, - 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 1, 76, 0, 14, 110, 101, 103, 97, 116, 105, - 118, 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 1, 76, 0, 14, 110, 101, 103, 97, - 116, 105, 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 1, 76, 0, 16, 112, 111, - 115, 80, 114, 101, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 1, 76, - 0, 16, 112, 111, 115, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, - 126, 0, 1, 76, 0, 14, 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120, - 113, 0, 126, 0, 1, 76, 0, 14, 112, 111, 115, 105, 116, 105, 118, 101, 83, 117, 102, 102, - 105, 120, 113, 0, 126, 0, 1, 76, 0, 17, 114, 111, 117, 110, 100, 105, 110, 103, 73, 110, - 99, 114, 101, 109, 101, 110, 116, 116, 0, 22, 76, 106, 97, 118, 97, 47, 109, 97, 116, 104, - 47, 66, 105, 103, 68, 101, 99, 105, 109, 97, 108, 59, 76, 0, 7, 115, 121, 109, 98, 111, - 108, 115, 116, 0, 39, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 116, 101, - 120, 116, 47, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, - 111, 108, 115, 59, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, - 116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, 109, 97, 116, -33, -10, -77, - -65, 19, 125, 7, -24, 3, 0, 11, 90, 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 85, - 115, 101, 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, - 105, 116, 115, 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, - 116, 115, 73, 0, 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, 110, - 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, 101, - 103, 101, 114, 68, 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116, - 105, 111, 110, 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103, - 101, 114, 68, 105, 103, 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114, - 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109, - 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97, - 114, 115, 101, 73, 110, 116, 101, 103, 101, 114, 79, 110, 108, 121, 73, 0, 21, 115, 101, 114, - 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 120, 114, - 0, 16, 106, 97, 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, 116, -5, -40, - -68, 18, -23, 15, 24, 67, 2, 0, 0, 120, 112, 1, 0, 127, 0, 0, 0, 0, 0, 0, - 1, 53, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 120, 0, 0, - 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 100, 0, 32, 0, 0, 0, 0, 0, 0, 0, - 6, 0, 0, 0, 2, 0, 116, 0, 1, 45, 116, 0, 1, 37, 116, 0, 1, 45, 116, 0, - 1, 37, 116, 0, 0, 113, 0, 126, 0, 8, 116, 0, 0, 116, 0, 1, 37, 112, 115, 114, - 0, 37, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, - 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 111, 108, 115, 80, - 29, 23, -103, 8, 104, -109, -100, 2, 0, 18, 67, 0, 16, 100, 101, 99, 105, 109, 97, 108, - 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, 105, 103, 105, 116, 67, 0, 11, - 101, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, 103, 114, 111, 117, 112, 105, - 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, 105, 110, 117, 115, 83, - 105, 103, 110, 67, 0, 17, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, - 116, 111, 114, 67, 0, 9, 112, 97, 100, 69, 115, 99, 97, 112, 101, 67, 0, 16, 112, 97, - 116, 116, 101, 114, 110, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 7, 112, 101, 114, - 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, 101, 110, 116, 67, 0, 8, 112, 108, 117, - 115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, - 110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 9, 122, 101, 114, 111, 68, 105, 103, 105, - 116, 76, 0, 3, 78, 97, 78, 113, 0, 126, 0, 1, 76, 0, 14, 99, 117, 114, 114, 101, - 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 1, 76, 0, 17, 101, 120, 112, - 111, 110, 101, 110, 116, 83, 101, 112, 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 1, 76, - 0, 8, 105, 110, 102, 105, 110, 105, 116, 121, 113, 0, 126, 0, 1, 76, 0, 18, 105, 110, - 116, 108, 67, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, - 1, 120, 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 46, 0, 42, 0, 59, 32, - 48, 0, 37, 0, 43, 0, 0, 0, 2, 0, 48, 116, 0, 3, -17, -65, -67, 116, 0, 1, - 36, 116, 0, 1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68, - }; - //NumberFormat.getScientificInstance(Locale.US) static byte[] scientificInstance = new byte[]{ -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, @@ -304,5 +232,291 @@ public class NumberFormatSerialTestData { 1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68, }; - final static byte[][] content = {generalInstance, currencyInstance, percentInstance, scientificInstance}; + static byte[] icu58Latest = { + -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, + 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 3, 98, -40, + 114, 48, 58, 3, 0, 36, 73, 0, 18, 80, 65, 82, 83, 69, 95, 77, 65, 88, 95, 69, 88, 80, 79, 78, + 69, 78, 84, 73, 0, 17, 99, 117, 114, 114, 101, 110, 99, 121, 83, 105, 103, 110, 67, 111, 117, + 110, 116, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, + 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 90, 0, 23, 101, 120, 112, 111, 110, 101, + 110, 116, 83, 105, 103, 110, 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 73, 0, 11, 102, + 111, 114, 109, 97, 116, 87, 105, 100, 116, 104, 66, 0, 12, 103, 114, 111, 117, 112, 105, 110, + 103, 83, 105, 122, 101, 66, 0, 13, 103, 114, 111, 117, 112, 105, 110, 103, 83, 105, 122, 101, + 50, 73, 0, 20, 109, 97, 120, 83, 105, 103, 110, 105, 102, 105, 99, 97, 110, 116, 68, 105, 103, + 105, 116, 115, 66, 0, 17, 109, 105, 110, 69, 120, 112, 111, 110, 101, 110, 116, 68, 105, 103, + 105, 116, 115, 73, 0, 20, 109, 105, 110, 83, 105, 103, 110, 105, 102, 105, 99, 97, 110, 116, 68, + 105, 103, 105, 116, 115, 73, 0, 10, 109, 117, 108, 116, 105, 112, 108, 105, 101, 114, 67, 0, 3, + 112, 97, 100, 73, 0, 11, 112, 97, 100, 80, 111, 115, 105, 116, 105, 111, 110, 90, 0, 15, 112, + 97, 114, 115, 101, 66, 105, 103, 68, 101, 99, 105, 109, 97, 108, 90, 0, 24, 112, 97, 114, 115, + 101, 82, 101, 113, 117, 105, 114, 101, 68, 101, 99, 105, 109, 97, 108, 80, 111, 105, 110, 116, + 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 73, 0, 21, 115, 101, 114, + 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 73, 0, 5, + 115, 116, 121, 108, 101, 90, 0, 22, 117, 115, 101, 69, 120, 112, 111, 110, 101, 110, 116, 105, + 97, 108, 78, 111, 116, 97, 116, 105, 111, 110, 90, 0, 20, 117, 115, 101, 83, 105, 103, 110, 105, + 102, 105, 99, 97, 110, 116, 68, 105, 103, 105, 116, 115, 76, 0, 10, 97, 116, 116, 114, 105, 98, + 117, 116, 101, 115, 116, 0, 21, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 65, 114, 114, + 97, 121, 76, 105, 115, 116, 59, 76, 0, 14, 99, 117, 114, 114, 101, 110, 99, 121, 67, 104, 111, + 105, 99, 101, 116, 0, 24, 76, 106, 97, 118, 97, 47, 116, 101, 120, 116, 47, 67, 104, 111, 105, + 99, 101, 70, 111, 114, 109, 97, 116, 59, 76, 0, 18, 99, 117, 114, 114, 101, 110, 99, 121, 80, + 108, 117, 114, 97, 108, 73, 110, 102, 111, 116, 0, 37, 76, 99, 111, 109, 47, 105, 98, 109, 47, + 105, 99, 117, 47, 116, 101, 120, 116, 47, 67, 117, 114, 114, 101, 110, 99, 121, 80, 108, 117, + 114, 97, 108, 73, 110, 102, 111, 59, 76, 0, 13, 99, 117, 114, 114, 101, 110, 99, 121, 85, 115, + 97, 103, 101, 116, 0, 41, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, + 105, 108, 47, 67, 117, 114, 114, 101, 110, 99, 121, 36, 67, 117, 114, 114, 101, 110, 99, 121, + 85, 115, 97, 103, 101, 59, 76, 0, 13, 102, 111, 114, 109, 97, 116, 80, 97, 116, 116, 101, 114, + 110, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, + 59, 76, 0, 11, 109, 97, 116, 104, 67, 111, 110, 116, 101, 120, 116, 116, 0, 30, 76, 99, 111, + 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 109, 97, 116, 104, 47, 77, 97, 116, 104, 67, 111, + 110, 116, 101, 120, 116, 59, 76, 0, 16, 110, 101, 103, 80, 114, 101, 102, 105, 120, 80, 97, 116, + 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 16, 110, 101, 103, 83, 117, 102, 102, 105, 120, + 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 14, 110, 101, 103, 97, 116, 105, 118, + 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, 14, 110, 101, 103, 97, 116, 105, + 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, 16, 112, 111, 115, 80, 114, + 101, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 16, 112, 111, + 115, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 14, + 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, + 14, 112, 111, 115, 105, 116, 105, 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 5, 76, + 0, 17, 114, 111, 117, 110, 100, 105, 110, 103, 73, 110, 99, 114, 101, 109, 101, 110, 116, 116, + 0, 22, 76, 106, 97, 118, 97, 47, 109, 97, 116, 104, 47, 66, 105, 103, 68, 101, 99, 105, 109, 97, + 108, 59, 76, 0, 7, 115, 121, 109, 98, 111, 108, 115, 116, 0, 39, 76, 99, 111, 109, 47, 105, 98, + 109, 47, 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, + 109, 97, 116, 83, 121, 109, 98, 111, 108, 115, 59, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, + 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, + 109, 97, 116, -33, -10, -77, -65, 19, 125, 7, -24, 3, 0, 14, 90, 0, 12, 103, 114, 111, 117, 112, + 105, 110, 103, 85, 115, 101, 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, + 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, + 103, 105, 116, 115, 73, 0, 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, + 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, + 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116, + 105, 111, 110, 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103, + 101, 114, 68, 105, 103, 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114, + 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109, + 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97, + 114, 115, 101, 73, 110, 116, 101, 103, 101, 114, 79, 110, 108, 121, 90, 0, 11, 112, 97, 114, + 115, 101, 83, 116, 114, 105, 99, 116, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, + 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 76, 0, 21, 99, 97, 112, 105, 116, 97, 108, + 105, 122, 97, 116, 105, 111, 110, 83, 101, 116, 116, 105, 110, 103, 116, 0, 33, 76, 99, 111, + 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 105, 115, 112, 108, 97, + 121, 67, 111, 110, 116, 101, 120, 116, 59, 76, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 116, + 0, 27, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 67, + 117, 114, 114, 101, 110, 99, 121, 59, 120, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, + 99, 117, 46, 116, 101, 120, 116, 46, 85, 70, 111, 114, 109, 97, 116, -69, 26, -15, 32, -39, 7, + 93, -64, 2, 0, 2, 76, 0, 12, 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, 101, 116, 0, 26, + 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 85, 76, 111, + 99, 97, 108, 101, 59, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, + 0, 13, 120, 114, 0, 16, 106, 97, 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, + 116, -5, -40, -68, 18, -23, 15, 24, 67, 2, 0, 0, 120, 112, 112, 112, 1, 3, 127, 0, 0, 0, 3, 0, + 0, 1, 53, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 126, 114, 0, 31, 99, 111, 109, 46, + 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 105, 115, 112, 108, 97, 121, 67, + 111, 110, 116, 101, 120, 116, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 114, 0, 14, 106, 97, 118, + 97, 46, 108, 97, 110, 103, 46, 69, 110, 117, 109, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 112, + 116, 0, 19, 67, 65, 80, 73, 84, 65, 76, 73, 90, 65, 84, 73, 79, 78, 95, 78, 79, 78, 69, 115, + 114, 0, 45, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, 108, 46, 77, + 101, 97, 115, 117, 114, 101, 85, 110, 105, 116, 36, 77, 101, 97, 115, 117, 114, 101, 85, 110, + 105, 116, 80, 114, 111, 120, 121, -55, -70, 119, -8, -15, 121, 121, -30, 12, 0, 0, 120, 112, + 119, 18, 0, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 0, 3, 85, 83, 68, 0, 0, 120, 120, 0, 0, + 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 32, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 115, 114, 0, 19, 106, 97, 118, 97, 46, + 117, 116, 105, 108, 46, 65, 114, 114, 97, 121, 76, 105, 115, 116, 120, -127, -46, 29, -103, -57, + 97, -99, 3, 0, 1, 73, 0, 4, 115, 105, 122, 101, 120, 112, 0, 0, 0, 0, 119, 4, 0, 0, 0, 0, 120, + 112, 112, 126, 114, 0, 39, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, + 108, 46, 67, 117, 114, 114, 101, 110, 99, 121, 36, 67, 117, 114, 114, 101, 110, 99, 121, 85, + 115, 97, 103, 101, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 113, 0, 126, 0, 17, 116, 0, 8, 83, 84, + 65, 78, 68, 65, 82, 68, 116, 0, 9, 35, 44, 35, 35, 48, 46, 35, 35, 35, 115, 114, 0, 28, 99, 111, + 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 109, 97, 116, 104, 46, 77, 97, 116, 104, 67, 111, + 110, 116, 101, 120, 116, 99, 105, 109, 109, 99, 49, 48, 48, 2, 0, 4, 73, 0, 6, 100, 105, 103, + 105, 116, 115, 73, 0, 4, 102, 111, 114, 109, 90, 0, 10, 108, 111, 115, 116, 68, 105, 103, 105, + 116, 115, 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 120, 112, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 116, 0, 1, 45, 116, 0, 0, 116, 0, 1, 45, 116, 0, 0, 116, 0, 0, + 113, 0, 126, 0, 31, 116, 0, 0, 116, 0, 0, 112, 115, 114, 0, 37, 99, 111, 109, 46, 105, 98, 109, + 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, + 97, 116, 83, 121, 109, 98, 111, 108, 115, 80, 29, 23, -103, 8, 104, -109, -100, 2, 0, 38, 67, 0, + 16, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, + 105, 103, 105, 116, 67, 0, 11, 101, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, + 103, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, + 105, 110, 117, 115, 83, 105, 103, 110, 67, 0, 25, 109, 111, 110, 101, 116, 97, 114, 121, 71, + 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 17, 109, + 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 112, 97, + 100, 69, 115, 99, 97, 112, 101, 67, 0, 16, 112, 97, 116, 116, 101, 114, 110, 83, 101, 112, 97, + 114, 97, 116, 111, 114, 67, 0, 7, 112, 101, 114, 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, + 101, 110, 116, 67, 0, 8, 112, 108, 117, 115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, + 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 8, 115, + 105, 103, 68, 105, 103, 105, 116, 67, 0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 116, 76, 0, + 3, 78, 97, 78, 113, 0, 126, 0, 5, 76, 0, 12, 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, + 101, 113, 0, 126, 0, 13, 76, 0, 15, 99, 117, 114, 114, 101, 110, 99, 121, 80, 97, 116, 116, 101, + 114, 110, 113, 0, 126, 0, 5, 91, 0, 19, 99, 117, 114, 114, 101, 110, 99, 121, 83, 112, 99, 65, + 102, 116, 101, 114, 83, 121, 109, 116, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, + 47, 83, 116, 114, 105, 110, 103, 59, 91, 0, 20, 99, 117, 114, 114, 101, 110, 99, 121, 83, 112, + 99, 66, 101, 102, 111, 114, 101, 83, 121, 109, 113, 0, 126, 0, 38, 76, 0, 14, 99, 117, 114, 114, + 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 5, 76, 0, 22, 100, 101, 99, 105, + 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, + 126, 0, 5, 91, 0, 12, 100, 105, 103, 105, 116, 83, 116, 114, 105, 110, 103, 115, 113, 0, 126, 0, + 38, 91, 0, 6, 100, 105, 103, 105, 116, 115, 116, 0, 2, 91, 67, 76, 0, 26, 101, 120, 112, 111, + 110, 101, 110, 116, 77, 117, 108, 116, 105, 112, 108, 105, 99, 97, 116, 105, 111, 110, 83, 105, + 103, 110, 113, 0, 126, 0, 5, 76, 0, 17, 101, 120, 112, 111, 110, 101, 110, 116, 83, 101, 112, + 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 5, 76, 0, 23, 103, 114, 111, 117, 112, 105, 110, + 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, + 76, 0, 8, 105, 110, 102, 105, 110, 105, 116, 121, 113, 0, 126, 0, 5, 76, 0, 18, 105, 110, 116, + 108, 67, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 5, 76, 0, + 11, 109, 105, 110, 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, 76, 0, 31, 109, + 111, 110, 101, 116, 97, 114, 121, 71, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, + 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, 76, 0, 23, 109, 111, 110, + 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, + 113, 0, 126, 0, 5, 76, 0, 13, 112, 101, 114, 77, 105, 108, 108, 83, 116, 114, 105, 110, 103, + 113, 0, 126, 0, 5, 76, 0, 13, 112, 101, 114, 99, 101, 110, 116, 83, 116, 114, 105, 110, 103, + 113, 0, 126, 0, 5, 76, 0, 10, 112, 108, 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, + 5, 76, 0, 15, 114, 101, 113, 117, 101, 115, 116, 101, 100, 76, 111, 99, 97, 108, 101, 116, 0, + 18, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 76, 111, 99, 97, 108, 101, 59, 76, 0, 7, + 117, 108, 111, 99, 97, 108, 101, 113, 0, 126, 0, 13, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, + 99, 97, 108, 101, 113, 0, 126, 0, 13, 120, 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 44, 0, 46, + 0, 42, 0, 59, 32, 48, 0, 37, 0, 43, 0, 0, 0, 8, 0, 64, 0, 48, 116, 0, 3, 78, 97, 78, 115, 114, + 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, 108, 46, 85, 76, + 111, 99, 97, 108, 101, 51, -114, -10, 104, 70, -48, 11, -31, 2, 0, 1, 76, 0, 8, 108, 111, 99, + 97, 108, 101, 73, 68, 113, 0, 126, 0, 5, 120, 112, 116, 0, 5, 101, 110, 95, 85, 83, 112, 117, + 114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 83, 116, 114, 105, 110, 103, + 59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0, 120, 112, 0, 0, 0, 3, 116, 0, 6, 91, 58, 94, + 83, 58, 93, 116, 0, 9, 91, 58, 100, 105, 103, 105, 116, 58, 93, 116, 0, 2, -62, -96, 117, 113, + 0, 126, 0, 46, 0, 0, 0, 3, 113, 0, 126, 0, 48, 113, 0, 126, 0, 49, 113, 0, 126, 0, 50, 116, 0, + 1, 36, 116, 0, 1, 46, 117, 113, 0, 126, 0, 46, 0, 0, 0, 10, 116, 0, 1, 48, 116, 0, 1, 49, 116, + 0, 1, 50, 116, 0, 1, 51, 116, 0, 1, 52, 116, 0, 1, 53, 116, 0, 1, 54, 116, 0, 1, 55, 116, 0, 1, + 56, 116, 0, 1, 57, 117, 114, 0, 2, 91, 67, -80, 38, 102, -80, -30, 93, -124, -84, 2, 0, 0, 120, + 112, 0, 0, 0, 10, 0, 48, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 116, 0, + 2, -61, -105, 116, 0, 1, 69, 116, 0, 1, 44, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68, + 116, 0, 1, 45, 113, 0, 126, 0, 69, 113, 0, 126, 0, 53, 116, 0, 3, -30, -128, -80, 116, 0, 1, 37, + 116, 0, 1, 43, 115, 114, 0, 16, 106, 97, 118, 97, 46, 117, 116, 105, 108, 46, 76, 111, 99, 97, + 108, 101, 126, -8, 17, 96, -100, 48, -7, -20, 3, 0, 6, 73, 0, 8, 104, 97, 115, 104, 99, 111, + 100, 101, 76, 0, 7, 99, 111, 117, 110, 116, 114, 121, 113, 0, 126, 0, 5, 76, 0, 10, 101, 120, + 116, 101, 110, 115, 105, 111, 110, 115, 113, 0, 126, 0, 5, 76, 0, 8, 108, 97, 110, 103, 117, 97, + 103, 101, 113, 0, 126, 0, 5, 76, 0, 6, 115, 99, 114, 105, 112, 116, 113, 0, 126, 0, 5, 76, 0, 7, + 118, 97, 114, 105, 97, 110, 116, 113, 0, 126, 0, 5, 120, 112, -1, -1, -1, -1, 116, 0, 2, 85, 83, + 116, 0, 0, 116, 0, 2, 101, 110, 113, 0, 126, 0, 79, 113, 0, 126, 0, 79, 120, 115, 113, 0, 126, + 0, 43, 113, 0, 126, 0, 45, 113, 0, 126, 0, 44, 120 + }; + + static byte[] newFromPattern = { + -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, + 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 3, 98, -40, + 114, 48, 58, 3, 0, 1, 73, 0, 18, 105, 99, 117, 77, 97, 116, 104, 67, 111, 110, 116, 101, 120, + 116, 70, 111, 114, 109, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, + 116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, 109, 97, 116, -33, -10, -77, + -65, 19, 125, 7, -24, 3, 0, 14, 90, 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 85, 115, 101, + 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, + 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 73, 0, + 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, + 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, + 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, + 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, + 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, + 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109, 117, 109, 73, 110, 116, + 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97, 114, 115, 101, 73, 110, + 116, 101, 103, 101, 114, 79, 110, 108, 121, 90, 0, 11, 112, 97, 114, 115, 101, 83, 116, 114, + 105, 99, 116, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, + 83, 116, 114, 101, 97, 109, 76, 0, 21, 99, 97, 112, 105, 116, 97, 108, 105, 122, 97, 116, 105, + 111, 110, 83, 101, 116, 116, 105, 110, 103, 116, 0, 33, 76, 99, 111, 109, 47, 105, 98, 109, 47, + 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 105, 115, 112, 108, 97, 121, 67, 111, 110, 116, + 101, 120, 116, 59, 76, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 116, 0, 27, 76, 99, 111, 109, + 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 67, 117, 114, 114, 101, 110, 99, + 121, 59, 120, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, + 116, 46, 85, 70, 111, 114, 109, 97, 116, -69, 26, -15, 32, -39, 7, 93, -64, 2, 0, 2, 76, 0, 12, + 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, 101, 116, 0, 26, 76, 99, 111, 109, 47, 105, 98, + 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 85, 76, 111, 99, 97, 108, 101, 59, 76, 0, 11, + 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 120, 114, 0, 16, 106, 97, + 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, 116, -5, -40, -68, 18, -23, 15, 24, + 67, 2, 0, 0, 120, 112, 112, 112, 1, 3, 40, 0, 0, 0, 3, 0, 0, 0, 40, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 2, 126, 114, 0, 31, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, + 101, 120, 116, 46, 68, 105, 115, 112, 108, 97, 121, 67, 111, 110, 116, 101, 120, 116, 0, 0, 0, + 0, 0, 0, 0, 0, 18, 0, 0, 120, 114, 0, 14, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 69, 110, + 117, 109, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 112, 116, 0, 19, 67, 65, 80, 73, 84, 65, 76, + 73, 90, 65, 84, 73, 79, 78, 95, 78, 79, 78, 69, 112, 120, 0, 0, 0, 0, 119, 4, 0, 0, 0, 0, 115, + 114, 0, 34, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 105, 109, 112, 108, 46, 110, + 117, 109, 98, 101, 114, 46, 80, 114, 111, 112, 101, 114, 116, 105, 101, 115, 56, -42, 52, -54, + -104, -87, -46, 123, 3, 0, 0, 120, 112, 119, 8, 0, 0, 0, 0, 0, 0, 0, 7, 116, 0, 12, 103, 114, + 111, 117, 112, 105, 110, 103, 83, 105, 122, 101, 115, 114, 0, 17, 106, 97, 118, 97, 46, 108, 97, + 110, 103, 46, 73, 110, 116, 101, 103, 101, 114, 18, -30, -96, -92, -9, -127, -121, 56, 2, 0, 1, + 73, 0, 5, 118, 97, 108, 117, 101, 120, 114, 0, 16, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, + 78, 117, 109, 98, 101, 114, -122, -84, -107, 29, 11, -108, -32, -117, 2, 0, 0, 120, 112, 0, 0, + 0, 3, 116, 0, 20, 109, 105, 110, 105, 109, 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, + 103, 105, 116, 115, 115, 113, 0, 126, 0, 15, 0, 0, 0, 2, 116, 0, 15, 112, 97, 100, 100, 105, + 110, 103, 76, 111, 99, 97, 116, 105, 111, 110, 126, 114, 0, 64, 99, 111, 109, 46, 105, 98, 109, + 46, 105, 99, 117, 46, 105, 109, 112, 108, 46, 110, 117, 109, 98, 101, 114, 46, 102, 111, 114, + 109, 97, 116, 116, 101, 114, 115, 46, 80, 97, 100, 100, 105, 110, 103, 70, 111, 114, 109, 97, + 116, 36, 80, 97, 100, 100, 105, 110, 103, 76, 111, 99, 97, 116, 105, 111, 110, 0, 0, 0, 0, 0, 0, + 0, 0, 18, 0, 0, 120, 113, 0, 126, 0, 9, 116, 0, 12, 65, 70, 84, 69, 82, 95, 80, 82, 69, 70, 73, + 88, 116, 0, 13, 112, 97, 100, 100, 105, 110, 103, 83, 116, 114, 105, 110, 103, 116, 0, 1, 42, + 116, 0, 12, 112, 97, 100, 100, 105, 110, 103, 87, 105, 100, 116, 104, 115, 113, 0, 126, 0, 15, + 0, 0, 0, 16, 116, 0, 21, 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120, + 80, 97, 116, 116, 101, 114, 110, 116, 0, 2, 65, 45, 116, 0, 21, 112, 111, 115, 105, 116, 105, + 118, 101, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 116, 0, 3, 98, -62, -92, + 120, 115, 114, 0, 37, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, + 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 111, 108, 115, + 80, 29, 23, -103, 8, 104, -109, -100, 2, 0, 38, 67, 0, 16, 100, 101, 99, 105, 109, 97, 108, 83, + 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, 105, 103, 105, 116, 67, 0, 11, 101, 120, + 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, 103, 114, 111, 117, 112, 105, 110, 103, + 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, 105, 110, 117, 115, 83, 105, 103, 110, + 67, 0, 25, 109, 111, 110, 101, 116, 97, 114, 121, 71, 114, 111, 117, 112, 105, 110, 103, 83, + 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 17, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, + 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 112, 97, 100, 69, 115, 99, 97, 112, 101, 67, 0, 16, + 112, 97, 116, 116, 101, 114, 110, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 7, 112, 101, + 114, 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, 101, 110, 116, 67, 0, 8, 112, 108, 117, + 115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, + 110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 8, 115, 105, 103, 68, 105, 103, 105, 116, 67, + 0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 116, 76, 0, 3, 78, 97, 78, 116, 0, 18, 76, 106, 97, + 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 12, 97, 99, 116, + 117, 97, 108, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 76, 0, 15, 99, 117, 114, 114, 101, + 110, 99, 121, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 33, 91, 0, 19, 99, 117, 114, 114, + 101, 110, 99, 121, 83, 112, 99, 65, 102, 116, 101, 114, 83, 121, 109, 116, 0, 19, 91, 76, 106, + 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 91, 0, 20, 99, 117, + 114, 114, 101, 110, 99, 121, 83, 112, 99, 66, 101, 102, 111, 114, 101, 83, 121, 109, 113, 0, + 126, 0, 34, 76, 0, 14, 99, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, + 126, 0, 33, 76, 0, 22, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, + 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 91, 0, 12, 100, 105, 103, 105, 116, 83, + 116, 114, 105, 110, 103, 115, 113, 0, 126, 0, 34, 91, 0, 6, 100, 105, 103, 105, 116, 115, 116, + 0, 2, 91, 67, 76, 0, 26, 101, 120, 112, 111, 110, 101, 110, 116, 77, 117, 108, 116, 105, 112, + 108, 105, 99, 97, 116, 105, 111, 110, 83, 105, 103, 110, 113, 0, 126, 0, 33, 76, 0, 17, 101, + 120, 112, 111, 110, 101, 110, 116, 83, 101, 112, 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 33, + 76, 0, 23, 103, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, + 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 8, 105, 110, 102, 105, 110, 105, 116, 121, + 113, 0, 126, 0, 33, 76, 0, 18, 105, 110, 116, 108, 67, 117, 114, 114, 101, 110, 99, 121, 83, + 121, 109, 98, 111, 108, 113, 0, 126, 0, 33, 76, 0, 11, 109, 105, 110, 117, 115, 83, 116, 114, + 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 31, 109, 111, 110, 101, 116, 97, 114, 121, 71, 114, + 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, + 103, 113, 0, 126, 0, 33, 76, 0, 23, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, + 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 13, 112, 101, + 114, 77, 105, 108, 108, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 13, 112, 101, + 114, 99, 101, 110, 116, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 10, 112, 108, + 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 15, 114, 101, 113, 117, 101, + 115, 116, 101, 100, 76, 111, 99, 97, 108, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 117, 116, + 105, 108, 47, 76, 111, 99, 97, 108, 101, 59, 76, 0, 7, 117, 108, 111, 99, 97, 108, 101, 113, 0, + 126, 0, 5, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 120, + 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 44, 0, 46, 0, 42, 0, 59, 32, 48, 0, 37, 0, 43, 0, 0, + 0, 8, 0, 64, 0, 48, 116, 0, 3, 78, 97, 78, 115, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, + 105, 99, 117, 46, 117, 116, 105, 108, 46, 85, 76, 111, 99, 97, 108, 101, 51, -114, -10, 104, 70, + -48, 11, -31, 2, 0, 1, 76, 0, 8, 108, 111, 99, 97, 108, 101, 73, 68, 113, 0, 126, 0, 33, 120, + 112, 116, 0, 5, 101, 110, 95, 85, 83, 112, 117, 114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108, + 97, 110, 103, 46, 83, 116, 114, 105, 110, 103, 59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0, + 120, 112, 0, 0, 0, 3, 116, 0, 6, 91, 58, 94, 83, 58, 93, 116, 0, 9, 91, 58, 100, 105, 103, 105, + 116, 58, 93, 116, 0, 2, -62, -96, 117, 113, 0, 126, 0, 42, 0, 0, 0, 3, 113, 0, 126, 0, 44, 113, + 0, 126, 0, 45, 113, 0, 126, 0, 46, 116, 0, 1, 36, 116, 0, 1, 46, 117, 113, 0, 126, 0, 42, 0, 0, + 0, 10, 116, 0, 1, 48, 116, 0, 1, 49, 116, 0, 1, 50, 116, 0, 1, 51, 116, 0, 1, 52, 116, 0, 1, 53, + 116, 0, 1, 54, 116, 0, 1, 55, 116, 0, 1, 56, 116, 0, 1, 57, 117, 114, 0, 2, 91, 67, -80, 38, + 102, -80, -30, 93, -124, -84, 2, 0, 0, 120, 112, 0, 0, 0, 10, 0, 48, 0, 49, 0, 50, 0, 51, 0, 52, + 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 116, 0, 2, -61, -105, 116, 0, 1, 69, 116, 0, 1, 44, 116, 0, + 3, -30, -120, -98, 116, 0, 3, 85, 83, 68, 116, 0, 1, 45, 113, 0, 126, 0, 65, 113, 0, 126, 0, 49, + 116, 0, 3, -30, -128, -80, 116, 0, 1, 37, 116, 0, 1, 43, 115, 114, 0, 16, 106, 97, 118, 97, 46, + 117, 116, 105, 108, 46, 76, 111, 99, 97, 108, 101, 126, -8, 17, 96, -100, 48, -7, -20, 3, 0, 6, + 73, 0, 8, 104, 97, 115, 104, 99, 111, 100, 101, 76, 0, 7, 99, 111, 117, 110, 116, 114, 121, 113, + 0, 126, 0, 33, 76, 0, 10, 101, 120, 116, 101, 110, 115, 105, 111, 110, 115, 113, 0, 126, 0, 33, + 76, 0, 8, 108, 97, 110, 103, 117, 97, 103, 101, 113, 0, 126, 0, 33, 76, 0, 6, 115, 99, 114, 105, + 112, 116, 113, 0, 126, 0, 33, 76, 0, 7, 118, 97, 114, 105, 97, 110, 116, 113, 0, 126, 0, 33, + 120, 112, -1, -1, -1, -1, 116, 0, 2, 85, 83, 116, 0, 0, 116, 0, 2, 101, 110, 113, 0, 126, 0, 75, + 113, 0, 126, 0, 75, 120, 115, 113, 0, 126, 0, 39, 113, 0, 126, 0, 41, 113, 0, 126, 0, 40, 120 + }; + + final static byte[][] content = { + generalInstance, + currencyInstance, + scientificInstance, + icu58Latest, + newFromPattern + }; } 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 897846b2be8..9c940fafc7f 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); - symbols.setDecimalSeparatorString("~~"); - symbols.setGroupingSeparatorString("^^"); - DecimalFormat fmt = new DecimalFormat("#,##0.0#", symbols); + expect2(fmt, 1234567.89, "(1),(2)(3)(4),(5)(6)(7).(8)(9)"); + + // Scientific notation should work. + fmt.applyPattern("@@@E0"); + expect2(fmt, 1230000, "(1).(2)(3)E(6)"); - expect2(fmt, 1234567.89, "(1)^^(2)(3)(4)^^(5)(6)(7)~~(8)(9)"); + // Grouping and decimal with multiple code points are not supported during parsing. + symbols.setDecimalSeparatorString("~~"); + symbols.setGroupingSeparatorString("^^"); + fmt.setDecimalFormatSymbols(symbols); + fmt.applyPattern("#,##0.0#"); + assertEquals("Custom decimal and grouping separator string with multiple characters", + fmt.format(1234567.89), "(1)^^(2)(3)(4)^^(5)(6)(7)~~(8)(9)"); + + // Digits starting at U+1D7CE MATHEMATICAL BOLD DIGIT ZERO + // These are all single code points, so parsing will work. + for (int i=0; i<10; i++) customDigits[i] = new String(Character.toChars(0x1D7CE+i)); + symbols.setDigitStrings(customDigits); + symbols.setDecimalSeparatorString("😁"); + symbols.setGroupingSeparatorString("😎"); + fmt.setDecimalFormatSymbols(symbols); + expect2(fmt, 1234.56, "𝟏😎𝟐𝟑𝟒😁𝟓𝟔"); } @Test public void TestArabicCurrencyPatternInfo() { ULocale arLocale = new ULocale("ar"); - + DecimalFormatSymbols symbols = new DecimalFormatSymbols(arLocale); String currSpacingPatn = symbols.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, true); if (currSpacingPatn==null || currSpacingPatn.length() == 0) { errln("locale ar, getPatternForCurrencySpacing returns null or 0-length string"); } - + DecimalFormat currAcctFormat = (DecimalFormat)NumberFormat.getInstance(arLocale, NumberFormat.ACCOUNTINGCURRENCYSTYLE); String currAcctPatn = currAcctFormat.toPattern(); if (currAcctPatn==null || currAcctPatn.length() == 0) { errln("locale ar, toPattern for ACCOUNTINGCURRENCYSTYLE returns null or 0-length string"); } } + + @Test + public void Test10436() { + DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(Locale.ENGLISH); + df.setRoundingMode(MathContext.ROUND_CEILING); + df.setMinimumFractionDigits(0); + df.setMaximumFractionDigits(0); + assertEquals("-.99 should round toward infinity", "-0", df.format(-0.99)); + } + + @Test + public void Test10765() { + NumberFormat fmt = NumberFormat.getInstance(new ULocale("en")); + fmt.setMinimumIntegerDigits(10); + FieldPosition pos = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR); + fmt.format(1234, new StringBuffer(), pos); + assertEquals("FieldPosition should report the first occurence", 1, pos.getBeginIndex()); + assertEquals("FieldPosition should report the first occurence", 2, pos.getEndIndex()); + } + + @Test + public void Test10997() { + NumberFormat fmt = NumberFormat.getCurrencyInstance(new ULocale("en-US")); + fmt.setMinimumFractionDigits(4); + fmt.setMaximumFractionDigits(4); + String str1 = fmt.format(new CurrencyAmount(123.45, Currency.getInstance("USD"))); + String str2 = fmt.format(new CurrencyAmount(123.45, Currency.getInstance("EUR"))); + assertEquals("minFrac 4 should be respected in default currency", "$123.4500", str1); + assertEquals("minFrac 4 should be respected in different currency", "€123.4500", str2); + } + + @Test + public void Test11020() { + DecimalFormatSymbols sym = new DecimalFormatSymbols(ULocale.FRANCE); + DecimalFormat fmt = new DecimalFormat("0.05E0", sym); + String result = fmt.format(12301.2).replace('\u00a0', ' '); + assertEquals("Rounding increment should be applied after magnitude scaling", "1,25E4", result); + } + + @Test + public void Test11025() { + String pattern = "¤¤ **####0.00"; + DecimalFormatSymbols sym = new DecimalFormatSymbols(ULocale.FRANCE); + DecimalFormat fmt = new DecimalFormat(pattern, sym); + String result = fmt.format(433.0); + assertEquals("Number should be padded to 11 characters", "EUR *433,00", result); + } + + @Test + public void Test11640() { + DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(); + df.applyPattern("¤¤¤ 0"); + String result = df.getPositivePrefix(); + assertEquals("Triple-currency should give long name on getPositivePrefix", "US dollars ", result); + } + + @Test + public void Test11645() { + String pattern = "#,##0.0#"; + DecimalFormat fmt = (DecimalFormat) NumberFormat.getInstance(); + fmt.applyPattern(pattern); + DecimalFormat fmtCopy; + + final int newMultiplier = 37; + fmtCopy = (DecimalFormat) fmt.clone(); + assertNotEquals("Value before setter", fmtCopy.getMultiplier(), newMultiplier); + fmtCopy.setMultiplier(newMultiplier); + assertEquals("Value after setter", fmtCopy.getMultiplier(), newMultiplier); + fmtCopy.applyPattern(pattern); + assertEquals("Value after applyPattern", fmtCopy.getMultiplier(), newMultiplier); + assertFalse("multiplier", fmt.equals(fmtCopy)); + + final int newRoundingMode = RoundingMode.CEILING.ordinal(); + fmtCopy = (DecimalFormat) fmt.clone(); + assertNotEquals("Value before setter", fmtCopy.getRoundingMode(), newRoundingMode); + fmtCopy.setRoundingMode(newRoundingMode); + assertEquals("Value after setter", fmtCopy.getRoundingMode(), newRoundingMode); + fmtCopy.applyPattern(pattern); + assertEquals("Value after applyPattern", fmtCopy.getRoundingMode(), newRoundingMode); + assertFalse("roundingMode", fmt.equals(fmtCopy)); + + final Currency newCurrency = Currency.getInstance("EAT"); + fmtCopy = (DecimalFormat) fmt.clone(); + assertNotEquals("Value before setter", fmtCopy.getCurrency(), newCurrency); + fmtCopy.setCurrency(newCurrency); + assertEquals("Value after setter", fmtCopy.getCurrency(), newCurrency); + fmtCopy.applyPattern(pattern); + assertEquals("Value after applyPattern", fmtCopy.getCurrency(), newCurrency); + assertFalse("currency", fmt.equals(fmtCopy)); + + final CurrencyUsage newCurrencyUsage = CurrencyUsage.CASH; + fmtCopy = (DecimalFormat) fmt.clone(); + assertNotEquals("Value before setter", fmtCopy.getCurrencyUsage(), newCurrencyUsage); + fmtCopy.setCurrencyUsage(CurrencyUsage.CASH); + assertEquals("Value after setter", fmtCopy.getCurrencyUsage(), newCurrencyUsage); + fmtCopy.applyPattern(pattern); + assertEquals("Value after applyPattern", fmtCopy.getCurrencyUsage(), newCurrencyUsage); + assertFalse("currencyUsage", fmt.equals(fmtCopy)); + } + + @Test + public void Test11646() { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(new ULocale("en_US")); + String pattern = "\u00a4\u00a4\u00a4 0.00 %\u00a4\u00a4"; + DecimalFormat fmt = new DecimalFormat(pattern, symbols); + + // Test equality with affixes. set affix methods can't capture special + // characters which is why equality should fail. + { + DecimalFormat fmtCopy = (DecimalFormat) fmt.clone(); + assertEquals("", fmt, fmtCopy); + fmtCopy.setPositivePrefix(fmtCopy.getPositivePrefix()); + assertNotEquals("", fmt, fmtCopy); + } + { + DecimalFormat fmtCopy = (DecimalFormat) fmt.clone(); + assertEquals("", fmt, fmtCopy); + fmtCopy.setPositiveSuffix(fmtCopy.getPositiveSuffix()); + assertNotEquals("", fmt, fmtCopy); + } + { + DecimalFormat fmtCopy = (DecimalFormat) fmt.clone(); + assertEquals("", fmt, fmtCopy); + fmtCopy.setNegativePrefix(fmtCopy.getNegativePrefix()); + assertNotEquals("", fmt, fmtCopy); + } + { + DecimalFormat fmtCopy = (DecimalFormat) fmt.clone(); + assertEquals("", fmt, fmtCopy); + fmtCopy.setNegativeSuffix(fmtCopy.getNegativeSuffix()); + assertNotEquals("", fmt, fmtCopy); + } + } + + @Test + public void Test11648() { + DecimalFormat df = new DecimalFormat("0.00"); + df.setScientificNotation(true); + String pat = df.toPattern(); + assertEquals("A valid scientific notation pattern should be produced", "0.00E0", pat); + } + + @Test + public void Test11649() { + String pattern = "\u00a4\u00a4\u00a4 0.00"; + DecimalFormat fmt = new DecimalFormat(pattern); + fmt.setCurrency(Currency.getInstance("USD")); + assertEquals("Triple currency sign should format long name", "US dollars 12.34", fmt.format(12.34)); + + String newPattern = fmt.toPattern(); + assertEquals("Should produce a valid pattern", pattern, newPattern); + + DecimalFormat fmt2 = new DecimalFormat(newPattern); + fmt2.setCurrency(Currency.getInstance("USD")); + assertEquals("Triple currency sign pattern should round-trip", "US dollars 12.34", fmt2.format(12.34)); + } + + @Test + public void Test11686() { + DecimalFormat df = new DecimalFormat(); + df.setPositiveSuffix("0K"); + df.setNegativeSuffix("0N"); + expect2(df, 123, "1230K"); + expect2(df, -123, "1230N"); + } + + @Test + public void Test11839() { + DecimalFormatSymbols dfs = new DecimalFormatSymbols(ULocale.ENGLISH); + dfs.setMinusSign('∸'); + dfs.setPlusSign('∔'); // ∔ U+2214 DOT PLUS + DecimalFormat df = new DecimalFormat("0.00+;0.00-", dfs); + String result = df.format(-1.234); + assertEquals("Locale-specific minus sign should be used", "1.23∸", result); + result = df.format(1.234); + assertEquals("Locale-specific plus sign should be used", "1.23∔", result); + } + + @Test + public void Test12753() { + ULocale locale = new ULocale("en-US"); + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); + symbols.setDecimalSeparator('*'); + DecimalFormat df = new DecimalFormat("0.00", symbols); + df.setDecimalPatternMatchRequired(true); + try { + df.parse("123"); + fail("Parsing integer succeeded even though setDecimalPatternMatchRequired was set"); + } catch (ParseException e) { + // Parse failed (expected) + } + } + + @Test + public void Test12962() { + String pat = "**0.00"; + DecimalFormat df = new DecimalFormat(pat); + String newPat = df.toPattern(); + assertEquals("Format width changed upon calling applyPattern", pat.length(), newPat.length()); + } + + @Test + public void Test10354() { + DecimalFormatSymbols dfs = new DecimalFormatSymbols(); + dfs.setNaN(""); + DecimalFormat df = new DecimalFormat(); + df.setDecimalFormatSymbols(dfs); + try { + df.formatToCharacterIterator(Double.NaN); + // pass + } catch (IllegalArgumentException e) { + throw new AssertionError(e); + } + } + + @Test + public void Test11913() { + NumberFormat df = DecimalFormat.getInstance(); + String result = df.format(new BigDecimal("1.23456789E400")); + assertEquals("Should format more than 309 digits", "12,345,678", result.substring(0, 10)); + assertEquals("Should format more than 309 digits", 534, result.length()); + } + + @Test + public void Test12045() { + if (logKnownIssue("12045", "XSU is missing from fr")) { return; } + + NumberFormat nf = NumberFormat.getInstance(new ULocale("fr"), NumberFormat.PLURALCURRENCYSTYLE); + ParsePosition ppos = new ParsePosition(0); + try { + CurrencyAmount result = nf.parseCurrency("2,34 XSU", ppos); + assertEquals("Parsing should succeed on XSU", + new CurrencyAmount(2.34, Currency.getInstance("XSU")), result); + // pass + } catch (Exception e) { + throw new AssertionError("Should have been able to parse XSU", e); + } + } + + @Test + public void Test11739() { + NumberFormat nf = NumberFormat.getCurrencyInstance(new ULocale("sr_BA")); + ((DecimalFormat) nf).applyPattern("0.0 ¤¤¤"); + ParsePosition ppos = new ParsePosition(0); + CurrencyAmount result = nf.parseCurrency("1.500 амерички долар", ppos); + assertEquals("Should parse to 1500 USD", new CurrencyAmount(1500, Currency.getInstance("USD")), result); + } + + @Test + public void Test11647() { + DecimalFormat df = new DecimalFormat(); + df.applyPattern("¤¤¤¤#"); + String actual = df.format(123); + assertEquals("Should replace 4 currency signs with U+FFFD", "\uFFFD123", actual); + } + + @Test + public void Test12567() { + DecimalFormat df1 = (DecimalFormat) NumberFormat.getInstance(NumberFormat.PLURALCURRENCYSTYLE); + DecimalFormat df2 = (DecimalFormat) NumberFormat.getInstance(NumberFormat.NUMBERSTYLE); + df2.setCurrency(df1.getCurrency()); + df2.setCurrencyPluralInfo(df1.getCurrencyPluralInfo()); + df1.applyPattern("0.00"); + df2.applyPattern("0.00"); + assertEquals("df1 == df2", df1, df2); + assertEquals("df2 == df1", df2, df1); + df2.setPositivePrefix("abc"); + assertNotEquals("df1 != df2", df1, df2); + assertNotEquals("df2 != df1", df2, df1); + } + + @Test + public void testPercentZero() { + DecimalFormat df = (DecimalFormat) NumberFormat.getPercentInstance(); + String actual = df.format(0); + assertEquals("Should have one zero digit", "0%", actual); + } + + @Test + public void testCurrencyZeroRounding() { + DecimalFormat df = (DecimalFormat) NumberFormat.getCurrencyInstance(); + df.setMaximumFractionDigits(0); + String actual = df.format(0); + assertEquals("Should have zero fraction digits", "$0", actual); + } + + @Test + public void testCustomCurrencySymbol() { + DecimalFormat df = (DecimalFormat) NumberFormat.getCurrencyInstance(); + df.setCurrency(Currency.getInstance("USD")); + DecimalFormatSymbols symbols = df.getDecimalFormatSymbols(); + symbols.setCurrencySymbol("#"); + df.setDecimalFormatSymbols(symbols); + String actual = df.format(123); + assertEquals("Should use '#' instad of '$'", "#123.00", actual); + } + + @Test + public void TestBasicSerializationRoundTrip() throws IOException, ClassNotFoundException { + DecimalFormat df0 = new DecimalFormat("A-**#####,#00.00b¤"); + + // Write to byte stream + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(df0); + oos.flush(); + baos.close(); + byte[] bytes = baos.toByteArray(); + + // Read from byte stream + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + Object obj = ois.readObject(); + ois.close(); + DecimalFormat df1 = (DecimalFormat) obj; + + // Test equality + assertEquals("Did not round-trip through serialization", df0, df1); + + // Test basic functionality + String str0 = df0.format(12345.67); + String str1 = df1.format(12345.67); + assertEquals("Serialized formatter does not produce same output", str0, str1); + } + + @Test + public void testGetSetCurrency() { + DecimalFormat df = new DecimalFormat("¤#"); + assertEquals("Currency should start out null", null, df.getCurrency()); + Currency curr = Currency.getInstance("EUR"); + df.setCurrency(curr); + assertEquals("Currency should equal EUR after set", curr, df.getCurrency()); + String result = df.format(123); + assertEquals("Currency should format as expected in EUR", "€123.00", result); + } + + @Test + public void testRoundingModeSetters() { + DecimalFormat df1 = new DecimalFormat(); + DecimalFormat df2 = new DecimalFormat(); + + df1.setRoundingMode(java.math.BigDecimal.ROUND_CEILING); + assertNotEquals("Rounding mode was set to a non-default", df1, df2); + df2.setRoundingMode(com.ibm.icu.math.BigDecimal.ROUND_CEILING); + assertEquals("Rounding mode from icu.math and java.math should be the same", df1, df2); + df2.setRoundingMode(java.math.RoundingMode.CEILING.ordinal()); + assertEquals("Rounding mode ordinal from java.math.RoundingMode should be the same", df1, df2); + } + + @Test + public void testSignificantDigitsMode() { + String[][] allExpected = { + {"12340.0", "12340.0", "12340.0"}, + {"1234.0", "1234.0", "1234.0"}, + {"123.4", "123.4", "123.4"}, + {"12.34", "12.34", "12.34"}, + {"1.234", "1.23", "1.23"}, + {"0.1234", "0.12", "0.123"}, + {"0.01234", "0.01", "0.0123"}, + {"0.001234", "0.00", "0.00123"} + }; + + DecimalFormat df = new DecimalFormat(); + df.setMinimumFractionDigits(1); + df.setMaximumFractionDigits(2); + df.setMinimumSignificantDigits(3); + df.setMaximumSignificantDigits(4); + df.setGroupingUsed(false); + + SignificantDigitsMode[] modes = new SignificantDigitsMode[] { + SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION, + SignificantDigitsMode.RESPECT_MAXIMUM_FRACTION, + SignificantDigitsMode.ENSURE_MINIMUM_SIGNIFICANT + }; + + for (double d = 12340.0, i=0; d > 0.001; d /= 10, i++) { + for (int j=0; j " + 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 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/format/TestMessageFormat.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java index 9160b960dba..08dc349e27d 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java @@ -883,8 +883,9 @@ public class TestMessageFormat extends com.ibm.icu.dev.test.TestFmwk { errln("parsed argument " + parsedArgs[0] + " != " + num); } } - catch (Exception e) { - errln("parse of '" + result + " returned exception: " + e.getMessage()); + catch (ParseException e) { + errln("parse of '" + result + "' returned exception: " + + e.getMessage() + " " + e.getErrorOffset()); } } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/AffixPatternUtilsTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/AffixPatternUtilsTest.java new file mode 100644 index 00000000000..5472c2ec560 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/AffixPatternUtilsTest.java @@ -0,0 +1,126 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.numbers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import com.ibm.icu.impl.number.AffixPatternUtils; +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.util.ULocale; + +public class AffixPatternUtilsTest { + + @Test + public void testEscape() { + Object[][] cases = { + {"", ""}, + {"abc", "abc"}, + {"-", "'-'"}, + {"-!", "'-'!"}, + {"−", "−"}, + {"---", "'---'"}, + {"-%-", "'-%-'"}, + {"'", "''"}, + {"-'", "'-'''"}, + {"-'-", "'-''-'"}, + {"a-'-", "a'-''-'"} + }; + + StringBuilder sb = new StringBuilder(); + for (Object[] cas : cases) { + String input = (String) cas[0]; + String expected = (String) cas[1]; + sb.setLength(0); + AffixPatternUtils.escape(input, sb); + assertEquals(expected, sb.toString()); + } + } + + @Test + public void testUnescape() { + Object[][] cases = { + {"", false, 0, ""}, + {"abc", false, 3, "abc"}, + {"-", false, 1, "−"}, + {"-!", false, 2, "−!"}, + {"+", false, 1, "\u061C+"}, + {"+!", false, 2, "\u061C+!"}, + {"‰", false, 1, "؉"}, + {"‰!", false, 2, "؉!"}, + {"-x", false, 2, "−x"}, + {"'-'x", false, 2, "-x"}, + {"'--''-'-x", false, 6, "--'-−x"}, + {"''", false, 1, "'"}, + {"''''", false, 2, "''"}, + {"''''''", false, 3, "'''"}, + {"''x''", false, 3, "'x'"}, + {"¤", true, 1, "$"}, + {"¤¤", true, 2, "XXX"}, + {"¤¤¤", true, 3, "long name"}, + {"¤¤¤¤", true, 4, "\uFFFD"}, + {"¤¤¤¤¤", true, 5, "\uFFFD"}, + {"¤¤¤a¤¤¤¤", true, 8, "long namea\uFFFD"}, + {"a¤¤¤¤b¤¤¤¤¤c", true, 12, "a\uFFFDb\uFFFDc"}, + {"¤!", true, 2, "$!"}, + {"¤¤!", true, 3, "XXX!"}, + {"¤¤¤!", true, 4, "long name!"}, + {"-¤¤", true, 3, "−XXX"}, + {"¤¤-", true, 3, "XXX−"}, + {"'¤'", false, 1, "¤"}, + {"%", false, 1, "٪\u061C"}, + {"'%'", false, 1, "%"}, + {"¤'-'%", true, 3, "$-٪\u061C"} + }; + + // ar_SA has an interesting percent sign and various Arabic letter marks + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("ar_SA")); + NumberStringBuilder sb = new NumberStringBuilder(); + + for (Object[] cas : cases) { + String input = (String) cas[0]; + boolean curr = (Boolean) cas[1]; + int length = (Integer) cas[2]; + String output = (String) cas[3]; + + assertEquals( + "Currency on <" + input + ">", curr, AffixPatternUtils.hasCurrencySymbols(input)); + assertEquals("Length on <" + input + ">", length, AffixPatternUtils.unescapedLength(input)); + + sb.clear(); + AffixPatternUtils.unescape(input, symbols, "$", "XXX", "long name", "−", sb); + assertEquals("Output on <" + input + ">", output, sb.toString()); + } + } + + @Test + public void testInvalid() { + String[] invalidExamples = {"'", "x'", "'x", "'x''", "''x'"}; + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("en_US")); + NumberStringBuilder sb = new NumberStringBuilder(); + + for (String str : invalidExamples) { + try { + AffixPatternUtils.hasCurrencySymbols(str); + fail("No exception was thrown on an invalid string"); + } catch (IllegalArgumentException e) { + // OK + } + try { + AffixPatternUtils.unescapedLength(str); + fail("No exception was thrown on an invalid string"); + } catch (IllegalArgumentException e) { + // OK + } + try { + AffixPatternUtils.unescape(str, symbols, "$", "XXX", "long name", "−", sb); + fail("No exception was thrown on an invalid string"); + } catch (IllegalArgumentException e) { + // OK + } + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/FormatQuantityTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/FormatQuantityTest.java new file mode 100644 index 00000000000..d5c5d8432d4 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/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.numbers; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.Test; + +import com.ibm.icu.dev.test.TestFmwk; +import com.ibm.icu.impl.number.Endpoint; +import com.ibm.icu.impl.number.Format; +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.FormatQuantity1; +import com.ibm.icu.impl.number.FormatQuantity2; +import com.ibm.icu.impl.number.FormatQuantity3; +import com.ibm.icu.impl.number.FormatQuantity4; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; + +/** TODO: This is a temporary name for this class. Suggestions for a better name? */ +public class FormatQuantityTest extends TestFmwk { + + @Test + public void testBehavior() throws ParseException { + + // Make a list of several formatters to test the behavior of FormatQuantity. + List 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/numbers/NumberStringBuilderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/NumberStringBuilderTest.java new file mode 100644 index 00000000000..c4178e29101 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/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.numbers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.text.FieldPosition; +import java.text.Format.Field; + +import org.junit.Test; + +import com.ibm.icu.impl.number.NumberStringBuilder; +import com.ibm.icu.text.NumberFormat; + +/** @author sffc */ +public class NumberStringBuilderTest { + private static final String[] EXAMPLE_STRINGS = { + "", + "xyz", + "The quick brown fox jumps over the lazy dog", + "😁", + "mixed 😇 and ASCII", + "with combining characters like 🇦🇧🇨🇩" + }; + + @Test + public void testInsertAppendCharSequence() { + + StringBuilder sb1 = new StringBuilder(); + NumberStringBuilder sb2 = new NumberStringBuilder(); + for (String str : EXAMPLE_STRINGS) { + NumberStringBuilder sb3 = new NumberStringBuilder(); + sb1.append(str); + sb2.append(str, null); + sb3.append(str, null); + assertCharSequenceEquals(sb1, sb2); + assertCharSequenceEquals(sb3, str); + + StringBuilder sb4 = new StringBuilder(); + NumberStringBuilder sb5 = new NumberStringBuilder(); + sb4.append("😇"); + sb4.append(str); + sb4.append("xx"); + sb5.append("😇xx", null); + sb5.insert(2, str, null); + assertCharSequenceEquals(sb4, sb5); + + int start = Math.min(1, str.length()); + int end = Math.min(10, str.length()); + sb4.insert(3, str, start, end); + sb5.insert(3, str, start, end, null); + assertCharSequenceEquals(sb4, sb5); + + sb4.append(str.toCharArray()); + sb5.append(str.toCharArray(), null); + assertCharSequenceEquals(sb4, sb5); + + sb4.insert(4, str.toCharArray()); + sb5.insert(4, str.toCharArray(), null); + assertCharSequenceEquals(sb4, sb5); + } + } + + @Test + public void testInsertAppendCodePoint() { + int[] cases = {0, 1, 60, 127, 128, 0x7fff, 0x8000, 0xffff, 0x10000, 0x1f000, 0x10ffff}; + + StringBuilder sb1 = new StringBuilder(); + NumberStringBuilder sb2 = new NumberStringBuilder(); + for (int cas : cases) { + NumberStringBuilder sb3 = new NumberStringBuilder(); + sb1.appendCodePoint(cas); + sb2.appendCodePoint(cas, null); + sb3.appendCodePoint(cas, null); + assertCharSequenceEquals(sb1, sb2); + assertEquals(Character.codePointAt(sb3, 0), cas); + + StringBuilder sb4 = new StringBuilder(); + NumberStringBuilder sb5 = new NumberStringBuilder(); + sb4.append("😇"); + sb4.appendCodePoint(cas); // Java StringBuilder has no insertCodePoint() + sb4.append("xx"); + sb5.append("😇xx", null); + sb5.insertCodePoint(2, cas, null); + assertCharSequenceEquals(sb4, sb5); + } + } + + @Test + public void testFields() { + for (String str : EXAMPLE_STRINGS) { + NumberStringBuilder sb = new NumberStringBuilder(); + sb.append(str, null); + sb.append(str, NumberFormat.Field.CURRENCY); + Field[] fields = sb.toFieldArray(); + assertEquals(str.length() * 2, fields.length); + for (int i = 0; i < str.length(); i++) { + assertEquals(null, fields[i]); + assertEquals(NumberFormat.Field.CURRENCY, fields[i + str.length()]); + } + + // Very basic FieldPosition test. More robust tests happen in NumberFormatTest. + // Let NumberFormatTest also take care of AttributedCharacterIterator material. + FieldPosition fp = new FieldPosition(NumberFormat.Field.CURRENCY); + sb.populateFieldPosition(fp, 0); + assertEquals(str.length(), fp.getBeginIndex()); + assertEquals(str.length() * 2, fp.getEndIndex()); + + if (str.length() > 0) { + sb.insertCodePoint(2, 100, NumberFormat.Field.INTEGER); + fields = sb.toFieldArray(); + assertEquals(str.length() * 2 + 1, fields.length); + assertEquals(fields[2], NumberFormat.Field.INTEGER); + } + + sb.append(sb.clone()); + sb.append(sb.toCharArray(), sb.toFieldArray()); + int numNull = 0; + int numCurr = 0; + int numInt = 0; + Field[] oldFields = fields; + fields = sb.toFieldArray(); + for (int i = 0; i < sb.length(); i++) { + assertEquals(oldFields[i % oldFields.length], fields[i]); + if (fields[i] == null) numNull++; + else if (fields[i] == NumberFormat.Field.CURRENCY) numCurr++; + else if (fields[i] == NumberFormat.Field.INTEGER) numInt++; + else throw new AssertionError("Encountered unknown field in " + str); + } + assertEquals(str.length() * 4, numNull); + assertEquals(numNull, numCurr); + assertEquals(str.length() > 0 ? 4 : 0, numInt); + + NumberStringBuilder sb2 = new NumberStringBuilder(); + sb2.append(sb); + assertTrue(sb.contentEquals(sb2)); + assertTrue(sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray())); + + sb2.insertCodePoint(0, 50, NumberFormat.Field.FRACTION); + assertTrue(!sb.contentEquals(sb2)); + assertTrue(!sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray())); + } + } + + @Test + public void testUnlimitedCapacity() { + NumberStringBuilder builder = new NumberStringBuilder(); + // The builder should never fail upon repeated appends. + for (int i = 0; i < 1000; i++) { + assertEquals(builder.length(), i); + builder.appendCodePoint('x', null); + assertEquals(builder.length(), i + 1); + } + } + + private static void assertCharSequenceEquals(CharSequence a, CharSequence b) { + assertEquals(a.toString(), b.toString()); + + assertEquals(a.length(), b.length()); + for (int i = 0; i < a.length(); i++) { + assertEquals(a.charAt(i), b.charAt(i)); + } + + int start = Math.min(2, a.length()); + int end = Math.min(12, a.length()); + if (start != end) { + assertCharSequenceEquals(a.subSequence(start, end), b.subSequence(start, end)); + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PatternStringTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PatternStringTest.java new file mode 100644 index 00000000000..d53de8903e2 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/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.numbers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.util.ULocale; + +/** @author sffc */ +public class PatternStringTest { + + @Test + public void testLocalized() { + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH); + symbols.setDecimalSeparatorString("a"); + symbols.setPercentString("b"); + symbols.setMinusSignString("."); + symbols.setPlusSignString("'"); + + String standard = "+-abcb''a''#,##0.0%'a%'"; + String localized = "’.'ab'c'b''a'''#,##0a0b'a%'"; + String toStandard = "+-'ab'c'b''a'''#,##0.0%'a%'"; + + assertEquals(localized, PatternString.convertLocalized(standard, symbols, true)); + assertEquals(toStandard, PatternString.convertLocalized(localized, symbols, false)); + } + + @Test + public void testToPatternSimple() { + String[][] cases = { + {"#", "0"}, + {"0", "0"}, + {"#0", "0"}, + {"###", "0"}, + {"0.##", "0.##"}, + {"0.00", "0.00"}, + {"0.00#", "0.00#"}, + {"#E0", "#E0"}, + {"0E0", "0E0"}, + {"#00E00", "#00E00"}, + {"#,##0", "#,##0"}, + {"#,##0E0", "#,##0E0"}, + {"#;#", "0;0"}, + {"#;-#", "0"}, // ignore a negative prefix pattern of '-' since that is the default + {"**##0", "**##0"}, + {"*'x'##0", "*x##0"}, + {"a''b0", "a''b0"}, + {"*''##0", "*''##0"}, + {"*📺##0", "*'📺'##0"}, + {"*'நி'##0", "*'நி'##0"}, + }; + + for (String[] cas : cases) { + String input = cas[0]; + String output = cas[1]; + + Properties properties = PatternString.parseToProperties(input); + String actual = PatternString.propertiesToString(properties); + assertEquals( + "Failed on input pattern '" + input + "', properties " + properties, output, actual); + } + } + + @Test + public void testToPatternWithProperties() { + Object[][] cases = { + {new Properties().setPositivePrefix("abc"), "abc#"}, + {new Properties().setPositiveSuffix("abc"), "#abc"}, + {new Properties().setPositivePrefixPattern("abc"), "abc#"}, + {new Properties().setPositiveSuffixPattern("abc"), "#abc"}, + {new Properties().setNegativePrefix("abc"), "#;abc#"}, + {new Properties().setNegativeSuffix("abc"), "#;#abc"}, + {new Properties().setNegativePrefixPattern("abc"), "#;abc#"}, + {new Properties().setNegativeSuffixPattern("abc"), "#;#abc"}, + {new Properties().setPositivePrefix("+"), "'+'#"}, + {new Properties().setPositivePrefixPattern("+"), "+#"}, + {new Properties().setPositivePrefix("+'"), "'+'''#"}, + {new Properties().setPositivePrefix("'+"), "'''+'#"}, + {new Properties().setPositivePrefix("'"), "''#"}, + {new Properties().setPositivePrefixPattern("+''"), "+''#"}, + }; + + for (Object[] cas : cases) { + Properties input = (Properties) cas[0]; + String output = (String) cas[1]; + + String actual = PatternString.propertiesToString(input); + assertEquals("Failed on input properties " + input, output, actual); + } + } + + @Test + public void testExceptionOnInvalid() { + String[] invalidPatterns = {"#.#.#", "0#", "0#.", ".#0", "0#.#0", "@0", "0@"}; + + for (String pattern : invalidPatterns) { + try { + PatternString.parseToProperties(pattern); + fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern); + } catch (IllegalArgumentException e) { + } + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PropertiesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PropertiesTest.java new file mode 100644 index 00000000000..ad3edf2d4c8 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/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.numbers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import com.ibm.icu.dev.test.serializable.SerializableTestUtility; +import com.ibm.icu.impl.number.Parse.ParseMode; +import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle; +import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode; +import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; +import com.ibm.icu.text.CurrencyPluralInfo; +import com.ibm.icu.text.MeasureFormat.FormatWidth; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Currency.CurrencyUsage; +import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.ULocale; + +public class PropertiesTest { + + @Test + public void testBasicEquals() { + Properties p1 = new Properties(); + Properties p2 = new Properties(); + assertEquals(p1, p2); + + p1.setPositivePrefix("abc"); + assertNotEquals(p1, p2); + p2.setPositivePrefix("xyz"); + assertNotEquals(p1, p2); + p1.setPositivePrefix("xyz"); + assertEquals(p1, p2); + } + + @Test + public void testFieldCoverage() { + Properties p0 = new Properties(); + Properties p1 = new Properties(); + Properties p2 = new Properties(); + Properties p3 = new Properties(); + Properties p4 = new Properties(); + + Set 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/numbers/RounderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/RounderTest.java new file mode 100644 index 00000000000..872fb1a607e --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/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.numbers; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.ibm.icu.impl.number.FormatQuantity; +import com.ibm.icu.impl.number.FormatQuantity4; +import com.ibm.icu.impl.number.Properties; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder; +import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode; + +public class RounderTest { + + @Test + public void testSignificantDigitsRounder() { + Object[][][][] cases = { + { + {{1, -1}, {0, 2}, {2, 4}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig + { + {0.0, "0.0", "0.0", "0"}, + {0.054321, "0.05432", "0.05", "0.054"}, + {0.54321, "0.5432", "0.54", "0.54"}, + {1.0, "1.0", "1.0", "1"}, + {5.4321, "5.432", "5.43", "5.43"}, + {10.0, "10", "10", "10"}, + {11.0, "11", "11", "11"}, + {100.0, "100", "100", "100"}, + {100.23, "100.2", "100.2", "100.2"}, + {543210.0, "543200", "543200", "543200"}, + } + }, + { + {{1, -1}, {0, 0}, {2, -1}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig + { + {0.0, "0.0", "0", "0"}, + {0.054321, "0.054321", "0", "0.054"}, + {0.54321, "0.54321", "1", "0.54"}, + {1.0, "1.0", "1", "1"}, + {5.4321, "5.4321", "5", "5.4"}, + {10.0, "10", "10", "10"}, + {11.0, "11", "11", "11"}, + {100.0, "100", "100", "100"}, + {100.23, "100.23", "100", "100"}, + {543210.0, "543210", "543210", "543210"}, + } + }, + { + {{0, 2}, {1, 2}, {3, 3}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig + { + {0.0, ".000", ".00", ".0"}, + {0.054321, ".0543", ".05", ".0543"}, + {0.54321, ".543", ".54", ".543"}, + {1.0, "1.00", "1.00", "1.0"}, + {5.4321, "5.43", "5.43", "5.43"}, + {10.0, "10.0", "10.0", "10.0"}, + {11.0, "11.0", "11.0", "11.0"}, + {100.0, "00.0", "00.0", "00.0"}, + {100.23, "00.2", "00.2", "00.2"}, + {543210.0, "10.0", "10.0", "10.0"} + } + } + }; + + int caseNumber = 0; + for (Object[][][] cas : cases) { + int minInt = (Integer) cas[0][0][0]; + int maxInt = (Integer) cas[0][0][1]; + int minFrac = (Integer) cas[0][1][0]; + int maxFrac = (Integer) cas[0][1][1]; + int minSig = (Integer) cas[0][2][0]; + int maxSig = (Integer) cas[0][2][1]; + + Properties properties = new Properties(); + FormatQuantity4 fq = new FormatQuantity4(); + properties.setMinimumIntegerDigits(minInt); + properties.setMaximumIntegerDigits(maxInt); + properties.setMinimumFractionDigits(minFrac); + properties.setMaximumFractionDigits(maxFrac); + properties.setMinimumSignificantDigits(minSig); + properties.setMaximumSignificantDigits(maxSig); + + int runNumber = 0; + for (Object[] run : cas[1]) { + double input = (Double) run[0]; + String expected1 = (String) run[1]; + String expected2 = (String) run[2]; + String expected3 = (String) run[3]; + + properties.setSignificantDigitsMode(SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION); + fq.setToDouble(input); + SignificantDigitsRounder.getInstance(properties).apply(fq); + assertEquals( + "Case " + caseNumber + ", run " + runNumber + ", mode 0: " + fq, + expected1, + formatQuantityToString(fq)); + + properties.setSignificantDigitsMode(SignificantDigitsMode.RESPECT_MAXIMUM_FRACTION); + fq.setToDouble(input); + SignificantDigitsRounder.getInstance(properties).apply(fq); + assertEquals( + "Case " + caseNumber + ", run " + runNumber + ", mode 1: " + fq, + expected2, + formatQuantityToString(fq)); + + properties.setSignificantDigitsMode(SignificantDigitsMode.ENSURE_MINIMUM_SIGNIFICANT); + fq.setToDouble(input); + SignificantDigitsRounder.getInstance(properties).apply(fq); + assertEquals( + "Case " + caseNumber + ", run " + runNumber + ", mode 2: " + fq, + expected3, + formatQuantityToString(fq)); + + runNumber++; + } + + caseNumber++; + } + } + + private String formatQuantityToString(FormatQuantity fq) { + StringBuilder sb = new StringBuilder(); + int udm = fq.getUpperDisplayMagnitude(); + int ldm = fq.getLowerDisplayMagnitude(); + if (udm == -1) sb.append('.'); + for (int m = udm; m >= ldm; m--) { + sb.append(fq.getDigit(m)); + if (m == 0 && m > ldm) sb.append('.'); + } + return sb.toString(); + } +} 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 f79d4fceaaf..ea291facc00 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 99660b0c05a..ba11db331b1 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.numbers.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 39e5a9d28ea..d95812a66bf 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; } -- 2.40.0