From: Shane Carr Date: Fri, 1 Sep 2017 08:30:17 +0000 (+0000) Subject: ICU-13177 Renaming classes and moving things around. No or very few behavior changes. X-Git-Tag: release-60-rc~98^2~27 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=c0f2ca5177331c99bd2157aa7559951cd0155a86;p=icu ICU-13177 Renaming classes and moving things around. No or very few behavior changes. X-SVN-Rev: 40364 --- 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 index 56c824c08ce..e0eef171262 100644 --- 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 @@ -89,6 +89,10 @@ public class AffixPatternUtils { /** Represents a sequence of six or more currency symbols. */ public static final int TYPE_CURRENCY_OVERFLOW = -15; + public static interface SymbolProvider { + public CharSequence getSymbol(int type); + } + /** * 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 @@ -255,10 +259,6 @@ public class AffixPatternUtils { } } - public static interface SymbolProvider { - public CharSequence getSymbol(int type); - } - /** * Executes the unescape state machine. Replaces the unquoted characters "-", "+", "%", "‰", and * "¤" with the corresponding symbols provided by the {@link SymbolProvider}, and inserts the @@ -276,26 +276,22 @@ public class AffixPatternUtils { NumberStringBuilder output, int position, SymbolProvider provider) { - // TODO: Is it worth removing this extra local object instantiation here? - NumberStringBuilder local = new NumberStringBuilder(10); assert affixPattern != null; + int length = 0; long tag = 0L; while (hasNext(tag, affixPattern)) { tag = nextToken(tag, affixPattern); int typeOrCp = getTypeOrCp(tag); if (typeOrCp == TYPE_CURRENCY_OVERFLOW) { // Don't go to the provider for this special case - local.appendCodePoint(0xFFFD, NumberFormat.Field.CURRENCY); + length += output.insertCodePoint(position + length, 0xFFFD, NumberFormat.Field.CURRENCY); } else if (typeOrCp < 0) { - local.append(provider.getSymbol(typeOrCp), getFieldForType(typeOrCp)); + length += output.insert(position + length, provider.getSymbol(typeOrCp), getFieldForType(typeOrCp)); } else { - local.appendCodePoint(typeOrCp, null); + length += output.insertCodePoint(position + length, typeOrCp, null); } } - if (output != null) { - output.insert(position, local); - } - return local.length(); + return length; } /** @@ -307,7 +303,9 @@ public class AffixPatternUtils { * @return true if the affix pattern contains the given token type; false otherwise. */ public static boolean containsType(CharSequence affixPattern, int type) { - if (affixPattern == null || affixPattern.length() == 0) return false; + if (affixPattern == null || affixPattern.length() == 0) { + return false; + } long tag = 0L; while (hasNext(tag, affixPattern)) { tag = nextToken(tag, affixPattern); @@ -548,7 +546,7 @@ public class AffixPatternUtils { public static int getTypeOrCp(long tag) { assert tag >= 0; int type = getType(tag); - return (type == 0) ? getCodePoint(tag) : -type; + return (type == TYPE_CODEPOINT) ? getCodePoint(tag) : -type; } /** diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java deleted file mode 100644 index 745b4d3344c..00000000000 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java +++ /dev/null @@ -1,447 +0,0 @@ -// © 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.ThingsNeedingNewHome.PadPosition; - -import newapi.impl.AffixPatternProvider; - -/** Implements a recursive descent parser for decimal format patterns. */ -public class LdmlPatternInfo { - - public static PatternParseResult parse(String patternString) { - ParserState state = new ParserState(patternString); - PatternParseResult result = new PatternParseResult(patternString); - consumePattern(state, result); - return result; - } - - /** - * An internal, intermediate data structure used for storing parse results before they are - * finalized into a DecimalFormatPattern.Builder. - */ - public static class PatternParseResult implements AffixPatternProvider { - public String pattern; - public LdmlPatternInfo.SubpatternParseResult positive; - public LdmlPatternInfo.SubpatternParseResult negative; - - private PatternParseResult(String pattern) { - this.pattern = pattern; - } - - @Override - public char charAt(int flags, int index) { - long endpoints = getEndpoints(flags); - int left = (int) (endpoints & 0xffffffff); - int right = (int) (endpoints >>> 32); - if (index < 0 || index >= right - left) { - throw new IndexOutOfBoundsException(); - } - return pattern.charAt(left + index); - } - - @Override - public int length(int flags) { - return getLengthFromEndpoints(getEndpoints(flags)); - } - - public static int getLengthFromEndpoints(long endpoints) { - int left = (int) (endpoints & 0xffffffff); - int right = (int) (endpoints >>> 32); - return right - left; - } - - public String getString(int flags) { - long endpoints = getEndpoints(flags); - int left = (int) (endpoints & 0xffffffff); - int right = (int) (endpoints >>> 32); - if (left == right) { - return ""; - } - return pattern.substring(left, right); - } - - private long getEndpoints(int flags) { - boolean prefix = (flags & Flags.PREFIX) != 0; - boolean isNegative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0; - boolean padding = (flags & Flags.PADDING) != 0; - if (isNegative && padding) { - return negative.paddingEndpoints; - } else if (padding) { - return positive.paddingEndpoints; - } else if (prefix && isNegative) { - return negative.prefixEndpoints; - } else if (prefix) { - return positive.prefixEndpoints; - } else if (isNegative) { - return negative.suffixEndpoints; - } else { - return positive.suffixEndpoints; - } - } - - @Override - public boolean positiveHasPlusSign() { - return positive.hasPlusSign; - } - - @Override - public boolean hasNegativeSubpattern() { - return negative != null; - } - - @Override - public boolean negativeHasMinusSign() { - return negative.hasMinusSign; - } - - @Override - public boolean hasCurrencySign() { - return positive.hasCurrencySign || (negative != null && negative.hasCurrencySign); - } - - @Override - public boolean containsSymbolType(int type) { - return AffixPatternUtils.containsType(pattern, type); - } - } - - public static class SubpatternParseResult { - public long groupingSizes = 0x0000ffffffff0000L; - public int minimumIntegerDigits = 0; - public int totalIntegerDigits = 0; - public int minimumFractionDigits = 0; - public int maximumFractionDigits = 0; - public int minimumSignificantDigits = 0; - public int maximumSignificantDigits = 0; - public boolean hasDecimal = false; - public int paddingWidth = 0; - public PadPosition paddingLocation = null; - public FormatQuantity4 rounding = null; - public boolean exponentShowPlusSign = false; - public int exponentDigits = 0; - public boolean hasPercentSign = false; - public boolean hasPerMilleSign = false; - public boolean hasCurrencySign = false; - public boolean hasMinusSign = false; - public boolean hasPlusSign = false; - - public long prefixEndpoints = 0; - public long suffixEndpoints = 0; - public long paddingEndpoints = 0; - } - - /** An internal class used for tracking the cursor during parsing of a pattern string. */ - private static class ParserState { - final String pattern; - int offset; - - ParserState(String pattern) { - this.pattern = pattern; - this.offset = 0; - } - - int peek() { - if (offset == pattern.length()) { - return -1; - } else { - return pattern.codePointAt(offset); - } - } - - int next() { - int codePoint = peek(); - offset += Character.charCount(codePoint); - return codePoint; - } - - IllegalArgumentException toParseException(String message) { - StringBuilder sb = new StringBuilder(); - sb.append("Malformed pattern for ICU DecimalFormat: \""); - sb.append(pattern); - sb.append("\": "); - sb.append(message); - sb.append(" at position "); - sb.append(offset); - return new IllegalArgumentException(sb.toString()); - } - } - - private static void consumePattern( - LdmlPatternInfo.ParserState state, LdmlPatternInfo.PatternParseResult result) { - // pattern := subpattern (';' subpattern)? - result.positive = new SubpatternParseResult(); - consumeSubpattern(state, result.positive); - if (state.peek() == ';') { - state.next(); // consume the ';' - // Don't consume the negative subpattern if it is empty (trailing ';') - if (state.peek() != -1) { - result.negative = new SubpatternParseResult(); - consumeSubpattern(state, result.negative); - } - } - if (state.peek() != -1) { - throw state.toParseException("Found unquoted special character"); - } - } - - private static void consumeSubpattern( - LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) { - // subpattern := literals? number exponent? literals? - consumePadding(state, result, PadPosition.BEFORE_PREFIX); - result.prefixEndpoints = consumeAffix(state, result); - consumePadding(state, result, PadPosition.AFTER_PREFIX); - consumeFormat(state, result); - consumeExponent(state, result); - consumePadding(state, result, PadPosition.BEFORE_SUFFIX); - result.suffixEndpoints = consumeAffix(state, result); - consumePadding(state, result, PadPosition.AFTER_SUFFIX); - } - - private static void consumePadding( - LdmlPatternInfo.ParserState state, - LdmlPatternInfo.SubpatternParseResult result, - PadPosition paddingLocation) { - if (state.peek() != '*') { - return; - } - result.paddingLocation = paddingLocation; - state.next(); // consume the '*' - result.paddingEndpoints |= state.offset; - consumeLiteral(state); - result.paddingEndpoints |= ((long) state.offset) << 32; - } - - private static long consumeAffix( - LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) { - // literals := { literal } - long endpoints = state.offset; - outer: - while (true) { - switch (state.peek()) { - case '#': - case '@': - case ';': - case '*': - case '.': - case ',': - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case -1: - // Characters that cannot appear unquoted in a literal - break outer; - - case '%': - result.hasPercentSign = true; - break; - - case '‰': - result.hasPerMilleSign = true; - break; - - case '¤': - result.hasCurrencySign = true; - break; - - case '-': - result.hasMinusSign = true; - break; - - case '+': - result.hasPlusSign = true; - break; - } - consumeLiteral(state); - } - endpoints |= ((long) state.offset) << 32; - return endpoints; - } - - private static void consumeLiteral(LdmlPatternInfo.ParserState state) { - if (state.peek() == -1) { - throw state.toParseException("Expected unquoted literal but found EOL"); - } else if (state.peek() == '\'') { - state.next(); // consume the starting quote - while (state.peek() != '\'') { - if (state.peek() == -1) { - throw state.toParseException("Expected quoted literal but found EOL"); - } else { - state.next(); // consume a quoted character - } - } - state.next(); // consume the ending quote - } else { - // consume a non-quoted literal character - state.next(); - } - } - - private static void consumeFormat( - LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) { - consumeIntegerFormat(state, result); - if (state.peek() == '.') { - state.next(); // consume the decimal point - result.hasDecimal = true; - result.paddingWidth += 1; - consumeFractionFormat(state, result); - } - } - - private static void consumeIntegerFormat( - LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) { - boolean seenSignificantDigitMarker = false; - boolean seenDigit = false; - - outer: - while (true) { - switch (state.peek()) { - case ',': - result.paddingWidth += 1; - result.groupingSizes <<= 16; - break; - - case '#': - if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point"); - result.paddingWidth += 1; - result.groupingSizes += 1; - result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1); - // no change to result.minimumIntegerDigits - // no change to result.minimumSignificantDigits - result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0); - if (result.rounding != null) { - result.rounding.appendDigit((byte) 0, 0, true); - } - break; - - case '@': - seenSignificantDigitMarker = true; - if (seenDigit) throw state.toParseException("Cannot mix 0 and @"); - result.paddingWidth += 1; - result.groupingSizes += 1; - result.totalIntegerDigits += 1; - // no change to result.minimumIntegerDigits - result.minimumSignificantDigits += 1; - result.maximumSignificantDigits += 1; - if (result.rounding != null) { - result.rounding.appendDigit((byte) 0, 0, true); - } - break; - - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - seenDigit = true; - if (seenSignificantDigitMarker) throw state.toParseException("Cannot mix @ and 0"); - // TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt - result.paddingWidth += 1; - result.groupingSizes += 1; - result.totalIntegerDigits += 1; - result.minimumIntegerDigits += 1; - // no change to result.minimumSignificantDigits - // no change to result.maximumSignificantDigits - if (state.peek() != '0' && result.rounding == null) { - result.rounding = new FormatQuantity4(); - } - if (result.rounding != null) { - result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true); - } - break; - - default: - break outer; - } - state.next(); // consume the symbol - } - - // Disallow patterns with a trailing ',' or with two ',' next to each other - short grouping1 = (short) (result.groupingSizes & 0xffff); - short grouping2 = (short) ((result.groupingSizes >>> 16) & 0xffff); - short grouping3 = (short) ((result.groupingSizes >>> 32) & 0xffff); - if (grouping1 == 0 && grouping2 != -1) { - throw state.toParseException("Trailing grouping separator is invalid"); - } - if (grouping2 == 0 && grouping3 != -1) { - throw state.toParseException("Grouping width of zero is invalid"); - } - } - - private static void consumeFractionFormat( - LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) { - int zeroCounter = 0; - boolean seenHash = false; - while (true) { - switch (state.peek()) { - case '#': - seenHash = true; - result.paddingWidth += 1; - // no change to result.minimumFractionDigits - result.maximumFractionDigits += 1; - zeroCounter++; - break; - - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - if (seenHash) throw state.toParseException("0 cannot follow # after decimal point"); - result.paddingWidth += 1; - result.minimumFractionDigits += 1; - result.maximumFractionDigits += 1; - if (state.peek() == '0') { - zeroCounter++; - } else { - if (result.rounding == null) { - result.rounding = new FormatQuantity4(); - } - result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false); - zeroCounter = 0; - } - break; - - default: - return; - } - state.next(); // consume the symbol - } - } - - private static void consumeExponent( - LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) { - if (state.peek() != 'E') { - return; - } - state.next(); // consume the E - result.paddingWidth++; - if (state.peek() == '+') { - state.next(); // consume the + - result.exponentShowPlusSign = true; - result.paddingWidth++; - } - while (state.peek() == '0') { - state.next(); // consume the 0 - result.exponentDigits += 1; - result.paddingWidth++; - } - } -} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java index 8bb55e145dd..11eb56bc85b 100644 --- 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 @@ -2,21 +2,15 @@ // 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; +import newapi.MutablePatternModifier; /** - * 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. + * A Modifier is an 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 + * A Modifier is usually immutable, except in cases such as {@link MutablePatternModifier}, which are mutable for performance + * reasons. */ public interface Modifier { @@ -28,8 +22,8 @@ public interface 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. + * The right index of the string within the string builder. Equal to length 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); @@ -50,62 +44,4 @@ public interface Modifier { * @return Whether the modifier is strong. */ public boolean isStrong(); - - /** - * 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 { - /** - * 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 { - /** - * 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 implements Modifier, PositiveNegativeModifier { - - @Override - public Modifier getModifier(boolean isNegative) { - return this; - } - } } 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 index 2d4878fb5e6..f103b5eb889 100644 --- 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 @@ -13,460 +13,490 @@ import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.NumberFormat.Field; /** - * A StringBuilder optimized for number formatting. It implements the following key features beyond - * a normal JDK StringBuilder: + * A StringBuilder optimized for number formatting. It implements the following key features beyond a normal JDK + * StringBuilder: * *

    - *
  1. Efficient prepend as well as append. - *
  2. Keeps tracks of Fields in an efficient manner. - *
  3. String operations are fast-pathed to code point operations when possible. + *
  4. Efficient prepend as well as append. + *
  5. Keeps tracks of Fields in an efficient manner. + *
  6. String operations are fast-pathed to code point operations when possible. *
*/ public class NumberStringBuilder implements CharSequence { - /** A constant, empty NumberStringBuilder. Do NOT call mutative operations on this. */ - public static final NumberStringBuilder EMPTY = new NumberStringBuilder(); - - 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; - } - - public NumberStringBuilder(NumberStringBuilder source) { - copyFrom(source); - } - - public void copyFrom(NumberStringBuilder source) { - chars = Arrays.copyOf(source.chars, source.chars.length); - fields = Arrays.copyOf(source.fields, source.fields.length); - zero = source.zero; - length = source.length; - } - - @Override - public int length() { - return length; - } - - public int codePointCount() { - return Character.codePointCount(this, 0, length()); - } - - @Override - public char charAt(int index) { - assert index >= 0; - assert index < length; - return chars[zero + index]; - } - - public Field fieldAt(int index) { - assert index >= 0; - assert index < length; - return fields[zero + index]; - } - - public NumberStringBuilder clear() { - zero = chars.length / 2; - length = 0; - return this; - } - - /** - * 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) { - if (this == other) { - throw new IllegalArgumentException("Cannot call insert/append on myself"); - } - int count = other.length; - if (count == 0) { - // Nothing to insert. - return 0; - } - int position = prepareForInsert(index, count); - for (int i = 0; i < count; i++) { - this.chars[position + i] = other.charAt(i); - this.fields[position + i] = other.fieldAt(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) { - // Java note: Keeping this code out of prepareForInsert() increases the speed of append operations. - int oldCapacity = chars.length; - int oldZero = zero; - char[] oldChars = chars; - Field[] oldFields = fields; - if (length + count > oldCapacity) { - int newCapacity = (length + count) * 2; - int newZero = newCapacity / 2 - (length + count) / 2; - - char[] newChars = new char[newCapacity]; - Field[] newFields = new Field[newCapacity]; - - // First copy the prefix and then the suffix, leaving room for the new chars that the - // caller wants to insert. - System.arraycopy(oldChars, oldZero, newChars, newZero, index); - System.arraycopy( - oldChars, oldZero + index, newChars, newZero + index + count, length - index); - System.arraycopy(oldFields, oldZero, newFields, newZero, index); - System.arraycopy( - oldFields, oldZero + index, newFields, newZero + index + count, length - index); - - chars = newChars; - fields = newFields; - zero = newZero; - length += count; - } else { - int newZero = oldCapacity / 2 - (length + count) / 2; - - // First copy the entire string to the location of the prefix, and then move the suffix - // to make room for the new chars that the caller wants to insert. - System.arraycopy(oldChars, oldZero, oldChars, newZero, length); - System.arraycopy( - oldChars, newZero + index, oldChars, newZero + index + count, length - index); - System.arraycopy(oldFields, oldZero, oldFields, newZero, length); - System.arraycopy( - oldFields, newZero + index, oldFields, 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 = new NumberStringBuilder(this); - 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 (charAt(i) != other.charAt(i) || fieldAt(i) != other.fieldAt(i)) { - return false; - } - } - return true; - } - - @Override - public int hashCode() { - throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable."); - } - - @Override - public boolean equals(Object other) { - throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable."); - } - - /** - * 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; + /** A constant, empty NumberStringBuilder. Do NOT call mutative operations on this. */ + public static final NumberStringBuilder EMPTY = new NumberStringBuilder(); + + 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; + } + + public NumberStringBuilder(NumberStringBuilder source) { + copyFrom(source); + } + + public void copyFrom(NumberStringBuilder source) { + chars = Arrays.copyOf(source.chars, source.chars.length); + fields = Arrays.copyOf(source.fields, source.fields.length); + zero = source.zero; + length = source.length; + } + + @Override + public int length() { + return length; + } + + public int codePointCount() { + return Character.codePointCount(this, 0, length()); + } + + @Override + public char charAt(int index) { + assert index >= 0; + assert index < length; + return chars[zero + index]; + } + + public Field fieldAt(int index) { + assert index >= 0; + assert index < length; + return fields[zero + index]; + } + + public int getFirstCodePoint() { + if (length == 0) { + return -1; } - 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 + offset); - fp.setEndIndex(fractionStart + offset); - } - } - - 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); + return Character.codePointAt(chars, zero, zero + length); + } + + public int getLastCodePoint() { + if (length == 0) { + return -1; + } + return Character.codePointBefore(chars, zero + length, zero); + } + + public int codePointAt(int index) { + return Character.codePointAt(chars, zero + index, zero + length); + } + + public int codePointBefore(int index) { + return Character.codePointBefore(chars, zero + index, zero); + } + + public NumberStringBuilder clear() { + zero = getCapacity() / 2; + length = 0; + return this; + } + + /** + * 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) { + if (this == other) { + throw new IllegalArgumentException("Cannot call insert/append on myself"); + } + int count = other.length; + if (count == 0) { + // Nothing to insert. + return 0; + } + int position = prepareForInsert(index, count); + for (int i = 0; i < count; i++) { + this.chars[position + i] = other.charAt(i); + this.fields[position + i] = other.fieldAt(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 < getCapacity()) { + // 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) { + // Java note: Keeping this code out of prepareForInsert() increases the speed of append operations. + int oldCapacity = getCapacity(); + int oldZero = zero; + char[] oldChars = chars; + Field[] oldFields = fields; + if (length + count > oldCapacity) { + int newCapacity = (length + count) * 2; + int newZero = newCapacity / 2 - (length + count) / 2; + + char[] newChars = new char[newCapacity]; + Field[] newFields = new Field[newCapacity]; + + // First copy the prefix and then the suffix, leaving room for the new chars that the + // caller wants to insert. + System.arraycopy(oldChars, oldZero, newChars, newZero, index); + System.arraycopy(oldChars, oldZero + index, newChars, newZero + index + count, length - index); + System.arraycopy(oldFields, oldZero, newFields, newZero, index); + System.arraycopy(oldFields, oldZero + index, newFields, newZero + index + count, length - index); + + chars = newChars; + fields = newFields; + zero = newZero; + length += count; + } else { + int newZero = oldCapacity / 2 - (length + count) / 2; + + // First copy the entire string to the location of the prefix, and then move the suffix + // to make room for the new chars that the caller wants to insert. + System.arraycopy(oldChars, oldZero, oldChars, newZero, length); + System.arraycopy(oldChars, newZero + index, oldChars, newZero + index + count, length - index); + System.arraycopy(oldFields, oldZero, oldFields, newZero, length); + System.arraycopy(oldFields, newZero + index, oldFields, newZero + index + count, length - index); + + zero = newZero; + length += count; + } + return zero + index; + } + + private int getCapacity() { + return chars.length; + } + + @Override + public CharSequence subSequence(int start, int end) { + if (start < 0 || end > length || end < start) { + throw new IndexOutOfBoundsException(); + } + NumberStringBuilder other = new NumberStringBuilder(this); + 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 (charAt(i) != other.charAt(i) || fieldAt(i) != other.fieldAt(i)) { + return false; + } } - current = field; - currentStart = i; - } + return true; + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable."); } - if (current != null) { - as.addAttribute(current, current, currentStart, length); + + @Override + public boolean equals(Object other) { + throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable."); + } + + /** + * 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 + offset); + fp.setEndIndex(fractionStart + offset); + } } - return as.getIterator(); - } + 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(); + } } 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 index e921a2eedeb..87b588ae0f9 100644 --- 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 @@ -837,7 +837,7 @@ public class Parse { private void addPattern(String pattern) { Properties properties = threadLocalProperties.get(); try { - PatternString.parseToExistingProperties(pattern, properties); + PatternAndPropertyUtils.parseToExistingProperties(pattern, properties); } catch (IllegalArgumentException e) { // This should only happen if there is a bug in CLDR data. Fail silently. } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternAndPropertyUtils.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternAndPropertyUtils.java new file mode 100644 index 00000000000..06a0dd12563 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternAndPropertyUtils.java @@ -0,0 +1,624 @@ +// © 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.PatternParser.ParsedPatternInfo; +import com.ibm.icu.impl.number.PatternParser.ParsedSubpatternInfo; +import com.ibm.icu.text.DecimalFormatSymbols; + +import newapi.impl.AffixPatternProvider; +import newapi.impl.Padder; +import newapi.impl.Padder.PadPosition; + +/** + * Handles parsing and creation of the compact pattern string representation of a decimal format. + */ +public class PatternAndPropertyUtils { + + /** + * 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. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link #IGNORE_ROUNDING_IF_CURRENCY}, or + * {@link #IGNORE_ROUNDING_NEVER}. + * @return A property bag object. + * @throws IllegalArgumentException + * If there is a syntax error in the pattern string. + */ + public static Properties parseToProperties(String pattern, int ignoreRounding) { + Properties properties = new Properties(); + parse(pattern, properties, ignoreRounding); + return properties; + } + + public static Properties parseToProperties(String pattern) { + return parseToProperties(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER); + } + + /** + * 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. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link #IGNORE_ROUNDING_IF_CURRENCY}, or + * {@link #IGNORE_ROUNDING_NEVER}. + * @throws IllegalArgumentException + * If there was a syntax error in the pattern string. + */ + public static void parseToExistingProperties(String pattern, Properties properties, int ignoreRounding) { + parse(pattern, properties, ignoreRounding); + } + + public static void parseToExistingProperties(String pattern, Properties properties) { + parseToExistingProperties(pattern, properties, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER); + } + + /** + * 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, -1) && firstGroupingSize != Math.min(dosMax, -1) + && groupingSize != firstGroupingSize) { + grouping = groupingSize; + grouping1 = groupingSize; + grouping2 = firstGroupingSize; + } else if (groupingSize != Math.min(dosMax, -1)) { + grouping = groupingSize; + grouping1 = 0; + grouping2 = groupingSize; + } else if (firstGroupingSize != Math.min(dosMax, -1)) { + 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, -1)) { + // Significant Digits. + while (digitsString.length() < minSig) { + digitsString.append('@'); + } + while (digitsString.length() < maxSig) { + digitsString.append('#'); + } + } else if (roundingInterval != null) { + // 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, -1)) { + 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 != -1) { + 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 = Padder.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. + * + *

+ * A greedy string-substitution strategy is used to substitute locale symbols. If two symbols are ambiguous or have + * the same prefix, the result is not well-defined. + * + *

+ * Locale symbols are not allowed to contain the ASCII quote character. + * + * @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(String input, DecimalFormatSymbols symbols, boolean toLocalized) { + if (input == null) + return null; + + // Construct a table of strings to be converted between localized and standard. + String[][] table = new String[21][2]; + int standIdx = toLocalized ? 0 : 1; + int localIdx = toLocalized ? 1 : 0; + table[0][standIdx] = "%"; + table[0][localIdx] = symbols.getPercentString(); + table[1][standIdx] = "‰"; + table[1][localIdx] = symbols.getPerMillString(); + table[2][standIdx] = "."; + table[2][localIdx] = symbols.getDecimalSeparatorString(); + table[3][standIdx] = ","; + table[3][localIdx] = symbols.getGroupingSeparatorString(); + table[4][standIdx] = "-"; + table[4][localIdx] = symbols.getMinusSignString(); + table[5][standIdx] = "+"; + table[5][localIdx] = symbols.getPlusSignString(); + table[6][standIdx] = ";"; + table[6][localIdx] = Character.toString(symbols.getPatternSeparator()); + table[7][standIdx] = "@"; + table[7][localIdx] = Character.toString(symbols.getSignificantDigit()); + table[8][standIdx] = "E"; + table[8][localIdx] = symbols.getExponentSeparator(); + table[9][standIdx] = "*"; + table[9][localIdx] = Character.toString(symbols.getPadEscape()); + table[10][standIdx] = "#"; + table[10][localIdx] = Character.toString(symbols.getDigit()); + for (int i = 0; i < 10; i++) { + table[11 + i][standIdx] = Character.toString((char) ('0' + i)); + table[11 + i][localIdx] = symbols.getDigitStringsLocal()[i]; + } + + // Special case: quotes are NOT allowed to be in any localIdx strings. + // Substitute them with '’' instead. + for (int i = 0; i < table.length; i++) { + table[i][localIdx] = table[i][localIdx].replace('\'', '’'); + } + + // Iterate through the string and convert. + // State table: + // 0 => base state + // 1 => first char inside a quoted sequence in input and output string + // 2 => inside a quoted sequence in input and output string + // 3 => first char after a close quote in input string; + // close quote still needs to be written to output string + // 4 => base state in input string; inside quoted sequence in output string + // 5 => first char inside a quoted sequence in input string; + // inside quoted sequence in output string + StringBuilder result = new StringBuilder(); + int state = 0; + outer: for (int offset = 0; offset < input.length(); offset++) { + char ch = input.charAt(offset); + + // Handle a quote character (state shift) + if (ch == '\'') { + if (state == 0) { + result.append('\''); + state = 1; + continue; + } else if (state == 1) { + result.append('\''); + state = 0; + continue; + } else if (state == 2) { + state = 3; + continue; + } else if (state == 3) { + result.append('\''); + result.append('\''); + state = 1; + continue; + } else if (state == 4) { + state = 5; + continue; + } else { + assert state == 5; + result.append('\''); + result.append('\''); + state = 4; + continue; + } + } + + if (state == 0 || state == 3 || state == 4) { + for (String[] pair : table) { + // Perform a greedy match on this symbol string + if (input.regionMatches(offset, pair[0], 0, pair[0].length())) { + // Skip ahead past this region for the next iteration + offset += pair[0].length() - 1; + if (state == 3 || state == 4) { + result.append('\''); + state = 0; + } + result.append(pair[1]); + continue outer; + } + } + // No replacement found. Check if a special quote is necessary + for (String[] pair : table) { + if (input.regionMatches(offset, pair[1], 0, pair[1].length())) { + if (state == 0) { + result.append('\''); + state = 4; + } + result.append(ch); + continue outer; + } + } + // Still nothing. Copy the char verbatim. (Add a close quote if necessary) + if (state == 3 || state == 4) { + result.append('\''); + state = 0; + } + result.append(ch); + } else { + assert state == 1 || state == 2 || state == 5; + result.append(ch); + state = 2; + } + } + // Resolve final quotes + if (state == 3 || state == 4) { + result.append('\''); + state = 0; + } + if (state != 0) { + throw new IllegalArgumentException("Malformed localized pattern: unterminated quote"); + } + return result.toString(); + } + + public static final int IGNORE_ROUNDING_NEVER = 0; + public static final int IGNORE_ROUNDING_IF_CURRENCY = 1; + public static final int IGNORE_ROUNDING_ALWAYS = 2; + + static void parse(String pattern, Properties properties, int ignoreRounding) { + if (pattern == null || pattern.length() == 0) { + // Backwards compatibility requires that we reset to the default values. + // TODO: Only overwrite the properties that "saveToProperties" normally touches? + properties.clear(); + return; + } + + // TODO: Use thread locals here? + ParsedPatternInfo patternInfo = PatternParser.parse(pattern); + saveToProperties(properties, patternInfo, ignoreRounding); + } + + /** Finalizes the temporary data stored in the ParsedPatternInfo to the Properties. */ + private static void saveToProperties(Properties properties, ParsedPatternInfo patternInfo, int _ignoreRounding) { + // Translate from PatternParseResult to Properties. + // Note that most data from "negative" is ignored per the specification of DecimalFormat. + + ParsedSubpatternInfo positive = patternInfo.positive; + ParsedSubpatternInfo negative = patternInfo.negative; + + boolean ignoreRounding; + if (_ignoreRounding == IGNORE_ROUNDING_NEVER) { + ignoreRounding = false; + } else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) { + ignoreRounding = positive.hasCurrencySign; + } else { + assert _ignoreRounding == IGNORE_ROUNDING_ALWAYS; + ignoreRounding = true; + } + + // Grouping settings + short grouping1 = (short) (positive.groupingSizes & 0xffff); + short grouping2 = (short) ((positive.groupingSizes >>> 16) & 0xffff); + short grouping3 = (short) ((positive.groupingSizes >>> 32) & 0xffff); + if (grouping2 != -1) { + properties.setGroupingSize(grouping1); + } else { + properties.setGroupingSize(-1); + } + if (grouping3 != -1) { + properties.setSecondaryGroupingSize(grouping2); + } else { + properties.setSecondaryGroupingSize(-1); + } + + // For backwards compatibility, require that the pattern emit at least one min digit. + int minInt, minFrac; + if (positive.integerTotal == 0 && positive.fractionTotal > 0) { + // patterns like ".##" + minInt = 0; + minFrac = Math.max(1, positive.fractionNumerals); + } else if (positive.integerNumerals == 0 && positive.fractionNumerals == 0) { + // patterns like "#.##" + minInt = 1; + minFrac = 0; + } else { + minInt = positive.integerNumerals; + minFrac = positive.fractionNumerals; + } + + // Rounding settings + // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage + if (positive.integerAtSigns > 0) { + properties.setMinimumFractionDigits(-1); + properties.setMaximumFractionDigits(-1); + properties.setRoundingIncrement(null); + properties.setMinimumSignificantDigits(positive.integerAtSigns); + properties.setMaximumSignificantDigits(positive.integerAtSigns + positive.integerTrailingHashSigns); + } else if (positive.rounding != null) { + if (!ignoreRounding) { + properties.setMinimumFractionDigits(minFrac); + properties.setMaximumFractionDigits(positive.fractionTotal); + properties.setRoundingIncrement( + positive.rounding.toBigDecimal().setScale(positive.fractionNumerals)); + } else { + properties.setMinimumFractionDigits(-1); + properties.setMaximumFractionDigits(-1); + properties.setRoundingIncrement(null); + } + properties.setMinimumSignificantDigits(-1); + properties.setMaximumSignificantDigits(-1); + } else { + if (!ignoreRounding) { + properties.setMinimumFractionDigits(minFrac); + properties.setMaximumFractionDigits(positive.fractionTotal); + properties.setRoundingIncrement(null); + } else { + properties.setMinimumFractionDigits(-1); + properties.setMaximumFractionDigits(-1); + properties.setRoundingIncrement(null); + } + properties.setMinimumSignificantDigits(-1); + properties.setMaximumSignificantDigits(-1); + } + + // If the pattern ends with a '.' then force the decimal point. + if (positive.hasDecimal && positive.fractionTotal == 0) { + properties.setDecimalSeparatorAlwaysShown(true); + } else { + properties.setDecimalSeparatorAlwaysShown(false); + } + + // Scientific notation settings + if (positive.exponentZeros > 0) { + properties.setExponentSignAlwaysShown(positive.exponentHasPlusSign); + properties.setMinimumExponentDigits(positive.exponentZeros); + if (positive.integerAtSigns == 0) { + // patterns without '@' can define max integer digits, used for engineering notation + properties.setMinimumIntegerDigits(positive.integerNumerals); + properties.setMaximumIntegerDigits(positive.integerTotal); + } else { + // patterns with '@' cannot define max integer digits + properties.setMinimumIntegerDigits(1); + properties.setMaximumIntegerDigits(-1); + } + } else { + properties.setExponentSignAlwaysShown(false); + properties.setMinimumExponentDigits(-1); + properties.setMinimumIntegerDigits(minInt); + properties.setMaximumIntegerDigits(-1); + } + + // Compute the affix patterns (required for both padding and affixes) + String posPrefix = patternInfo.getString(AffixPatternProvider.Flags.PREFIX); + String posSuffix = patternInfo.getString(0); + + // Padding settings + if (positive.paddingEndpoints != 0) { + // The width of the positive prefix and suffix templates are included in the padding + int paddingWidth = positive.widthExceptAffixes + AffixPatternUtils.estimateLength(posPrefix) + + AffixPatternUtils.estimateLength(posSuffix); + properties.setFormatWidth(paddingWidth); + String rawPaddingString = patternInfo.getString(AffixPatternProvider.Flags.PADDING); + if (rawPaddingString.length() == 1) { + properties.setPadString(rawPaddingString); + } else if (rawPaddingString.length() == 2) { + if (rawPaddingString.charAt(0) == '\'') { + properties.setPadString("'"); + } else { + properties.setPadString(rawPaddingString); + } + } else { + properties.setPadString(rawPaddingString.substring(1, rawPaddingString.length() - 1)); + } + assert positive.paddingLocation != null; + properties.setPadPosition(positive.paddingLocation); + } else { + properties.setFormatWidth(-1); + properties.setPadString(null); + properties.setPadPosition(null); + } + + // Set the affixes + // Always call the setter, even if the prefixes are empty, especially in the case of the + // negative prefix pattern, to prevent default values from overriding the pattern. + properties.setPositivePrefixPattern(posPrefix); + properties.setPositiveSuffixPattern(posSuffix); + if (negative != null) { + properties.setNegativePrefixPattern(patternInfo + .getString(AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN | AffixPatternProvider.Flags.PREFIX)); + properties.setNegativeSuffixPattern(patternInfo.getString(AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN)); + } 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(0); + } + } +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternParser.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternParser.java new file mode 100644 index 00000000000..f469d75587f --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternParser.java @@ -0,0 +1,443 @@ +// © 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 newapi.impl.AffixPatternProvider; +import newapi.impl.Padder.PadPosition; + +/** Implements a recursive descent parser for decimal format patterns. */ +public class PatternParser { + + /** + * Runs the recursive descent parser on the given pattern string, returning a data structure with raw information + * about the pattern string. + * + *

+ * To obtain a more useful form of the data, consider using {@link PatternAndPropertyUtils#parse} instead. + * + * @param patternString + * The LDML decimal format pattern (Excel-style pattern) to parse. + * @return The results of the parse. + */ + public static ParsedPatternInfo parse(String patternString) { + ParserState state = new ParserState(patternString); + ParsedPatternInfo result = new ParsedPatternInfo(patternString); + consumePattern(state, result); + return result; + } + + /** + * Contains information about + * @author sffc + * + */ + public static class ParsedPatternInfo implements AffixPatternProvider { + public String pattern; + public ParsedSubpatternInfo positive; + public ParsedSubpatternInfo negative; + + private ParsedPatternInfo(String pattern) { + this.pattern = pattern; + } + + @Override + public char charAt(int flags, int index) { + long endpoints = getEndpoints(flags); + int left = (int) (endpoints & 0xffffffff); + int right = (int) (endpoints >>> 32); + if (index < 0 || index >= right - left) { + throw new IndexOutOfBoundsException(); + } + return pattern.charAt(left + index); + } + + @Override + public int length(int flags) { + return getLengthFromEndpoints(getEndpoints(flags)); + } + + public static int getLengthFromEndpoints(long endpoints) { + int left = (int) (endpoints & 0xffffffff); + int right = (int) (endpoints >>> 32); + return right - left; + } + + public String getString(int flags) { + long endpoints = getEndpoints(flags); + int left = (int) (endpoints & 0xffffffff); + int right = (int) (endpoints >>> 32); + if (left == right) { + return ""; + } + return pattern.substring(left, right); + } + + private long getEndpoints(int flags) { + boolean prefix = (flags & Flags.PREFIX) != 0; + boolean isNegative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0; + boolean padding = (flags & Flags.PADDING) != 0; + if (isNegative && padding) { + return negative.paddingEndpoints; + } else if (padding) { + return positive.paddingEndpoints; + } else if (prefix && isNegative) { + return negative.prefixEndpoints; + } else if (prefix) { + return positive.prefixEndpoints; + } else if (isNegative) { + return negative.suffixEndpoints; + } else { + return positive.suffixEndpoints; + } + } + + @Override + public boolean positiveHasPlusSign() { + return positive.hasPlusSign; + } + + @Override + public boolean hasNegativeSubpattern() { + return negative != null; + } + + @Override + public boolean negativeHasMinusSign() { + return negative.hasMinusSign; + } + + @Override + public boolean hasCurrencySign() { + return positive.hasCurrencySign || (negative != null && negative.hasCurrencySign); + } + + @Override + public boolean containsSymbolType(int type) { + return AffixPatternUtils.containsType(pattern, type); + } + } + + public static class ParsedSubpatternInfo { + public long groupingSizes = 0x0000ffffffff0000L; + public int integerLeadingHashSigns = 0; + public int integerTrailingHashSigns = 0; + public int integerNumerals = 0; + public int integerAtSigns = 0; + public int integerTotal = 0; // for convenience + public int fractionNumerals = 0; + public int fractionHashSigns = 0; + public int fractionTotal = 0; // for convenience + public boolean hasDecimal = false; + public int widthExceptAffixes = 0; + public PadPosition paddingLocation = null; + public FormatQuantity4 rounding = null; + public boolean exponentHasPlusSign = false; + public int exponentZeros = 0; + public boolean hasPercentSign = false; + public boolean hasPerMilleSign = false; + public boolean hasCurrencySign = false; + public boolean hasMinusSign = false; + public boolean hasPlusSign = false; + + public long prefixEndpoints = 0; + public long suffixEndpoints = 0; + public long paddingEndpoints = 0; + } + + /** An internal class used for tracking the cursor during parsing of a pattern string. */ + private static class ParserState { + final String pattern; + int offset; + + ParserState(String pattern) { + this.pattern = pattern; + this.offset = 0; + } + + int peek() { + if (offset == pattern.length()) { + return -1; + } else { + return pattern.codePointAt(offset); + } + } + + int next() { + int codePoint = peek(); + offset += Character.charCount(codePoint); + return codePoint; + } + + IllegalArgumentException toParseException(String message) { + StringBuilder sb = new StringBuilder(); + sb.append("Malformed pattern for ICU DecimalFormat: \""); + sb.append(pattern); + sb.append("\": "); + sb.append(message); + sb.append(" at position "); + sb.append(offset); + return new IllegalArgumentException(sb.toString()); + } + } + + private static void consumePattern(ParserState state, ParsedPatternInfo result) { + // pattern := subpattern (';' subpattern)? + result.positive = new ParsedSubpatternInfo(); + consumeSubpattern(state, result.positive); + if (state.peek() == ';') { + state.next(); // consume the ';' + // Don't consume the negative subpattern if it is empty (trailing ';') + if (state.peek() != -1) { + result.negative = new ParsedSubpatternInfo(); + consumeSubpattern(state, result.negative); + } + } + if (state.peek() != -1) { + throw state.toParseException("Found unquoted special character"); + } + } + + private static void consumeSubpattern(ParserState state, ParsedSubpatternInfo result) { + // subpattern := literals? number exponent? literals? + consumePadding(state, result, PadPosition.BEFORE_PREFIX); + result.prefixEndpoints = consumeAffix(state, result); + consumePadding(state, result, PadPosition.AFTER_PREFIX); + consumeFormat(state, result); + consumeExponent(state, result); + consumePadding(state, result, PadPosition.BEFORE_SUFFIX); + result.suffixEndpoints = consumeAffix(state, result); + consumePadding(state, result, PadPosition.AFTER_SUFFIX); + } + + private static void consumePadding(ParserState state, ParsedSubpatternInfo result, PadPosition paddingLocation) { + if (state.peek() != '*') { + return; + } + result.paddingLocation = paddingLocation; + state.next(); // consume the '*' + result.paddingEndpoints |= state.offset; + consumeLiteral(state); + result.paddingEndpoints |= ((long) state.offset) << 32; + } + + private static long consumeAffix(ParserState state, ParsedSubpatternInfo result) { + // literals := { literal } + long endpoints = state.offset; + outer: while (true) { + switch (state.peek()) { + case '#': + case '@': + case ';': + case '*': + case '.': + case ',': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case -1: + // Characters that cannot appear unquoted in a literal + break outer; + + case '%': + result.hasPercentSign = true; + break; + + case '‰': + result.hasPerMilleSign = true; + break; + + case '¤': + result.hasCurrencySign = true; + break; + + case '-': + result.hasMinusSign = true; + break; + + case '+': + result.hasPlusSign = true; + break; + } + consumeLiteral(state); + } + endpoints |= ((long) state.offset) << 32; + return endpoints; + } + + private static void consumeLiteral(ParserState state) { + if (state.peek() == -1) { + throw state.toParseException("Expected unquoted literal but found EOL"); + } else if (state.peek() == '\'') { + state.next(); // consume the starting quote + while (state.peek() != '\'') { + if (state.peek() == -1) { + throw state.toParseException("Expected quoted literal but found EOL"); + } else { + state.next(); // consume a quoted character + } + } + state.next(); // consume the ending quote + } else { + // consume a non-quoted literal character + state.next(); + } + } + + private static void consumeFormat(ParserState state, ParsedSubpatternInfo result) { + consumeIntegerFormat(state, result); + if (state.peek() == '.') { + state.next(); // consume the decimal point + result.hasDecimal = true; + result.widthExceptAffixes += 1; + consumeFractionFormat(state, result); + } + } + + private static void consumeIntegerFormat(ParserState state, ParsedSubpatternInfo result) { + outer: while (true) { + switch (state.peek()) { + case ',': + result.widthExceptAffixes += 1; + result.groupingSizes <<= 16; + break; + + case '#': + if (result.integerNumerals > 0) { + throw state.toParseException("# cannot follow 0 before decimal point"); + } + result.widthExceptAffixes += 1; + result.groupingSizes += 1; + if (result.integerAtSigns > 0) { + result.integerTrailingHashSigns += 1; + } else { + result.integerLeadingHashSigns += 1; + } + result.integerTotal += 1; + break; + + case '@': + if (result.integerNumerals > 0) { + throw state.toParseException("Cannot mix 0 and @"); + } + if (result.integerTrailingHashSigns > 0) { + throw state.toParseException("Cannot nest # inside of a run of @"); + } + result.widthExceptAffixes += 1; + result.groupingSizes += 1; + result.integerAtSigns += 1; + result.integerTotal += 1; + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (result.integerAtSigns > 0) { + throw state.toParseException("Cannot mix @ and 0"); + } + result.widthExceptAffixes += 1; + result.groupingSizes += 1; + result.integerNumerals += 1; + result.integerTotal += 1; + if (state.peek() != '0' && result.rounding == null) { + result.rounding = new FormatQuantity4(); + } + if (result.rounding != null) { + result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true); + } + break; + + default: + break outer; + } + state.next(); // consume the symbol + } + + // Disallow patterns with a trailing ',' or with two ',' next to each other + short grouping1 = (short) (result.groupingSizes & 0xffff); + short grouping2 = (short) ((result.groupingSizes >>> 16) & 0xffff); + short grouping3 = (short) ((result.groupingSizes >>> 32) & 0xffff); + if (grouping1 == 0 && grouping2 != -1) { + throw state.toParseException("Trailing grouping separator is invalid"); + } + if (grouping2 == 0 && grouping3 != -1) { + throw state.toParseException("Grouping width of zero is invalid"); + } + } + + private static void consumeFractionFormat(ParserState state, ParsedSubpatternInfo result) { + int zeroCounter = 0; + while (true) { + switch (state.peek()) { + case '#': + result.widthExceptAffixes += 1; + result.fractionHashSigns += 1; + result.fractionTotal += 1; + zeroCounter++; + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (result.fractionHashSigns > 0) { + throw state.toParseException("0 cannot follow # after decimal point"); + } + result.widthExceptAffixes += 1; + result.fractionNumerals += 1; + result.fractionTotal += 1; + if (state.peek() == '0') { + zeroCounter++; + } else { + if (result.rounding == null) { + result.rounding = new FormatQuantity4(); + } + result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false); + zeroCounter = 0; + } + break; + + default: + return; + } + state.next(); // consume the symbol + } + } + + private static void consumeExponent(ParserState state, ParsedSubpatternInfo result) { + if (state.peek() != 'E') { + return; + } + state.next(); // consume the E + result.widthExceptAffixes++; + if (state.peek() == '+') { + state.next(); // consume the + + result.exponentHasPlusSign = true; + result.widthExceptAffixes++; + } + while (state.peek() == '0') { + state.next(); // consume the 0 + result.exponentZeros += 1; + result.widthExceptAffixes++; + } + } +} 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 deleted file mode 100644 index 417e1e0a597..00000000000 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java +++ /dev/null @@ -1,618 +0,0 @@ -// © 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.ThingsNeedingNewHome.PadPosition; -import com.ibm.icu.text.DecimalFormatSymbols; - -import newapi.impl.AffixPatternProvider; - -/** - * 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. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link - * #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}. - * @return A property bag object. - * @throws IllegalArgumentException If there is a syntax error in the pattern string. - */ - public static Properties parseToProperties(String pattern, int ignoreRounding) { - Properties properties = new Properties(); - parse(pattern, properties, ignoreRounding); - return properties; - } - - public static Properties parseToProperties(String pattern) { - return parseToProperties(pattern, PatternString.IGNORE_ROUNDING_NEVER); - } - - /** - * 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. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link - * #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}. - * @throws IllegalArgumentException If there was a syntax error in the pattern string. - */ - public static void parseToExistingProperties( - String pattern, Properties properties, int ignoreRounding) { - parse(pattern, properties, ignoreRounding); - } - - public static void parseToExistingProperties(String pattern, Properties properties) { - parseToExistingProperties(pattern, properties, PatternString.IGNORE_ROUNDING_NEVER); - } - - /** - * 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, -1) - && firstGroupingSize != Math.min(dosMax, -1) - && groupingSize != firstGroupingSize) { - grouping = groupingSize; - grouping1 = groupingSize; - grouping2 = firstGroupingSize; - } else if (groupingSize != Math.min(dosMax, -1)) { - grouping = groupingSize; - grouping1 = 0; - grouping2 = groupingSize; - } else if (firstGroupingSize != Math.min(dosMax, -1)) { - 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, -1)) { - // Significant Digits. - while (digitsString.length() < minSig) { - digitsString.append('@'); - } - while (digitsString.length() < maxSig) { - digitsString.append('#'); - } - } else if (roundingInterval != null) { - // 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, -1)) { - 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 != -1) { - 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 = ThingsNeedingNewHome.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. - * - *

A greedy string-substitution strategy is used to substitute locale symbols. If two symbols - * are ambiguous or have the same prefix, the result is not well-defined. - * - *

Locale symbols are not allowed to contain the ASCII quote character. - * - * @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( - String input, DecimalFormatSymbols symbols, boolean toLocalized) { - if (input == null) return null; - - // Construct a table of strings to be converted between localized and standard. - String[][] table = new String[21][2]; - int standIdx = toLocalized ? 0 : 1; - int localIdx = toLocalized ? 1 : 0; - table[0][standIdx] = "%"; - table[0][localIdx] = symbols.getPercentString(); - table[1][standIdx] = "‰"; - table[1][localIdx] = symbols.getPerMillString(); - table[2][standIdx] = "."; - table[2][localIdx] = symbols.getDecimalSeparatorString(); - table[3][standIdx] = ","; - table[3][localIdx] = symbols.getGroupingSeparatorString(); - table[4][standIdx] = "-"; - table[4][localIdx] = symbols.getMinusSignString(); - table[5][standIdx] = "+"; - table[5][localIdx] = symbols.getPlusSignString(); - table[6][standIdx] = ";"; - table[6][localIdx] = Character.toString(symbols.getPatternSeparator()); - table[7][standIdx] = "@"; - table[7][localIdx] = Character.toString(symbols.getSignificantDigit()); - table[8][standIdx] = "E"; - table[8][localIdx] = symbols.getExponentSeparator(); - table[9][standIdx] = "*"; - table[9][localIdx] = Character.toString(symbols.getPadEscape()); - table[10][standIdx] = "#"; - table[10][localIdx] = Character.toString(symbols.getDigit()); - for (int i = 0; i < 10; i++) { - table[11 + i][standIdx] = Character.toString((char) ('0' + i)); - table[11 + i][localIdx] = symbols.getDigitStringsLocal()[i]; - } - - // Special case: quotes are NOT allowed to be in any localIdx strings. - // Substitute them with '’' instead. - for (int i = 0; i < table.length; i++) { - table[i][localIdx] = table[i][localIdx].replace('\'', '’'); - } - - // Iterate through the string and convert. - // State table: - // 0 => base state - // 1 => first char inside a quoted sequence in input and output string - // 2 => inside a quoted sequence in input and output string - // 3 => first char after a close quote in input string; - // close quote still needs to be written to output string - // 4 => base state in input string; inside quoted sequence in output string - // 5 => first char inside a quoted sequence in input string; - // inside quoted sequence in output string - StringBuilder result = new StringBuilder(); - int state = 0; - outer: - for (int offset = 0; offset < input.length(); offset++) { - char ch = input.charAt(offset); - - // Handle a quote character (state shift) - if (ch == '\'') { - if (state == 0) { - result.append('\''); - state = 1; - continue; - } else if (state == 1) { - result.append('\''); - state = 0; - continue; - } else if (state == 2) { - state = 3; - continue; - } else if (state == 3) { - result.append('\''); - result.append('\''); - state = 1; - continue; - } else if (state == 4) { - state = 5; - continue; - } else { - assert state == 5; - result.append('\''); - result.append('\''); - state = 4; - continue; - } - } - - if (state == 0 || state == 3 || state == 4) { - for (String[] pair : table) { - // Perform a greedy match on this symbol string - if (input.regionMatches(offset, pair[0], 0, pair[0].length())) { - // Skip ahead past this region for the next iteration - offset += pair[0].length() - 1; - if (state == 3 || state == 4) { - result.append('\''); - state = 0; - } - result.append(pair[1]); - continue outer; - } - } - // No replacement found. Check if a special quote is necessary - for (String[] pair : table) { - if (input.regionMatches(offset, pair[1], 0, pair[1].length())) { - if (state == 0) { - result.append('\''); - state = 4; - } - result.append(ch); - continue outer; - } - } - // Still nothing. Copy the char verbatim. (Add a close quote if necessary) - if (state == 3 || state == 4) { - result.append('\''); - state = 0; - } - result.append(ch); - } else { - assert state == 1 || state == 2 || state == 5; - result.append(ch); - state = 2; - } - } - // Resolve final quotes - if (state == 3 || state == 4) { - result.append('\''); - state = 0; - } - if (state != 0) { - throw new IllegalArgumentException("Malformed localized pattern: unterminated quote"); - } - return result.toString(); - } - - public static final int IGNORE_ROUNDING_NEVER = 0; - public static final int IGNORE_ROUNDING_IF_CURRENCY = 1; - public static final int IGNORE_ROUNDING_ALWAYS = 2; - - static void parse(String pattern, Properties properties, int ignoreRounding) { - if (pattern == null || pattern.length() == 0) { - // Backwards compatibility requires that we reset to the default values. - // TODO: Only overwrite the properties that "saveToProperties" normally touches? - properties.clear(); - return; - } - - // TODO: Use whitespace characters from PatternProps - // TODO: Use thread locals here. - LdmlPatternInfo.PatternParseResult result = LdmlPatternInfo.parse(pattern); - saveToProperties(properties, result, ignoreRounding); - } - - /** Finalizes the temporary data stored in the PatternParseResult to the Builder. */ - private static void saveToProperties( - Properties properties, LdmlPatternInfo.PatternParseResult ppr, int _ignoreRounding) { - // Translate from PatternParseResult to Properties. - // Note that most data from "negative" is ignored per the specification of DecimalFormat. - - LdmlPatternInfo.SubpatternParseResult positive = ppr.positive; - LdmlPatternInfo.SubpatternParseResult negative = ppr.negative; - String pattern = ppr.pattern; - - boolean ignoreRounding; - if (_ignoreRounding == IGNORE_ROUNDING_NEVER) { - ignoreRounding = false; - } else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) { - ignoreRounding = positive.hasCurrencySign; - } else { - assert _ignoreRounding == IGNORE_ROUNDING_ALWAYS; - ignoreRounding = true; - } - - // Grouping settings - short grouping1 = (short) (positive.groupingSizes & 0xffff); - short grouping2 = (short) ((positive.groupingSizes >>> 16) & 0xffff); - short grouping3 = (short) ((positive.groupingSizes >>> 32) & 0xffff); - if (grouping2 != -1) { - properties.setGroupingSize(grouping1); - } else { - properties.setGroupingSize(-1); - } - if (grouping3 != -1) { - properties.setSecondaryGroupingSize(grouping2); - } else { - properties.setSecondaryGroupingSize(-1); - } - - // 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(-1); - properties.setMaximumFractionDigits(-1); - properties.setRoundingIncrement(null); - properties.setMinimumSignificantDigits(positive.minimumSignificantDigits); - properties.setMaximumSignificantDigits(positive.maximumSignificantDigits); - } else if (positive.rounding != null) { - if (!ignoreRounding) { - properties.setMinimumFractionDigits(minFrac); - properties.setMaximumFractionDigits(positive.maximumFractionDigits); - properties.setRoundingIncrement( - positive.rounding.toBigDecimal().setScale(positive.minimumFractionDigits)); - } else { - properties.setMinimumFractionDigits(-1); - properties.setMaximumFractionDigits(-1); - properties.setRoundingIncrement(null); - } - properties.setMinimumSignificantDigits(-1); - properties.setMaximumSignificantDigits(-1); - } else { - if (!ignoreRounding) { - properties.setMinimumFractionDigits(minFrac); - properties.setMaximumFractionDigits(positive.maximumFractionDigits); - properties.setRoundingIncrement(null); - } else { - properties.setMinimumFractionDigits(-1); - properties.setMaximumFractionDigits(-1); - properties.setRoundingIncrement(null); - } - properties.setMinimumSignificantDigits(-1); - properties.setMaximumSignificantDigits(-1); - } - - // 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(-1); - } - } else { - properties.setExponentSignAlwaysShown(false); - properties.setMinimumExponentDigits(-1); - properties.setMinimumIntegerDigits(minInt); - properties.setMaximumIntegerDigits(-1); - } - - // Compute the affix patterns (required for both padding and affixes) - String posPrefix = ppr.getString(AffixPatternProvider.Flags.PREFIX); - String posSuffix = ppr.getString(0); - - // Padding settings - if (positive.paddingEndpoints != 0) { - // The width of the positive prefix and suffix templates are included in the padding - int paddingWidth = - positive.paddingWidth - + AffixPatternUtils.estimateLength(posPrefix) - + AffixPatternUtils.estimateLength(posSuffix); - properties.setFormatWidth(paddingWidth); - String rawPaddingString = ppr.getString(AffixPatternProvider.Flags.PADDING); - if (rawPaddingString.length() == 1) { - properties.setPadString(rawPaddingString); - } else if (rawPaddingString.length() == 2) { - if (rawPaddingString.charAt(0) == '\'') { - properties.setPadString("'"); - } else { - properties.setPadString(rawPaddingString); - } - } else { - properties.setPadString(rawPaddingString.substring(1, rawPaddingString.length() - 1)); - } - assert positive.paddingLocation != null; - properties.setPadPosition(positive.paddingLocation); - } else { - properties.setFormatWidth(-1); - properties.setPadString(null); - properties.setPadPosition(null); - } - - // Set the affixes - // Always call the setter, even if the prefixes are empty, especially in the case of the - // negative prefix pattern, to prevent default values from overriding the pattern. - properties.setPositivePrefixPattern(posPrefix); - properties.setPositiveSuffixPattern(posSuffix); - if (negative != null) { - properties.setNegativePrefixPattern( - ppr.getString( - AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN | AffixPatternProvider.Flags.PREFIX)); - properties.setNegativeSuffixPattern( - ppr.getString(AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN)); - } 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(0); - } - } -} 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 index d86c66945aa..dc36c545d7e 100644 --- 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 @@ -17,13 +17,14 @@ import java.util.Map; import com.ibm.icu.impl.number.Parse.GroupingMode; import com.ibm.icu.impl.number.Parse.ParseMode; -import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition; import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; import com.ibm.icu.text.CurrencyPluralInfo; import com.ibm.icu.text.PluralRules; import com.ibm.icu.util.Currency; import com.ibm.icu.util.Currency.CurrencyUsage; +import newapi.impl.Padder.PadPosition; + public class Properties implements Cloneable, Serializable { private static final Properties DEFAULT = new Properties(); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ThingsNeedingNewHome.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ThingsNeedingNewHome.java deleted file mode 100644 index e322c87bdb0..00000000000 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ThingsNeedingNewHome.java +++ /dev/null @@ -1,59 +0,0 @@ -// © 2017 and later: Unicode, Inc. and others. -// License & terms of use: http://www.unicode.org/copyright.html#License -package com.ibm.icu.impl.number; - -/** @author sffc */ -public class ThingsNeedingNewHome { - public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space - - 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 - } - } - } - - /** - * 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(Properties properties) { - return ((properties.getCurrency() != null) - || properties.getCurrencyPluralInfo() != null - || properties.getCurrencyUsage() != null - || AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern()) - || AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern()) - || AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern()) - || AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern())); - } -} 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 index bc2f9ae3e66..876fa72b404 100644 --- 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 @@ -3,15 +3,20 @@ package com.ibm.icu.impl.number.modifiers; import com.ibm.icu.impl.number.Modifier; -import com.ibm.icu.impl.number.Modifier.AffixModifier; + +// TODO: This class is currently unused, but it might be useful for something in the future. +// Should probably be moved to a different package. + import com.ibm.icu.impl.number.NumberStringBuilder; 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 { +/** + * The canonical implementation of {@link Modifier}, containing a prefix and suffix string. + */ +public class ConstantAffixModifier implements Modifier { // TODO: Avoid making a new instance by default if prefix and suffix are empty - public static final AffixModifier EMPTY = new ConstantAffixModifier(); + public static final ConstantAffixModifier EMPTY = new ConstantAffixModifier(); private final String prefix; private final String suffix; @@ -69,26 +74,6 @@ public class ConstantAffixModifier extends Modifier.BaseModifier implements Affi return strong; } - public boolean contentEquals(CharSequence _prefix, CharSequence _suffix) { - if (_prefix == null && !prefix.isEmpty()) - return false; - if (_suffix == null && !suffix.isEmpty()) - return false; - if (_prefix != null && prefix.length() != _prefix.length()) - return false; - if (_suffix != null && 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("", prefix, suffix); 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 index b2e0164c2b5..26f57d7870f 100644 --- 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 @@ -3,7 +3,6 @@ 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.text.NumberFormat.Field; @@ -11,11 +10,10 @@ 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(); +public class ConstantMultiFieldModifier implements Modifier { + // NOTE: In Java, these are stored as array pointers. In C++, the NumberStringBuilder is stored by + // value and is treated internally as immutable. protected final char[] prefixChars; protected final char[] suffixChars; protected final Field[] prefixFields; @@ -30,14 +28,6 @@ public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements this.strong = strong; } - private ConstantMultiFieldModifier() { - prefixChars = new char[0]; - suffixChars = new char[0]; - prefixFields = new Field[0]; - suffixFields = new Field[0]; - strong = false; - } - @Override public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { // Insert the suffix first since inserting the prefix will change the rightIndex @@ -56,10 +46,6 @@ public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements return strong; } - public boolean contentEquals(NumberStringBuilder prefix, NumberStringBuilder suffix) { - return prefix.contentEquals(prefixChars, prefixFields) && suffix.contentEquals(suffixChars, suffixFields); - } - @Override public String toString() { NumberStringBuilder temp = new NumberStringBuilder(); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/CurrencySpacingEnabledModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/CurrencySpacingEnabledModifier.java index 66b7c2d6204..d9318849a62 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/CurrencySpacingEnabledModifier.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/CurrencySpacingEnabledModifier.java @@ -10,163 +10,144 @@ import com.ibm.icu.text.UnicodeSet; /** Identical to {@link ConstantMultiFieldModifier}, but supports currency spacing. */ public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier { - // These are the default currency spacing UnicodeSets in CLDR. - // Pre-compute them for performance. - // TODO: Is there a way to write a unit test to make sure these hard-coded values - // stay consistent with CLDR? - private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze(); - private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze(); + // These are the default currency spacing UnicodeSets in CLDR. + // Pre-compute them for performance. + // The unit test testCurrencySpacingPatternStability() will start failing if these change in CLDR. + private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze(); + private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze(); - // Constants for better readability. Types are for compiler checking. - static final byte PREFIX = 0; - static final byte SUFFIX = 1; - static final short IN_CURRENCY = 0; - static final short IN_NUMBER = 1; + // Constants for better readability. Types are for compiler checking. + static final byte PREFIX = 0; + static final byte SUFFIX = 1; + static final short IN_CURRENCY = 0; + static final short IN_NUMBER = 1; - private final UnicodeSet afterPrefixUnicodeSet; - private final String afterPrefixInsert; - private final UnicodeSet beforeSuffixUnicodeSet; - private final String beforeSuffixInsert; + private final UnicodeSet afterPrefixUnicodeSet; + private final String afterPrefixInsert; + private final UnicodeSet beforeSuffixUnicodeSet; + private final String beforeSuffixInsert; - /** Build code path */ - public CurrencySpacingEnabledModifier( - NumberStringBuilder prefix, - NumberStringBuilder suffix, - boolean strong, - DecimalFormatSymbols symbols) { - super(prefix, suffix, strong); + /** Safe code path */ + public CurrencySpacingEnabledModifier(NumberStringBuilder prefix, NumberStringBuilder suffix, boolean strong, + DecimalFormatSymbols symbols) { + super(prefix, suffix, strong); - // Check for currency spacing. Do not build the UnicodeSets unless there is - // a currency code point at a boundary. - if (prefixFields.length > 0 - && prefixFields[prefixFields.length - 1] == NumberFormat.Field.CURRENCY) { - int prefixCp = Character.codePointBefore(prefixChars, prefixChars.length); - UnicodeSet prefixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, PREFIX); - if (prefixUnicodeSet.contains(prefixCp)) { - afterPrefixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, PREFIX); - afterPrefixInsert = getInsertString(symbols, PREFIX); - } else { - afterPrefixUnicodeSet = null; - afterPrefixInsert = null; - } - } else { - afterPrefixUnicodeSet = null; - afterPrefixInsert = null; + // Check for currency spacing. Do not build the UnicodeSets unless there is + // a currency code point at a boundary. + if (prefix.length() > 0 && prefix.fieldAt(prefix.length() - 1) == NumberFormat.Field.CURRENCY) { + int prefixCp = prefix.getLastCodePoint(); + UnicodeSet prefixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, PREFIX); + if (prefixUnicodeSet.contains(prefixCp)) { + afterPrefixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, PREFIX); + afterPrefixUnicodeSet.freeze(); // no-op if set is already frozen + afterPrefixInsert = getInsertString(symbols, PREFIX); + } else { + afterPrefixUnicodeSet = null; + afterPrefixInsert = null; + } + } else { + afterPrefixUnicodeSet = null; + afterPrefixInsert = null; + } + if (suffix.length() > 0 && suffix.fieldAt(0) == NumberFormat.Field.CURRENCY) { + int suffixCp = suffix.getLastCodePoint(); + UnicodeSet suffixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, SUFFIX); + if (suffixUnicodeSet.contains(suffixCp)) { + beforeSuffixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, SUFFIX); + beforeSuffixUnicodeSet.freeze(); // no-op if set is already frozen + beforeSuffixInsert = getInsertString(symbols, SUFFIX); + } else { + beforeSuffixUnicodeSet = null; + beforeSuffixInsert = null; + } + } else { + beforeSuffixUnicodeSet = null; + beforeSuffixInsert = null; + } } - if (suffixFields.length > 0 && suffixFields[0] == NumberFormat.Field.CURRENCY) { - int suffixCp = Character.codePointAt(suffixChars, 0); - UnicodeSet suffixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, SUFFIX); - if (suffixUnicodeSet.contains(suffixCp)) { - beforeSuffixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, SUFFIX); - beforeSuffixInsert = getInsertString(symbols, SUFFIX); - } else { - beforeSuffixUnicodeSet = null; - beforeSuffixInsert = null; - } - } else { - beforeSuffixUnicodeSet = null; - beforeSuffixInsert = null; - } - } - /** Non-build code path */ - public static int applyCurrencySpacing( - NumberStringBuilder output, - int prefixStart, - int prefixLen, - int suffixStart, - int suffixLen, - DecimalFormatSymbols symbols) { - int length = 0; - boolean hasPrefix = (prefixLen > 0); - boolean hasSuffix = (suffixLen > 0); - boolean hasNumber = (suffixStart - prefixStart - prefixLen > 0); // could be empty string - if (hasPrefix && hasNumber) { - length += applyCurrencySpacingAffix(output, prefixStart + prefixLen, PREFIX, symbols); - } - if (hasSuffix && hasNumber) { - length += applyCurrencySpacingAffix(output, suffixStart + length, SUFFIX, symbols); - } - return length; - } + /** Safe code path */ + @Override + public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { + // Currency spacing logic + int length = 0; + if (rightIndex - leftIndex > 0 && afterPrefixUnicodeSet != null + && afterPrefixUnicodeSet.contains(output.codePointAt(leftIndex))) { + // TODO: Should we use the CURRENCY field here? + length += output.insert(leftIndex, afterPrefixInsert, null); + } + if (rightIndex - leftIndex > 0 && beforeSuffixUnicodeSet != null + && beforeSuffixUnicodeSet.contains(output.codePointBefore(rightIndex))) { + // TODO: Should we use the CURRENCY field here? + length += output.insert(rightIndex + length, beforeSuffixInsert, null); + } - private static int applyCurrencySpacingAffix( - NumberStringBuilder output, int index, byte affix, DecimalFormatSymbols symbols) { - // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix. - // This works even if the last code point in the prefix is 2 code units because the - // field value gets populated to both indices in the field array. - NumberFormat.Field affixField = - (affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index); - if (affixField != NumberFormat.Field.CURRENCY) { - return 0; - } - int affixCp = - (affix == PREFIX) - ? Character.codePointBefore(output, index) - : Character.codePointAt(output, index); - UnicodeSet affixUniset = getUnicodeSet(symbols, IN_CURRENCY, affix); - if (!affixUniset.contains(affixCp)) { - return 0; + // Call super for the remaining logic + length += super.apply(output, leftIndex, rightIndex + length); + return length; } - int numberCp = - (affix == PREFIX) - ? Character.codePointAt(output, index) - : Character.codePointBefore(output, index); - UnicodeSet numberUniset = getUnicodeSet(symbols, IN_NUMBER, affix); - if (!numberUniset.contains(numberCp)) { - return 0; - } - String spacingString = getInsertString(symbols, affix); - - // NOTE: This next line *inserts* the spacing string, triggering an arraycopy. - // It would be more efficient if this could be done before affixes were attached, - // so that it could be prepended/appended instead of inserted. - // However, the build code path is more efficient, and this is the most natural - // place to put currency spacing in the non-build code path. - // TODO: Should we use the CURRENCY field here? - return output.insert(index, spacingString, null); - } - private static UnicodeSet getUnicodeSet(DecimalFormatSymbols symbols, short position, byte affix) { - String pattern = - symbols.getPatternForCurrencySpacing( - position == IN_CURRENCY - ? DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH - : DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, - affix == SUFFIX); - if (pattern.equals("[:digit:]")) { - return UNISET_DIGIT; - } else if (pattern.equals("[:^S:]")) { - return UNISET_NOTS; - } else { - return new UnicodeSet(pattern); + /** Unsafe code path */ + public static int applyCurrencySpacing(NumberStringBuilder output, int prefixStart, int prefixLen, int suffixStart, + int suffixLen, DecimalFormatSymbols symbols) { + int length = 0; + boolean hasPrefix = (prefixLen > 0); + boolean hasSuffix = (suffixLen > 0); + boolean hasNumber = (suffixStart - prefixStart - prefixLen > 0); // could be empty string + if (hasPrefix && hasNumber) { + length += applyCurrencySpacingAffix(output, prefixStart + prefixLen, PREFIX, symbols); + } + if (hasSuffix && hasNumber) { + length += applyCurrencySpacingAffix(output, suffixStart + length, SUFFIX, symbols); + } + return length; } - } - private static String getInsertString(DecimalFormatSymbols symbols, byte affix) { - return symbols.getPatternForCurrencySpacing( - DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX); - } + /** Unsafe code path */ + private static int applyCurrencySpacingAffix(NumberStringBuilder output, int index, byte affix, + DecimalFormatSymbols symbols) { + // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix. + // This works even if the last code point in the prefix is 2 code units because the + // field value gets populated to both indices in the field array. + NumberFormat.Field affixField = (affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index); + if (affixField != NumberFormat.Field.CURRENCY) { + return 0; + } + int affixCp = (affix == PREFIX) ? output.codePointBefore(index) : output.codePointAt(index); + UnicodeSet affixUniset = getUnicodeSet(symbols, IN_CURRENCY, affix); + if (!affixUniset.contains(affixCp)) { + return 0; + } + int numberCp = (affix == PREFIX) ? output.codePointAt(index) : output.codePointBefore(index); + UnicodeSet numberUniset = getUnicodeSet(symbols, IN_NUMBER, affix); + if (!numberUniset.contains(numberCp)) { + return 0; + } + String spacingString = getInsertString(symbols, affix); - @Override - public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) { - // Currency spacing logic - int length = 0; - if (rightIndex - leftIndex > 0 - && afterPrefixUnicodeSet != null - && afterPrefixUnicodeSet.contains(Character.codePointAt(output, leftIndex))) { - // TODO: Should we use the CURRENCY field here? - length += output.insert(leftIndex, afterPrefixInsert, null); + // NOTE: This next line *inserts* the spacing string, triggering an arraycopy. + // It would be more efficient if this could be done before affixes were attached, + // so that it could be prepended/appended instead of inserted. + // However, the build code path is more efficient, and this is the most natural + // place to put currency spacing in the non-build code path. + // TODO: Should we use the CURRENCY field here? + return output.insert(index, spacingString, null); } - if (rightIndex - leftIndex > 0 - && beforeSuffixUnicodeSet != null - && beforeSuffixUnicodeSet.contains(Character.codePointBefore(output, rightIndex))) { - // TODO: Should we use the CURRENCY field here? - length += output.insert(rightIndex + length, beforeSuffixInsert, null); + + private static UnicodeSet getUnicodeSet(DecimalFormatSymbols symbols, short position, byte affix) { + String pattern = symbols + .getPatternForCurrencySpacing(position == IN_CURRENCY ? DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH + : DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, affix == SUFFIX); + if (pattern.equals("[:digit:]")) { + return UNISET_DIGIT; + } else if (pattern.equals("[:^S:]")) { + return UNISET_NOTS; + } else { + return new UnicodeSet(pattern); + } } - // Call super for the remaining logic - length += super.apply(output, leftIndex, rightIndex + length); - return length; - } + private static String getInsertString(DecimalFormatSymbols symbols, byte affix) { + return symbols.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX); + } } 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 deleted file mode 100644 index 15ba186916a..00000000000 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java +++ /dev/null @@ -1,54 +0,0 @@ -// © 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.Modifier; - -// 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. - -// TODO: This class is currently unused. Probably should be deleted. - -/** - * 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 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; - } -} 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 deleted file mode 100644 index 6ccd243f8c6..00000000000 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java +++ /dev/null @@ -1,31 +0,0 @@ -// © 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; - -// TODO: This class is currently unused. Should probably be deleted. - -/** A class containing a positive form and a negative form of {@link ConstantAffixModifier}. */ -public class PositiveNegativeAffixModifier 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; - } -} 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 index b88b112ad44..f49360b9195 100644 --- 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 @@ -11,15 +11,15 @@ 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 { +public class SimpleModifier implements Modifier { private final String compiledPattern; private final Field field; private final boolean strong; - private final int prefixLength; private final int suffixOffset; private final int suffixLength; + /** TODO: This is copied from SimpleFormatterImpl. */ private static final int ARG_NUM_LIMIT = 0x100; /** Creates a modifier that uses the SimpleFormatter string formats. */ @@ -79,7 +79,6 @@ public class SimpleModifier extends Modifier.BaseModifier { * @return The number of characters (UTF-16 code points) that were added to the StringBuilder. */ public int formatAsPrefixSuffix(NumberStringBuilder result, int startIndex, int endIndex, Field field) { - assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 1; if (prefixLength > 0) { result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field); } @@ -89,28 +88,4 @@ public class SimpleModifier extends Modifier.BaseModifier { } return prefixLength + suffixLength; } - - /** 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); - new SimpleModifier(compiledPattern, null, false).apply(output, (Integer) outputs[j][1], - (Integer) outputs[j][2]); - String expected = expecteds[j][i]; - String actual = output.toString(); - assert expected.equals(actual); - } - } - } } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java index d56f5d7e2f6..d84fdbe0f75 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java @@ -13,11 +13,10 @@ import java.text.FieldPosition; import java.text.ParseException; import java.text.ParsePosition; +import com.ibm.icu.impl.number.AffixPatternUtils; import com.ibm.icu.impl.number.Parse; -import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.PatternAndPropertyUtils; import com.ibm.icu.impl.number.Properties; -import com.ibm.icu.impl.number.ThingsNeedingNewHome; -import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.math.BigDecimal; import com.ibm.icu.math.MathContext; @@ -33,6 +32,7 @@ import newapi.LocalizedNumberFormatter; import newapi.NumberFormatter; import newapi.NumberPropertyMapper; import newapi.impl.MacroProps; +import newapi.impl.Padder.PadPosition; /** * {@icuenhanced java.text.DecimalFormat}.{@icu _usage_} DecimalFormat is the primary @@ -301,7 +301,7 @@ public class DecimalFormat extends NumberFormat { properties = new Properties(); exportedProperties = new Properties(); // Regression: ignore pattern rounding information if the pattern has currency symbols. - setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY); + setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY); refreshFormatter(); } @@ -330,7 +330,7 @@ public class DecimalFormat extends NumberFormat { properties = new Properties(); exportedProperties = new Properties(); // Regression: ignore pattern rounding information if the pattern has currency symbols. - setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY); + setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY); refreshFormatter(); } @@ -359,7 +359,7 @@ public class DecimalFormat extends NumberFormat { properties = new Properties(); exportedProperties = new Properties(); // Regression: ignore pattern rounding information if the pattern has currency symbols. - setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY); + setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY); refreshFormatter(); } @@ -402,9 +402,9 @@ public class DecimalFormat extends NumberFormat { || choice == CASHCURRENCYSTYLE || choice == STANDARDCURRENCYSTYLE || choice == PLURALCURRENCYSTYLE) { - setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_ALWAYS); + setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_ALWAYS); } else { - setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY); + setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY); } refreshFormatter(); } @@ -445,7 +445,7 @@ public class DecimalFormat extends NumberFormat { * @stable ICU 2.0 */ public synchronized void applyPattern(String pattern) { - setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_NEVER); + setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER); // Backwards compatibility: clear out user-specified prefix and suffix, // as well as CurrencyPluralInfo. properties.setPositivePrefix(null); @@ -469,7 +469,7 @@ public class DecimalFormat extends NumberFormat { * @stable ICU 2.0 */ public synchronized void applyLocalizedPattern(String localizedPattern) { - String pattern = PatternString.convertLocalized(localizedPattern, symbols, false); + String pattern = PatternAndPropertyUtils.convertLocalized(localizedPattern, symbols, false); applyPattern(pattern); } @@ -752,7 +752,7 @@ public class DecimalFormat extends NumberFormat { if (!(obj instanceof Number)) throw new IllegalArgumentException(); Number number = (Number) obj; FormattedNumber output = formatter.format(number); - return output.toAttributedCharacterIterator(); + return output.getAttributes(); } /** @@ -2378,12 +2378,12 @@ public class DecimalFormat extends NumberFormat { // so that CurrencyUsage is reflected properly. // TODO: Consider putting this logic in PatternString.java instead. Properties tprops = threadLocalProperties.get().copyFrom(properties); - if (ThingsNeedingNewHome.useCurrency(properties)) { + if (useCurrency(properties)) { tprops.setMinimumFractionDigits(exportedProperties.getMinimumFractionDigits()); tprops.setMaximumFractionDigits(exportedProperties.getMaximumFractionDigits()); tprops.setRoundingIncrement(exportedProperties.getRoundingIncrement()); } - return PatternString.propertiesToString(tprops); + return PatternAndPropertyUtils.propertiesToString(tprops); } /** @@ -2396,7 +2396,21 @@ public class DecimalFormat extends NumberFormat { */ public synchronized String toLocalizedPattern() { String pattern = toPattern(); - return PatternString.convertLocalized(pattern, symbols, true); + return PatternAndPropertyUtils.convertLocalized(pattern, symbols, true); + } + + /** + * Converts this DecimalFormat to a NumberFormatter. Starting in ICU 60, + * NumberFormatter is the recommended way to format numbers. + * + * @return An instance of {@link LocalizedNumberFormatter} with the same behavior as this instance of + * DecimalFormat. + * @see NumberFormatter + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ + public LocalizedNumberFormatter toNumberFormatter() { + return formatter; } /** @@ -2460,6 +2474,20 @@ public class DecimalFormat extends NumberFormat { } } + /** + * Returns true if the currency is set in The property bag or if currency symbols are present in + * the prefix/suffix pattern. + */ + private static boolean useCurrency(Properties properties) { + return ((properties.getCurrency() != null) + || properties.getCurrencyPluralInfo() != null + || properties.getCurrencyUsage() != null + || AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern()) + || AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern()) + || AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern()) + || AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern())); + } + /** * Updates the property bag with settings from the given pattern. * @@ -2467,15 +2495,15 @@ public class DecimalFormat extends NumberFormat { * @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. One of {@link - * PatternString#IGNORE_ROUNDING_ALWAYS}, {@link PatternString#IGNORE_ROUNDING_IF_CURRENCY}, - * or {@link PatternString#IGNORE_ROUNDING_NEVER}. - * @see PatternString#parseToExistingProperties + * PatternAndPropertyUtils#IGNORE_ROUNDING_ALWAYS}, {@link PatternAndPropertyUtils#IGNORE_ROUNDING_IF_CURRENCY}, + * or {@link PatternAndPropertyUtils#IGNORE_ROUNDING_NEVER}. + * @see PatternAndPropertyUtils#parseToExistingProperties */ void setPropertiesFromPattern(String pattern, int ignoreRounding) { if (pattern == null) { throw new NullPointerException(); } - PatternString.parseToExistingProperties(pattern, properties, ignoreRounding); + PatternAndPropertyUtils.parseToExistingProperties(pattern, properties, ignoreRounding); } /** 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 430b13f05fd..d28454db6ee 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 @@ -1134,8 +1134,6 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { *

For more information, see UTS#35 section 5.10.2. * - *

Note: ICU4J does not currently use this information. - * * @param itemType one of CURRENCY_SPC_CURRENCY_MATCH, CURRENCY_SPC_SURROUNDING_MATCH * or CURRENCY_SPC_INSERT * @param beforeCurrency true to get the beforeCurrency values, false diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/Dimensionless.java b/icu4j/main/classes/core/src/com/ibm/icu/util/Dimensionless.java deleted file mode 100644 index 96cb431e49b..00000000000 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/Dimensionless.java +++ /dev/null @@ -1,19 +0,0 @@ -// © 2017 and later: Unicode, Inc. and others. -// License & terms of use: http://www.unicode.org/copyright.html#License -package com.ibm.icu.util; - -public class Dimensionless extends MeasureUnit { - - public static final Dimensionless BASE = - (Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "base"); - - public static final Dimensionless PERCENT = - (Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "percent"); - - public static final Dimensionless PERMILLE = - (Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "permille"); - - protected Dimensionless(String subType) { - super("dimensionless", subType); - } -} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java index ce502158129..4cd71a258ac 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java @@ -195,8 +195,8 @@ public class MeasureUnit implements Serializable { factory = CURRENCY_FACTORY; } else if ("duration".equals(type)) { factory = TIMEUNIT_FACTORY; - } else if ("dimensionless".equals(type)) { - factory = DIMENSIONLESS_FACTORY; + } else if ("none".equals(type)) { + factory = NOUNIT_FACTORY; } else { factory = UNIT_FACTORY; } @@ -251,10 +251,10 @@ public class MeasureUnit implements Serializable { } }; - static Factory DIMENSIONLESS_FACTORY = new Factory() { + static Factory NOUNIT_FACTORY = new Factory() { @Override public MeasureUnit create(String type, String subType) { - return new Dimensionless(subType); + return new NoUnit(subType); } }; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/NoUnit.java b/icu4j/main/classes/core/src/com/ibm/icu/util/NoUnit.java new file mode 100644 index 00000000000..41d4ae55be5 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/NoUnit.java @@ -0,0 +1,19 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.util; + +public class NoUnit extends MeasureUnit { + + public static final NoUnit BASE = + (NoUnit) MeasureUnit.internalGetInstance("none", "base"); + + public static final NoUnit PERCENT = + (NoUnit) MeasureUnit.internalGetInstance("none", "percent"); + + public static final NoUnit PERMILLE = + (NoUnit) MeasureUnit.internalGetInstance("none", "permille"); + + protected NoUnit(String subType) { + super("none", subType); + } +} diff --git a/icu4j/main/classes/core/src/newapi/CompactNotation.java b/icu4j/main/classes/core/src/newapi/CompactNotation.java index c45bb3778c9..e0830ec91da 100644 --- a/icu4j/main/classes/core/src/newapi/CompactNotation.java +++ b/icu4j/main/classes/core/src/newapi/CompactNotation.java @@ -8,17 +8,17 @@ import java.util.Set; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.number.FormatQuantity; -import com.ibm.icu.impl.number.LdmlPatternInfo; -import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult; +import com.ibm.icu.impl.number.PatternParser; +import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo; import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; import com.ibm.icu.text.CompactDecimalFormat.CompactType; import com.ibm.icu.text.PluralRules; import com.ibm.icu.util.ULocale; -import newapi.MurkyModifier.ImmutableMurkyModifier; +import newapi.MutablePatternModifier.ImmutableMurkyModifier; import newapi.impl.CompactData; import newapi.impl.MicroProps; -import newapi.impl.QuantityChain; +import newapi.impl.MicroPropsGenerator; public class CompactNotation extends Notation { @@ -35,8 +35,8 @@ public class CompactNotation extends Notation { this.compactCustomData = compactCustomData; } - /* package-private */ QuantityChain withLocaleData(ULocale dataLocale, CompactType compactType, PluralRules rules, - MurkyModifier buildReference, QuantityChain parent) { + /* package-private */ MicroPropsGenerator withLocaleData(ULocale dataLocale, CompactType compactType, PluralRules rules, + MutablePatternModifier buildReference, MicroPropsGenerator parent) { CompactData data; if (compactStyle != null) { data = CompactData.getInstance(dataLocale, compactType, compactStyle); @@ -46,7 +46,7 @@ public class CompactNotation extends Notation { return new CompactImpl(data, rules, buildReference, parent); } - private static class CompactImpl implements QuantityChain { + private static class CompactImpl implements MicroPropsGenerator { private static class CompactModInfo { public ImmutableMurkyModifier mod; @@ -56,9 +56,9 @@ public class CompactNotation extends Notation { final PluralRules rules; final CompactData data; final Map precomputedMods; - final QuantityChain parent; + final MicroPropsGenerator parent; - private CompactImpl(CompactData data, PluralRules rules, MurkyModifier buildReference, QuantityChain parent) { + private CompactImpl(CompactData data, PluralRules rules, MutablePatternModifier buildReference, MicroPropsGenerator parent) { this.data = data; this.rules = rules; if (buildReference != null) { @@ -73,23 +73,23 @@ public class CompactNotation extends Notation { /** Used by the safe code path */ private static Map precomputeAllModifiers(CompactData data, - MurkyModifier buildReference) { + MutablePatternModifier buildReference) { Map precomputedMods = new HashMap(); Set allPatterns = data.getAllPatterns(); for (String patternString : allPatterns) { CompactModInfo info = new CompactModInfo(); - PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString); + ParsedPatternInfo patternInfo = PatternParser.parse(patternString); buildReference.setPatternInfo(patternInfo); info.mod = buildReference.createImmutable(); - info.numDigits = patternInfo.positive.totalIntegerDigits; + info.numDigits = patternInfo.positive.integerTotal; precomputedMods.put(patternString, info); } return precomputedMods; } @Override - public MicroProps withQuantity(FormatQuantity input) { - MicroProps micros = parent.withQuantity(input); + public MicroProps processQuantity(FormatQuantity input) { + MicroProps micros = parent.processQuantity(input); assert micros.rounding != null; // Treat zero as if it had magnitude 0 @@ -111,17 +111,17 @@ public class CompactNotation extends Notation { // Use the default (non-compact) modifier. // No need to take any action. } else if (precomputedMods != null) { - // Build code path. + // Safe code path. CompactModInfo info = precomputedMods.get(patternString); info.mod.applyToMicros(micros, input); numDigits = info.numDigits; } else { - // Non-build code path. + // Unsafe code path. // Overwrite the PatternInfo in the existing modMiddle - assert micros.modMiddle instanceof MurkyModifier; - PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString); - ((MurkyModifier) micros.modMiddle).setPatternInfo(patternInfo); - numDigits = patternInfo.positive.totalIntegerDigits; + assert micros.modMiddle instanceof MutablePatternModifier; + ParsedPatternInfo patternInfo = PatternParser.parse(patternString); + ((MutablePatternModifier) micros.modMiddle).setPatternInfo(patternInfo); + numDigits = patternInfo.positive.integerTotal; } // FIXME: Deal with numDigits == 0 (Awaiting a test case) diff --git a/icu4j/main/classes/core/src/newapi/FormattedNumber.java b/icu4j/main/classes/core/src/newapi/FormattedNumber.java index 33f432361d6..310086c0490 100644 --- a/icu4j/main/classes/core/src/newapi/FormattedNumber.java +++ b/icu4j/main/classes/core/src/newapi/FormattedNumber.java @@ -55,7 +55,7 @@ public class FormattedNumber { fq.populateUFieldPosition(fieldPosition); } - public AttributedCharacterIterator toAttributedCharacterIterator() { + public AttributedCharacterIterator getAttributes() { return nsb.getIterator(); } diff --git a/icu4j/main/classes/core/src/newapi/FractionRounder.java b/icu4j/main/classes/core/src/newapi/FractionRounder.java index b6dd1f46600..3d2a5f7eac2 100644 --- a/icu4j/main/classes/core/src/newapi/FractionRounder.java +++ b/icu4j/main/classes/core/src/newapi/FractionRounder.java @@ -22,13 +22,13 @@ public abstract class FractionRounder extends Rounder { *

* This setting does not affect the number of trailing zeros. For example, 3.01 would print as "3", not "3.0". * - * @param minFigures + * @param minSignificantDigits * The number of significant figures to guarantee. * @return An immutable object for chaining. */ - public Rounder withMinFigures(int minFigures) { - if (minFigures > 0 && minFigures <= MAX_VALUE) { - return constructFractionSignificant(this, minFigures, -1); + public Rounder withMinDigits(int minSignificantDigits) { + if (minSignificantDigits > 0 && minSignificantDigits <= MAX_VALUE) { + return constructFractionSignificant(this, minSignificantDigits, -1); } else { throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); } @@ -46,13 +46,13 @@ public abstract class FractionRounder extends Rounder { * This setting does not affect the number of trailing zeros. For example, with fixed fraction of 2, 123.4 would * become "120.00". * - * @param maxFigures + * @param maxSignificantDigits * Round the number to no more than this number of significant figures. * @return An immutable object for chaining. */ - public Rounder withMaxFigures(int maxFigures) { - if (maxFigures > 0 && maxFigures <= MAX_VALUE) { - return constructFractionSignificant(this, -1, maxFigures); + public Rounder withMaxDigits(int maxSignificantDigits) { + if (maxSignificantDigits > 0 && maxSignificantDigits <= MAX_VALUE) { + return constructFractionSignificant(this, -1, maxSignificantDigits); } else { throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); } diff --git a/icu4j/main/classes/core/src/newapi/Grouper.java b/icu4j/main/classes/core/src/newapi/Grouper.java index 0db2586769e..e4aeadc712b 100644 --- a/icu4j/main/classes/core/src/newapi/Grouper.java +++ b/icu4j/main/classes/core/src/newapi/Grouper.java @@ -3,7 +3,7 @@ package newapi; import com.ibm.icu.impl.number.FormatQuantity; -import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult; +import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo; public class Grouper { @@ -64,11 +64,11 @@ public class Grouper { } } - static Grouper normalizeType(Grouper grouping, PatternParseResult patternInfo) { + static Grouper normalizeType(Grouper grouping, ParsedPatternInfo patternInfo) { return grouping.withLocaleData(patternInfo); } - Grouper withLocaleData(PatternParseResult patternInfo) { + Grouper withLocaleData(ParsedPatternInfo patternInfo) { if (grouping1 != -2) { return this; } diff --git a/icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java b/icu4j/main/classes/core/src/newapi/LongNameHandler.java similarity index 64% rename from icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java rename to icu4j/main/classes/core/src/newapi/LongNameHandler.java index 8fee3a347e1..222c6f1f8c9 100644 --- a/icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java +++ b/icu4j/main/classes/core/src/newapi/LongNameHandler.java @@ -11,28 +11,33 @@ import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.number.FormatQuantity; import com.ibm.icu.impl.number.Modifier; import com.ibm.icu.impl.number.modifiers.SimpleModifier; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.text.NumberFormat.Field; import com.ibm.icu.text.PluralRules; import com.ibm.icu.util.Currency; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; +import newapi.NumberFormatter.UnitWidth; import newapi.impl.MeasureData; import newapi.impl.MicroProps; -import newapi.impl.QuantityChain; +import newapi.impl.MicroPropsGenerator; -class MurkyLongNameHandler implements QuantityChain { +class LongNameHandler implements MicroPropsGenerator { private final Map data; /* unsafe */ PluralRules rules; - /* unsafe */ QuantityChain parent; + /* unsafe */ MicroPropsGenerator parent; - private MurkyLongNameHandler(Map data) { + private LongNameHandler(Map data) { this.data = data; } - public static MurkyLongNameHandler getCurrencyLongNameModifiers(ULocale loc, Currency currency) { + /** For use by the "safe" code path */ + private LongNameHandler(LongNameHandler other) { + this.data = other.data; + } + + public static LongNameHandler getCurrencyLongNameModifiers(ULocale loc, Currency currency) { Map data = CurrencyData.provider.getInstance(loc, true).getUnitPatterns(); Map result = new EnumMap(StandardPlural.class); StringBuilder sb = new StringBuilder(); @@ -46,10 +51,10 @@ class MurkyLongNameHandler implements QuantityChain { Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false); result.put(plural, mod); } - return new MurkyLongNameHandler(result); + return new LongNameHandler(result); } - public static MurkyLongNameHandler getMeasureUnitModifiers(ULocale loc, MeasureUnit unit, FormatWidth width) { + public static LongNameHandler getMeasureUnitModifiers(ULocale loc, MeasureUnit unit, UnitWidth width) { Map simpleFormats = MeasureData.getMeasureData(loc, unit, width); Map result = new EnumMap(StandardPlural.class); StringBuilder sb = new StringBuilder(); @@ -62,13 +67,28 @@ class MurkyLongNameHandler implements QuantityChain { Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false); result.put(plural, mod); } - return new MurkyLongNameHandler(result); + return new LongNameHandler(result); } - public QuantityChain withLocaleData(PluralRules rules, boolean safe, QuantityChain parent) { + /** + * Applies locale data and inserts a long-name handler into the quantity chain. + * + * @param rules + * The PluralRules instance to reference. + * @param safe + * If true, creates a new object to insert into the quantity chain. If false, re-uses this + * object in the quantity chain. + * @param parent + * The old head of the quantity chain. + * @return The new head of the quantity chain. + */ + public MicroPropsGenerator withLocaleData(PluralRules rules, boolean safe, MicroPropsGenerator parent) { if (safe) { // Safe code path: return a new object - return new ImmutableLongNameHandler(data, rules, parent); + LongNameHandler copy = new LongNameHandler(this); + copy.rules = rules; + copy.parent = parent; + return copy; } else { // Unsafe code path: re-use this object! this.rules = rules; @@ -78,34 +98,12 @@ class MurkyLongNameHandler implements QuantityChain { } @Override - public MicroProps withQuantity(FormatQuantity quantity) { - MicroProps micros = parent.withQuantity(quantity); + public MicroProps processQuantity(FormatQuantity quantity) { + MicroProps micros = parent.processQuantity(quantity); // TODO: Avoid the copy here? FormatQuantity copy = quantity.createCopy(); micros.rounding.apply(copy); micros.modOuter = data.get(copy.getStandardPlural(rules)); return micros; } - - public static class ImmutableLongNameHandler implements QuantityChain { - final Map data; - final PluralRules rules; - final QuantityChain parent; - - public ImmutableLongNameHandler(Map data, PluralRules rules, QuantityChain parent) { - this.data = data; - this.rules = rules; - this.parent = parent; - } - - @Override - public MicroProps withQuantity(FormatQuantity quantity) { - MicroProps micros = parent.withQuantity(quantity); - // TODO: Avoid the copy here? - FormatQuantity copy = quantity.createCopy(); - micros.rounding.apply(copy); - micros.modOuter = data.get(copy.getStandardPlural(rules)); - return micros; - } - } } diff --git a/icu4j/main/classes/core/src/newapi/MurkyModifier.java b/icu4j/main/classes/core/src/newapi/MutablePatternModifier.java similarity index 90% rename from icu4j/main/classes/core/src/newapi/MurkyModifier.java rename to icu4j/main/classes/core/src/newapi/MutablePatternModifier.java index f71e15ffbcf..f730b0bdeb2 100644 --- a/icu4j/main/classes/core/src/newapi/MurkyModifier.java +++ b/icu4j/main/classes/core/src/newapi/MutablePatternModifier.java @@ -6,20 +6,20 @@ import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.number.AffixPatternUtils; import com.ibm.icu.impl.number.AffixPatternUtils.SymbolProvider; import com.ibm.icu.impl.number.FormatQuantity; -import com.ibm.icu.impl.number.LdmlPatternInfo; +import com.ibm.icu.impl.number.PatternParser; import com.ibm.icu.impl.number.Modifier; import com.ibm.icu.impl.number.NumberStringBuilder; import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier; import com.ibm.icu.impl.number.modifiers.CurrencySpacingEnabledModifier; import com.ibm.icu.text.DecimalFormatSymbols; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.text.PluralRules; import com.ibm.icu.util.Currency; import newapi.NumberFormatter.SignDisplay; +import newapi.NumberFormatter.UnitWidth; import newapi.impl.AffixPatternProvider; import newapi.impl.MicroProps; -import newapi.impl.QuantityChain; +import newapi.impl.MicroPropsGenerator; /** * This class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's affixes in @@ -37,11 +37,12 @@ import newapi.impl.QuantityChain; *

* This is a MUTABLE, NON-THREAD-SAFE class designed for performance. Do NOT save references to this or attempt to use * it from multiple threads! Instead, you can obtain a safe, immutable decimal format pattern modifier by calling - * {@link MurkyModifier#createImmutable}, in effect treating this instance as a builder for the immutable variant. + * {@link MutablePatternModifier#createImmutable}, in effect treating this instance as a builder for the immutable + * variant. * * FIXME: Make this package-private */ -public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, QuantityChain { +public class MutablePatternModifier implements Modifier, SymbolProvider, CharSequence, MicroPropsGenerator { // Modifier details final boolean isStrong; @@ -53,7 +54,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu // Symbol details DecimalFormatSymbols symbols; - FormatWidth unitWidth; + UnitWidth unitWidth; String currency1; String currency2; String[] currency3; @@ -64,7 +65,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu StandardPlural plural; // QuantityChain details - QuantityChain parent; + MicroPropsGenerator parent; // Transient CharSequence fields boolean inCharSequenceMode; @@ -79,13 +80,13 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu * {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should be considered * as non-strong. */ - public MurkyModifier(boolean isStrong) { + public MutablePatternModifier(boolean isStrong) { this.isStrong = isStrong; } /** * Sets a reference to the parsed decimal format pattern, usually obtained from - * {@link LdmlPatternInfo#parse(String)}, but any implementation of {@link AffixPatternProvider} is accepted. + * {@link PatternParser#parse(String)}, but any implementation of {@link AffixPatternProvider} is accepted. */ public void setPatternInfo(AffixPatternProvider patternInfo) { this.patternInfo = patternInfo; @@ -118,7 +119,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu * Required if the triple currency sign, "¤¤¤", appears in the pattern, which can be determined from the * convenience method {@link #needsPlurals()}. */ - public void setSymbols(DecimalFormatSymbols symbols, Currency currency, FormatWidth unitWidth, PluralRules rules) { + public void setSymbols(DecimalFormatSymbols symbols, Currency currency, UnitWidth unitWidth, PluralRules rules) { assert (rules != null) == needsPlurals(); this.symbols = symbols; this.unitWidth = unitWidth; @@ -182,7 +183,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu * The QuantityChain to which to chain this immutable. * @return An immutable that supports both positive and negative numbers. */ - public ImmutableMurkyModifier createImmutableAndChain(QuantityChain parent) { + public ImmutableMurkyModifier createImmutableAndChain(MicroPropsGenerator parent) { NumberStringBuilder a = new NumberStringBuilder(); NumberStringBuilder b = new NumberStringBuilder(); if (needsPlurals()) { @@ -217,25 +218,25 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu } } - public static interface ImmutableMurkyModifier extends QuantityChain { + public static interface ImmutableMurkyModifier extends MicroPropsGenerator { public void applyToMicros(MicroProps micros, FormatQuantity quantity); } public static class ImmutableMurkyModifierWithoutPlurals implements ImmutableMurkyModifier { final Modifier positive; final Modifier negative; - final QuantityChain parent; + final MicroPropsGenerator parent; - public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative, QuantityChain parent) { + public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative, MicroPropsGenerator parent) { this.positive = positive; this.negative = negative; this.parent = parent; } @Override - public MicroProps withQuantity(FormatQuantity quantity) { + public MicroProps processQuantity(FormatQuantity quantity) { assert parent != null; - MicroProps micros = parent.withQuantity(quantity); + MicroProps micros = parent.processQuantity(quantity); applyToMicros(micros, quantity); return micros; } @@ -253,9 +254,9 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu public static class ImmutableMurkyModifierWithPlurals implements ImmutableMurkyModifier { final Modifier[] mods; final PluralRules rules; - final QuantityChain parent; + final MicroPropsGenerator parent; - public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules, QuantityChain parent) { + public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules, MicroPropsGenerator parent) { assert mods.length == getModsLength(); assert rules != null; this.mods = mods; @@ -272,9 +273,9 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu } @Override - public MicroProps withQuantity(FormatQuantity quantity) { + public MicroProps processQuantity(FormatQuantity quantity) { assert parent != null; - MicroProps micros = parent.withQuantity(quantity); + MicroProps micros = parent.processQuantity(quantity); applyToMicros(micros, quantity); return micros; } @@ -290,14 +291,14 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu } } - public QuantityChain addToChain(QuantityChain parent) { + public MicroPropsGenerator addToChain(MicroPropsGenerator parent) { this.parent = parent; return this; } @Override - public MicroProps withQuantity(FormatQuantity fq) { - MicroProps micros = parent.withQuantity(fq); + public MicroProps processQuantity(FormatQuantity fq) { + MicroProps micros = parent.processQuantity(fq); if (needsPlurals()) { // TODO: Fix this. Avoid the copy. FormatQuantity copy = fq.createCopy(); @@ -321,7 +322,8 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu @Override public int getPrefixLength() { - return insertPrefix(null, 0); + NumberStringBuilder dummy = new NumberStringBuilder(); + return insertPrefix(dummy, 0); } @Override @@ -356,7 +358,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu return symbols.getPerMillString(); case AffixPatternUtils.TYPE_CURRENCY_SINGLE: // FormatWidth ISO overrides the singular currency symbol - if (unitWidth == FormatWidth.SHORT) { + if (unitWidth == UnitWidth.ISO_CODE) { return currency2; } else { return currency1; @@ -387,7 +389,8 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu inCharSequenceMode = true; // Should the output render '+' where '-' would normally appear in the pattern? - plusReplacesMinusSign = !isNegative && signDisplay == SignDisplay.ALWAYS + plusReplacesMinusSign = !isNegative + && (signDisplay == SignDisplay.ALWAYS || signDisplay == SignDisplay.ACCOUNTING_ALWAYS) && patternInfo.positiveHasPlusSign() == false; // Should we use the negative affix pattern? (If not, we will use the positive one) diff --git a/icu4j/main/classes/core/src/newapi/NumberFormatter.java b/icu4j/main/classes/core/src/newapi/NumberFormatter.java index c3abf489e40..7cab5dffeb3 100644 --- a/icu4j/main/classes/core/src/newapi/NumberFormatter.java +++ b/icu4j/main/classes/core/src/newapi/NumberFormatter.java @@ -4,48 +4,43 @@ package newapi; import java.util.Locale; -import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; public final class NumberFormatter { - private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter(); - - // This could possibly be combined into MeasureFormat.FormatWidth - public static enum CurrencyDisplay { - SYMBOL, // ¤ - ISO_4217, // ¤¤ - DISPLAY_NAME, // ¤¤¤ - SYMBOL_NARROW, // ¤¤¤¤ - HIDDEN, // uses currency rounding and formatting but omits the currency symbol - // TODO: For hidden, what to do if currency symbol appears in the middle, as in Portugal ? - } - - public static enum DecimalMarkDisplay { - AUTO, - ALWAYS, - } - - public static enum SignDisplay { - AUTO, - ALWAYS, - NEVER, - } - - public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) { - // FIXME - throw new UnsupportedOperationException(); - } - - public static UnlocalizedNumberFormatter with() { - return BASE; - } - - public static LocalizedNumberFormatter withLocale(Locale locale) { - return BASE.locale(locale); - } - - public static LocalizedNumberFormatter withLocale(ULocale locale) { - return BASE.locale(locale); - } + private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter(); + + public static enum UnitWidth { + NARROW, // ¤¤¤¤¤ or narrow measure unit + SHORT, // ¤ or short measure unit (DEFAULT) + ISO_CODE, // ¤¤; undefined for measure unit + FULL_NAME, // ¤¤¤ or wide unit + HIDDEN, // no unit is displayed, but other unit effects are obeyed (like currency rounding) + // TODO: For hidden, what to do if currency symbol appears in the middle, as in Portugal ? + } + + public static enum DecimalMarkDisplay { + AUTO, ALWAYS, + } + + public static enum SignDisplay { + AUTO, ALWAYS, NEVER, ACCOUNTING, ACCOUNTING_ALWAYS, + } + + public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) { + // FIXME + throw new UnsupportedOperationException(); + } + + public static UnlocalizedNumberFormatter with() { + return BASE; + } + + public static LocalizedNumberFormatter withLocale(Locale locale) { + return BASE.locale(locale); + } + + public static LocalizedNumberFormatter withLocale(ULocale locale) { + return BASE.locale(locale); + } } diff --git a/icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java b/icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java index 112a3092935..c370b2436b6 100644 --- a/icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java +++ b/icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java @@ -5,14 +5,22 @@ package newapi; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.text.NumberingSystem; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.Measure; import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.NoUnit; import com.ibm.icu.util.ULocale; import newapi.NumberFormatter.DecimalMarkDisplay; import newapi.NumberFormatter.SignDisplay; +import newapi.NumberFormatter.UnitWidth; import newapi.impl.MacroProps; import newapi.impl.Padder; +/** + * An abstract base class for specifying settings related to number formatting. This class is implemented by + * {@link UnlocalizedNumberFormatter} and {@link LocalizedNumberFormatter}. + */ public abstract class NumberFormatterSettings> { static final int KEY_MACROS = 0; @@ -41,35 +49,292 @@ public abstract class NumberFormatterSettings + *

  • Simple notation: "12,300" + *
  • Scientific notation: "1.23E4" + *
  • Compact notation: "12K" + * + * + *

    + * All notation styles will be properly localized with locale data, and all notation styles are compatible with + * units, rounding strategies, and other number formatter settings. + * + *

    + * Pass this method the return value of a {@link Notation} factory method. For example: + * + *

    +     * NumberFormatter.with().notation(Notation.compactShort())
    +     * 
    + * + * The default is to use simple notation. + * + * @param notation + * The notation strategy to use. + * @return The fluent chain. + * @see Notation + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ public T notation(Notation notation) { return create(KEY_NOTATION, notation); } + /** + * Specifies the unit (unit of measure, currency, or percent) to associate with rendered numbers. + * + * + * + *

    + * Note: The unit can also be specified by passing a {@link Measure} to + * {@link LocalizedNumberFormatter#format(Measure)}. Units specified via the format method take precedence over + * units specified here. + * + *

    + * All units will be properly localized with locale data, and all units are compatible with notation styles, + * rounding strategies, and other number formatter settings. + * + *

    + * Pass this method any instance of {@link MeasureUnit}. For units of measure: + * + *

    +     * NumberFormatter.with().unit(MeasureUnit.METER)
    +     * 
    + * + * Currency: + * + *
    +     * NumberFormatter.with().unit(Currency.getInstance("USD"))
    +     * 
    + * + * Percent: + * + *
    +     * NumberFormatter.with().unit(NoUnit.PERCENT)
    +     * 
    + * + * The default is to render without units (equivalent to {@link NoUnit#BASE}). + * + * @param unit + * The unit to render. + * @return The fluent chain. + * @see MeasureUnit + * @see Currency + * @see NoUnit + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ public T unit(MeasureUnit unit) { return create(KEY_UNIT, unit); } + /** + * Specifies the rounding strategy to use when formatting numbers. + * + * + * + *

    + * Pass this method the return value of one of the factory methods on {@link Rounder}. For example: + * + *

    +     * NumberFormatter.with().rounding(Rounder.fixedFraction(2))
    +     * 
    + * + * The default is to not perform rounding. + * + * @param rounder + * The rounding strategy to use. + * @return The fluent chain. + * @see Rounder + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ public T rounding(Rounder rounder) { return create(KEY_ROUNDER, rounder); } + /** + * Specifies the grouping strategy to use when formatting numbers. + * + * + * + *

    + * The exact grouping widths will be chosen based on the locale. + * + *

    + * Pass this method the return value of one of the factory methods on {@link Grouper}. For example: + * + *

    +     * NumberFormatter.with().grouping(Grouper.min2())
    +     * 
    + * + * The default is to perform grouping without concern for the minimum grouping digits. + * + * @param grouper + * The grouping strategy to use. + * @return The fluent chain. + * @see Grouper + * @see Notation + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ public T grouping(Grouper grouper) { return create(KEY_GROUPER, grouper); } + /** + * Specifies the minimum and maximum number of digits to render before the decimal mark. + * + * + * + *

    + * Pass this method the return value of {@link IntegerWidth#zeroFillTo(int)}. For example: + * + *

    +     * NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2))
    +     * 
    + * + * The default is to have one minimum integer digit. + * + * @param style + * The integer width to use. + * @return The fluent chain. + * @see IntegerWidth + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ public T integerWidth(IntegerWidth style) { return create(KEY_INTEGER, style); } + /** + * Specifies the symbols (decimal separator, grouping separator, percent sign, numerals, etc.) to use when rendering + * numbers. + * + * + * + *

    + * Pass this method an instance of {@link DecimalFormatSymbols}. For example: + * + *

    +     * NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("de_CH")))
    +     * 
    + * + *

    + * Note: DecimalFormatSymbols automatically chooses the best numbering system based on the locale. + * In the examples above, the first three are using the Latin numbering system, and the fourth is using the Myanmar + * numbering system. + * + *

    + * Note: The instance of DecimalFormatSymbols will be copied: changes made to the symbols object + * after passing it into the fluent chain will not be seen. + * + *

    + * Note: Calling this method will override the NumberingSystem previously specified in + * {@link #symbols(NumberingSystem)}. + * + *

    + * The default is to choose the symbols based on the locale specified in the fluent chain. + * + * @param symbols + * The DecimalFormatSymbols to use. + * @return The fluent chain. + * @see DecimalFormatSymbols + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ public T symbols(DecimalFormatSymbols symbols) { + symbols = (DecimalFormatSymbols) symbols.clone(); return create(KEY_SYMBOLS, symbols); } + /** + * Specifies that the given numbering system should be used when fetching symbols. + * + *

    + * + *

    + * Pass this method an instance of {@link NumberingSystem}. For example, to force the locale to always use the Latin + * alphabet numbering system (ASCII digits): + * + *

    +     * NumberFormatter.with().symbols(NumberingSystem.LATIN)
    +     * 
    + * + *

    + * Note: Calling this method will override the DecimalFormatSymbols previously specified in + * {@link #symbols(DecimalFormatSymbols)}. + * + *

    + * The default is to choose the best numbering system for the locale. + * + * @param ns + * The NumberingSystem to use. + * @return The fluent chain. + * @see NumberingSystem + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ public T symbols(NumberingSystem ns) { return create(KEY_SYMBOLS, ns); } - public T unitWidth(FormatWidth style) { + /** + * Sets the width of the unit (measure unit or currency). + * + *

    + * + *

    + * Pass an element from the {@link FormatWidth} enum to this setter. For example: + * + *

    +     * NumberFormatter.with().unitWidth(FormatWidth.SHORT)
    +     * 
    + * + *

    + * The default is the narrow width. + * + * @param style + * The with to use when rendering numbers. + * @return The fluent chain + * @see FormatWidth + * @provisional This API might change or be removed in a future release. + * @draft ICU 60 + */ + public T unitWidth(UnitWidth style) { return create(KEY_UNIT_WIDTH, style); } @@ -161,7 +426,7 @@ public abstract class NumberFormatterSettings= 0 && minMaxFrac <= MAX_VALUE) { - return constructFraction(minMaxFrac, minMaxFrac); + public static FractionRounder fixedFraction(int minMaxFractionDigits) { + if (minMaxFractionDigits >= 0 && minMaxFractionDigits <= MAX_VALUE) { + return constructFraction(minMaxFractionDigits, minMaxFractionDigits); } else { throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE); } } - public static FractionRounder minFraction(int minFrac) { - if (minFrac >= 0 && minFrac < MAX_VALUE) { - return constructFraction(minFrac, -1); + public static FractionRounder minFraction(int minFractionDigits) { + if (minFractionDigits >= 0 && minFractionDigits < MAX_VALUE) { + return constructFraction(minFractionDigits, -1); } else { throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE); } } - public static FractionRounder maxFraction(int maxFrac) { - if (maxFrac >= 0 && maxFrac < MAX_VALUE) { - return constructFraction(0, maxFrac); + public static FractionRounder maxFraction(int maxFractionDigits) { + if (maxFractionDigits >= 0 && maxFractionDigits < MAX_VALUE) { + return constructFraction(0, maxFractionDigits); } else { throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE); } } - public static FractionRounder minMaxFraction(int minFrac, int maxFrac) { - if (minFrac >= 0 && maxFrac <= MAX_VALUE && minFrac <= maxFrac) { - return constructFraction(minFrac, maxFrac); + public static FractionRounder minMaxFraction(int minFractionDigits, int maxFractionDigits) { + if (minFractionDigits >= 0 && maxFractionDigits <= MAX_VALUE && minFractionDigits <= maxFractionDigits) { + return constructFraction(minFractionDigits, maxFractionDigits); } else { throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE); } } - public static Rounder fixedFigures(int minMaxSig) { - if (minMaxSig > 0 && minMaxSig <= MAX_VALUE) { - return constructSignificant(minMaxSig, minMaxSig); + public static Rounder fixedDigits(int minMaxSignificantDigits) { + if (minMaxSignificantDigits > 0 && minMaxSignificantDigits <= MAX_VALUE) { + return constructSignificant(minMaxSignificantDigits, minMaxSignificantDigits); } else { throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); } } - public static Rounder minFigures(int minSig) { - if (minSig > 0 && minSig <= MAX_VALUE) { - return constructSignificant(minSig, -1); + public static Rounder minDigits(int minSignificantDigits) { + if (minSignificantDigits > 0 && minSignificantDigits <= MAX_VALUE) { + return constructSignificant(minSignificantDigits, -1); } else { throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); } } - public static Rounder maxFigures(int maxSig) { - if (maxSig > 0 && maxSig <= MAX_VALUE) { - return constructSignificant(0, maxSig); + public static Rounder maxDigits(int maxSignificantDigits) { + if (maxSignificantDigits > 0 && maxSignificantDigits <= MAX_VALUE) { + return constructSignificant(0, maxSignificantDigits); } else { throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); } } - public static Rounder minMaxFigures(int minSig, int maxSig) { - if (minSig > 0 && maxSig <= MAX_VALUE && minSig <= maxSig) { - return constructSignificant(minSig, maxSig); + public static Rounder minMaxDigits(int minSignificantDigits, int maxSignificantDigits) { + if (minSignificantDigits > 0 && maxSignificantDigits <= MAX_VALUE && minSignificantDigits <= maxSignificantDigits) { + return constructSignificant(minSignificantDigits, maxSignificantDigits); } else { throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE); } diff --git a/icu4j/main/classes/core/src/newapi/ScientificNotation.java b/icu4j/main/classes/core/src/newapi/ScientificNotation.java index 3c2214c98b8..3ac73e293ea 100644 --- a/icu4j/main/classes/core/src/newapi/ScientificNotation.java +++ b/icu4j/main/classes/core/src/newapi/ScientificNotation.java @@ -12,7 +12,7 @@ import newapi.NumberFormatter.SignDisplay; import newapi.Rounder.SignificantRounderImpl; import newapi.impl.MicroProps; import newapi.impl.MultiplierProducer; -import newapi.impl.QuantityChain; +import newapi.impl.MicroPropsGenerator; @SuppressWarnings("unused") public class ScientificNotation extends Notation implements Cloneable { @@ -56,19 +56,19 @@ public class ScientificNotation extends Notation implements Cloneable { } } - /* package-private */ QuantityChain withLocaleData(DecimalFormatSymbols symbols, boolean build, - QuantityChain parent) { + /* package-private */ MicroPropsGenerator withLocaleData(DecimalFormatSymbols symbols, boolean build, + MicroPropsGenerator parent) { return new MurkyScientificHandler(symbols, build, parent); } - private class MurkyScientificHandler implements QuantityChain, MultiplierProducer, Modifier { + private class MurkyScientificHandler implements MicroPropsGenerator, MultiplierProducer, Modifier { final DecimalFormatSymbols symbols; final ImmutableScientificModifier[] precomputedMods; - final QuantityChain parent; + final MicroPropsGenerator parent; /* unsafe */ int exponent; - private MurkyScientificHandler(DecimalFormatSymbols symbols, boolean safe, QuantityChain parent) { + private MurkyScientificHandler(DecimalFormatSymbols symbols, boolean safe, MicroPropsGenerator parent) { this.symbols = symbols; this.parent = parent; @@ -84,8 +84,8 @@ public class ScientificNotation extends Notation implements Cloneable { } @Override - public MicroProps withQuantity(FormatQuantity quantity) { - MicroProps micros = parent.withQuantity(quantity); + public MicroProps processQuantity(FormatQuantity quantity) { + MicroProps micros = parent.processQuantity(quantity); assert micros.rounding != null; // Treat zero as if it had magnitude 0 diff --git a/icu4j/main/classes/core/src/newapi/SkeletonBuilder.java b/icu4j/main/classes/core/src/newapi/SkeletonBuilder.java index bc77e212793..b44385b16b2 100644 --- a/icu4j/main/classes/core/src/newapi/SkeletonBuilder.java +++ b/icu4j/main/classes/core/src/newapi/SkeletonBuilder.java @@ -8,15 +8,15 @@ import java.math.RoundingMode; import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; import com.ibm.icu.text.DecimalFormatSymbols; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.text.NumberingSystem; import com.ibm.icu.util.Currency; import com.ibm.icu.util.Currency.CurrencyUsage; -import com.ibm.icu.util.Dimensionless; import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.NoUnit; import newapi.NumberFormatter.DecimalMarkDisplay; import newapi.NumberFormatter.SignDisplay; +import newapi.NumberFormatter.UnitWidth; import newapi.Rounder.CurrencyRounderImpl; import newapi.Rounder.FracSigRounderImpl; import newapi.Rounder.FractionRounderImpl; @@ -228,7 +228,7 @@ final class SkeletonBuilder { } private static void unitToSkeleton(MeasureUnit value, StringBuilder sb) { - if (value.getType().equals("dimensionless")) { + if (value.getType().equals("none")) { if (value.getSubtype().equals("percent")) { sb.append('%'); } else if (value.getSubtype().equals("permille")) { @@ -255,12 +255,12 @@ final class SkeletonBuilder { if (c0 == '%') { char c = safeCharAt(skeleton, offset++); if (c == '%') { - result = Dimensionless.PERCENT; + result = NoUnit.PERCENT; } else { - result = Dimensionless.PERMILLE; + result = NoUnit.PERMILLE; } } else if (c0 == 'B') { - result = Dimensionless.BASE; + result = NoUnit.BASE; } else if (c0 == '$') { String currencyCode = skeleton.substring(offset, offset + 3); offset += 3; @@ -360,10 +360,10 @@ final class SkeletonBuilder { char c1 = skeleton.charAt(offset++); if (c1 == '<') { char c2 = skeleton.charAt(offset++); - result = temp.withMaxFigures(c2 - '0'); + result = temp.withMaxDigits(c2 - '0'); } else if (c1 == '>') { char c2 = skeleton.charAt(offset++); - result = temp.withMinFigures(c2 - '0'); + result = temp.withMinDigits(c2 - '0'); } else { result = temp; } @@ -518,7 +518,7 @@ final class SkeletonBuilder { } } - private static void unitWidthToSkeleton(FormatWidth value, StringBuilder sb) { + private static void unitWidthToSkeleton(UnitWidth value, StringBuilder sb) { sb.append(value.name()); } @@ -526,7 +526,7 @@ final class SkeletonBuilder { int originalOffset = offset; StringBuilder sb = new StringBuilder(); offset += consumeUntil(skeleton, offset, ' ', sb); - output.unitWidth = Enum.valueOf(FormatWidth.class, sb.toString()); + output.unitWidth = Enum.valueOf(UnitWidth.class, sb.toString()); return offset - originalOffset; } diff --git a/icu4j/main/classes/core/src/newapi/Worker1.java b/icu4j/main/classes/core/src/newapi/Worker1.java index 1453d7fc3d5..1034e7b6ba7 100644 --- a/icu4j/main/classes/core/src/newapi/Worker1.java +++ b/icu4j/main/classes/core/src/newapi/Worker1.java @@ -3,122 +3,145 @@ package newapi; import com.ibm.icu.impl.number.FormatQuantity; -import com.ibm.icu.impl.number.LdmlPatternInfo; -import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult; +import com.ibm.icu.impl.number.PatternParser; +import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo; import com.ibm.icu.impl.number.NumberStringBuilder; import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier; import com.ibm.icu.text.CompactDecimalFormat.CompactType; import com.ibm.icu.text.DecimalFormatSymbols; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.NumberingSystem; import com.ibm.icu.text.PluralRules; import com.ibm.icu.util.Currency; import com.ibm.icu.util.Currency.CurrencyUsage; -import com.ibm.icu.util.Dimensionless; +import com.ibm.icu.util.NoUnit; import com.ibm.icu.util.ULocale; import newapi.NumberFormatter.DecimalMarkDisplay; import newapi.NumberFormatter.SignDisplay; +import newapi.NumberFormatter.UnitWidth; import newapi.impl.MacroProps; import newapi.impl.MicroProps; +import newapi.impl.MicroPropsGenerator; import newapi.impl.Padder; -import newapi.impl.QuantityChain; public class Worker1 { public static Worker1 fromMacros(MacroProps macros) { - return new Worker1(make(macros, true)); + // Build a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly. + MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, true); + return new Worker1(microPropsGenerator); } public static MicroProps applyStatic(MacroProps macros, FormatQuantity inValue, NumberStringBuilder outString) { - MicroProps micros = make(macros, false).withQuantity(inValue); - applyStatic(micros, inValue, outString); + // Build an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once. + MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false); + MicroProps micros = microPropsGenerator.processQuantity(inValue); + microsToString(micros, inValue, outString); return micros; } private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX"); - final QuantityChain microsGenerator; + final MicroPropsGenerator microPropsGenerator; - private Worker1(QuantityChain microsGenerator) { - this.microsGenerator = microsGenerator; + private Worker1(MicroPropsGenerator microsGenerator) { + this.microPropsGenerator = microsGenerator; } public MicroProps apply(FormatQuantity inValue, NumberStringBuilder outString) { - MicroProps micros = microsGenerator.withQuantity(inValue); - applyStatic(micros, inValue, outString); + MicroProps micros = microPropsGenerator.processQuantity(inValue); + microsToString(micros, inValue, outString); return micros; } ////////// - private static QuantityChain make(MacroProps input, boolean build) { + /** + * Synthesizes the MacroProps into a MicroPropsGenerator. All information, including the locale, is encoded into the + * MicroPropsGenerator, except for the quantity itself, which is left abstract and must be provided to the returned + * MicroPropsGenerator instance. + * + * @see MicroPropsGenerator + * @param macros + * The {@link MacroProps} to consume. This method does not mutate the MacroProps instance. + * @param safe + * If true, the returned MicroPropsGenerator will be thread-safe. If false, the returned value will + * not be thread-safe, intended for a single "one-shot" use only. Building the thread-safe + * object is more expensive. + * @return + */ + private static MicroPropsGenerator macrosToMicroGenerator(MacroProps macros, boolean safe) { String innerPattern = null; - MurkyLongNameHandler longNames = null; + LongNameHandler longNames = null; Rounder defaultRounding = Rounder.none(); Currency currency = DEFAULT_CURRENCY; - FormatWidth unitWidth = null; + UnitWidth unitWidth = null; boolean perMille = false; - PluralRules rules = input.rules; + PluralRules rules = macros.rules; - MicroProps micros = new MicroProps(build); - QuantityChain chain = micros; + MicroProps micros = new MicroProps(safe); + MicroPropsGenerator chain = micros; // Copy over the simple settings - micros.sign = input.sign == null ? SignDisplay.AUTO : input.sign; - micros.decimal = input.decimal == null ? DecimalMarkDisplay.AUTO : input.decimal; + micros.sign = macros.sign == null ? SignDisplay.AUTO : macros.sign; + micros.decimal = macros.decimal == null ? DecimalMarkDisplay.AUTO : macros.decimal; micros.multiplier = 0; - micros.integerWidth = input.integerWidth == null ? IntegerWidth.zeroFillTo(1) : input.integerWidth; + micros.integerWidth = macros.integerWidth == null ? IntegerWidth.zeroFillTo(1) : macros.integerWidth; - if (input.unit == null || input.unit == Dimensionless.BASE) { + if (macros.unit == null || macros.unit == NoUnit.BASE) { // No units; default format - innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE); - } else if (input.unit == Dimensionless.PERCENT) { + innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.NUMBERSTYLE); + } else if (macros.unit == NoUnit.PERCENT) { // Percent - innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE); + innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.PERCENTSTYLE); micros.multiplier += 2; - } else if (input.unit == Dimensionless.PERMILLE) { + } else if (macros.unit == NoUnit.PERMILLE) { // Permille - innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE); + innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.PERCENTSTYLE); micros.multiplier += 3; perMille = true; - } else if (input.unit instanceof Currency && input.unitWidth != FormatWidth.WIDE) { + } else if (macros.unit instanceof Currency && macros.unitWidth != UnitWidth.FULL_NAME) { // Narrow, short, or ISO currency. - // TODO: Accounting style? - innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.CURRENCYSTYLE); + // TODO: Although ACCOUNTING and ACCOUNTING_ALWAYS are only supported in currencies right now, + // the API contract allows us to add support to other units. + if (macros.sign == SignDisplay.ACCOUNTING || macros.sign == SignDisplay.ACCOUNTING_ALWAYS) { + innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.ACCOUNTINGCURRENCYSTYLE); + } else { + innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.CURRENCYSTYLE); + } defaultRounding = Rounder.currency(CurrencyUsage.STANDARD); - currency = (Currency) input.unit; + currency = (Currency) macros.unit; micros.useCurrency = true; - unitWidth = (input.unitWidth == null) ? FormatWidth.NARROW : input.unitWidth; - } else if (input.unit instanceof Currency) { + unitWidth = (macros.unitWidth == null) ? UnitWidth.SHORT : macros.unitWidth; + } else if (macros.unit instanceof Currency) { // Currency long name - innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE); - longNames = MurkyLongNameHandler.getCurrencyLongNameModifiers(input.loc, (Currency) input.unit); + innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.NUMBERSTYLE); + longNames = LongNameHandler.getCurrencyLongNameModifiers(macros.loc, (Currency) macros.unit); defaultRounding = Rounder.currency(CurrencyUsage.STANDARD); - currency = (Currency) input.unit; + currency = (Currency) macros.unit; micros.useCurrency = true; - unitWidth = input.unitWidth = FormatWidth.WIDE; + unitWidth = UnitWidth.FULL_NAME; } else { // MeasureUnit - innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE); - unitWidth = (input.unitWidth == null) ? FormatWidth.SHORT : input.unitWidth; - longNames = MurkyLongNameHandler.getMeasureUnitModifiers(input.loc, input.unit, unitWidth); + innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.NUMBERSTYLE); + unitWidth = (macros.unitWidth == null) ? UnitWidth.SHORT : macros.unitWidth; + longNames = LongNameHandler.getMeasureUnitModifiers(macros.loc, macros.unit, unitWidth); } // Parse the pattern, which is used for grouping and affixes only. - PatternParseResult patternInfo = LdmlPatternInfo.parse(innerPattern); + ParsedPatternInfo patternInfo = PatternParser.parse(innerPattern); // Symbols - if (input.symbols == null) { - micros.symbols = DecimalFormatSymbols.getInstance(input.loc); - } else if (input.symbols instanceof DecimalFormatSymbols) { - micros.symbols = (DecimalFormatSymbols) input.symbols; - } else if (input.symbols instanceof NumberingSystem) { + if (macros.symbols == null) { + micros.symbols = DecimalFormatSymbols.getInstance(macros.loc); + } else if (macros.symbols instanceof DecimalFormatSymbols) { + micros.symbols = (DecimalFormatSymbols) macros.symbols; + } else if (macros.symbols instanceof NumberingSystem) { // TODO: Do this more efficiently. Will require modifying DecimalFormatSymbols. - NumberingSystem ns = (NumberingSystem) input.symbols; - ULocale temp = input.loc.setKeywordValue("numbers", ns.getName()); + NumberingSystem ns = (NumberingSystem) macros.symbols; + ULocale temp = macros.loc.setKeywordValue("numbers", ns.getName()); micros.symbols = DecimalFormatSymbols.getInstance(temp); } else { throw new AssertionError(); @@ -130,23 +153,23 @@ public class Worker1 { // Multiplier (compatibility mode value). // An int magnitude multiplier is used when not in compatibility mode to // reduce object creations. - if (input.multiplier != null) { - chain = input.multiplier.copyAndChain(chain); + if (macros.multiplier != null) { + chain = macros.multiplier.copyAndChain(chain); } // Rounding strategy - if (input.rounder != null) { - micros.rounding = Rounder.normalizeType(input.rounder, currency); - } else if (input.notation instanceof CompactNotation) { + if (macros.rounder != null) { + micros.rounding = Rounder.normalizeType(macros.rounder, currency); + } else if (macros.notation instanceof CompactNotation) { micros.rounding = Rounder.COMPACT_STRATEGY; } else { micros.rounding = Rounder.normalizeType(defaultRounding, currency); } // Grouping strategy - if (input.grouper != null) { - micros.grouping = Grouper.normalizeType(input.grouper, patternInfo); - } else if (input.notation instanceof CompactNotation) { + if (macros.grouper != null) { + micros.grouping = Grouper.normalizeType(macros.grouper, patternInfo); + } else if (macros.notation instanceof CompactNotation) { // Compact notation uses minGrouping by default since ICU 59 micros.grouping = Grouper.normalizeType(Grouper.min2(), patternInfo); } else { @@ -154,8 +177,8 @@ public class Worker1 { } // Inner modifier (scientific notation) - if (input.notation instanceof ScientificNotation) { - chain = ((ScientificNotation) input.notation).withLocaleData(micros.symbols, build, chain); + if (macros.notation instanceof ScientificNotation) { + chain = ((ScientificNotation) macros.notation).withLocaleData(micros.symbols, safe, chain); } else { // No inner modifier required micros.modInner = ConstantAffixModifier.EMPTY; @@ -163,39 +186,39 @@ public class Worker1 { // Middle modifier (patterns, positive/negative, currency symbols, percent) // The default middle modifier is weak (thus the false argument). - MurkyModifier murkyMod = new MurkyModifier(false); - murkyMod.setPatternInfo((input.affixProvider != null) ? input.affixProvider : patternInfo); - murkyMod.setPatternAttributes(micros.sign, perMille); - if (murkyMod.needsPlurals()) { + MutablePatternModifier patternMod = new MutablePatternModifier(false); + patternMod.setPatternInfo((macros.affixProvider != null) ? macros.affixProvider : patternInfo); + patternMod.setPatternAttributes(micros.sign, perMille); + if (patternMod.needsPlurals()) { if (rules == null) { // Lazily create PluralRules - rules = PluralRules.forLocale(input.loc); + rules = PluralRules.forLocale(macros.loc); } - murkyMod.setSymbols(micros.symbols, currency, unitWidth, rules); + patternMod.setSymbols(micros.symbols, currency, unitWidth, rules); } else { - murkyMod.setSymbols(micros.symbols, currency, unitWidth, null); + patternMod.setSymbols(micros.symbols, currency, unitWidth, null); } - if (build) { - chain = murkyMod.createImmutableAndChain(chain); + if (safe) { + chain = patternMod.createImmutableAndChain(chain); } else { - chain = murkyMod.addToChain(chain); + chain = patternMod.addToChain(chain); } // Outer modifier (CLDR units and currency long names) if (longNames != null) { if (rules == null) { // Lazily create PluralRules - rules = PluralRules.forLocale(input.loc); + rules = PluralRules.forLocale(macros.loc); } - chain = longNames.withLocaleData(rules, build, chain); + chain = longNames.withLocaleData(rules, safe, chain); } else { // No outer modifier required micros.modOuter = ConstantAffixModifier.EMPTY; } // Padding strategy - if (input.padder != null) { - micros.padding = input.padder; + if (macros.padder != null) { + micros.padding = macros.padder; } else { micros.padding = Padder.none(); } @@ -203,14 +226,14 @@ public class Worker1 { // Compact notation // NOTE: Compact notation can (but might not) override the middle modifier and rounding. // It therefore needs to go at the end of the chain. - if (input.notation instanceof CompactNotation) { + if (macros.notation instanceof CompactNotation) { if (rules == null) { // Lazily create PluralRules - rules = PluralRules.forLocale(input.loc); + rules = PluralRules.forLocale(macros.loc); } - CompactType compactType = (input.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL; - chain = ((CompactNotation) input.notation).withLocaleData(input.loc, compactType, rules, - build ? murkyMod : null, chain); + CompactType compactType = (macros.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL; + chain = ((CompactNotation) macros.notation).withLocaleData(macros.loc, compactType, rules, + safe ? patternMod : null, chain); } return chain; @@ -218,58 +241,67 @@ public class Worker1 { ////////// - private static int applyStatic(MicroProps micros, FormatQuantity inValue, NumberStringBuilder outString) { - inValue.adjustMagnitude(micros.multiplier); - micros.rounding.apply(inValue); + /** + * Synthesizes the output string from a MicroProps and FormatQuantity. + * + * @param micros + * The MicroProps after the quantity has been consumed. Will not be mutated. + * @param quantity + * The FormatQuantity to be rendered. May be mutated. + * @param string + * The output string. Will be mutated. + */ + private static void microsToString(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) { + quantity.adjustMagnitude(micros.multiplier); + micros.rounding.apply(quantity); if (micros.integerWidth.maxInt == -1) { - inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE); + quantity.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE); } else { - inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt); + quantity.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt); } - int length = writeNumber(micros, inValue, outString); + int length = writeNumber(micros, quantity, string); // NOTE: When range formatting is added, these modifiers can bubble up. // For now, apply them all here at once. - length += micros.padding.applyModsAndMaybePad(micros, outString, 0, length); - return length; + length += micros.padding.applyModsAndMaybePad(micros, string, 0, length); } - private static int writeNumber(MicroProps micros, FormatQuantity input, NumberStringBuilder string) { + private static int writeNumber(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) { int length = 0; - if (input.isInfinite()) { + if (quantity.isInfinite()) { length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER); - } else if (input.isNaN()) { + } else if (quantity.isNaN()) { length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER); } else { // Add the integer digits - length += writeIntegerDigits(micros, input, string); + length += writeIntegerDigits(micros, quantity, string); // Add the decimal point - if (input.getLowerDisplayMagnitude() < 0 || micros.decimal == DecimalMarkDisplay.ALWAYS) { + if (quantity.getLowerDisplayMagnitude() < 0 || micros.decimal == DecimalMarkDisplay.ALWAYS) { length += string.insert(length, micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString() : micros.symbols.getDecimalSeparatorString(), NumberFormat.Field.DECIMAL_SEPARATOR); } // Add the fraction digits - length += writeFractionDigits(micros, input, string); + length += writeFractionDigits(micros, quantity, string); } return length; } - private static int writeIntegerDigits(MicroProps micros, FormatQuantity input, NumberStringBuilder string) { + private static int writeIntegerDigits(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) { int length = 0; - int integerCount = input.getUpperDisplayMagnitude() + 1; + int integerCount = quantity.getUpperDisplayMagnitude() + 1; for (int i = 0; i < integerCount; i++) { // Add grouping separator - if (micros.grouping.groupAtPosition(i, input)) { + if (micros.grouping.groupAtPosition(i, quantity)) { length += string.insert(0, micros.useCurrency ? micros.symbols.getMonetaryGroupingSeparatorString() : micros.symbols.getGroupingSeparatorString(), NumberFormat.Field.GROUPING_SEPARATOR); } // Get and append the next digit value - byte nextDigit = input.getDigit(i); + byte nextDigit = quantity.getDigit(i); if (micros.symbols.getCodePointZero() != -1) { length += string.insertCodePoint(0, micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.INTEGER); @@ -281,12 +313,12 @@ public class Worker1 { return length; } - private static int writeFractionDigits(MicroProps micros, FormatQuantity input, NumberStringBuilder string) { + private static int writeFractionDigits(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) { int length = 0; - int fractionCount = -input.getLowerDisplayMagnitude(); + int fractionCount = -quantity.getLowerDisplayMagnitude(); for (int i = 0; i < fractionCount; i++) { // Get and append the next digit value - byte nextDigit = input.getDigit(-i - 1); + byte nextDigit = quantity.getDigit(-i - 1); if (micros.symbols.getCodePointZero() != -1) { length += string.appendCodePoint(micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.FRACTION); diff --git a/icu4j/main/classes/core/src/newapi/impl/MacroProps.java b/icu4j/main/classes/core/src/newapi/impl/MacroProps.java index e135116b0f7..8146daffdf5 100644 --- a/icu4j/main/classes/core/src/newapi/impl/MacroProps.java +++ b/icu4j/main/classes/core/src/newapi/impl/MacroProps.java @@ -4,7 +4,6 @@ package newapi.impl; import java.util.Objects; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.text.PluralRules; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; @@ -12,10 +11,10 @@ import com.ibm.icu.util.ULocale; import newapi.Grouper; import newapi.IntegerWidth; import newapi.Notation; -import newapi.NumberFormatter; -import newapi.Rounder; import newapi.NumberFormatter.DecimalMarkDisplay; import newapi.NumberFormatter.SignDisplay; +import newapi.NumberFormatter.UnitWidth; +import newapi.Rounder; public class MacroProps implements Cloneable { public Notation notation; @@ -25,7 +24,7 @@ public class MacroProps implements Cloneable { public Padder padder; public IntegerWidth integerWidth; public Object symbols; - public FormatWidth unitWidth; + public UnitWidth unitWidth; public SignDisplay sign; public DecimalMarkDisplay decimal; public AffixPatternProvider affixProvider; // not in API; for JDK compatibility mode only diff --git a/icu4j/main/classes/core/src/newapi/impl/MeasureData.java b/icu4j/main/classes/core/src/newapi/impl/MeasureData.java index d48c129bde7..c45220478be 100644 --- a/icu4j/main/classes/core/src/newapi/impl/MeasureData.java +++ b/icu4j/main/classes/core/src/newapi/impl/MeasureData.java @@ -9,11 +9,12 @@ import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.UResource; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; +import newapi.NumberFormatter.UnitWidth; + public class MeasureData { private static final class ShanesMeasureUnitSink extends UResource.Sink { @@ -42,14 +43,14 @@ public class MeasureData { } public static Map getMeasureData( - ULocale locale, MeasureUnit unit, FormatWidth width) { + ULocale locale, MeasureUnit unit, UnitWidth width) { ICUResourceBundle resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale); StringBuilder key = new StringBuilder(); key.append("units"); - if (width == FormatWidth.NARROW) { + if (width == UnitWidth.NARROW) { key.append("Narrow"); - } else if (width == FormatWidth.SHORT) { + } else if (width == UnitWidth.SHORT) { key.append("Short"); } key.append("/"); diff --git a/icu4j/main/classes/core/src/newapi/impl/MicroProps.java b/icu4j/main/classes/core/src/newapi/impl/MicroProps.java index dc0aa88bdb2..3c9d9262cc9 100644 --- a/icu4j/main/classes/core/src/newapi/impl/MicroProps.java +++ b/icu4j/main/classes/core/src/newapi/impl/MicroProps.java @@ -13,7 +13,7 @@ import newapi.Rounder; import newapi.NumberFormatter.DecimalMarkDisplay; import newapi.NumberFormatter.SignDisplay; -public class MicroProps implements Cloneable, QuantityChain { +public class MicroProps implements Cloneable, MicroPropsGenerator { // Populated globally: public SignDisplay sign; public DecimalFormatSymbols symbols; @@ -41,7 +41,7 @@ public class MicroProps implements Cloneable, QuantityChain { } @Override - public MicroProps withQuantity(FormatQuantity quantity) { + public MicroProps processQuantity(FormatQuantity quantity) { if (immutable) { return (MicroProps) this.clone(); } else { diff --git a/icu4j/main/classes/core/src/newapi/impl/MicroPropsGenerator.java b/icu4j/main/classes/core/src/newapi/impl/MicroPropsGenerator.java new file mode 100644 index 00000000000..41822b077ea --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/MicroPropsGenerator.java @@ -0,0 +1,54 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi.impl; + +import com.ibm.icu.impl.number.FormatQuantity; + +/** + * This interface is used when all number formatting settings, including the locale, are known, except for the quantity + * itself. The {@link #processQuantity} method performs the final step in the number processing pipeline: it uses the + * quantity to generate a finalized {@link MicroProps}, which can be used to render the number to output. + * + *

    + * In other words, this interface is used for the parts of number processing that are quantity-dependent. + * + *

    + * In order to allow for multiple different objects to all mutate the same MicroProps, a "chain" of MicroPropsGenerators + * are linked together, and each one is responsible for manipulating a certain quantity-dependent part of the + * MicroProps. At the top of the linked list is a base instance of {@link MicroProps} with properties that are not + * quantity-dependent. Each element in the linked list calls {@link #processQuantity} on its "parent", then does its + * work, and then returns the result. + * + *

    + * A class implementing MicroPropsGenerator looks something like this: + * + *

    + * class Foo implements MicroPropsGenerator {
    + *     private final MicroPropsGenerator parent;
    + *
    + *     public Foo(MicroPropsGenerator parent) {
    + *         this.parent = parent;
    + *     }
    + *
    + *     @Override
    + *     public MicroProps processQuantity(FormatQuantity quantity) {
    + *         MicroProps micros = this.parent.processQuantity(quantity);
    + *         // Perform manipulations on micros and/or quantity
    + *         return micros;
    + *     }
    + * }
    + * 
    + * + * @author sffc + * + */ +public interface MicroPropsGenerator { + /** + * Considers the given {@link FormatQuantity}, optionally mutates it, and returns a {@link MicroProps}. + * + * @param quantity + * The quantity for consideration and optional mutation. + * @return A MicroProps instance resolved for the quantity. + */ + public MicroProps processQuantity(FormatQuantity quantity); +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/impl/MicroPropsMutator.java b/icu4j/main/classes/core/src/newapi/impl/MicroPropsMutator.java new file mode 100644 index 00000000000..3c01a3b8636 --- /dev/null +++ b/icu4j/main/classes/core/src/newapi/impl/MicroPropsMutator.java @@ -0,0 +1,13 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package newapi.impl; + +/** + * @author sffc + * + */ +public interface MicroPropsMutator { + + public void mutateMicros(MicroProps micros, T value); + +} diff --git a/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java b/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java index 1c737717c64..40dcffa292a 100644 --- a/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java +++ b/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java @@ -6,10 +6,10 @@ import java.math.BigDecimal; import com.ibm.icu.impl.number.FormatQuantity; -public class MultiplierImpl implements QuantityChain, Cloneable { +public class MultiplierImpl implements MicroPropsGenerator, Cloneable { final int magnitudeMultiplier; final BigDecimal bigDecimalMultiplier; - final QuantityChain parent; + final MicroPropsGenerator parent; public MultiplierImpl(int magnitudeMultiplier) { this.magnitudeMultiplier = magnitudeMultiplier; @@ -23,19 +23,19 @@ public class MultiplierImpl implements QuantityChain, Cloneable { parent = null; } - private MultiplierImpl(MultiplierImpl base, QuantityChain parent) { + private MultiplierImpl(MultiplierImpl base, MicroPropsGenerator parent) { this.magnitudeMultiplier = base.magnitudeMultiplier; this.bigDecimalMultiplier = base.bigDecimalMultiplier; this.parent = parent; } - public QuantityChain copyAndChain(QuantityChain parent) { + public MicroPropsGenerator copyAndChain(MicroPropsGenerator parent) { return new MultiplierImpl(this, parent); } @Override - public MicroProps withQuantity(FormatQuantity quantity) { - MicroProps micros = parent.withQuantity(quantity); + public MicroProps processQuantity(FormatQuantity quantity) { + MicroProps micros = parent.processQuantity(quantity); quantity.adjustMagnitude(magnitudeMultiplier); if (bigDecimalMultiplier != null) { quantity.multiplyBy(bigDecimalMultiplier); diff --git a/icu4j/main/classes/core/src/newapi/impl/Padder.java b/icu4j/main/classes/core/src/newapi/impl/Padder.java index faacd505926..91b2099903b 100644 --- a/icu4j/main/classes/core/src/newapi/impl/Padder.java +++ b/icu4j/main/classes/core/src/newapi/impl/Padder.java @@ -3,9 +3,46 @@ package newapi.impl; import com.ibm.icu.impl.number.NumberStringBuilder; -import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition; public class Padder { + public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space + + 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 + } + } + } private static final Padder NONE = new Padder(null, -1, null); diff --git a/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java b/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java deleted file mode 100644 index 4f7a7965603..00000000000 --- a/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java +++ /dev/null @@ -1,10 +0,0 @@ -// © 2017 and later: Unicode, Inc. and others. -// License & terms of use: http://www.unicode.org/copyright.html#License -package newapi.impl; - -import com.ibm.icu.impl.number.FormatQuantity; - -public interface QuantityChain { - //QuantityChain addToChain(QuantityChain parent); - MicroProps withQuantity(FormatQuantity quantity); -} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/newapi/impl/demo.java b/icu4j/main/classes/core/src/newapi/impl/demo.java index 3f8dcea922a..8deaa788a26 100644 --- a/icu4j/main/classes/core/src/newapi/impl/demo.java +++ b/icu4j/main/classes/core/src/newapi/impl/demo.java @@ -5,21 +5,21 @@ package newapi.impl; import java.math.RoundingMode; import com.ibm.icu.text.DecimalFormatSymbols; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.text.NumberingSystem; import com.ibm.icu.util.Currency; import com.ibm.icu.util.Currency.CurrencyUsage; -import com.ibm.icu.util.Dimensionless; import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.NoUnit; import com.ibm.icu.util.ULocale; import newapi.Grouper; import newapi.Notation; import newapi.NumberFormatter; -import newapi.Rounder; -import newapi.UnlocalizedNumberFormatter; import newapi.NumberFormatter.DecimalMarkDisplay; import newapi.NumberFormatter.SignDisplay; +import newapi.NumberFormatter.UnitWidth; +import newapi.Rounder; +import newapi.UnlocalizedNumberFormatter; public class demo { public static void main(String[] args) { @@ -31,9 +31,9 @@ public class demo { .notation(Notation.engineering().withMinExponentDigits(2)) .notation(Notation.simple()) .unit(Currency.getInstance("GBP")) - .unit(Dimensionless.PERCENT) + .unit(NoUnit.PERCENT) .unit(MeasureUnit.CUBIC_METER) - .unitWidth(FormatWidth.SHORT) + .unitWidth(UnitWidth.SHORT) // .rounding(Rounding.fixedSignificantDigits(3)) // .rounding( // (BigDecimal input) -> { 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 12f0b43b300..099a2e16b16 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 @@ -17,6 +17,7 @@ import java.io.Serializable; import java.lang.reflect.Field; import java.text.FieldPosition; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -41,6 +42,7 @@ import com.ibm.icu.text.NumberFormat; import com.ibm.icu.util.Currency; import com.ibm.icu.util.Measure; import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.NoUnit; import com.ibm.icu.util.TimeUnit; import com.ibm.icu.util.TimeUnitAmount; import com.ibm.icu.util.ULocale; @@ -241,7 +243,6 @@ public class MeasureUnitTest extends TestFmwk { } } -/* @Test public void testZZZ() { // various generateXXX calls go here, see @@ -254,7 +255,6 @@ public class MeasureUnitTest extends TestFmwk { //generateCXXBackwardCompatibilityTest("59"); // for measfmttest.cpp, create TestCompatible59 //updateJAVAVersions("59"); // for MeasureUnitTest.java, JAVA_VERSIONS } -*/ @Test public void TestCompatible53() { @@ -2000,7 +2000,7 @@ public class MeasureUnitTest extends TestFmwk { if (type.equals("currency") || type.equals("compound") || type.equals("coordinate") - || type.equals("dimensionless")) { + || type.equals("none")) { continue; } for (MeasureUnit unit : MeasureUnit.getAvailable(type)) { @@ -2147,6 +2147,9 @@ public class MeasureUnitTest extends TestFmwk { System.out.println(""); TreeMap> allUnits = getAllUnits(); + // Hack: for C++, add NoUnits here, but ignore them when printing the create methods. + allUnits.put("none", Arrays.asList(new MeasureUnit[]{NoUnit.BASE, NoUnit.PERCENT, NoUnit.PERMILLE})); + System.out.println("static const int32_t gOffsets[] = {"); int index = 0; for (Map.Entry> entry : allUnits.entrySet()) { @@ -2246,7 +2249,7 @@ public class MeasureUnitTest extends TestFmwk { for (Map.Entry> entry : allUnits.entrySet()) { String type = entry.getKey(); - if (type.equals("currency")) { + if (type.equals("currency") || type.equals("none")) { continue; } for (MeasureUnit unit : entry.getValue()) { 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 index 3374fbf498e..57c559d3373 100644 --- 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 @@ -10,9 +10,8 @@ import org.junit.Test; import com.ibm.icu.dev.test.TestUtil; import com.ibm.icu.impl.number.Parse.ParseMode; -import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.PatternAndPropertyUtils; import com.ibm.icu.impl.number.Properties; -import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.DecimalFormat_ICU58; import com.ibm.icu.util.CurrencyAmount; @@ -20,6 +19,7 @@ import com.ibm.icu.util.ULocale; import newapi.LocalizedNumberFormatter; import newapi.NumberPropertyMapper; +import newapi.impl.Padder.PadPosition; public class NumberFormatDataDrivenTest { @@ -509,8 +509,8 @@ public class NumberFormatDataDrivenTest { } if (tuple.localizedPattern != null) { DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(tuple.locale); - String converted = PatternString.convertLocalized(tuple.localizedPattern, symbols, false); - PatternString.parseToExistingProperties(converted, properties); + String converted = PatternAndPropertyUtils.convertLocalized(tuple.localizedPattern, symbols, false); + PatternAndPropertyUtils.parseToExistingProperties(converted, properties); } if (tuple.lenient != null) { properties.setParseMode(tuple.lenient == 0 ? ParseMode.STRICT : ParseMode.LENIENT); @@ -548,11 +548,11 @@ public class NumberFormatDataDrivenTest { String pattern = (tuple.pattern == null) ? "0" : tuple.pattern; ULocale locale = (tuple.locale == null) ? ULocale.ENGLISH : tuple.locale; Properties properties = - PatternString.parseToProperties( + PatternAndPropertyUtils.parseToProperties( pattern, tuple.currency != null - ? PatternString.IGNORE_ROUNDING_ALWAYS - : PatternString.IGNORE_ROUNDING_NEVER); + ? PatternAndPropertyUtils.IGNORE_ROUNDING_ALWAYS + : PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER); propertiesFromTuple(tuple, properties); DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); LocalizedNumberFormatter fmt = NumberPropertyMapper.create(properties, symbols).locale(locale); 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 697002ad2e2..20cb8f1ea04 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 @@ -5188,7 +5188,7 @@ public class NumberFormatTest extends TestFmwk { } @Test - public void Test13113() { + public void Test13113_MalformedPatterns() { String[][] cases = { {"'", "quoted literal"}, {"ab#c'd", "quoted literal"}, @@ -5197,7 +5197,8 @@ public class NumberFormatTest extends TestFmwk { {".#0", "0 cannot follow #"}, {"@0", "Cannot mix @ and 0"}, {"0@", "Cannot mix 0 and @"}, - {"#x#", "unquoted special character"} + {"#x#", "unquoted special character"}, + {"@#@", "# inside of a run of @"}, }; for (String[] cas : cases) { try { diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/AffixPatternUtilsTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/AffixPatternUtilsTest.java index c0552fda28b..2bb88e1f47b 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/AffixPatternUtilsTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/AffixPatternUtilsTest.java @@ -15,6 +15,42 @@ import com.ibm.icu.util.ULocale; public class AffixPatternUtilsTest { + private static final SymbolProvider DEFAULT_SYMBOL_PROVIDER = + new SymbolProvider() { + // ar_SA has an interesting percent sign and various Arabic letter marks + private final DecimalFormatSymbols SYMBOLS = + DecimalFormatSymbols.getInstance(new ULocale("ar_SA")); + + @Override + public CharSequence getSymbol(int type) { + switch (type) { + case AffixPatternUtils.TYPE_MINUS_SIGN: + return "−"; + case AffixPatternUtils.TYPE_PLUS_SIGN: + return SYMBOLS.getPlusSignString(); + case AffixPatternUtils.TYPE_PERCENT: + return SYMBOLS.getPercentString(); + case AffixPatternUtils.TYPE_PERMILLE: + return SYMBOLS.getPerMillString(); + case AffixPatternUtils.TYPE_CURRENCY_SINGLE: + return "$"; + case AffixPatternUtils.TYPE_CURRENCY_DOUBLE: + return "XXX"; + case AffixPatternUtils.TYPE_CURRENCY_TRIPLE: + return "long name"; + case AffixPatternUtils.TYPE_CURRENCY_QUAD: + return "\uFFFD"; + case AffixPatternUtils.TYPE_CURRENCY_QUINT: + // TODO: Add support for narrow currency symbols here. + return "\uFFFD"; + case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW: + return "\uFFFD"; + default: + throw new AssertionError(); + } + } + }; + @Test public void testEscape() { Object[][] cases = { @@ -183,42 +219,6 @@ public class AffixPatternUtilsTest { assertEquals("Symbol provider into middle", "abcd123efg", sb.toString()); } - private static final SymbolProvider DEFAULT_SYMBOL_PROVIDER = - new SymbolProvider() { - // ar_SA has an interesting percent sign and various Arabic letter marks - private final DecimalFormatSymbols SYMBOLS = - DecimalFormatSymbols.getInstance(new ULocale("ar_SA")); - - @Override - public CharSequence getSymbol(int type) { - switch (type) { - case AffixPatternUtils.TYPE_MINUS_SIGN: - return "−"; - case AffixPatternUtils.TYPE_PLUS_SIGN: - return SYMBOLS.getPlusSignString(); - case AffixPatternUtils.TYPE_PERCENT: - return SYMBOLS.getPercentString(); - case AffixPatternUtils.TYPE_PERMILLE: - return SYMBOLS.getPerMillString(); - case AffixPatternUtils.TYPE_CURRENCY_SINGLE: - return "$"; - case AffixPatternUtils.TYPE_CURRENCY_DOUBLE: - return "XXX"; - case AffixPatternUtils.TYPE_CURRENCY_TRIPLE: - return "long name"; - case AffixPatternUtils.TYPE_CURRENCY_QUAD: - return "\uFFFD"; - case AffixPatternUtils.TYPE_CURRENCY_QUINT: - // TODO: Add support for narrow currency symbols here. - return "\uFFFD"; - case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW: - return "\uFFFD"; - default: - throw new AssertionError(); - } - } - }; - private static String unescapeWithDefaults(String input) { NumberStringBuilder nsb = new NumberStringBuilder(); AffixPatternUtils.unescape(input, nsb, 0, DEFAULT_SYMBOL_PROVIDER); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java new file mode 100644 index 00000000000..54b0e53cdb4 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java @@ -0,0 +1,152 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.dev.test.number; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +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.modifiers.ConstantAffixModifier; +import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier; +import com.ibm.icu.impl.number.modifiers.CurrencySpacingEnabledModifier; +import com.ibm.icu.impl.number.modifiers.SimpleModifier; +import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.util.ULocale; + +public class ModifierTest { + @Test + public void testConstantAffixModifier() { + assertModifierEquals(ConstantAffixModifier.EMPTY, 0, false, "|", "n"); + + Modifier mod1 = new ConstantAffixModifier("a", "b", NumberFormat.Field.PERCENT, true); + assertModifierEquals(mod1, 1, true, "a|b", "%n%"); + } + + @Test + public void testConstantMultiFieldModifier() { + NumberStringBuilder prefix = new NumberStringBuilder(); + NumberStringBuilder suffix = new NumberStringBuilder(); + Modifier mod1 = new ConstantMultiFieldModifier(prefix, suffix, true); + assertModifierEquals(mod1, 0, true, "|", "n"); + + prefix.append("a", NumberFormat.Field.PERCENT); + suffix.append("b", NumberFormat.Field.CURRENCY); + Modifier mod2 = new ConstantMultiFieldModifier(prefix, suffix, true); + assertModifierEquals(mod2, 1, true, "a|b", "%n$"); + + // Make sure the first modifier is still the same (that it stayed constant) + assertModifierEquals(mod1, 0, true, "|", "n"); + } + + @Test + public void testSimpleModifier() { + 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 } }; + int[] prefixLens = { 0, 1, 2, 0, 4 }; + String[][] expectedCharFields = { { "|", "n" }, { "X|Y", "%n%" }, { "XX|YYY", "%%n%%%" }, { "|YY", "n%%" }, + { "XXXX|", "%%%%n" } }; + 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++) { + String pattern = patterns[i]; + String compiledPattern = SimpleFormatterImpl + .compileToStringMinMaxArguments(pattern, new StringBuilder(), 1, 1); + Modifier mod = new SimpleModifier(compiledPattern, NumberFormat.Field.PERCENT, false); + assertModifierEquals(mod, prefixLens[i], false, expectedCharFields[i][0], expectedCharFields[i][1]); + + // Test strange insertion positions + for (int j = 0; j < outputs.length; j++) { + NumberStringBuilder output = new NumberStringBuilder(); + output.append((String) outputs[j][0], null); + mod.apply(output, (Integer) outputs[j][1], (Integer) outputs[j][2]); + String expected = expecteds[j][i]; + String actual = output.toString(); + assertEquals(expected, actual); + } + } + } + + @Test + public void testCurrencySpacingEnabledModifier() { + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH); + NumberStringBuilder prefix = new NumberStringBuilder(); + NumberStringBuilder suffix = new NumberStringBuilder(); + Modifier mod1 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols); + assertModifierEquals(mod1, 0, true, "|", "n"); + + prefix.append("USD", NumberFormat.Field.CURRENCY); + Modifier mod2 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols); + assertModifierEquals(mod2, 3, true, "USD|", "$$$n"); + + // Test the default currency spacing rules + NumberStringBuilder sb = new NumberStringBuilder(); + sb.append("123", NumberFormat.Field.INTEGER); + NumberStringBuilder sb1 = new NumberStringBuilder(sb); + assertModifierEquals(mod2, sb1, 3, true, "USD\u00A0123", "$$$niii"); + + // Compare with the unsafe code path + NumberStringBuilder sb2 = new NumberStringBuilder(sb); + sb2.insert(0, "USD", NumberFormat.Field.CURRENCY); + CurrencySpacingEnabledModifier.applyCurrencySpacing(sb2, 0, 3, 6, 0, symbols); + assertTrue(sb1.toDebugString() + " vs " + sb2.toDebugString(), sb1.contentEquals(sb2)); + + // Test custom patterns + // The following line means that the last char of the number should be a | (rather than a digit) + symbols.setPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, true, "[|]"); + suffix.append("XYZ", NumberFormat.Field.CURRENCY); + Modifier mod3 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols); + assertModifierEquals(mod3, 3, true, "USD|\u00A0XYZ", "$$$nn$$$"); + } + + @Test + public void testCurrencySpacingPatternStability() { + // This test checks for stability of the currency spacing patterns in CLDR. + // For efficiency, ICU caches the most common currency spacing UnicodeSets. + // If this test starts failing, please update the method #getUnicodeSet() in + // BOTH CurrencySpacingEnabledModifier.java AND in C++. + DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(new ULocale("en-US")); + assertEquals( + "[:^S:]", + dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, true)); + assertEquals( + "[:^S:]", + dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, false)); + assertEquals( + "[:digit:]", + dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, true)); + assertEquals( + "[:digit:]", + dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, false)); + } + + private void assertModifierEquals( + Modifier mod, + int expectedPrefixLength, + boolean expectedStrong, + String expectedChars, + String expectedFields) { + NumberStringBuilder sb = new NumberStringBuilder(); + sb.appendCodePoint('|', null); + assertModifierEquals(mod, sb, expectedPrefixLength, expectedStrong, expectedChars, expectedFields); + } + + private void assertModifierEquals( + Modifier mod, + NumberStringBuilder sb, + int expectedPrefixLength, + boolean expectedStrong, + String expectedChars, + String expectedFields) { + mod.apply(sb, 0, sb.length()); + assertEquals("Prefix length on " + sb, expectedPrefixLength, mod.getPrefixLength()); + assertEquals("Strong on " + sb, expectedStrong, mod.isStrong()); + assertEquals("", sb.toDebugString()); + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java index 686bd4a4c5c..0bc996b5a3e 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java @@ -6,27 +6,27 @@ import static org.junit.Assert.assertEquals; import org.junit.Test; -import com.ibm.icu.impl.number.LdmlPatternInfo; +import com.ibm.icu.impl.number.PatternParser; import com.ibm.icu.impl.number.NumberStringBuilder; import com.ibm.icu.text.DecimalFormatSymbols; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.util.Currency; import com.ibm.icu.util.ULocale; -import newapi.MurkyModifier; +import newapi.MutablePatternModifier; import newapi.NumberFormatter.SignDisplay; +import newapi.NumberFormatter.UnitWidth; public class MurkyModifierTest { @Test public void basic() { - MurkyModifier murky = new MurkyModifier(false); - murky.setPatternInfo(LdmlPatternInfo.parse("a0b")); + MutablePatternModifier murky = new MutablePatternModifier(false); + murky.setPatternInfo(PatternParser.parse("a0b")); murky.setPatternAttributes(SignDisplay.AUTO, false); murky.setSymbols( DecimalFormatSymbols.getInstance(ULocale.ENGLISH), Currency.getInstance("USD"), - FormatWidth.SHORT, + UnitWidth.SHORT, null); murky.setNumberProperties(false, null); assertEquals("a", getPrefix(murky)); @@ -41,7 +41,7 @@ public class MurkyModifierTest { assertEquals("a", getPrefix(murky)); assertEquals("b", getSuffix(murky)); - murky.setPatternInfo(LdmlPatternInfo.parse("a0b;c-0d")); + murky.setPatternInfo(PatternParser.parse("a0b;c-0d")); murky.setPatternAttributes(SignDisplay.AUTO, false); murky.setNumberProperties(false, null); assertEquals("a", getPrefix(murky)); @@ -57,13 +57,13 @@ public class MurkyModifierTest { assertEquals("d", getSuffix(murky)); } - private static String getPrefix(MurkyModifier murky) { + private static String getPrefix(MutablePatternModifier murky) { NumberStringBuilder nsb = new NumberStringBuilder(); murky.apply(nsb, 0, 0); return nsb.subSequence(0, murky.getPrefixLength()).toString(); } - private static String getSuffix(MurkyModifier murky) { + private static String getSuffix(MutablePatternModifier murky) { NumberStringBuilder nsb = new NumberStringBuilder(); murky.apply(nsb, 0, 0); return nsb.subSequence(murky.getPrefixLength(), nsb.length()).toString(); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java index 5ba0e95756d..460aae4c1cb 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java @@ -11,16 +11,14 @@ import java.util.Locale; import org.junit.Ignore; import org.junit.Test; -import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition; import com.ibm.icu.text.DecimalFormatSymbols; -import com.ibm.icu.text.MeasureFormat.FormatWidth; import com.ibm.icu.text.NumberingSystem; import com.ibm.icu.util.Currency; import com.ibm.icu.util.Currency.CurrencyUsage; import com.ibm.icu.util.CurrencyAmount; -import com.ibm.icu.util.Dimensionless; import com.ibm.icu.util.Measure; import com.ibm.icu.util.MeasureUnit; +import com.ibm.icu.util.NoUnit; import com.ibm.icu.util.ULocale; import newapi.FormattedNumber; @@ -31,1194 +29,1296 @@ import newapi.Notation; import newapi.NumberFormatter; import newapi.NumberFormatter.DecimalMarkDisplay; import newapi.NumberFormatter.SignDisplay; -import newapi.impl.Padder; +import newapi.NumberFormatter.UnitWidth; import newapi.NumberPropertyMapper; import newapi.Rounder; import newapi.UnlocalizedNumberFormatter; +import newapi.impl.Padder; +import newapi.impl.Padder.PadPosition; public class NumberFormatterTest { - private static final Currency USD = Currency.getInstance("USD"); - private static final Currency GBP = Currency.getInstance("GBP"); - private static final Currency CZK = Currency.getInstance("CZK"); - - @Test - public void notationSimple() { - assertFormatDescending( - "Basic", - "", - NumberFormatter.with(), - ULocale.ENGLISH, - "87,650", - "8,765", - "876.5", - "87.65", - "8.765", - "0.8765", - "0.08765", - "0.008765", - "0"); - - assertFormatSingle( - "Basic with Negative Sign", - "", - NumberFormatter.with(), - ULocale.ENGLISH, - -9876543.21, - "-9,876,543.21"); - } - - @Test - public void notationScientific() { - assertFormatDescending( - "Scientific", - "E", - NumberFormatter.with().notation(Notation.scientific()), - ULocale.ENGLISH, - "8.765E4", - "8.765E3", - "8.765E2", - "8.765E1", - "8.765E0", - "8.765E-1", - "8.765E-2", - "8.765E-3", - "0E0"); - - assertFormatDescending( - "Engineering", - "E3", - NumberFormatter.with().notation(Notation.engineering()), - ULocale.ENGLISH, - "87.65E3", - "8.765E3", - "876.5E0", - "87.65E0", - "8.765E0", - "876.5E-3", - "87.65E-3", - "8.765E-3", - "0E0"); - - assertFormatDescending( - "Scientific sign always shown", - "E+", - NumberFormatter.with() - .notation(Notation.scientific().withExponentSignDisplay(SignDisplay.ALWAYS)), - ULocale.ENGLISH, - "8.765E+4", - "8.765E+3", - "8.765E+2", - "8.765E+1", - "8.765E+0", - "8.765E-1", - "8.765E-2", - "8.765E-3", - "0E+0"); - - assertFormatDescending( - "Scientific min exponent digits", - "E00", - NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)), - ULocale.ENGLISH, - "8.765E04", - "8.765E03", - "8.765E02", - "8.765E01", - "8.765E00", - "8.765E-01", - "8.765E-02", - "8.765E-03", - "0E00"); - - assertFormatSingle( - "Scientific Negative", - "E", - NumberFormatter.with().notation(Notation.scientific()), - ULocale.ENGLISH, - -1000000, - "-1E6"); - } - - @Test - public void notationCompact() { - assertFormatDescending( - "Compact Short", - "C", - NumberFormatter.with().notation(Notation.compactShort()), - ULocale.ENGLISH, - "88K", - "8.8K", - "876", - "88", - "8.8", - "0.88", - "0.088", - "0.0088", - "0"); - - assertFormatDescending( - "Compact Long", - "CC", - NumberFormatter.with().notation(Notation.compactLong()), - ULocale.ENGLISH, - "88 thousand", - "8.8 thousand", - "876", - "88", - "8.8", - "0.88", - "0.088", - "0.0088", - "0"); - - assertFormatDescending( - "Compact Short Currency", - "C $USD", - NumberFormatter.with().notation(Notation.compactShort()).unit(USD), - ULocale.ENGLISH, - "$88K", - "$8.8K", - "$876", - "$88", - "$8.8", - "$0.88", - "$0.088", - "$0.0088", - "$0"); - - // Note: Most locales don't have compact long currency, so this currently falls back to short. - assertFormatDescending( - "Compact Long Currency", - "CC $USD", - NumberFormatter.with().notation(Notation.compactLong()).unit(USD), - ULocale.ENGLISH, - "$88K", - "$8.8K", - "$876", - "$88", - "$8.8", - "$0.88", - "$0.088", - "$0.0088", - "$0"); - - assertFormatSingle( - "Compact Plural One", - "CC", - NumberFormatter.with().notation(Notation.compactLong()), - ULocale.forLanguageTag("es"), - 1000000, - "1 millón"); - - assertFormatSingle( - "Compact Plural Other", - "CC", - NumberFormatter.with().notation(Notation.compactLong()), - ULocale.forLanguageTag("es"), - 2000000, - "2 millones"); - - assertFormatSingle( - "Compact with Negative Sign", - "C", - NumberFormatter.with().notation(Notation.compactShort()), - ULocale.ENGLISH, - -9876543.21, - "-9.9M"); - } - - @Test - public void unitMeasure() { - assertFormatDescending( - "Meters Short", - "U:length:meter", - NumberFormatter.with().unit(MeasureUnit.METER), - ULocale.ENGLISH, - "87,650 m", - "8,765 m", - "876.5 m", - "87.65 m", - "8.765 m", - "0.8765 m", - "0.08765 m", - "0.008765 m", - "0 m"); - - assertFormatDescending( - "Meters Long", - "U:length:meter unit-width=WIDE", - NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(FormatWidth.WIDE), - ULocale.ENGLISH, - "87,650 meters", - "8,765 meters", - "876.5 meters", - "87.65 meters", - "8.765 meters", - "0.8765 meters", - "0.08765 meters", - "0.008765 meters", - "0 meters"); - - assertFormatDescending( - "Compact Meters Long", - "CC U:length:meter unit-width=WIDE", - NumberFormatter.with() - .notation(Notation.compactLong()) - .unit(MeasureUnit.METER) - .unitWidth(FormatWidth.WIDE), - ULocale.ENGLISH, - "88 thousand meters", - "8.8 thousand meters", - "876 meters", - "88 meters", - "8.8 meters", - "0.88 meters", - "0.088 meters", - "0.0088 meters", - "0 meters"); - - assertFormatSingleMeasure( - "Meters with Measure Input", - "unit-width=WIDE", - NumberFormatter.with().unitWidth(FormatWidth.WIDE), - ULocale.ENGLISH, - new Measure(5.43, MeasureUnit.METER), - "5.43 meters"); - - assertFormatSingle( - "Meters with Negative Sign", - "U:length:meter", - NumberFormatter.with().unit(MeasureUnit.METER), - ULocale.ENGLISH, - -9876543.21, - "-9,876,543.21 m"); - } - - @Test - public void unitCurrency() { - assertFormatDescending( - "Currency", - "$GBP", - NumberFormatter.with().unit(GBP), - ULocale.ENGLISH, - "£87,650.00", - "£8,765.00", - "£876.50", - "£87.65", - "£8.76", - "£0.88", - "£0.09", - "£0.01", - "£0.00"); - - assertFormatDescending( - "Currency ISO", - "$GBP unit-width=SHORT", - NumberFormatter.with().unit(GBP).unitWidth(FormatWidth.SHORT), - ULocale.ENGLISH, - "GBP 87,650.00", - "GBP 8,765.00", - "GBP 876.50", - "GBP 87.65", - "GBP 8.76", - "GBP 0.88", - "GBP 0.09", - "GBP 0.01", - "GBP 0.00"); - - assertFormatDescending( - "Currency Long Name", - "$GBP unit-width=WIDE", - NumberFormatter.with().unit(GBP).unitWidth(FormatWidth.WIDE), - ULocale.ENGLISH, - "87,650.00 British pounds", - "8,765.00 British pounds", - "876.50 British pounds", - "87.65 British pounds", - "8.76 British pounds", - "0.88 British pounds", - "0.09 British pounds", - "0.01 British pounds", - "0.00 British pounds"); - - assertFormatSingleMeasure( - "Currency with CurrencyAmount Input", - "", - NumberFormatter.with(), - ULocale.ENGLISH, - new CurrencyAmount(5.43, GBP), - "£5.43"); - - assertFormatSingle( - "Currency Long Name from Pattern Syntax", - "$GBP F0 grouping=none integer-width=1- symbols=loc:en_US sign=AUTO decimal=AUTO", - NumberPropertyMapper.create("0 ¤¤¤", DecimalFormatSymbols.getInstance()).unit(GBP), - ULocale.ENGLISH, - 1234567.89, - "1234568 British pounds"); - - assertFormatSingle( - "Currency with Negative Sign", - "$GBP", - NumberFormatter.with().unit(GBP), - ULocale.ENGLISH, - -9876543.21, - "-£9,876,543.21"); - } - - @Test - public void unitPercent() { - assertFormatDescending( - "Percent", - "%", - NumberFormatter.with().unit(Dimensionless.PERCENT), - ULocale.ENGLISH, - "8,765,000%", - "876,500%", - "87,650%", - "8,765%", - "876.5%", - "87.65%", - "8.765%", - "0.8765%", - "0%"); - - assertFormatDescending( - "Permille", - "%%", - NumberFormatter.with().unit(Dimensionless.PERMILLE), - ULocale.ENGLISH, - "87,650,000‰", - "8,765,000‰", - "876,500‰", - "87,650‰", - "8,765‰", - "876.5‰", - "87.65‰", - "8.765‰", - "0‰"); - - assertFormatSingle( - "Percent with Negative Sign", - "%", - NumberFormatter.with().unit(Dimensionless.PERCENT), - ULocale.ENGLISH, - -0.987654321, - "-98.7654321%"); - } - - @Test - public void roundingFraction() { - assertFormatDescending( - "Integer", - "F0", - NumberFormatter.with().rounding(Rounder.integer()), - ULocale.ENGLISH, - "87,650", - "8,765", - "876", - "88", - "9", - "1", - "0", - "0", - "0"); - - assertFormatDescending( - "Fixed Fraction", - "F3", - NumberFormatter.with().rounding(Rounder.fixedFraction(3)), - ULocale.ENGLISH, - "87,650.000", - "8,765.000", - "876.500", - "87.650", - "8.765", - "0.876", - "0.088", - "0.009", - "0.000"); - - assertFormatDescending( - "Min Fraction", - "F1-", - NumberFormatter.with().rounding(Rounder.minFraction(1)), - ULocale.ENGLISH, - "87,650.0", - "8,765.0", - "876.5", - "87.65", - "8.765", - "0.8765", - "0.08765", - "0.008765", - "0.0"); - - assertFormatDescending( - "Max Fraction", - "F-1", - NumberFormatter.with().rounding(Rounder.maxFraction(1)), - ULocale.ENGLISH, - "87,650", - "8,765", - "876.5", - "87.6", - "8.8", - "0.9", - "0.1", - "0", - "0"); - - assertFormatDescending( - "Min/Max Fraction", - "F1-3", - NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3)), - ULocale.ENGLISH, - "87,650.0", - "8,765.0", - "876.5", - "87.65", - "8.765", - "0.876", - "0.088", - "0.009", - "0.0"); - } - - @Test - public void roundingFigures() { - assertFormatSingle( - "Fixed Significant", - "S3", - NumberFormatter.with().rounding(Rounder.fixedFigures(3)), - ULocale.ENGLISH, - -98, - "-98.0"); - - assertFormatSingle( - "Fixed Significant Rounding", - "S3", - NumberFormatter.with().rounding(Rounder.fixedFigures(3)), - ULocale.ENGLISH, - -98.7654321, - "-98.8"); - - assertFormatSingle( - "Fixed Significant Zero", - "S3", - NumberFormatter.with().rounding(Rounder.fixedFigures(3)), - ULocale.ENGLISH, - 0, - "0.00"); - - assertFormatSingle( - "Min Significant", - "S2-", - NumberFormatter.with().rounding(Rounder.minFigures(2)), - ULocale.ENGLISH, - -9, - "-9.0"); - - assertFormatSingle( - "Max Significant", - "S-4", - NumberFormatter.with().rounding(Rounder.maxFigures(4)), - ULocale.ENGLISH, - 98.7654321, - "98.77"); - - assertFormatSingle( - "Min/Max Significant", - "S3-4", - NumberFormatter.with().rounding(Rounder.minMaxFigures(3, 4)), - ULocale.ENGLISH, - 9.99999, - "10.0"); - } - - @Test - public void roundingFractionFigures() { - assertFormatDescending( - "Basic Significant", // for comparison - "S-2", - NumberFormatter.with().rounding(Rounder.maxFigures(2)), - ULocale.ENGLISH, - "88,000", - "8,800", - "880", - "88", - "8.8", - "0.88", - "0.088", - "0.0088", - "0"); - - assertFormatDescending( - "FracSig minMaxFrac minSig", - "F1-2>3", - NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 2).withMinFigures(3)), - ULocale.ENGLISH, - "87,650.0", - "8,765.0", - "876.5", - "87.65", - "8.76", - "0.876", // minSig beats maxFrac - "0.0876", // minSig beats maxFrac - "0.00876", // minSig beats maxFrac - "0.0"); - - assertFormatDescending( - "FracSig minMaxFrac maxSig A", - "F1-3<2", - NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3).withMaxFigures(2)), - ULocale.ENGLISH, - "88,000.0", // maxSig beats maxFrac - "8,800.0", // maxSig beats maxFrac - "880.0", // maxSig beats maxFrac - "88.0", // maxSig beats maxFrac - "8.8", // maxSig beats maxFrac - "0.88", // maxSig beats maxFrac - "0.088", - "0.009", - "0.0"); - - assertFormatDescending( - "FracSig minMaxFrac maxSig B", - "F2<2", - NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMaxFigures(2)), - ULocale.ENGLISH, - "88,000.00", // maxSig beats maxFrac - "8,800.00", // maxSig beats maxFrac - "880.00", // maxSig beats maxFrac - "88.00", // maxSig beats maxFrac - "8.80", // maxSig beats maxFrac - "0.88", - "0.09", - "0.01", - "0.00"); - } - - @Test - public void roundingOther() { - assertFormatDescending( - "Rounding None", - "Y", - NumberFormatter.with().rounding(Rounder.none()), - ULocale.ENGLISH, - "87,650", - "8,765", - "876.5", - "87.65", - "8.765", - "0.8765", - "0.08765", - "0.008765", - "0"); - - assertFormatDescending( - "Increment", - "M0.5", - NumberFormatter.with().rounding(Rounder.increment(BigDecimal.valueOf(0.5))), - ULocale.ENGLISH, - "87,650.0", - "8,765.0", - "876.5", - "87.5", - "9.0", - "1.0", - "0.0", - "0.0", - "0.0"); - - assertFormatDescending( - "Currency Standard", - "$CZK GSTANDARD", - NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.STANDARD)).unit(CZK), - ULocale.ENGLISH, - "CZK 87,650.00", - "CZK 8,765.00", - "CZK 876.50", - "CZK 87.65", - "CZK 8.76", - "CZK 0.88", - "CZK 0.09", - "CZK 0.01", - "CZK 0.00"); - - assertFormatDescending( - "Currency Cash", - "$CZK GCASH", - NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH)).unit(CZK), - ULocale.ENGLISH, - "CZK 87,650", - "CZK 8,765", - "CZK 876", - "CZK 88", - "CZK 9", - "CZK 1", - "CZK 0", - "CZK 0", - "CZK 0"); - - assertFormatDescending( - "Currency not in top-level fluent chain", - "F0", - NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH).withCurrency(CZK)), - ULocale.ENGLISH, - "87,650", - "8,765", - "876", - "88", - "9", - "1", - "0", - "0", - "0"); - } - - @Test - public void grouping() { - // Dimensionless.PERMILLE multiplies all the number by 10^3 (good for testing grouping). - // Note that en-US is already performed in the unitPercent() function. - assertFormatDescending( - "Indic Grouping", - "%% grouping=defaults", - NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.defaults()), - new ULocale("en-IN"), - "8,76,50,000‰", - "87,65,000‰", - "8,76,500‰", - "87,650‰", - "8,765‰", - "876.5‰", - "87.65‰", - "8.765‰", - "0‰"); - - assertFormatDescending( - "Western Grouping, Min 2", - "%% grouping=min2", - NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.min2()), - ULocale.ENGLISH, - "87,650,000‰", - "8,765,000‰", - "876,500‰", - "87,650‰", - "8765‰", - "876.5‰", - "87.65‰", - "8.765‰", - "0‰"); - - assertFormatDescending( - "Indic Grouping, Min 2", - "%% grouping=min2", - NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.min2()), - new ULocale("en-IN"), - "8,76,50,000‰", - "87,65,000‰", - "8,76,500‰", - "87,650‰", - "8765‰", - "876.5‰", - "87.65‰", - "8.765‰", - "0‰"); - - assertFormatDescending( - "No Grouping", - "%% grouping=none", - NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.none()), - new ULocale("en-IN"), - "87650000‰", - "8765000‰", - "876500‰", - "87650‰", - "8765‰", - "876.5‰", - "87.65‰", - "8.765‰", - "0‰"); - } - - @Test - public void padding() { - assertFormatDescending( - "Padding", - "", - NumberFormatter.with().padding(Padder.none()), - ULocale.ENGLISH, - "87,650", - "8,765", - "876.5", - "87.65", - "8.765", - "0.8765", - "0.08765", - "0.008765", - "0"); - - assertFormatDescending( - "Padding", - "", - NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)), - ULocale.ENGLISH, - "**87,650", - "***8,765", - "***876.5", - "***87.65", - "***8.765", - "**0.8765", - "*0.08765", - "0.008765", - "*******0"); - - assertFormatDescending( - "Padding with code points", - "", - NumberFormatter.with().padding(Padder.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)), - ULocale.ENGLISH, - "𐇤𐇤87,650", - "𐇤𐇤𐇤8,765", - "𐇤𐇤𐇤876.5", - "𐇤𐇤𐇤87.65", - "𐇤𐇤𐇤8.765", - "𐇤𐇤0.8765", - "𐇤0.08765", - "0.008765", - "𐇤𐇤𐇤𐇤𐇤𐇤𐇤0"); - - assertFormatDescending( - "Padding with wide digits", - "symbols=ns:mathsanb", - NumberFormatter.with() - .padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)) - .symbols(NumberingSystem.getInstanceByName("mathsanb")), - ULocale.ENGLISH, - "**𝟴𝟳,𝟲𝟱𝟬", - "***𝟴,𝟳𝟲𝟱", - "***𝟴𝟳𝟲.𝟱", - "***𝟴𝟳.𝟲𝟱", - "***𝟴.𝟳𝟲𝟱", - "**𝟬.𝟴𝟳𝟲𝟱", - "*𝟬.𝟬𝟴𝟳𝟲𝟱", - "𝟬.𝟬𝟬𝟴𝟳𝟲𝟱", - "*******𝟬"); - - assertFormatDescending( - "Padding with currency spacing", - "$GBP unit-width=SHORT", - NumberFormatter.with() - .padding(Padder.codePoints('*', 10, PadPosition.AFTER_PREFIX)) - .unit(GBP) - .unitWidth(FormatWidth.SHORT), - ULocale.ENGLISH, - "GBP 87,650.00", - "GBP 8,765.00", - "GBP 876.50", - "GBP**87.65", - "GBP***8.76", - "GBP***0.88", - "GBP***0.09", - "GBP***0.01", - "GBP***0.00"); - - assertFormatSingle( - "Pad Before Prefix", - "", - NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_PREFIX)), - ULocale.ENGLISH, - -88.88, - "**-88.88"); - - assertFormatSingle( - "Pad After Prefix", - "", - NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)), - ULocale.ENGLISH, - -88.88, - "-**88.88"); - - assertFormatSingle( - "Pad Before Suffix", - "%", - NumberFormatter.with() - .padding(Padder.codePoints('*', 8, PadPosition.BEFORE_SUFFIX)) - .unit(Dimensionless.PERCENT), - ULocale.ENGLISH, - 0.8888, - "88.88**%"); - - assertFormatSingle( - "Pad After Suffix", - "%", - NumberFormatter.with() - .padding(Padder.codePoints('*', 8, PadPosition.AFTER_SUFFIX)) - .unit(Dimensionless.PERCENT), - ULocale.ENGLISH, - 0.8888, - "88.88%**"); - } - - @Test - public void integerWidth() { - assertFormatDescending( - "Integer Width Default", - "integer-width=1-", - NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1)), - ULocale.ENGLISH, - "87,650", - "8,765", - "876.5", - "87.65", - "8.765", - "0.8765", - "0.08765", - "0.008765", - "0"); - - assertFormatDescending( - "Integer Width Zero Fill 0", - "integer-width=0-", - NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)), - ULocale.ENGLISH, - "87,650", - "8,765", - "876.5", - "87.65", - "8.765", - ".8765", - ".08765", - ".008765", - ""); // FIXME: Avoid the empty string here? - - assertFormatDescending( - "Integer Width Zero Fill 3", - "integer-width=3-", - NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(3)), - ULocale.ENGLISH, - "87,650", - "8,765", - "876.5", - "087.65", - "008.765", - "000.8765", - "000.08765", - "000.008765", - "000"); - - assertFormatDescending( - "Integer Width Max 3", - "integer-width=1-3", - NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1).truncateAt(3)), - ULocale.ENGLISH, - "650", - "765", - "876.5", - "87.65", - "8.765", - "0.8765", - "0.08765", - "0.008765", - "0"); - - assertFormatDescending( - "Integer Width Fixed 2", - "integer-width=2", - NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)), - ULocale.ENGLISH, - "50", - "65", - "76.5", - "87.65", - "08.765", - "00.8765", - "00.08765", - "00.008765", - "00"); - } - - @Test - public void symbols() { - assertFormatDescending( - "French Symbols with Japanese Data 1", - "symbols=loc:fr", - NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)), - ULocale.JAPAN, - "87 650", - "8 765", - "876,5", - "87,65", - "8,765", - "0,8765", - "0,08765", - "0,008765", - "0"); - - assertFormatSingle( - "French Symbols with Japanese Data 2", - "C symbols=loc:fr", - NumberFormatter.with() - .notation(Notation.compactShort()) - .symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)), - ULocale.JAPAN, - 12345, - "1,2\u4E07"); - - assertFormatDescending( - "Latin Numbering System with Arabic Data", - "$USD symbols=ns:latn", - NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD), - new ULocale("ar"), - "87,650.00 US$", - "8,765.00 US$", - "876.50 US$", - "87.65 US$", - "8.76 US$", - "0.88 US$", - "0.09 US$", - "0.01 US$", - "0.00 US$"); - - assertFormatDescending( - "Math Numbering System with French Data", - "symbols=ns:mathsanb", - NumberFormatter.with().symbols(NumberingSystem.getInstanceByName("mathsanb")), - ULocale.FRENCH, - "𝟴𝟳 𝟲𝟱𝟬", - "𝟴 𝟳𝟲𝟱", - "𝟴𝟳𝟲,𝟱", - "𝟴𝟳,𝟲𝟱", - "𝟴,𝟳𝟲𝟱", - "𝟬,𝟴𝟳𝟲𝟱", - "𝟬,𝟬𝟴𝟳𝟲𝟱", - "𝟬,𝟬𝟬𝟴𝟳𝟲𝟱", - "𝟬"); - } - - @Test - @Ignore("This feature is not currently available.") - public void symbolsOverride() { - DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(ULocale.ENGLISH); - dfs.setCurrencySymbol("@"); - dfs.setInternationalCurrencySymbol("foo"); - assertFormatSingle( - "Custom Short Currency Symbol", - "$XXX", - NumberFormatter.with().unit(Currency.getInstance("XXX")).symbols(dfs), - ULocale.ENGLISH, - 12.3, - "@ 12.30"); - } - - @Test - public void sign() { - assertFormatSingle( - "Sign Auto Positive", - "sign=AUTO", - NumberFormatter.with().sign(SignDisplay.AUTO), - ULocale.ENGLISH, - 444444, - "444,444"); - - assertFormatSingle( - "Sign Auto Negative", - "sign=AUTO", - NumberFormatter.with().sign(SignDisplay.AUTO), - ULocale.ENGLISH, - -444444, - "-444,444"); - - assertFormatSingle( - "Sign Always Positive", - "sign=ALWAYS", - NumberFormatter.with().sign(SignDisplay.ALWAYS), - ULocale.ENGLISH, - 444444, - "+444,444"); - - assertFormatSingle( - "Sign Always Negative", - "sign=ALWAYS", - NumberFormatter.with().sign(SignDisplay.ALWAYS), - ULocale.ENGLISH, - -444444, - "-444,444"); - - assertFormatSingle( - "Sign Never Positive", - "sign=NEVER", - NumberFormatter.with().sign(SignDisplay.NEVER), - ULocale.ENGLISH, - 444444, - "444,444"); - - assertFormatSingle( - "Sign Never Negative", - "sign=NEVER", - NumberFormatter.with().sign(SignDisplay.NEVER), - ULocale.ENGLISH, - -444444, - "444,444"); - } - - @Test - public void decimal() { - assertFormatDescending( - "Decimal Default", - "decimal=AUTO", - NumberFormatter.with().decimal(DecimalMarkDisplay.AUTO), - ULocale.ENGLISH, - "87,650", - "8,765", - "876.5", - "87.65", - "8.765", - "0.8765", - "0.08765", - "0.008765", - "0"); - - assertFormatDescending( - "Decimal Always Shown", - "decimal=ALWAYS", - NumberFormatter.with().decimal(DecimalMarkDisplay.ALWAYS), - ULocale.ENGLISH, - "87,650.", - "8,765.", - "876.5", - "87.65", - "8.765", - "0.8765", - "0.08765", - "0.008765", - "0."); - } - - @Test - public void locale() { - // Coverage for the locale setters. - assertEquals( - NumberFormatter.with().locale(ULocale.ENGLISH), - NumberFormatter.with().locale(Locale.ENGLISH)); - assertNotEquals( - NumberFormatter.with().locale(ULocale.ENGLISH), - NumberFormatter.with().locale(Locale.FRENCH)); - } - - @Test - public void getPrefixSuffix() { - Object[][] cases = { - { - NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(FormatWidth.SHORT), - "GBP", - "", - "-GBP", - "" - }, - { - NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(FormatWidth.WIDE), - "", - " British pounds", - "-", - " British pounds" - } - }; - - for (Object[] cas : cases) { - LocalizedNumberFormatter f = (LocalizedNumberFormatter) cas[0]; - String posPrefix = (String) cas[1]; - String posSuffix = (String) cas[2]; - String negPrefix = (String) cas[3]; - String negSuffix = (String) cas[4]; - FormattedNumber positive = f.format(1); - FormattedNumber negative = f.format(-1); - assertEquals(posPrefix, positive.getPrefix()); - assertEquals(posSuffix, positive.getSuffix()); - assertEquals(negPrefix, negative.getPrefix()); - assertEquals(negSuffix, negative.getSuffix()); + private static final Currency USD = Currency.getInstance("USD"); + private static final Currency GBP = Currency.getInstance("GBP"); + private static final Currency CZK = Currency.getInstance("CZK"); + + @Test + public void notationSimple() { + assertFormatDescending( + "Basic", + "", + NumberFormatter.with(), + ULocale.ENGLISH, + "87,650", + "8,765", + "876.5", + "87.65", + "8.765", + "0.8765", + "0.08765", + "0.008765", + "0"); + + assertFormatSingle( + "Basic with Negative Sign", + "", + NumberFormatter.with(), + ULocale.ENGLISH, + -9876543.21, + "-9,876,543.21"); + } + + @Test + public void notationScientific() { + assertFormatDescending( + "Scientific", + "E", + NumberFormatter.with().notation(Notation.scientific()), + ULocale.ENGLISH, + "8.765E4", + "8.765E3", + "8.765E2", + "8.765E1", + "8.765E0", + "8.765E-1", + "8.765E-2", + "8.765E-3", + "0E0"); + + assertFormatDescending( + "Engineering", + "E3", + NumberFormatter.with().notation(Notation.engineering()), + ULocale.ENGLISH, + "87.65E3", + "8.765E3", + "876.5E0", + "87.65E0", + "8.765E0", + "876.5E-3", + "87.65E-3", + "8.765E-3", + "0E0"); + + assertFormatDescending( + "Scientific sign always shown", + "E+", + NumberFormatter.with().notation(Notation.scientific().withExponentSignDisplay(SignDisplay.ALWAYS)), + ULocale.ENGLISH, + "8.765E+4", + "8.765E+3", + "8.765E+2", + "8.765E+1", + "8.765E+0", + "8.765E-1", + "8.765E-2", + "8.765E-3", + "0E+0"); + + assertFormatDescending( + "Scientific min exponent digits", + "E00", + NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)), + ULocale.ENGLISH, + "8.765E04", + "8.765E03", + "8.765E02", + "8.765E01", + "8.765E00", + "8.765E-01", + "8.765E-02", + "8.765E-03", + "0E00"); + + assertFormatSingle( + "Scientific Negative", + "E", + NumberFormatter.with().notation(Notation.scientific()), + ULocale.ENGLISH, + -1000000, + "-1E6"); + } + + @Test + public void notationCompact() { + assertFormatDescending( + "Compact Short", + "C", + NumberFormatter.with().notation(Notation.compactShort()), + ULocale.ENGLISH, + "88K", + "8.8K", + "876", + "88", + "8.8", + "0.88", + "0.088", + "0.0088", + "0"); + + assertFormatDescending( + "Compact Long", + "CC", + NumberFormatter.with().notation(Notation.compactLong()), + ULocale.ENGLISH, + "88 thousand", + "8.8 thousand", + "876", + "88", + "8.8", + "0.88", + "0.088", + "0.0088", + "0"); + + assertFormatDescending( + "Compact Short Currency", + "C $USD", + NumberFormatter.with().notation(Notation.compactShort()).unit(USD), + ULocale.ENGLISH, + "$88K", + "$8.8K", + "$876", + "$88", + "$8.8", + "$0.88", + "$0.088", + "$0.0088", + "$0"); + + // Note: Most locales don't have compact long currency, so this currently falls back to short. + assertFormatDescending( + "Compact Long Currency", + "CC $USD", + NumberFormatter.with().notation(Notation.compactLong()).unit(USD), + ULocale.ENGLISH, + "$88K", + "$8.8K", + "$876", + "$88", + "$8.8", + "$0.88", + "$0.088", + "$0.0088", + "$0"); + + assertFormatSingle( + "Compact Plural One", + "CC", + NumberFormatter.with().notation(Notation.compactLong()), + ULocale.forLanguageTag("es"), + 1000000, + "1 millón"); + + assertFormatSingle( + "Compact Plural Other", + "CC", + NumberFormatter.with().notation(Notation.compactLong()), + ULocale.forLanguageTag("es"), + 2000000, + "2 millones"); + + assertFormatSingle( + "Compact with Negative Sign", + "C", + NumberFormatter.with().notation(Notation.compactShort()), + ULocale.ENGLISH, + -9876543.21, + "-9.9M"); + + assertFormatSingle( + "Compact Rounding", + "C", + NumberFormatter.with().notation(Notation.compactShort()), + ULocale.ENGLISH, + 990000, + "990K"); + + assertFormatSingle( + "Compact Rounding", + "C", + NumberFormatter.with().notation(Notation.compactShort()), + ULocale.ENGLISH, + 999000, + "999K"); + + assertFormatSingle( + "Compact Rounding", + "C", + NumberFormatter.with().notation(Notation.compactShort()), + ULocale.ENGLISH, + 999900, + "1M"); + + assertFormatSingle( + "Compact Rounding", + "C", + NumberFormatter.with().notation(Notation.compactShort()), + ULocale.ENGLISH, + 9900000, + "9.9M"); + + assertFormatSingle( + "Compact Rounding", + "C", + NumberFormatter.with().notation(Notation.compactShort()), + ULocale.ENGLISH, + 9990000, + "10M"); + } + + @Test + public void unitMeasure() { + assertFormatDescending( + "Meters Short", + "U:length:meter", + NumberFormatter.with().unit(MeasureUnit.METER), + ULocale.ENGLISH, + "87,650 m", + "8,765 m", + "876.5 m", + "87.65 m", + "8.765 m", + "0.8765 m", + "0.08765 m", + "0.008765 m", + "0 m"); + + assertFormatDescending( + "Meters Long", + "U:length:meter unit-width=FULL_NAME", + NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME), + ULocale.ENGLISH, + "87,650 meters", + "8,765 meters", + "876.5 meters", + "87.65 meters", + "8.765 meters", + "0.8765 meters", + "0.08765 meters", + "0.008765 meters", + "0 meters"); + + assertFormatDescending( + "Compact Meters Long", + "CC U:length:meter unit-width=FULL_NAME", + NumberFormatter.with().notation(Notation.compactLong()).unit(MeasureUnit.METER) + .unitWidth(UnitWidth.FULL_NAME), + ULocale.ENGLISH, + "88 thousand meters", + "8.8 thousand meters", + "876 meters", + "88 meters", + "8.8 meters", + "0.88 meters", + "0.088 meters", + "0.0088 meters", + "0 meters"); + + assertFormatSingleMeasure( + "Meters with Measure Input", + "unit-width=FULL_NAME", + NumberFormatter.with().unitWidth(UnitWidth.FULL_NAME), + ULocale.ENGLISH, + new Measure(5.43, MeasureUnit.METER), + "5.43 meters"); + + assertFormatSingleMeasure( + "Measure format method takes precedence over fluent chain", + "U:length:meter", + NumberFormatter.with().unit(MeasureUnit.METER), + ULocale.ENGLISH, + new Measure(5.43, USD), + "$5.43"); + + assertFormatSingle( + "Meters with Negative Sign", + "U:length:meter", + NumberFormatter.with().unit(MeasureUnit.METER), + ULocale.ENGLISH, + -9876543.21, + "-9,876,543.21 m"); + } + + @Test + public void unitCurrency() { + assertFormatDescending( + "Currency", + "$GBP", + NumberFormatter.with().unit(GBP), + ULocale.ENGLISH, + "£87,650.00", + "£8,765.00", + "£876.50", + "£87.65", + "£8.76", + "£0.88", + "£0.09", + "£0.01", + "£0.00"); + + assertFormatDescending( + "Currency ISO", + "$GBP unit-width=ISO_CODE", + NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.ISO_CODE), + ULocale.ENGLISH, + "GBP 87,650.00", + "GBP 8,765.00", + "GBP 876.50", + "GBP 87.65", + "GBP 8.76", + "GBP 0.88", + "GBP 0.09", + "GBP 0.01", + "GBP 0.00"); + + assertFormatDescending( + "Currency Long Name", + "$GBP unit-width=FULL_NAME", + NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.FULL_NAME), + ULocale.ENGLISH, + "87,650.00 British pounds", + "8,765.00 British pounds", + "876.50 British pounds", + "87.65 British pounds", + "8.76 British pounds", + "0.88 British pounds", + "0.09 British pounds", + "0.01 British pounds", + "0.00 British pounds"); + + assertFormatSingleMeasure( + "Currency with CurrencyAmount Input", + "", + NumberFormatter.with(), + ULocale.ENGLISH, + new CurrencyAmount(5.43, GBP), + "£5.43"); + + assertFormatSingle( + "Currency Long Name from Pattern Syntax", + "$GBP F0 grouping=none integer-width=1- symbols=loc:en_US sign=AUTO decimal=AUTO", + NumberPropertyMapper.create("0 ¤¤¤", DecimalFormatSymbols.getInstance()).unit(GBP), + ULocale.ENGLISH, + 1234567.89, + "1234568 British pounds"); + + assertFormatSingle( + "Currency with Negative Sign", + "$GBP", + NumberFormatter.with().unit(GBP), + ULocale.ENGLISH, + -9876543.21, + "-£9,876,543.21"); + } + + @Test + public void unitPercent() { + assertFormatDescending( + "Percent", + "%", + NumberFormatter.with().unit(NoUnit.PERCENT), + ULocale.ENGLISH, + "8,765,000%", + "876,500%", + "87,650%", + "8,765%", + "876.5%", + "87.65%", + "8.765%", + "0.8765%", + "0%"); + + assertFormatDescending( + "Permille", + "%%", + NumberFormatter.with().unit(NoUnit.PERMILLE), + ULocale.ENGLISH, + "87,650,000‰", + "8,765,000‰", + "876,500‰", + "87,650‰", + "8,765‰", + "876.5‰", + "87.65‰", + "8.765‰", + "0‰"); + + assertFormatSingle( + "NoUnit Base", + "B", + NumberFormatter.with().unit(NoUnit.BASE), + ULocale.ENGLISH, + 51423, + "51,423"); + + assertFormatSingle( + "Percent with Negative Sign", + "%", + NumberFormatter.with().unit(NoUnit.PERCENT), + ULocale.ENGLISH, + -0.987654321, + "-98.7654321%"); } - } - - @Test - public void plurals() { - // TODO: Expand this test. - - assertFormatSingle( - "Plural 1", - "$USD F0 unit-width=WIDE", - NumberFormatter.with() - .unit(USD) - .unitWidth(FormatWidth.WIDE) - .rounding(Rounder.fixedFraction(0)), - ULocale.ENGLISH, - 1, - "1 US dollar"); - - assertFormatSingle( - "Plural 1.00", - "$USD F2 unit-width=WIDE", - NumberFormatter.with() - .unit(USD) - .unitWidth(FormatWidth.WIDE) - .rounding(Rounder.fixedFraction(2)), - ULocale.ENGLISH, - 1, - "1.00 US dollars"); - } - - private static void assertFormatDescending( - String message, - String skeleton, - UnlocalizedNumberFormatter f, - ULocale locale, - String... expected) { - assert expected.length == 9; - assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton()); - final double[] inputs = - new double[] {87650, 8765, 876.5, 87.65, 8.765, 0.8765, 0.08765, 0.008765, 0}; - LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation - LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation - for (int i = 0; i < 9; i++) { - double d = inputs[i]; - String actual1 = l1.format(d).toString(); - assertEquals(message + ": L1: " + d, expected[i], actual1); - String actual2 = l2.format(d).toString(); - assertEquals(message + ": L2: " + d, expected[i], actual2); + + @Test + public void roundingFraction() { + assertFormatDescending( + "Integer", + "F0", + NumberFormatter.with().rounding(Rounder.integer()), + ULocale.ENGLISH, + "87,650", + "8,765", + "876", + "88", + "9", + "1", + "0", + "0", + "0"); + + assertFormatDescending( + "Fixed Fraction", + "F3", + NumberFormatter.with().rounding(Rounder.fixedFraction(3)), + ULocale.ENGLISH, + "87,650.000", + "8,765.000", + "876.500", + "87.650", + "8.765", + "0.876", + "0.088", + "0.009", + "0.000"); + + assertFormatDescending( + "Min Fraction", + "F1-", + NumberFormatter.with().rounding(Rounder.minFraction(1)), + ULocale.ENGLISH, + "87,650.0", + "8,765.0", + "876.5", + "87.65", + "8.765", + "0.8765", + "0.08765", + "0.008765", + "0.0"); + + assertFormatDescending( + "Max Fraction", + "F-1", + NumberFormatter.with().rounding(Rounder.maxFraction(1)), + ULocale.ENGLISH, + "87,650", + "8,765", + "876.5", + "87.6", + "8.8", + "0.9", + "0.1", + "0", + "0"); + + assertFormatDescending( + "Min/Max Fraction", + "F1-3", + NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3)), + ULocale.ENGLISH, + "87,650.0", + "8,765.0", + "876.5", + "87.65", + "8.765", + "0.876", + "0.088", + "0.009", + "0.0"); + } + + @Test + public void roundingFigures() { + assertFormatSingle( + "Fixed Significant", + "S3", + NumberFormatter.with().rounding(Rounder.fixedDigits(3)), + ULocale.ENGLISH, + -98, + "-98.0"); + + assertFormatSingle( + "Fixed Significant Rounding", + "S3", + NumberFormatter.with().rounding(Rounder.fixedDigits(3)), + ULocale.ENGLISH, + -98.7654321, + "-98.8"); + + assertFormatSingle( + "Fixed Significant Zero", + "S3", + NumberFormatter.with().rounding(Rounder.fixedDigits(3)), + ULocale.ENGLISH, + 0, + "0.00"); + + assertFormatSingle( + "Min Significant", + "S2-", + NumberFormatter.with().rounding(Rounder.minDigits(2)), + ULocale.ENGLISH, + -9, + "-9.0"); + + assertFormatSingle( + "Max Significant", + "S-4", + NumberFormatter.with().rounding(Rounder.maxDigits(4)), + ULocale.ENGLISH, + 98.7654321, + "98.77"); + + assertFormatSingle( + "Min/Max Significant", + "S3-4", + NumberFormatter.with().rounding(Rounder.minMaxDigits(3, 4)), + ULocale.ENGLISH, + 9.99999, + "10.0"); + } + + @Test + public void roundingFractionFigures() { + assertFormatDescending( + "Basic Significant", // for comparison + "S-2", + NumberFormatter.with().rounding(Rounder.maxDigits(2)), + ULocale.ENGLISH, + "88,000", + "8,800", + "880", + "88", + "8.8", + "0.88", + "0.088", + "0.0088", + "0"); + + assertFormatDescending( + "FracSig minMaxFrac minSig", + "F1-2>3", + NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 2).withMinDigits(3)), + ULocale.ENGLISH, + "87,650.0", + "8,765.0", + "876.5", + "87.65", + "8.76", + "0.876", // minSig beats maxFrac + "0.0876", // minSig beats maxFrac + "0.00876", // minSig beats maxFrac + "0.0"); + + assertFormatDescending( + "FracSig minMaxFrac maxSig A", + "F1-3<2", + NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3).withMaxDigits(2)), + ULocale.ENGLISH, + "88,000.0", // maxSig beats maxFrac + "8,800.0", // maxSig beats maxFrac + "880.0", // maxSig beats maxFrac + "88.0", // maxSig beats maxFrac + "8.8", // maxSig beats maxFrac + "0.88", // maxSig beats maxFrac + "0.088", + "0.009", + "0.0"); + + assertFormatDescending( + "FracSig minMaxFrac maxSig B", + "F2<2", + NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMaxDigits(2)), + ULocale.ENGLISH, + "88,000.00", // maxSig beats maxFrac + "8,800.00", // maxSig beats maxFrac + "880.00", // maxSig beats maxFrac + "88.00", // maxSig beats maxFrac + "8.80", // maxSig beats maxFrac + "0.88", + "0.09", + "0.01", + "0.00"); + } + + @Test + public void roundingOther() { + assertFormatDescending( + "Rounding None", + "Y", + NumberFormatter.with().rounding(Rounder.none()), + ULocale.ENGLISH, + "87,650", + "8,765", + "876.5", + "87.65", + "8.765", + "0.8765", + "0.08765", + "0.008765", + "0"); + + assertFormatDescending( + "Increment", + "M0.5", + NumberFormatter.with().rounding(Rounder.increment(BigDecimal.valueOf(0.5))), + ULocale.ENGLISH, + "87,650.0", + "8,765.0", + "876.5", + "87.5", + "9.0", + "1.0", + "0.0", + "0.0", + "0.0"); + + assertFormatDescending( + "Currency Standard", + "$CZK GSTANDARD", + NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.STANDARD)).unit(CZK), + ULocale.ENGLISH, + "CZK 87,650.00", + "CZK 8,765.00", + "CZK 876.50", + "CZK 87.65", + "CZK 8.76", + "CZK 0.88", + "CZK 0.09", + "CZK 0.01", + "CZK 0.00"); + + assertFormatDescending( + "Currency Cash", + "$CZK GCASH", + NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH)).unit(CZK), + ULocale.ENGLISH, + "CZK 87,650", + "CZK 8,765", + "CZK 876", + "CZK 88", + "CZK 9", + "CZK 1", + "CZK 0", + "CZK 0", + "CZK 0"); + + assertFormatDescending( + "Currency not in top-level fluent chain", + "F0", + NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH).withCurrency(CZK)), + ULocale.ENGLISH, + "87,650", + "8,765", + "876", + "88", + "9", + "1", + "0", + "0", + "0"); + } + + @Test + public void grouping() { + // NoUnit.PERMILLE multiplies all the number by 10^3 (good for testing grouping). + // Note that en-US is already performed in the unitPercent() function. + assertFormatDescending( + "Indic Grouping", + "%% grouping=defaults", + NumberFormatter.with().unit(NoUnit.PERMILLE).grouping(Grouper.defaults()), + new ULocale("en-IN"), + "8,76,50,000‰", + "87,65,000‰", + "8,76,500‰", + "87,650‰", + "8,765‰", + "876.5‰", + "87.65‰", + "8.765‰", + "0‰"); + + assertFormatDescending( + "Western Grouping, Min 2", + "%% grouping=min2", + NumberFormatter.with().unit(NoUnit.PERMILLE).grouping(Grouper.min2()), + ULocale.ENGLISH, + "87,650,000‰", + "8,765,000‰", + "876,500‰", + "87,650‰", + "8765‰", + "876.5‰", + "87.65‰", + "8.765‰", + "0‰"); + + assertFormatDescending( + "Indic Grouping, Min 2", + "%% grouping=min2", + NumberFormatter.with().unit(NoUnit.PERMILLE).grouping(Grouper.min2()), + new ULocale("en-IN"), + "8,76,50,000‰", + "87,65,000‰", + "8,76,500‰", + "87,650‰", + "8765‰", + "876.5‰", + "87.65‰", + "8.765‰", + "0‰"); + + assertFormatDescending( + "No Grouping", + "%% grouping=none", + NumberFormatter.with().unit(NoUnit.PERMILLE).grouping(Grouper.none()), + new ULocale("en-IN"), + "87650000‰", + "8765000‰", + "876500‰", + "87650‰", + "8765‰", + "876.5‰", + "87.65‰", + "8.765‰", + "0‰"); + } + + @Test + public void padding() { + assertFormatDescending( + "Padding", + "", + NumberFormatter.with().padding(Padder.none()), + ULocale.ENGLISH, + "87,650", + "8,765", + "876.5", + "87.65", + "8.765", + "0.8765", + "0.08765", + "0.008765", + "0"); + + assertFormatDescending( + "Padding", + "", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)), + ULocale.ENGLISH, + "**87,650", + "***8,765", + "***876.5", + "***87.65", + "***8.765", + "**0.8765", + "*0.08765", + "0.008765", + "*******0"); + + assertFormatDescending( + "Padding with code points", + "", + NumberFormatter.with().padding(Padder.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)), + ULocale.ENGLISH, + "𐇤𐇤87,650", + "𐇤𐇤𐇤8,765", + "𐇤𐇤𐇤876.5", + "𐇤𐇤𐇤87.65", + "𐇤𐇤𐇤8.765", + "𐇤𐇤0.8765", + "𐇤0.08765", + "0.008765", + "𐇤𐇤𐇤𐇤𐇤𐇤𐇤0"); + + assertFormatDescending( + "Padding with wide digits", + "symbols=ns:mathsanb", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)) + .symbols(NumberingSystem.getInstanceByName("mathsanb")), + ULocale.ENGLISH, + "**𝟴𝟳,𝟲𝟱𝟬", + "***𝟴,𝟳𝟲𝟱", + "***𝟴𝟳𝟲.𝟱", + "***𝟴𝟳.𝟲𝟱", + "***𝟴.𝟳𝟲𝟱", + "**𝟬.𝟴𝟳𝟲𝟱", + "*𝟬.𝟬𝟴𝟳𝟲𝟱", + "𝟬.𝟬𝟬𝟴𝟳𝟲𝟱", + "*******𝟬"); + + assertFormatDescending( + "Padding with currency spacing", + "$GBP unit-width=ISO_CODE", + NumberFormatter.with().padding(Padder.codePoints('*', 10, PadPosition.AFTER_PREFIX)).unit(GBP) + .unitWidth(UnitWidth.ISO_CODE), + ULocale.ENGLISH, + "GBP 87,650.00", + "GBP 8,765.00", + "GBP 876.50", + "GBP**87.65", + "GBP***8.76", + "GBP***0.88", + "GBP***0.09", + "GBP***0.01", + "GBP***0.00"); + + assertFormatSingle( + "Pad Before Prefix", + "", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_PREFIX)), + ULocale.ENGLISH, + -88.88, + "**-88.88"); + + assertFormatSingle( + "Pad After Prefix", + "", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)), + ULocale.ENGLISH, + -88.88, + "-**88.88"); + + assertFormatSingle( + "Pad Before Suffix", + "%", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_SUFFIX)) + .unit(NoUnit.PERCENT), + ULocale.ENGLISH, + 0.8888, + "88.88**%"); + + assertFormatSingle( + "Pad After Suffix", + "%", + NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_SUFFIX)) + .unit(NoUnit.PERCENT), + ULocale.ENGLISH, + 0.8888, + "88.88%**"); + } + + @Test + public void integerWidth() { + assertFormatDescending( + "Integer Width Default", + "integer-width=1-", + NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1)), + ULocale.ENGLISH, + "87,650", + "8,765", + "876.5", + "87.65", + "8.765", + "0.8765", + "0.08765", + "0.008765", + "0"); + + assertFormatDescending( + "Integer Width Zero Fill 0", + "integer-width=0-", + NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)), + ULocale.ENGLISH, + "87,650", + "8,765", + "876.5", + "87.65", + "8.765", + ".8765", + ".08765", + ".008765", + ""); // TODO: Avoid the empty string here? + + assertFormatDescending( + "Integer Width Zero Fill 3", + "integer-width=3-", + NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(3)), + ULocale.ENGLISH, + "87,650", + "8,765", + "876.5", + "087.65", + "008.765", + "000.8765", + "000.08765", + "000.008765", + "000"); + + assertFormatDescending( + "Integer Width Max 3", + "integer-width=1-3", + NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1).truncateAt(3)), + ULocale.ENGLISH, + "650", + "765", + "876.5", + "87.65", + "8.765", + "0.8765", + "0.08765", + "0.008765", + "0"); + + assertFormatDescending( + "Integer Width Fixed 2", + "integer-width=2", + NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)), + ULocale.ENGLISH, + "50", + "65", + "76.5", + "87.65", + "08.765", + "00.8765", + "00.08765", + "00.008765", + "00"); + } + + @Test + public void symbols() { + assertFormatDescending( + "French Symbols with Japanese Data 1", + "symbols=loc:fr", + NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)), + ULocale.JAPAN, + "87 650", + "8 765", + "876,5", + "87,65", + "8,765", + "0,8765", + "0,08765", + "0,008765", + "0"); + + assertFormatSingle( + "French Symbols with Japanese Data 2", + "C symbols=loc:fr", + NumberFormatter.with().notation(Notation.compactShort()) + .symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)), + ULocale.JAPAN, + 12345, + "1,2\u4E07"); + + assertFormatDescending( + "Latin Numbering System with Arabic Data", + "$USD symbols=ns:latn", + NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD), + new ULocale("ar"), + "87,650.00 US$", + "8,765.00 US$", + "876.50 US$", + "87.65 US$", + "8.76 US$", + "0.88 US$", + "0.09 US$", + "0.01 US$", + "0.00 US$"); + + assertFormatDescending( + "Math Numbering System with French Data", + "symbols=ns:mathsanb", + NumberFormatter.with().symbols(NumberingSystem.getInstanceByName("mathsanb")), + ULocale.FRENCH, + "𝟴𝟳 𝟲𝟱𝟬", + "𝟴 𝟳𝟲𝟱", + "𝟴𝟳𝟲,𝟱", + "𝟴𝟳,𝟲𝟱", + "𝟴,𝟳𝟲𝟱", + "𝟬,𝟴𝟳𝟲𝟱", + "𝟬,𝟬𝟴𝟳𝟲𝟱", + "𝟬,𝟬𝟬𝟴𝟳𝟲𝟱", + "𝟬"); + + assertFormatSingle( + "Swiss Symbols (used in documentation)", + "symbols=loc:de_CH", + NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("de-CH"))), + ULocale.ENGLISH, + 12345.67, + "12’345.67"); + + assertFormatSingle( + "Myanmar Symbols (used in documentation)", + "symbols=loc:my_MY", + NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("my_MY"))), + ULocale.ENGLISH, + 12345.67, + "\u1041\u1042,\u1043\u1044\u1045.\u1046\u1047"); + + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("de-CH")); + UnlocalizedNumberFormatter f = NumberFormatter.with().symbols(symbols); + symbols.setGroupingSeparatorString("!"); + assertFormatSingle( + "Symbols object should be copied", + "symbols=loc:de_CH", + f, + ULocale.ENGLISH, + 12345.67, + "12’345.67"); + + assertFormatSingle( + "The last symbols setter wins", + "symbols=ns:latn", + NumberFormatter.with().symbols(symbols).symbols(NumberingSystem.LATIN), + ULocale.ENGLISH, + 12345.67, + "12,345.67"); + + assertFormatSingle( + "The last symbols setter wins", + "symbols=loc:de_CH", + NumberFormatter.with().symbols(NumberingSystem.LATIN).symbols(symbols), + ULocale.ENGLISH, + 12345.67, + "12!345.67"); + } + + @Test + @Ignore("This feature is not currently available.") + public void symbolsOverride() { + DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(ULocale.ENGLISH); + dfs.setCurrencySymbol("@"); + dfs.setInternationalCurrencySymbol("foo"); + assertFormatSingle( + "Custom Short Currency Symbol", + "$XXX", + NumberFormatter.with().unit(Currency.getInstance("XXX")).symbols(dfs), + ULocale.ENGLISH, + 12.3, + "@ 12.30"); + } + + @Test + public void sign() { + assertFormatSingle( + "Sign Auto Positive", + "sign=AUTO", + NumberFormatter.with().sign(SignDisplay.AUTO), + ULocale.ENGLISH, + 444444, + "444,444"); + + assertFormatSingle( + "Sign Auto Negative", + "sign=AUTO", + NumberFormatter.with().sign(SignDisplay.AUTO), + ULocale.ENGLISH, + -444444, + "-444,444"); + + assertFormatSingle( + "Sign Always Positive", + "sign=ALWAYS", + NumberFormatter.with().sign(SignDisplay.ALWAYS), + ULocale.ENGLISH, + 444444, + "+444,444"); + + assertFormatSingle( + "Sign Always Negative", + "sign=ALWAYS", + NumberFormatter.with().sign(SignDisplay.ALWAYS), + ULocale.ENGLISH, + -444444, + "-444,444"); + + assertFormatSingle( + "Sign Never Positive", + "sign=NEVER", + NumberFormatter.with().sign(SignDisplay.NEVER), + ULocale.ENGLISH, + 444444, + "444,444"); + + assertFormatSingle( + "Sign Never Negative", + "sign=NEVER", + NumberFormatter.with().sign(SignDisplay.NEVER), + ULocale.ENGLISH, + -444444, + "444,444"); + + assertFormatSingle( + "Sign Accounting Positive", + "$USD sign=ACCOUNTING", + NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD), + ULocale.ENGLISH, + 444444, + "$444,444.00"); + + assertFormatSingle( + "Sign Accounting Negative", + "$USD sign=ACCOUNTING", + NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD), + ULocale.ENGLISH, + -444444, + "($444,444.00)"); + + assertFormatSingle( + "Sign Accounting-Always Positive", + "$USD sign=ACCOUNTING_ALWAYS", + NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD), + ULocale.ENGLISH, + 444444, + "+$444,444.00"); + + assertFormatSingle( + "Sign Accounting-Always Negative", + "$USD sign=ACCOUNTING_ALWAYS", + NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD), + ULocale.ENGLISH, + -444444, + "($444,444.00)"); + } + + @Test + public void decimal() { + assertFormatDescending( + "Decimal Default", + "decimal=AUTO", + NumberFormatter.with().decimal(DecimalMarkDisplay.AUTO), + ULocale.ENGLISH, + "87,650", + "8,765", + "876.5", + "87.65", + "8.765", + "0.8765", + "0.08765", + "0.008765", + "0"); + + assertFormatDescending( + "Decimal Always Shown", + "decimal=ALWAYS", + NumberFormatter.with().decimal(DecimalMarkDisplay.ALWAYS), + ULocale.ENGLISH, + "87,650.", + "8,765.", + "876.5", + "87.65", + "8.765", + "0.8765", + "0.08765", + "0.008765", + "0."); + } + + @Test + public void locale() { + // Coverage for the locale setters. + assertEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.with().locale(Locale.ENGLISH)); + assertNotEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.with().locale(Locale.FRENCH)); + } + + @Test + public void getPrefixSuffix() { + Object[][] cases = { + { NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(UnitWidth.ISO_CODE), "GBP", "", "-GBP", + "" }, + { NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(UnitWidth.FULL_NAME), "", + " British pounds", "-", " British pounds" } }; + + for (Object[] cas : cases) { + LocalizedNumberFormatter f = (LocalizedNumberFormatter) cas[0]; + String posPrefix = (String) cas[1]; + String posSuffix = (String) cas[2]; + String negPrefix = (String) cas[3]; + String negSuffix = (String) cas[4]; + FormattedNumber positive = f.format(1); + FormattedNumber negative = f.format(-1); + assertEquals(posPrefix, positive.getPrefix()); + assertEquals(posSuffix, positive.getSuffix()); + assertEquals(negPrefix, negative.getPrefix()); + assertEquals(negSuffix, negative.getSuffix()); + } + } + + @Test + public void plurals() { + // TODO: Expand this test. + + assertFormatSingle( + "Plural 1", + "$USD F0 unit-width=FULL_NAME", + NumberFormatter.with().unit(USD).unitWidth(UnitWidth.FULL_NAME).rounding(Rounder.fixedFraction(0)), + ULocale.ENGLISH, + 1, + "1 US dollar"); + + assertFormatSingle( + "Plural 1.00", + "$USD F2 unit-width=FULL_NAME", + NumberFormatter.with().unit(USD).unitWidth(UnitWidth.FULL_NAME).rounding(Rounder.fixedFraction(2)), + ULocale.ENGLISH, + 1, + "1.00 US dollars"); + } + + private static void assertFormatDescending( + String message, + String skeleton, + UnlocalizedNumberFormatter f, + ULocale locale, + String... expected) { + assert expected.length == 9; + assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton()); + final double[] inputs = new double[] { 87650, 8765, 876.5, 87.65, 8.765, 0.8765, 0.08765, 0.008765, 0 }; + LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation + LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation + for (int i = 0; i < 9; i++) { + double d = inputs[i]; + String actual1 = l1.format(d).toString(); + assertEquals(message + ": L1: " + d, expected[i], actual1); + String actual2 = l2.format(d).toString(); + assertEquals(message + ": L2: " + d, expected[i], actual2); + } + } + + private static void assertFormatSingle( + String message, + String skeleton, + UnlocalizedNumberFormatter f, + ULocale locale, + Number input, + String expected) { + assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton()); + LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation + LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation + String actual1 = l1.format(input).toString(); + assertEquals(message + ": Unsafe Path: " + input, expected, actual1); + String actual2 = l2.format(input).toString(); + assertEquals(message + ": Safe Path: " + input, expected, actual2); + } + + private static void assertFormatSingleMeasure( + String message, + String skeleton, + UnlocalizedNumberFormatter f, + ULocale locale, + Measure input, + String expected) { + assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton()); + LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation + LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation + String actual1 = l1.format(input).toString(); + assertEquals(message + ": Unsafe Path: " + input, expected, actual1); + String actual2 = l2.format(input).toString(); + assertEquals(message + ": Safe Path: " + input, expected, actual2); } - } - - private static void assertFormatSingle( - String message, - String skeleton, - UnlocalizedNumberFormatter f, - ULocale locale, - Number input, - String expected) { - assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton()); - LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation - LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation - String actual1 = l1.format(input).toString(); - assertEquals(message + ": L1: " + input, expected, actual1); - String actual2 = l2.format(input).toString(); - assertEquals(message + ": L2: " + input, expected, actual2); - } - - private static void assertFormatSingleMeasure( - String message, - String skeleton, - UnlocalizedNumberFormatter f, - ULocale locale, - Measure input, - String expected) { - assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton()); - LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation - LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation - String actual1 = l1.format(input).toString(); - assertEquals(message + ": L1: " + input, expected, actual1); - String actual2 = l2.format(input).toString(); - assertEquals(message + ": L2: " + input, expected, actual2); - } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java index 009b8b2383d..8d9814cd697 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java @@ -183,6 +183,30 @@ public class NumberStringBuilderTest { } } + @Test + public void testCodePoints() { + NumberStringBuilder nsb = new NumberStringBuilder(); + assertEquals("First is -1 on empty string", -1, nsb.getFirstCodePoint()); + assertEquals("Last is -1 on empty string", -1, nsb.getLastCodePoint()); + assertEquals("Length is 0 on empty string", 0, nsb.codePointCount()); + + nsb.append("q", null); + assertEquals("First is q", 'q', nsb.getFirstCodePoint()); + assertEquals("Last is q", 'q', nsb.getLastCodePoint()); + assertEquals("0th is q", 'q', nsb.codePointAt(0)); + assertEquals("Before 1st is q", 'q', nsb.codePointBefore(1)); + assertEquals("Code point count is 1", 1, nsb.codePointCount()); + + // 🚀 is two char16s + nsb.append("🚀", null); + assertEquals("First is still q", 'q', nsb.getFirstCodePoint()); + assertEquals("Last is space ship", 128640, nsb.getLastCodePoint()); + assertEquals("1st is space ship", 128640, nsb.codePointAt(1)); + assertEquals("Before 1st is q", 'q', nsb.codePointBefore(1)); + assertEquals("Before 3rd is space ship", 128640, nsb.codePointBefore(3)); + assertEquals("Code point count is 2", 2, nsb.codePointCount()); + } + private static void assertCharSequenceEquals(CharSequence a, CharSequence b) { assertEquals(a.toString(), b.toString()); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java index b9bf2efe97f..39b44f528a0 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java @@ -7,7 +7,7 @@ import static org.junit.Assert.fail; import org.junit.Test; -import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.PatternAndPropertyUtils; import com.ibm.icu.impl.number.Properties; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.util.ULocale; @@ -27,8 +27,8 @@ public class PatternStringTest { 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)); + assertEquals(localized, PatternAndPropertyUtils.convertLocalized(standard, symbols, true)); + assertEquals(toStandard, PatternAndPropertyUtils.convertLocalized(localized, symbols, false)); } @Test @@ -60,8 +60,8 @@ public class PatternStringTest { String input = cas[0]; String output = cas[1]; - Properties properties = PatternString.parseToProperties(input); - String actual = PatternString.propertiesToString(properties); + Properties properties = PatternAndPropertyUtils.parseToProperties(input); + String actual = PatternAndPropertyUtils.propertiesToString(properties); assertEquals( "Failed on input pattern '" + input + "', properties " + properties, output, actual); } @@ -90,7 +90,7 @@ public class PatternStringTest { Properties input = (Properties) cas[0]; String output = (String) cas[1]; - String actual = PatternString.propertiesToString(input); + String actual = PatternAndPropertyUtils.propertiesToString(input); assertEquals("Failed on input properties " + input, output, actual); } } @@ -103,7 +103,7 @@ public class PatternStringTest { for (String pattern : invalidPatterns) { try { - PatternString.parseToProperties(pattern); + PatternAndPropertyUtils.parseToProperties(pattern); fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern); } catch (IllegalArgumentException e) { } @@ -112,8 +112,8 @@ public class PatternStringTest { @Test public void testBug13117() { - Properties expected = PatternString.parseToProperties("0"); - Properties actual = PatternString.parseToProperties("0;"); + Properties expected = PatternAndPropertyUtils.parseToProperties("0"); + Properties actual = PatternAndPropertyUtils.parseToProperties("0;"); assertEquals("Should not consume negative subpattern", expected, actual); } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PropertiesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PropertiesTest.java index d8e786a9a94..25546fe3389 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PropertiesTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PropertiesTest.java @@ -31,7 +31,7 @@ import org.junit.Test; import com.ibm.icu.dev.test.serializable.SerializableTestUtility; import com.ibm.icu.impl.number.Parse.GroupingMode; import com.ibm.icu.impl.number.Parse.ParseMode; -import com.ibm.icu.impl.number.PatternString; +import com.ibm.icu.impl.number.PatternAndPropertyUtils; import com.ibm.icu.impl.number.Properties; import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition; import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; @@ -310,7 +310,7 @@ public class PropertiesTest { Properties props0 = new Properties(); // Write values to some of the fields - PatternString.parseToExistingProperties("A-**####,#00.00#b¤", props0); + PatternAndPropertyUtils.parseToExistingProperties("A-**####,#00.00#b¤", props0); // Write to byte stream ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -337,7 +337,7 @@ public class PropertiesTest { public Object[] getTestObjects() { return new Object[] { new Properties(), - PatternString.parseToProperties("x#,##0.00%"), + PatternAndPropertyUtils.parseToProperties("x#,##0.00%"), new Properties().setCompactStyle(CompactStyle.LONG).setMinimumExponentDigits(2) }; } 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 82a5e8e3411..fb1cd7b7641 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 @@ -827,7 +827,7 @@ public class SerializableTestUtility { map.put("com.ibm.icu.text.PluralRules$FixedDecimal", new PluralRulesTest.FixedDecimalHandler()); 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.util.Dimensionless", new MeasureUnitTest.MeasureUnitHandler()); + map.put("com.ibm.icu.util.NoUnit", new MeasureUnitTest.MeasureUnitHandler()); map.put("com.ibm.icu.text.MeasureFormat", new MeasureUnitTest.MeasureFormatHandler()); map.put("com.ibm.icu.impl.number.Properties", new PropertiesTest.PropertiesHandler());