/** 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
}
}
- 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
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;
}
/**
* @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);
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;
}
/**
+++ /dev/null
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package com.ibm.icu.impl.number;
-
-import com.ibm.icu.impl.number.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++;
- }
- }
-}
// 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 {
* @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);
* @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.
- *
- * <p>
- * Implements {@link PositiveNegativeModifier} only so that instances of this class can be used when a
- * {@link PositiveNegativeModifier} is required.
- */
- public abstract static class BaseModifier implements Modifier, PositiveNegativeModifier {
-
- @Override
- public Modifier getModifier(boolean isNegative) {
- return this;
- }
- }
}
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:
*
* <ol>
- * <li>Efficient prepend as well as append.
- * <li>Keeps tracks of Fields in an efficient manner.
- * <li>String operations are fast-pathed to code point operations when possible.
+ * <li>Efficient prepend as well as append.
+ * <li>Keeps tracks of Fields in an efficient manner.
+ * <li>String operations are fast-pathed to code point operations when possible.
* </ol>
*/
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.
- *
- * <p>For a string intended be used for debugging, use {@link #toDebugString}.
- */
- @Override
- public String toString() {
- return new String(chars, zero, length);
- }
-
- private static final Map<Field, Character> fieldToDebugChar = new HashMap<Field, Character>();
-
- static {
- fieldToDebugChar.put(NumberFormat.Field.SIGN, '-');
- fieldToDebugChar.put(NumberFormat.Field.INTEGER, 'i');
- fieldToDebugChar.put(NumberFormat.Field.FRACTION, 'f');
- fieldToDebugChar.put(NumberFormat.Field.EXPONENT, 'e');
- fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SIGN, '+');
- fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SYMBOL, 'E');
- fieldToDebugChar.put(NumberFormat.Field.DECIMAL_SEPARATOR, '.');
- fieldToDebugChar.put(NumberFormat.Field.GROUPING_SEPARATOR, ',');
- fieldToDebugChar.put(NumberFormat.Field.PERCENT, '%');
- fieldToDebugChar.put(NumberFormat.Field.PERMILLE, '‰');
- fieldToDebugChar.put(NumberFormat.Field.CURRENCY, '$');
- }
-
- /**
- * Returns a string that includes field information, for debugging purposes.
- *
- * <p>For example, if the string is "-12.345", the debug string will be something like
- * "<NumberStringBuilder [-123.45] [-iii.ff]>"
- *
- * @return A string for debugging purposes.
- */
- public String toDebugString() {
- StringBuilder sb = new StringBuilder();
- sb.append("<NumberStringBuilder [");
- sb.append(this.toString());
- sb.append("] [");
- for (int i = zero; i < zero + length; i++) {
- if (fields[i] == null) {
- sb.append('n');
- } else {
- sb.append(fieldToDebugChar.get(fields[i]));
- }
- }
- sb.append("]>");
- return sb.toString();
- }
-
- /** @return A new array containing the contents of this string builder. */
- public char[] toCharArray() {
- return Arrays.copyOfRange(chars, zero, zero + length);
- }
-
- /** @return A new array containing the field values of this string builder. */
- public Field[] toFieldArray() {
- return Arrays.copyOfRange(fields, zero, zero + length);
- }
-
- /**
- * @return Whether the contents and field values of this string builder are equal to the given
- * chars and fields.
- * @see #toCharArray
- * @see #toFieldArray
- */
- public boolean contentEquals(char[] chars, Field[] fields) {
- if (chars.length != length) return false;
- if (fields.length != length) return false;
- for (int i = 0; i < length; i++) {
- if (this.chars[zero + i] != chars[i]) return false;
- if (this.fields[zero + i] != fields[i]) return false;
- }
- return true;
- }
-
- /**
- * @param other The instance to compare.
- * @return Whether the contents of this instance is currently equal to the given instance.
- */
- public boolean contentEquals(NumberStringBuilder other) {
- if (length != other.length) return false;
- for (int i = 0; i < length; i++) {
- if (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.
+ *
+ * <p>
+ * For a string intended be used for debugging, use {@link #toDebugString}.
+ */
+ @Override
+ public String toString() {
+ return new String(chars, zero, length);
+ }
+
+ private static final Map<Field, Character> fieldToDebugChar = new HashMap<Field, Character>();
+
+ static {
+ fieldToDebugChar.put(NumberFormat.Field.SIGN, '-');
+ fieldToDebugChar.put(NumberFormat.Field.INTEGER, 'i');
+ fieldToDebugChar.put(NumberFormat.Field.FRACTION, 'f');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT, 'e');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SIGN, '+');
+ fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SYMBOL, 'E');
+ fieldToDebugChar.put(NumberFormat.Field.DECIMAL_SEPARATOR, '.');
+ fieldToDebugChar.put(NumberFormat.Field.GROUPING_SEPARATOR, ',');
+ fieldToDebugChar.put(NumberFormat.Field.PERCENT, '%');
+ fieldToDebugChar.put(NumberFormat.Field.PERMILLE, '‰');
+ fieldToDebugChar.put(NumberFormat.Field.CURRENCY, '$');
+ }
+
+ /**
+ * Returns a string that includes field information, for debugging purposes.
+ *
+ * <p>
+ * For example, if the string is "-12.345", the debug string will be something like "<NumberStringBuilder
+ * [-123.45] [-iii.ff]>"
+ *
+ * @return A string for debugging purposes.
+ */
+ public String toDebugString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<NumberStringBuilder [");
+ sb.append(this.toString());
+ sb.append("] [");
+ for (int i = zero; i < zero + length; i++) {
+ if (fields[i] == null) {
+ sb.append('n');
+ } else {
+ sb.append(fieldToDebugChar.get(fields[i]));
+ }
+ }
+ sb.append("]>");
+ return sb.toString();
+ }
+
+ /** @return A new array containing the contents of this string builder. */
+ public char[] toCharArray() {
+ return Arrays.copyOfRange(chars, zero, zero + length);
+ }
+
+ /** @return A new array containing the field values of this string builder. */
+ public Field[] toFieldArray() {
+ return Arrays.copyOfRange(fields, zero, zero + length);
+ }
+
+ /**
+ * @return Whether the contents and field values of this string builder are equal to the given chars and fields.
+ * @see #toCharArray
+ * @see #toFieldArray
+ */
+ public boolean contentEquals(char[] chars, Field[] fields) {
+ if (chars.length != length)
+ return false;
+ if (fields.length != length)
+ return false;
+ for (int i = 0; i < length; i++) {
+ if (this.chars[zero + i] != chars[i])
+ return false;
+ if (this.fields[zero + i] != fields[i])
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param other
+ * The instance to compare.
+ * @return Whether the contents of this instance is currently equal to the given instance.
+ */
+ public boolean contentEquals(NumberStringBuilder other) {
+ if (length != other.length)
+ return false;
+ for (int i = 0; i < length; i++) {
+ if (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();
+ }
}
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.
}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.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.
+ *
+ * <p>
+ * Since pattern strings support only a subset of the functionality available in a property bag, a new property bag
+ * created from the string returned by this function may not be the same as the original property bag.
+ *
+ * @param properties
+ * The property bag to serialize.
+ * @return A pattern string approximately serializing the property bag.
+ */
+ public static String propertiesToString(Properties properties) {
+ StringBuilder sb = new StringBuilder();
+
+ // Convenience references
+ // The Math.min() calls prevent DoS
+ int dosMax = 100;
+ int groupingSize = Math.min(properties.getSecondaryGroupingSize(), dosMax);
+ int firstGroupingSize = Math.min(properties.getGroupingSize(), dosMax);
+ int paddingWidth = Math.min(properties.getFormatWidth(), dosMax);
+ PadPosition paddingLocation = properties.getPadPosition();
+ String paddingString = properties.getPadString();
+ int minInt = Math.max(Math.min(properties.getMinimumIntegerDigits(), dosMax), 0);
+ int maxInt = Math.min(properties.getMaximumIntegerDigits(), dosMax);
+ int minFrac = Math.max(Math.min(properties.getMinimumFractionDigits(), dosMax), 0);
+ int maxFrac = Math.min(properties.getMaximumFractionDigits(), dosMax);
+ int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax);
+ int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax);
+ boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+ int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax);
+ boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown();
+ String pp = properties.getPositivePrefix();
+ String ppp = properties.getPositivePrefixPattern();
+ String ps = properties.getPositiveSuffix();
+ String psp = properties.getPositiveSuffixPattern();
+ String np = properties.getNegativePrefix();
+ String npp = properties.getNegativePrefixPattern();
+ String ns = properties.getNegativeSuffix();
+ String nsp = properties.getNegativeSuffixPattern();
+
+ // Prefixes
+ if (ppp != null)
+ sb.append(ppp);
+ AffixPatternUtils.escape(pp, sb);
+ int afterPrefixPos = sb.length();
+
+ // Figure out the grouping sizes.
+ int grouping1, grouping2, grouping;
+ if (groupingSize != Math.min(dosMax, -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 <em>fr-FR</em>, the period in the pattern "0.000" means "decimal" in standard notation (as it
+ * does in every other locale), but it means "grouping" in localized notation.
+ *
+ * <p>
+ * 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.
+ *
+ * <p>
+ * 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);
+ }
+ }
+}
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import 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.
+ *
+ * <p>
+ * 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++;
+ }
+ }
+}
+++ /dev/null
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package com.ibm.icu.impl.number;
-
-import java.math.BigDecimal;
-
-import com.ibm.icu.impl.number.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.
- *
- * <p>Since pattern strings support only a subset of the functionality available in a property
- * bag, a new property bag created from the string returned by this function may not be the same
- * as the original property bag.
- *
- * @param properties The property bag to serialize.
- * @return A pattern string approximately serializing the property bag.
- */
- public static String propertiesToString(Properties properties) {
- StringBuilder sb = new StringBuilder();
-
- // Convenience references
- // The Math.min() calls prevent DoS
- int dosMax = 100;
- int groupingSize = Math.min(properties.getSecondaryGroupingSize(), dosMax);
- int firstGroupingSize = Math.min(properties.getGroupingSize(), dosMax);
- int paddingWidth = Math.min(properties.getFormatWidth(), dosMax);
- PadPosition paddingLocation = properties.getPadPosition();
- String paddingString = properties.getPadString();
- int minInt = Math.max(Math.min(properties.getMinimumIntegerDigits(), dosMax), 0);
- int maxInt = Math.min(properties.getMaximumIntegerDigits(), dosMax);
- int minFrac = Math.max(Math.min(properties.getMinimumFractionDigits(), dosMax), 0);
- int maxFrac = Math.min(properties.getMaximumFractionDigits(), dosMax);
- int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax);
- int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax);
- boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
- int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax);
- boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown();
- String pp = properties.getPositivePrefix();
- String ppp = properties.getPositivePrefixPattern();
- String ps = properties.getPositiveSuffix();
- String psp = properties.getPositiveSuffixPattern();
- String np = properties.getNegativePrefix();
- String npp = properties.getNegativePrefixPattern();
- String ns = properties.getNegativeSuffix();
- String nsp = properties.getNegativeSuffixPattern();
-
- // Prefixes
- if (ppp != null) sb.append(ppp);
- AffixPatternUtils.escape(pp, sb);
- int afterPrefixPos = sb.length();
-
- // Figure out the grouping sizes.
- int grouping1, grouping2, grouping;
- if (groupingSize != Math.min(dosMax, -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 <em>fr-FR</em>, the period in the
- * pattern "0.000" means "decimal" in standard notation (as it does in every other locale), but it
- * means "grouping" in localized notation.
- *
- * <p>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.
- *
- * <p>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);
- }
- }
-}
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();
+++ /dev/null
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package com.ibm.icu.impl.number;
-
-/** @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()));
- }
-}
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;
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("<ConstantAffixModifier prefix:'%s' suffix:'%s'>", prefix, suffix);
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;
* 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;
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
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();
/** 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);
+ }
}
+++ /dev/null
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package com.ibm.icu.impl.number.modifiers;
-
-import com.ibm.icu.impl.StandardPlural;
-import com.ibm.icu.impl.number.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 <code>put</code> 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;
- }
-}
+++ /dev/null
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package com.ibm.icu.impl.number.modifiers;
-
-import com.ibm.icu.impl.number.Modifier;
-import com.ibm.icu.impl.number.Modifier.AffixModifier;
-
-// 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;
- }
-}
* 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. */
* @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);
}
}
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);
- }
- }
- }
}
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;
import newapi.NumberFormatter;
import newapi.NumberPropertyMapper;
import newapi.impl.MacroProps;
+import newapi.impl.Padder.PadPosition;
/**
* {@icuenhanced java.text.DecimalFormat}.{@icu _usage_} <code>DecimalFormat</code> is the primary
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();
}
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();
}
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();
}
|| 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();
}
* @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);
* @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);
}
if (!(obj instanceof Number)) throw new IllegalArgumentException();
Number number = (Number) obj;
FormattedNumber output = formatter.format(number);
- return output.toAttributedCharacterIterator();
+ return output.getAttributes();
}
/**
// 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);
}
/**
*/
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;
}
/**
}
}
+ /**
+ * 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.
*
* @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);
}
/**
* <p>For more information, see <a href="http://www.unicode.org/reports/tr35/#Currencies"
* >UTS#35 section 5.10.2</a>.
*
- * <p><strong>Note:</strong> 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 <code>beforeCurrency</code> values, false
+++ /dev/null
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package com.ibm.icu.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);
- }
-}
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;
}
}
};
- 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);
}
};
--- /dev/null
+// © 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);
+ }
+}
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 {
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);
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;
final PluralRules rules;
final CompactData data;
final Map<String, CompactModInfo> 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) {
/** Used by the safe code path */
private static Map<String, CompactModInfo> precomputeAllModifiers(CompactData data,
- MurkyModifier buildReference) {
+ MutablePatternModifier buildReference) {
Map<String, CompactModInfo> precomputedMods = new HashMap<String, CompactModInfo>();
Set<String> 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
// 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)
fq.populateUFieldPosition(fieldPosition);
}
- public AttributedCharacterIterator toAttributedCharacterIterator() {
+ public AttributedCharacterIterator getAttributes() {
return nsb.getIterator();
}
* <p>
* 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);
}
* 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);
}
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 {
}
}
- 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;
}
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<StandardPlural, Modifier> data;
/* unsafe */ PluralRules rules;
- /* unsafe */ QuantityChain parent;
+ /* unsafe */ MicroPropsGenerator parent;
- private MurkyLongNameHandler(Map<StandardPlural, Modifier> data) {
+ private LongNameHandler(Map<StandardPlural, Modifier> 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<String, String> data = CurrencyData.provider.getInstance(loc, true).getUnitPatterns();
Map<StandardPlural, Modifier> result = new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
StringBuilder sb = new StringBuilder();
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<StandardPlural, String> simpleFormats = MeasureData.getMeasureData(loc, unit, width);
Map<StandardPlural, Modifier> result = new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
StringBuilder sb = new StringBuilder();
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 <em>this</em>
+ * 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;
}
@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<StandardPlural, Modifier> data;
- final PluralRules rules;
- final QuantityChain parent;
-
- public ImmutableLongNameHandler(Map<StandardPlural, Modifier> 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;
- }
- }
}
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
* <p>
* 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;
// Symbol details
DecimalFormatSymbols symbols;
- FormatWidth unitWidth;
+ UnitWidth unitWidth;
String currency1;
String currency2;
String[] currency3;
StandardPlural plural;
// QuantityChain details
- QuantityChain parent;
+ MicroPropsGenerator parent;
// Transient CharSequence fields
boolean inCharSequenceMode;
* {@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;
* 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;
* 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()) {
}
}
- 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;
}
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;
}
@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;
}
}
}
- 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();
@Override
public int getPrefixLength() {
- return insertPrefix(null, 0);
+ NumberStringBuilder dummy = new NumberStringBuilder();
+ return insertPrefix(dummy, 0);
}
@Override
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;
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)
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);
+ }
}
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<T extends NumberFormatterSettings<?>> {
static final int KEY_MACROS = 0;
this.value = value;
}
+ /**
+ * Specifies the notation style (simple, scientific, or compact) for rendering numbers.
+ *
+ * <ul>
+ * <li>Simple notation: "12,300"
+ * <li>Scientific notation: "1.23E4"
+ * <li>Compact notation: "12K"
+ * </ul>
+ *
+ * <p>
+ * 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.
+ *
+ * <p>
+ * Pass this method the return value of a {@link Notation} factory method. For example:
+ *
+ * <pre>
+ * NumberFormatter.with().notation(Notation.compactShort())
+ * </pre>
+ *
+ * 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.
+ *
+ * <ul>
+ * <li>Unit of measure: "12.3 meters"
+ * <li>Currency: "$12.30"
+ * <li>Percent: "12.3%"
+ * </ul>
+ *
+ * <p>
+ * <strong>Note:</strong> 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.
+ *
+ * <p>
+ * All units will be properly localized with locale data, and all units are compatible with notation styles,
+ * rounding strategies, and other number formatter settings.
+ *
+ * <p>
+ * Pass this method any instance of {@link MeasureUnit}. For units of measure:
+ *
+ * <pre>
+ * NumberFormatter.with().unit(MeasureUnit.METER)
+ * </pre>
+ *
+ * Currency:
+ *
+ * <pre>
+ * NumberFormatter.with().unit(Currency.getInstance("USD"))
+ * </pre>
+ *
+ * Percent:
+ *
+ * <pre>
+ * NumberFormatter.with().unit(NoUnit.PERCENT)
+ * </pre>
+ *
+ * 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.
+ *
+ * <ul>
+ * <li>Round to 3 decimal places: "3.142"
+ * <li>Round to 3 significant figures: "3.14"
+ * <li>Round to the closest nickel: "3.15"
+ * <li>Do not perform rounding: "3.1415926..."
+ * </ul>
+ *
+ * <p>
+ * Pass this method the return value of one of the factory methods on {@link Rounder}. For example:
+ *
+ * <pre>
+ * NumberFormatter.with().rounding(Rounder.fixedFraction(2))
+ * </pre>
+ *
+ * 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.
+ *
+ * <ul>
+ * <li>Default grouping: "12,300" and "1,230"
+ * <li>Grouping with at least 2 digits: "12,300" and "1230"
+ * <li>No grouping: "12300" and "1230"
+ * </ul>
+ *
+ * <p>
+ * The exact grouping widths will be chosen based on the locale.
+ *
+ * <p>
+ * Pass this method the return value of one of the factory methods on {@link Grouper}. For example:
+ *
+ * <pre>
+ * NumberFormatter.with().grouping(Grouper.min2())
+ * </pre>
+ *
+ * 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.
+ *
+ * <ul>
+ * <li>Zero minimum integer digits: ".08"
+ * <li>One minimum integer digit: "0.08"
+ * <li>Two minimum integer digits: "00.08"
+ * </ul>
+ *
+ * <p>
+ * Pass this method the return value of {@link IntegerWidth#zeroFillTo(int)}. For example:
+ *
+ * <pre>
+ * NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2))
+ * </pre>
+ *
+ * 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.
+ *
+ * <ul>
+ * <li><em>en_US</em> symbols: "12,345.67"
+ * <li><em>fr_FR</em> symbols: "12 345,67"
+ * <li><em>de_CH</em> symbols: "12’345.67"
+ * <li><em>my_MY</em> symbols: "၁၂,၃၄၅.၆၇"
+ * </ul>
+ *
+ * <p>
+ * Pass this method an instance of {@link DecimalFormatSymbols}. For example:
+ *
+ * <pre>
+ * NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("de_CH")))
+ * </pre>
+ *
+ * <p>
+ * <strong>Note:</strong> 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.
+ *
+ * <p>
+ * <strong>Note:</strong> The instance of DecimalFormatSymbols will be copied: changes made to the symbols object
+ * after passing it into the fluent chain will not be seen.
+ *
+ * <p>
+ * <strong>Note:</strong> Calling this method will override the NumberingSystem previously specified in
+ * {@link #symbols(NumberingSystem)}.
+ *
+ * <p>
+ * 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.
+ *
+ * <ul>
+ * <li>Latin numbering system: "12,345"
+ * <li>Myanmar numbering system: "၁၂,၃၄၅"
+ * <li>Math Sans Bold numbering system: "𝟭𝟮,𝟯𝟰𝟱"
+ * </ul>
+ *
+ * <p>
+ * Pass this method an instance of {@link NumberingSystem}. For example, to force the locale to always use the Latin
+ * alphabet numbering system (ASCII digits):
+ *
+ * <pre>
+ * NumberFormatter.with().symbols(NumberingSystem.LATIN)
+ * </pre>
+ *
+ * <p>
+ * <strong>Note:</strong> Calling this method will override the DecimalFormatSymbols previously specified in
+ * {@link #symbols(DecimalFormatSymbols)}.
+ *
+ * <p>
+ * 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).
+ *
+ * <ul>
+ * <li>Narrow: "$12.00", "12 m"
+ * <li>Short: "12.00 USD", "12 m"
+ * <li>Wide: "12.00 US dollars", "12 meters"
+ * <li>Hidden: "12.00", "12"
+ * </ul>
+ *
+ * <p>
+ * Pass an element from the {@link FormatWidth} enum to this setter. For example:
+ *
+ * <pre>
+ * NumberFormatter.with().unitWidth(FormatWidth.SHORT)
+ * </pre>
+ *
+ * <p>
+ * 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);
}
break;
case KEY_UNIT_WIDTH:
if (macros.unitWidth == null) {
- macros.unitWidth = (FormatWidth) current.value;
+ macros.unitWidth = (UnitWidth) current.value;
}
break;
case KEY_SIGN:
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.AffixPatternUtils;
-import com.ibm.icu.impl.number.LdmlPatternInfo;
-import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
-import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.PatternAndPropertyUtils;
+import com.ibm.icu.impl.number.PatternParser;
+import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
* public API if there is demand.
*/
public static UnlocalizedNumberFormatter create(String pattern, DecimalFormatSymbols symbols) {
- Properties properties = PatternString.parseToProperties(pattern);
+ Properties properties = PatternAndPropertyUtils.parseToProperties(pattern);
return create(properties, symbols);
}
private final AffixPatternProvider[] affixesByPlural;
public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) {
- affixesByPlural = new PatternParseResult[StandardPlural.COUNT];
+ affixesByPlural = new ParsedPatternInfo[StandardPlural.COUNT];
for (StandardPlural plural : StandardPlural.VALUES) {
- affixesByPlural[plural.ordinal()] = LdmlPatternInfo
+ affixesByPlural[plural.ordinal()] = PatternParser
.parse(cpi.getCurrencyPluralPattern(plural.getKeyword()));
}
}
return constructFraction(0, 0);
}
- public static FractionRounder fixedFraction(int minMaxFrac) {
- if (minMaxFrac >= 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);
}
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 {
}
}
- /* 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;
}
@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
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;
}
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")) {
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;
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;
}
}
}
- private static void unitWidthToSkeleton(FormatWidth value, StringBuilder sb) {
+ private static void unitWidthToSkeleton(UnitWidth value, StringBuilder sb) {
sb.append(value.name());
}
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;
}
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
+ * <em>not</em> 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();
// 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 {
}
// 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;
// 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();
}
// 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;
//////////
- 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);
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);
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;
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;
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
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 {
}
public static Map<StandardPlural, String> 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("/");
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;
}
@Override
- public MicroProps withQuantity(FormatQuantity quantity) {
+ public MicroProps processQuantity(FormatQuantity quantity) {
if (immutable) {
return (MicroProps) this.clone();
} else {
--- /dev/null
+// © 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.
+ *
+ * <p>
+ * In other words, this interface is used for the parts of number processing that are <em>quantity-dependent</em>.
+ *
+ * <p>
+ * 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.
+ *
+ * <p>
+ * A class implementing MicroPropsGenerator looks something like this:
+ *
+ * <pre>
+ * 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;
+ * }
+ * }
+ * </pre>
+ *
+ * @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
--- /dev/null
+// © 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<T> {
+
+ public void mutateMicros(MicroProps micros, T value);
+
+}
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;
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);
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);
+++ /dev/null
-// © 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
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) {
.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) -> {
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;
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;
}
}
-/*
@Test
public void testZZZ() {
// various generateXXX calls go here, see
//generateCXXBackwardCompatibilityTest("59"); // for measfmttest.cpp, create TestCompatible59
//updateJAVAVersions("59"); // for MeasureUnitTest.java, JAVA_VERSIONS
}
-*/
@Test
public void TestCompatible53() {
if (type.equals("currency")
|| type.equals("compound")
|| type.equals("coordinate")
- || type.equals("dimensionless")) {
+ || type.equals("none")) {
continue;
}
for (MeasureUnit unit : MeasureUnit.getAvailable(type)) {
System.out.println("");
TreeMap<String, List<MeasureUnit>> 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<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
for (Map.Entry<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
String type = entry.getKey();
- if (type.equals("currency")) {
+ if (type.equals("currency") || type.equals("none")) {
continue;
}
for (MeasureUnit unit : entry.getValue()) {
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;
import newapi.LocalizedNumberFormatter;
import newapi.NumberPropertyMapper;
+import newapi.impl.Padder.PadPosition;
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);
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);
}
@Test
- public void Test13113() {
+ public void Test13113_MalformedPatterns() {
String[][] cases = {
{"'", "quoted literal"},
{"ab#c'd", "quoted literal"},
{".#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 {
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 = {
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);
--- /dev/null
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.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("<NumberStringBuilder [" + expectedChars + "] [" + expectedFields + "]>", sb.toDebugString());
+ }
+}
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));
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));
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();
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;
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);
- }
}
}
}
+ @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());
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;
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
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);
}
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);
}
}
for (String pattern : invalidPatterns) {
try {
- PatternString.parseToProperties(pattern);
+ PatternAndPropertyUtils.parseToProperties(pattern);
fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern);
} catch (IllegalArgumentException e) {
}
@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);
}
}
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;
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();
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)
};
}
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());