ICU-13177 Renaming classes and moving things around. No or very few behavior changes.

X-SVN-Rev: 40364
This commit is contained in:
Shane Carr 2017-09-01 08:30:17 +00:00
parent 20e5b7b910
commit c0f2ca5177
54 changed files with 3998 additions and 3555 deletions

View File

@ -89,6 +89,10 @@ public class AffixPatternUtils {
/** Represents a sequence of six or more currency symbols. */
public static final int TYPE_CURRENCY_OVERFLOW = -15;
public static interface SymbolProvider {
public CharSequence getSymbol(int type);
}
/**
* Estimates the number of code points present in an unescaped version of the affix pattern string
* (one that would be returned by {@link #unescape}), assuming that all interpolated symbols
@ -255,10 +259,6 @@ public class AffixPatternUtils {
}
}
public static interface SymbolProvider {
public CharSequence getSymbol(int type);
}
/**
* Executes the unescape state machine. Replaces the unquoted characters "-", "+", "%", "", and
* "¤" with the corresponding symbols provided by the {@link SymbolProvider}, and inserts the
@ -276,26 +276,22 @@ public class AffixPatternUtils {
NumberStringBuilder output,
int position,
SymbolProvider provider) {
// TODO: Is it worth removing this extra local object instantiation here?
NumberStringBuilder local = new NumberStringBuilder(10);
assert affixPattern != null;
int length = 0;
long tag = 0L;
while (hasNext(tag, affixPattern)) {
tag = nextToken(tag, affixPattern);
int typeOrCp = getTypeOrCp(tag);
if (typeOrCp == TYPE_CURRENCY_OVERFLOW) {
// Don't go to the provider for this special case
local.appendCodePoint(0xFFFD, NumberFormat.Field.CURRENCY);
length += output.insertCodePoint(position + length, 0xFFFD, NumberFormat.Field.CURRENCY);
} else if (typeOrCp < 0) {
local.append(provider.getSymbol(typeOrCp), getFieldForType(typeOrCp));
length += output.insert(position + length, provider.getSymbol(typeOrCp), getFieldForType(typeOrCp));
} else {
local.appendCodePoint(typeOrCp, null);
length += output.insertCodePoint(position + length, typeOrCp, null);
}
}
if (output != null) {
output.insert(position, local);
}
return local.length();
return length;
}
/**
@ -307,7 +303,9 @@ public class AffixPatternUtils {
* @return true if the affix pattern contains the given token type; false otherwise.
*/
public static boolean containsType(CharSequence affixPattern, int type) {
if (affixPattern == null || affixPattern.length() == 0) return false;
if (affixPattern == null || affixPattern.length() == 0) {
return false;
}
long tag = 0L;
while (hasNext(tag, affixPattern)) {
tag = nextToken(tag, affixPattern);
@ -548,7 +546,7 @@ public class AffixPatternUtils {
public static int getTypeOrCp(long tag) {
assert tag >= 0;
int type = getType(tag);
return (type == 0) ? getCodePoint(tag) : -type;
return (type == TYPE_CODEPOINT) ? getCodePoint(tag) : -type;
}
/**

View File

@ -1,447 +0,0 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
import newapi.impl.AffixPatternProvider;
/** Implements a recursive descent parser for decimal format patterns. */
public class LdmlPatternInfo {
public static PatternParseResult parse(String patternString) {
ParserState state = new ParserState(patternString);
PatternParseResult result = new PatternParseResult(patternString);
consumePattern(state, result);
return result;
}
/**
* An internal, intermediate data structure used for storing parse results before they are
* finalized into a DecimalFormatPattern.Builder.
*/
public static class PatternParseResult implements AffixPatternProvider {
public String pattern;
public LdmlPatternInfo.SubpatternParseResult positive;
public LdmlPatternInfo.SubpatternParseResult negative;
private PatternParseResult(String pattern) {
this.pattern = pattern;
}
@Override
public char charAt(int flags, int index) {
long endpoints = getEndpoints(flags);
int left = (int) (endpoints & 0xffffffff);
int right = (int) (endpoints >>> 32);
if (index < 0 || index >= right - left) {
throw new IndexOutOfBoundsException();
}
return pattern.charAt(left + index);
}
@Override
public int length(int flags) {
return getLengthFromEndpoints(getEndpoints(flags));
}
public static int getLengthFromEndpoints(long endpoints) {
int left = (int) (endpoints & 0xffffffff);
int right = (int) (endpoints >>> 32);
return right - left;
}
public String getString(int flags) {
long endpoints = getEndpoints(flags);
int left = (int) (endpoints & 0xffffffff);
int right = (int) (endpoints >>> 32);
if (left == right) {
return "";
}
return pattern.substring(left, right);
}
private long getEndpoints(int flags) {
boolean prefix = (flags & Flags.PREFIX) != 0;
boolean isNegative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
boolean padding = (flags & Flags.PADDING) != 0;
if (isNegative && padding) {
return negative.paddingEndpoints;
} else if (padding) {
return positive.paddingEndpoints;
} else if (prefix && isNegative) {
return negative.prefixEndpoints;
} else if (prefix) {
return positive.prefixEndpoints;
} else if (isNegative) {
return negative.suffixEndpoints;
} else {
return positive.suffixEndpoints;
}
}
@Override
public boolean positiveHasPlusSign() {
return positive.hasPlusSign;
}
@Override
public boolean hasNegativeSubpattern() {
return negative != null;
}
@Override
public boolean negativeHasMinusSign() {
return negative.hasMinusSign;
}
@Override
public boolean hasCurrencySign() {
return positive.hasCurrencySign || (negative != null && negative.hasCurrencySign);
}
@Override
public boolean containsSymbolType(int type) {
return AffixPatternUtils.containsType(pattern, type);
}
}
public static class SubpatternParseResult {
public long groupingSizes = 0x0000ffffffff0000L;
public int minimumIntegerDigits = 0;
public int totalIntegerDigits = 0;
public int minimumFractionDigits = 0;
public int maximumFractionDigits = 0;
public int minimumSignificantDigits = 0;
public int maximumSignificantDigits = 0;
public boolean hasDecimal = false;
public int paddingWidth = 0;
public PadPosition paddingLocation = null;
public FormatQuantity4 rounding = null;
public boolean exponentShowPlusSign = false;
public int exponentDigits = 0;
public boolean hasPercentSign = false;
public boolean hasPerMilleSign = false;
public boolean hasCurrencySign = false;
public boolean hasMinusSign = false;
public boolean hasPlusSign = false;
public long prefixEndpoints = 0;
public long suffixEndpoints = 0;
public long paddingEndpoints = 0;
}
/** An internal class used for tracking the cursor during parsing of a pattern string. */
private static class ParserState {
final String pattern;
int offset;
ParserState(String pattern) {
this.pattern = pattern;
this.offset = 0;
}
int peek() {
if (offset == pattern.length()) {
return -1;
} else {
return pattern.codePointAt(offset);
}
}
int next() {
int codePoint = peek();
offset += Character.charCount(codePoint);
return codePoint;
}
IllegalArgumentException toParseException(String message) {
StringBuilder sb = new StringBuilder();
sb.append("Malformed pattern for ICU DecimalFormat: \"");
sb.append(pattern);
sb.append("\": ");
sb.append(message);
sb.append(" at position ");
sb.append(offset);
return new IllegalArgumentException(sb.toString());
}
}
private static void consumePattern(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.PatternParseResult result) {
// pattern := subpattern (';' subpattern)?
result.positive = new SubpatternParseResult();
consumeSubpattern(state, result.positive);
if (state.peek() == ';') {
state.next(); // consume the ';'
// Don't consume the negative subpattern if it is empty (trailing ';')
if (state.peek() != -1) {
result.negative = new SubpatternParseResult();
consumeSubpattern(state, result.negative);
}
}
if (state.peek() != -1) {
throw state.toParseException("Found unquoted special character");
}
}
private static void consumeSubpattern(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
// subpattern := literals? number exponent? literals?
consumePadding(state, result, PadPosition.BEFORE_PREFIX);
result.prefixEndpoints = consumeAffix(state, result);
consumePadding(state, result, PadPosition.AFTER_PREFIX);
consumeFormat(state, result);
consumeExponent(state, result);
consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
result.suffixEndpoints = consumeAffix(state, result);
consumePadding(state, result, PadPosition.AFTER_SUFFIX);
}
private static void consumePadding(
LdmlPatternInfo.ParserState state,
LdmlPatternInfo.SubpatternParseResult result,
PadPosition paddingLocation) {
if (state.peek() != '*') {
return;
}
result.paddingLocation = paddingLocation;
state.next(); // consume the '*'
result.paddingEndpoints |= state.offset;
consumeLiteral(state);
result.paddingEndpoints |= ((long) state.offset) << 32;
}
private static long consumeAffix(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
// literals := { literal }
long endpoints = state.offset;
outer:
while (true) {
switch (state.peek()) {
case '#':
case '@':
case ';':
case '*':
case '.':
case ',':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case -1:
// Characters that cannot appear unquoted in a literal
break outer;
case '%':
result.hasPercentSign = true;
break;
case '‰':
result.hasPerMilleSign = true;
break;
case '¤':
result.hasCurrencySign = true;
break;
case '-':
result.hasMinusSign = true;
break;
case '+':
result.hasPlusSign = true;
break;
}
consumeLiteral(state);
}
endpoints |= ((long) state.offset) << 32;
return endpoints;
}
private static void consumeLiteral(LdmlPatternInfo.ParserState state) {
if (state.peek() == -1) {
throw state.toParseException("Expected unquoted literal but found EOL");
} else if (state.peek() == '\'') {
state.next(); // consume the starting quote
while (state.peek() != '\'') {
if (state.peek() == -1) {
throw state.toParseException("Expected quoted literal but found EOL");
} else {
state.next(); // consume a quoted character
}
}
state.next(); // consume the ending quote
} else {
// consume a non-quoted literal character
state.next();
}
}
private static void consumeFormat(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
consumeIntegerFormat(state, result);
if (state.peek() == '.') {
state.next(); // consume the decimal point
result.hasDecimal = true;
result.paddingWidth += 1;
consumeFractionFormat(state, result);
}
}
private static void consumeIntegerFormat(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
boolean seenSignificantDigitMarker = false;
boolean seenDigit = false;
outer:
while (true) {
switch (state.peek()) {
case ',':
result.paddingWidth += 1;
result.groupingSizes <<= 16;
break;
case '#':
if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
result.paddingWidth += 1;
result.groupingSizes += 1;
result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
// no change to result.minimumIntegerDigits
// no change to result.minimumSignificantDigits
result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
if (result.rounding != null) {
result.rounding.appendDigit((byte) 0, 0, true);
}
break;
case '@':
seenSignificantDigitMarker = true;
if (seenDigit) throw state.toParseException("Cannot mix 0 and @");
result.paddingWidth += 1;
result.groupingSizes += 1;
result.totalIntegerDigits += 1;
// no change to result.minimumIntegerDigits
result.minimumSignificantDigits += 1;
result.maximumSignificantDigits += 1;
if (result.rounding != null) {
result.rounding.appendDigit((byte) 0, 0, true);
}
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
seenDigit = true;
if (seenSignificantDigitMarker) throw state.toParseException("Cannot mix @ and 0");
// TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
result.paddingWidth += 1;
result.groupingSizes += 1;
result.totalIntegerDigits += 1;
result.minimumIntegerDigits += 1;
// no change to result.minimumSignificantDigits
// no change to result.maximumSignificantDigits
if (state.peek() != '0' && result.rounding == null) {
result.rounding = new FormatQuantity4();
}
if (result.rounding != null) {
result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
}
break;
default:
break outer;
}
state.next(); // consume the symbol
}
// Disallow patterns with a trailing ',' or with two ',' next to each other
short grouping1 = (short) (result.groupingSizes & 0xffff);
short grouping2 = (short) ((result.groupingSizes >>> 16) & 0xffff);
short grouping3 = (short) ((result.groupingSizes >>> 32) & 0xffff);
if (grouping1 == 0 && grouping2 != -1) {
throw state.toParseException("Trailing grouping separator is invalid");
}
if (grouping2 == 0 && grouping3 != -1) {
throw state.toParseException("Grouping width of zero is invalid");
}
}
private static void consumeFractionFormat(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
int zeroCounter = 0;
boolean seenHash = false;
while (true) {
switch (state.peek()) {
case '#':
seenHash = true;
result.paddingWidth += 1;
// no change to result.minimumFractionDigits
result.maximumFractionDigits += 1;
zeroCounter++;
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (seenHash) throw state.toParseException("0 cannot follow # after decimal point");
result.paddingWidth += 1;
result.minimumFractionDigits += 1;
result.maximumFractionDigits += 1;
if (state.peek() == '0') {
zeroCounter++;
} else {
if (result.rounding == null) {
result.rounding = new FormatQuantity4();
}
result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
zeroCounter = 0;
}
break;
default:
return;
}
state.next(); // consume the symbol
}
}
private static void consumeExponent(
LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
if (state.peek() != 'E') {
return;
}
state.next(); // consume the E
result.paddingWidth++;
if (state.peek() == '+') {
state.next(); // consume the +
result.exponentShowPlusSign = true;
result.paddingWidth++;
}
while (state.peek() == '0') {
state.next(); // consume the 0
result.exponentDigits += 1;
result.paddingWidth++;
}
}
}

View File

@ -2,21 +2,15 @@
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
import com.ibm.icu.impl.number.modifiers.SimpleModifier;
import newapi.MutablePatternModifier;
/**
* A Modifier is an immutable object that can be passed through the formatting pipeline until it is finally applied to
* the string builder. A Modifier usually contains a prefix and a suffix that are applied, but it could contain
* something else, like a {@link com.ibm.icu.text.SimpleFormatter} pattern.
* A Modifier is an object that can be passed through the formatting pipeline until it is finally applied to the string
* builder. A Modifier usually contains a prefix and a suffix that are applied, but it could contain something else,
* like a {@link com.ibm.icu.text.SimpleFormatter} pattern.
*
* @see PositiveNegativeAffixModifier
* @see ConstantAffixModifier
* @see GeneralPluralModifier
* @see SimpleModifier
* A Modifier is usually immutable, except in cases such as {@link MutablePatternModifier}, which are mutable for performance
* reasons.
*/
public interface Modifier {
@ -28,8 +22,8 @@ public interface Modifier {
* @param leftIndex
* The left index of the string within the builder. Equal to 0 when only one number is being formatted.
* @param rightIndex
* The right index of the string within the string builder. Equal to length-1 when only one number is
* being formatted.
* The right index of the string within the string builder. Equal to length when only one number is being
* formatted.
* @return The number of characters (UTF-16 code units) that were added to the string builder.
*/
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex);
@ -50,62 +44,4 @@ public interface Modifier {
* @return Whether the modifier is strong.
*/
public boolean isStrong();
/**
* An interface for a modifier that contains both a positive and a negative form. Note that a class implementing
* {@link PositiveNegativeModifier} is not necessarily a {@link Modifier} itself. Rather, it returns a
* {@link Modifier} when {@link #getModifier} is called.
*/
public static interface PositiveNegativeModifier {
/**
* Converts this {@link PositiveNegativeModifier} to a {@link Modifier} given the negative sign.
*
* @param isNegative
* true if the negative form of this modifier should be used; false if the positive form should be
* used.
* @return A Modifier corresponding to the negative sign.
*/
public Modifier getModifier(boolean isNegative);
}
/**
* An interface for a modifier that contains both a positive and a negative form for all six standard plurals. Note
* that a class implementing {@link PositiveNegativePluralModifier} is not necessarily a {@link Modifier} itself.
* Rather, it returns a {@link Modifier} when {@link #getModifier} is called.
*/
public static interface PositiveNegativePluralModifier {
/**
* Converts this {@link PositiveNegativePluralModifier} to a {@link Modifier} given the negative sign and the
* standard plural.
*
* @param plural
* The StandardPlural to use.
* @param isNegative
* true if the negative form of this modifier should be used; false if the positive form should be
* used.
* @return A Modifier corresponding to the negative sign.
*/
public Modifier getModifier(StandardPlural plural, boolean isNegative);
}
/**
* An interface for a modifier that is represented internally by a prefix string and a suffix string.
*/
public static interface AffixModifier extends Modifier {
}
/**
* A starter implementation with defaults for some of the basic methods.
*
* <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;
}
}
}

View File

@ -13,460 +13,490 @@ import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberFormat.Field;
/**
* A StringBuilder optimized for number formatting. It implements the following key features beyond
* a normal JDK StringBuilder:
* A StringBuilder optimized for number formatting. It implements the following key features beyond a normal JDK
* StringBuilder:
*
* <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();
/** 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;
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
* "&lt;NumberStringBuilder [-123.45] [-iii.ff]&gt;"
*
* @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;
}
public NumberStringBuilder() {
this(40);
}
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());
public NumberStringBuilder(int capacity) {
chars = new char[capacity];
fields = new Field[capacity];
zero = capacity / 2;
length = 0;
}
/* com.ibm.icu.text.NumberFormat. */ Field field = (Field) rawField;
public NumberStringBuilder(NumberStringBuilder source) {
copyFrom(source);
}
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;
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;
}
return Character.codePointAt(chars, zero, zero + length);
}
// 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 int getLastCodePoint() {
if (length == 0) {
return -1;
}
return Character.codePointBefore(chars, zero + length, zero);
}
}
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) {
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 "&lt;NumberStringBuilder
* [-123.45] [-iii.ff]&gt;"
*
* @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;
}
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);
}
current = field;
currentStart = i;
}
}
if (current != null) {
as.addAttribute(current, current, currentStart, i);
as.addAttribute(current, current, currentStart, length);
}
current = field;
currentStart = i;
}
}
if (current != null) {
as.addAttribute(current, current, currentStart, length);
}
return as.getIterator();
}
return as.getIterator();
}
}

View File

@ -837,7 +837,7 @@ public class Parse {
private void addPattern(String pattern) {
Properties properties = threadLocalProperties.get();
try {
PatternString.parseToExistingProperties(pattern, properties);
PatternAndPropertyUtils.parseToExistingProperties(pattern, properties);
} catch (IllegalArgumentException e) {
// This should only happen if there is a bug in CLDR data. Fail silently.
}

View File

@ -0,0 +1,624 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
import com.ibm.icu.impl.number.PatternParser.ParsedSubpatternInfo;
import com.ibm.icu.text.DecimalFormatSymbols;
import newapi.impl.AffixPatternProvider;
import newapi.impl.Padder;
import newapi.impl.Padder.PadPosition;
/**
* Handles parsing and creation of the compact pattern string representation of a decimal format.
*/
public class PatternAndPropertyUtils {
/**
* Parses a pattern string into a new property bag.
*
* @param pattern
* The pattern string, like "#,##0.00"
* @param ignoreRounding
* Whether to leave out rounding information (minFrac, maxFrac, and rounding increment) when parsing the
* pattern. This may be desirable if a custom rounding mode, such as CurrencyUsage, is to be used
* instead. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link #IGNORE_ROUNDING_IF_CURRENCY}, or
* {@link #IGNORE_ROUNDING_NEVER}.
* @return A property bag object.
* @throws IllegalArgumentException
* If there is a syntax error in the pattern string.
*/
public static Properties parseToProperties(String pattern, int ignoreRounding) {
Properties properties = new Properties();
parse(pattern, properties, ignoreRounding);
return properties;
}
public static Properties parseToProperties(String pattern) {
return parseToProperties(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER);
}
/**
* Parses a pattern string into an existing property bag. All properties that can be encoded into a pattern string
* will be overwritten with either their default value or with the value coming from the pattern string. Properties
* that cannot be encoded into a pattern string, such as rounding mode, are not modified.
*
* @param pattern
* The pattern string, like "#,##0.00"
* @param properties
* The property bag object to overwrite.
* @param ignoreRounding
* Whether to leave out rounding information (minFrac, maxFrac, and rounding increment) when parsing the
* pattern. This may be desirable if a custom rounding mode, such as CurrencyUsage, is to be used
* instead. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link #IGNORE_ROUNDING_IF_CURRENCY}, or
* {@link #IGNORE_ROUNDING_NEVER}.
* @throws IllegalArgumentException
* If there was a syntax error in the pattern string.
*/
public static void parseToExistingProperties(String pattern, Properties properties, int ignoreRounding) {
parse(pattern, properties, ignoreRounding);
}
public static void parseToExistingProperties(String pattern, Properties properties) {
parseToExistingProperties(pattern, properties, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER);
}
/**
* Creates a pattern string from a property bag.
*
* <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);
}
}
}

View File

@ -0,0 +1,443 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import newapi.impl.AffixPatternProvider;
import newapi.impl.Padder.PadPosition;
/** Implements a recursive descent parser for decimal format patterns. */
public class PatternParser {
/**
* Runs the recursive descent parser on the given pattern string, returning a data structure with raw information
* about the pattern string.
*
* <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++;
}
}
}

View File

@ -1,618 +0,0 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
import com.ibm.icu.text.DecimalFormatSymbols;
import newapi.impl.AffixPatternProvider;
/**
* Handles parsing and creation of the compact pattern string representation of a decimal format.
*/
public class PatternString {
/**
* Parses a pattern string into a new property bag.
*
* @param pattern The pattern string, like "#,##0.00"
* @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
* increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
* as CurrencyUsage, is to be used instead. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link
* #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}.
* @return A property bag object.
* @throws IllegalArgumentException If there is a syntax error in the pattern string.
*/
public static Properties parseToProperties(String pattern, int ignoreRounding) {
Properties properties = new Properties();
parse(pattern, properties, ignoreRounding);
return properties;
}
public static Properties parseToProperties(String pattern) {
return parseToProperties(pattern, PatternString.IGNORE_ROUNDING_NEVER);
}
/**
* Parses a pattern string into an existing property bag. All properties that can be encoded into
* a pattern string will be overwritten with either their default value or with the value coming
* from the pattern string. Properties that cannot be encoded into a pattern string, such as
* rounding mode, are not modified.
*
* @param pattern The pattern string, like "#,##0.00"
* @param properties The property bag object to overwrite.
* @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
* increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
* as CurrencyUsage, is to be used instead. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link
* #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}.
* @throws IllegalArgumentException If there was a syntax error in the pattern string.
*/
public static void parseToExistingProperties(
String pattern, Properties properties, int ignoreRounding) {
parse(pattern, properties, ignoreRounding);
}
public static void parseToExistingProperties(String pattern, Properties properties) {
parseToExistingProperties(pattern, properties, PatternString.IGNORE_ROUNDING_NEVER);
}
/**
* Creates a pattern string from a property bag.
*
* <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);
}
}
}

View File

@ -17,13 +17,14 @@ import java.util.Map;
import com.ibm.icu.impl.number.Parse.GroupingMode;
import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CurrencyPluralInfo;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import newapi.impl.Padder.PadPosition;
public class Properties implements Cloneable, Serializable {
private static final Properties DEFAULT = new Properties();

View File

@ -1,59 +0,0 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
/** @author sffc */
public class ThingsNeedingNewHome {
public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space
public enum PadPosition {
BEFORE_PREFIX,
AFTER_PREFIX,
BEFORE_SUFFIX,
AFTER_SUFFIX;
public static PadPosition fromOld(int old) {
switch (old) {
case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX:
return PadPosition.BEFORE_PREFIX;
case com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX:
return PadPosition.AFTER_PREFIX;
case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX:
return PadPosition.BEFORE_SUFFIX;
case com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX:
return PadPosition.AFTER_SUFFIX;
default:
throw new IllegalArgumentException("Don't know how to map " + old);
}
}
public int toOld() {
switch (this) {
case BEFORE_PREFIX:
return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX;
case AFTER_PREFIX:
return com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX;
case BEFORE_SUFFIX:
return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX;
case AFTER_SUFFIX:
return com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX;
default:
return -1; // silence compiler errors
}
}
}
/**
* Returns true if the currency is set in The property bag or if currency symbols are present in
* the prefix/suffix pattern.
*/
public static boolean useCurrency(Properties properties) {
return ((properties.getCurrency() != null)
|| properties.getCurrencyPluralInfo() != null
|| properties.getCurrencyUsage() != null
|| AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern()));
}
}

View File

@ -3,15 +3,20 @@
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.Modifier.AffixModifier;
// TODO: This class is currently unused, but it might be useful for something in the future.
// Should probably be moved to a different package.
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.text.NumberFormat.Field;
/** The canonical implementation of {@link Modifier}, containing a prefix and suffix string. */
public class ConstantAffixModifier extends Modifier.BaseModifier implements AffixModifier {
/**
* The canonical implementation of {@link Modifier}, containing a prefix and suffix string.
*/
public class ConstantAffixModifier implements Modifier {
// TODO: Avoid making a new instance by default if prefix and suffix are empty
public static final AffixModifier EMPTY = new ConstantAffixModifier();
public static final ConstantAffixModifier EMPTY = new ConstantAffixModifier();
private final String prefix;
private final String suffix;
@ -69,26 +74,6 @@ public class ConstantAffixModifier extends Modifier.BaseModifier implements Affi
return strong;
}
public boolean contentEquals(CharSequence _prefix, CharSequence _suffix) {
if (_prefix == null && !prefix.isEmpty())
return false;
if (_suffix == null && !suffix.isEmpty())
return false;
if (_prefix != null && prefix.length() != _prefix.length())
return false;
if (_suffix != null && suffix.length() != _suffix.length())
return false;
for (int i = 0; i < prefix.length(); i++) {
if (prefix.charAt(i) != _prefix.charAt(i))
return false;
}
for (int i = 0; i < suffix.length(); i++) {
if (suffix.charAt(i) != _suffix.charAt(i))
return false;
}
return true;
}
@Override
public String toString() {
return String.format("<ConstantAffixModifier prefix:'%s' suffix:'%s'>", prefix, suffix);

View File

@ -3,7 +3,6 @@
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.Modifier.AffixModifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.text.NumberFormat.Field;
@ -11,11 +10,10 @@ import com.ibm.icu.text.NumberFormat.Field;
* An implementation of {@link Modifier} that allows for multiple types of fields in the same modifier. Constructed
* based on the contents of two {@link NumberStringBuilder} instances (one for the prefix, one for the suffix).
*/
public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements AffixModifier {
// TODO: Avoid making a new instance by default if prefix and suffix are empty
public static final ConstantMultiFieldModifier EMPTY = new ConstantMultiFieldModifier();
public class ConstantMultiFieldModifier implements Modifier {
// NOTE: In Java, these are stored as array pointers. In C++, the NumberStringBuilder is stored by
// value and is treated internally as immutable.
protected final char[] prefixChars;
protected final char[] suffixChars;
protected final Field[] prefixFields;
@ -30,14 +28,6 @@ public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements
this.strong = strong;
}
private ConstantMultiFieldModifier() {
prefixChars = new char[0];
suffixChars = new char[0];
prefixFields = new Field[0];
suffixFields = new Field[0];
strong = false;
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
// Insert the suffix first since inserting the prefix will change the rightIndex
@ -56,10 +46,6 @@ public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements
return strong;
}
public boolean contentEquals(NumberStringBuilder prefix, NumberStringBuilder suffix) {
return prefix.contentEquals(prefixChars, prefixFields) && suffix.contentEquals(suffixChars, suffixFields);
}
@Override
public String toString() {
NumberStringBuilder temp = new NumberStringBuilder();

View File

@ -10,163 +10,144 @@ import com.ibm.icu.text.UnicodeSet;
/** Identical to {@link ConstantMultiFieldModifier}, but supports currency spacing. */
public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier {
// These are the default currency spacing UnicodeSets in CLDR.
// Pre-compute them for performance.
// TODO: Is there a way to write a unit test to make sure these hard-coded values
// stay consistent with CLDR?
private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze();
private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze();
// These are the default currency spacing UnicodeSets in CLDR.
// Pre-compute them for performance.
// The unit test testCurrencySpacingPatternStability() will start failing if these change in CLDR.
private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze();
private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze();
// Constants for better readability. Types are for compiler checking.
static final byte PREFIX = 0;
static final byte SUFFIX = 1;
static final short IN_CURRENCY = 0;
static final short IN_NUMBER = 1;
// Constants for better readability. Types are for compiler checking.
static final byte PREFIX = 0;
static final byte SUFFIX = 1;
static final short IN_CURRENCY = 0;
static final short IN_NUMBER = 1;
private final UnicodeSet afterPrefixUnicodeSet;
private final String afterPrefixInsert;
private final UnicodeSet beforeSuffixUnicodeSet;
private final String beforeSuffixInsert;
private final UnicodeSet afterPrefixUnicodeSet;
private final String afterPrefixInsert;
private final UnicodeSet beforeSuffixUnicodeSet;
private final String beforeSuffixInsert;
/** Build code path */
public CurrencySpacingEnabledModifier(
NumberStringBuilder prefix,
NumberStringBuilder suffix,
boolean strong,
DecimalFormatSymbols symbols) {
super(prefix, suffix, strong);
/** Safe code path */
public CurrencySpacingEnabledModifier(NumberStringBuilder prefix, NumberStringBuilder suffix, boolean strong,
DecimalFormatSymbols symbols) {
super(prefix, suffix, strong);
// Check for currency spacing. Do not build the UnicodeSets unless there is
// a currency code point at a boundary.
if (prefixFields.length > 0
&& prefixFields[prefixFields.length - 1] == NumberFormat.Field.CURRENCY) {
int prefixCp = Character.codePointBefore(prefixChars, prefixChars.length);
UnicodeSet prefixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, PREFIX);
if (prefixUnicodeSet.contains(prefixCp)) {
afterPrefixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, PREFIX);
afterPrefixInsert = getInsertString(symbols, PREFIX);
} else {
afterPrefixUnicodeSet = null;
afterPrefixInsert = null;
}
} else {
afterPrefixUnicodeSet = null;
afterPrefixInsert = null;
}
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;
}
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;
}
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);
}
}
private static String getInsertString(DecimalFormatSymbols symbols, byte affix) {
return symbols.getPatternForCurrencySpacing(
DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX);
}
@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);
}
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);
// 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;
}
}
// Call super for the remaining logic
length += super.apply(output, leftIndex, rightIndex + length);
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);
}
// Call super for the remaining logic
length += super.apply(output, leftIndex, rightIndex + length);
return length;
}
/** 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;
}
/** 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);
// 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);
}
}
private static String getInsertString(DecimalFormatSymbols symbols, byte affix) {
return symbols.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX);
}
}

View File

@ -1,54 +0,0 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.Modifier;
// TODO: Is it okay that this class is not completely immutable? Right now it is internal-only.
// Freezable or Builder could be used if necessary.
// TODO: This class is currently unused. Probably should be deleted.
/**
* A basic implementation of {@link com.ibm.icu.impl.number.Modifier.PositiveNegativePluralModifier}
* that is built on the fly using its <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;
}
}

View File

@ -1,31 +0,0 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.Modifier.AffixModifier;
// TODO: This class is currently unused. Should probably be deleted.
/** A class containing a positive form and a negative form of {@link ConstantAffixModifier}. */
public class PositiveNegativeAffixModifier implements Modifier.PositiveNegativeModifier {
private final AffixModifier positive;
private final AffixModifier negative;
/**
* Constructs an instance using the two {@link ConstantMultiFieldModifier} classes for positive
* and negative.
*
* @param positive The positive-form Modifier.
* @param negative The negative-form Modifier.
*/
public PositiveNegativeAffixModifier(AffixModifier positive, AffixModifier negative) {
this.positive = positive;
this.negative = negative;
}
@Override
public Modifier getModifier(boolean isNegative) {
return isNegative ? negative : positive;
}
}

View File

@ -11,15 +11,15 @@ import com.ibm.icu.text.NumberFormat.Field;
* The second primary implementation of {@link Modifier}, this one consuming a {@link com.ibm.icu.text.SimpleFormatter}
* pattern.
*/
public class SimpleModifier extends Modifier.BaseModifier {
public class SimpleModifier implements Modifier {
private final String compiledPattern;
private final Field field;
private final boolean strong;
private final int prefixLength;
private final int suffixOffset;
private final int suffixLength;
/** TODO: This is copied from SimpleFormatterImpl. */
private static final int ARG_NUM_LIMIT = 0x100;
/** Creates a modifier that uses the SimpleFormatter string formats. */
@ -79,7 +79,6 @@ public class SimpleModifier extends Modifier.BaseModifier {
* @return The number of characters (UTF-16 code points) that were added to the StringBuilder.
*/
public int formatAsPrefixSuffix(NumberStringBuilder result, int startIndex, int endIndex, Field field) {
assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 1;
if (prefixLength > 0) {
result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field);
}
@ -89,28 +88,4 @@ public class SimpleModifier extends Modifier.BaseModifier {
}
return prefixLength + suffixLength;
}
/** TODO: Move this to a test file somewhere, once we figure out what to do with the method. */
public static void testFormatAsPrefixSuffix() {
String[] patterns = { "{0}", "X{0}Y", "XX{0}YYY", "{0}YY", "XXXX{0}" };
Object[][] outputs = { { "", 0, 0 }, { "abcde", 0, 0 }, { "abcde", 2, 2 }, { "abcde", 1, 3 } };
String[][] expecteds = { { "", "XY", "XXYYY", "YY", "XXXX" },
{ "abcde", "XYabcde", "XXYYYabcde", "YYabcde", "XXXXabcde" },
{ "abcde", "abXYcde", "abXXYYYcde", "abYYcde", "abXXXXcde" },
{ "abcde", "aXbcYde", "aXXbcYYYde", "abcYYde", "aXXXXbcde" } };
for (int i = 0; i < patterns.length; i++) {
for (int j = 0; j < outputs.length; j++) {
String pattern = patterns[i];
String compiledPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern,
new StringBuilder(), 1, 1);
NumberStringBuilder output = new NumberStringBuilder();
output.append((String) outputs[j][0], null);
new SimpleModifier(compiledPattern, null, false).apply(output, (Integer) outputs[j][1],
(Integer) outputs[j][2]);
String expected = expecteds[j][i];
String actual = output.toString();
assert expected.equals(actual);
}
}
}
}

View File

@ -13,11 +13,10 @@ import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import com.ibm.icu.impl.number.AffixPatternUtils;
import com.ibm.icu.impl.number.Parse;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.PatternAndPropertyUtils;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.ThingsNeedingNewHome;
import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.math.BigDecimal;
import com.ibm.icu.math.MathContext;
@ -33,6 +32,7 @@ import newapi.LocalizedNumberFormatter;
import newapi.NumberFormatter;
import newapi.NumberPropertyMapper;
import newapi.impl.MacroProps;
import newapi.impl.Padder.PadPosition;
/**
* {@icuenhanced java.text.DecimalFormat}.{@icu _usage_} <code>DecimalFormat</code> is the primary
@ -301,7 +301,7 @@ public class DecimalFormat extends NumberFormat {
properties = new Properties();
exportedProperties = new Properties();
// Regression: ignore pattern rounding information if the pattern has currency symbols.
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY);
refreshFormatter();
}
@ -330,7 +330,7 @@ public class DecimalFormat extends NumberFormat {
properties = new Properties();
exportedProperties = new Properties();
// Regression: ignore pattern rounding information if the pattern has currency symbols.
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY);
refreshFormatter();
}
@ -359,7 +359,7 @@ public class DecimalFormat extends NumberFormat {
properties = new Properties();
exportedProperties = new Properties();
// Regression: ignore pattern rounding information if the pattern has currency symbols.
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY);
refreshFormatter();
}
@ -402,9 +402,9 @@ public class DecimalFormat extends NumberFormat {
|| choice == CASHCURRENCYSTYLE
|| choice == STANDARDCURRENCYSTYLE
|| choice == PLURALCURRENCYSTYLE) {
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_ALWAYS);
setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_ALWAYS);
} else {
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY);
}
refreshFormatter();
}
@ -445,7 +445,7 @@ public class DecimalFormat extends NumberFormat {
* @stable ICU 2.0
*/
public synchronized void applyPattern(String pattern) {
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_NEVER);
setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER);
// Backwards compatibility: clear out user-specified prefix and suffix,
// as well as CurrencyPluralInfo.
properties.setPositivePrefix(null);
@ -469,7 +469,7 @@ public class DecimalFormat extends NumberFormat {
* @stable ICU 2.0
*/
public synchronized void applyLocalizedPattern(String localizedPattern) {
String pattern = PatternString.convertLocalized(localizedPattern, symbols, false);
String pattern = PatternAndPropertyUtils.convertLocalized(localizedPattern, symbols, false);
applyPattern(pattern);
}
@ -752,7 +752,7 @@ public class DecimalFormat extends NumberFormat {
if (!(obj instanceof Number)) throw new IllegalArgumentException();
Number number = (Number) obj;
FormattedNumber output = formatter.format(number);
return output.toAttributedCharacterIterator();
return output.getAttributes();
}
/**
@ -2378,12 +2378,12 @@ public class DecimalFormat extends NumberFormat {
// so that CurrencyUsage is reflected properly.
// TODO: Consider putting this logic in PatternString.java instead.
Properties tprops = threadLocalProperties.get().copyFrom(properties);
if (ThingsNeedingNewHome.useCurrency(properties)) {
if (useCurrency(properties)) {
tprops.setMinimumFractionDigits(exportedProperties.getMinimumFractionDigits());
tprops.setMaximumFractionDigits(exportedProperties.getMaximumFractionDigits());
tprops.setRoundingIncrement(exportedProperties.getRoundingIncrement());
}
return PatternString.propertiesToString(tprops);
return PatternAndPropertyUtils.propertiesToString(tprops);
}
/**
@ -2396,7 +2396,21 @@ public class DecimalFormat extends NumberFormat {
*/
public synchronized String toLocalizedPattern() {
String pattern = toPattern();
return PatternString.convertLocalized(pattern, symbols, true);
return PatternAndPropertyUtils.convertLocalized(pattern, symbols, true);
}
/**
* Converts this DecimalFormat to a NumberFormatter. Starting in ICU 60,
* NumberFormatter is the recommended way to format numbers.
*
* @return An instance of {@link LocalizedNumberFormatter} with the same behavior as this instance of
* DecimalFormat.
* @see NumberFormatter
* @provisional This API might change or be removed in a future release.
* @draft ICU 60
*/
public LocalizedNumberFormatter toNumberFormatter() {
return formatter;
}
/**
@ -2460,6 +2474,20 @@ public class DecimalFormat extends NumberFormat {
}
}
/**
* Returns true if the currency is set in The property bag or if currency symbols are present in
* the prefix/suffix pattern.
*/
private static boolean useCurrency(Properties properties) {
return ((properties.getCurrency() != null)
|| properties.getCurrencyPluralInfo() != null
|| properties.getCurrencyUsage() != null
|| AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern()));
}
/**
* Updates the property bag with settings from the given pattern.
*
@ -2467,15 +2495,15 @@ public class DecimalFormat extends NumberFormat {
* @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
* increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
* as CurrencyUsage, is to be used instead. One of {@link
* PatternString#IGNORE_ROUNDING_ALWAYS}, {@link PatternString#IGNORE_ROUNDING_IF_CURRENCY},
* or {@link PatternString#IGNORE_ROUNDING_NEVER}.
* @see PatternString#parseToExistingProperties
* PatternAndPropertyUtils#IGNORE_ROUNDING_ALWAYS}, {@link PatternAndPropertyUtils#IGNORE_ROUNDING_IF_CURRENCY},
* or {@link PatternAndPropertyUtils#IGNORE_ROUNDING_NEVER}.
* @see PatternAndPropertyUtils#parseToExistingProperties
*/
void setPropertiesFromPattern(String pattern, int ignoreRounding) {
if (pattern == null) {
throw new NullPointerException();
}
PatternString.parseToExistingProperties(pattern, properties, ignoreRounding);
PatternAndPropertyUtils.parseToExistingProperties(pattern, properties, ignoreRounding);
}
/**

View File

@ -1134,8 +1134,6 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
* <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

View File

@ -1,19 +0,0 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.util;
public class Dimensionless extends MeasureUnit {
public static final Dimensionless BASE =
(Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "base");
public static final Dimensionless PERCENT =
(Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "percent");
public static final Dimensionless PERMILLE =
(Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "permille");
protected Dimensionless(String subType) {
super("dimensionless", subType);
}
}

View File

@ -195,8 +195,8 @@ public class MeasureUnit implements Serializable {
factory = CURRENCY_FACTORY;
} else if ("duration".equals(type)) {
factory = TIMEUNIT_FACTORY;
} else if ("dimensionless".equals(type)) {
factory = DIMENSIONLESS_FACTORY;
} else if ("none".equals(type)) {
factory = NOUNIT_FACTORY;
} else {
factory = UNIT_FACTORY;
}
@ -251,10 +251,10 @@ public class MeasureUnit implements Serializable {
}
};
static Factory DIMENSIONLESS_FACTORY = new Factory() {
static Factory NOUNIT_FACTORY = new Factory() {
@Override
public MeasureUnit create(String type, String subType) {
return new Dimensionless(subType);
return new NoUnit(subType);
}
};

View File

@ -0,0 +1,19 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.util;
public class NoUnit extends MeasureUnit {
public static final NoUnit BASE =
(NoUnit) MeasureUnit.internalGetInstance("none", "base");
public static final NoUnit PERCENT =
(NoUnit) MeasureUnit.internalGetInstance("none", "percent");
public static final NoUnit PERMILLE =
(NoUnit) MeasureUnit.internalGetInstance("none", "permille");
protected NoUnit(String subType) {
super("none", subType);
}
}

View File

@ -8,17 +8,17 @@ import java.util.Set;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.LdmlPatternInfo;
import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
import com.ibm.icu.impl.number.PatternParser;
import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CompactDecimalFormat.CompactType;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.ULocale;
import newapi.MurkyModifier.ImmutableMurkyModifier;
import newapi.MutablePatternModifier.ImmutableMurkyModifier;
import newapi.impl.CompactData;
import newapi.impl.MicroProps;
import newapi.impl.QuantityChain;
import newapi.impl.MicroPropsGenerator;
public class CompactNotation extends Notation {
@ -35,8 +35,8 @@ public class CompactNotation extends Notation {
this.compactCustomData = compactCustomData;
}
/* package-private */ QuantityChain withLocaleData(ULocale dataLocale, CompactType compactType, PluralRules rules,
MurkyModifier buildReference, QuantityChain parent) {
/* package-private */ MicroPropsGenerator withLocaleData(ULocale dataLocale, CompactType compactType, PluralRules rules,
MutablePatternModifier buildReference, MicroPropsGenerator parent) {
CompactData data;
if (compactStyle != null) {
data = CompactData.getInstance(dataLocale, compactType, compactStyle);
@ -46,7 +46,7 @@ public class CompactNotation extends Notation {
return new CompactImpl(data, rules, buildReference, parent);
}
private static class CompactImpl implements QuantityChain {
private static class CompactImpl implements MicroPropsGenerator {
private static class CompactModInfo {
public ImmutableMurkyModifier mod;
@ -56,9 +56,9 @@ public class CompactNotation extends Notation {
final PluralRules rules;
final CompactData data;
final Map<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) {
@ -73,23 +73,23 @@ public class CompactNotation extends Notation {
/** 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
@ -111,17 +111,17 @@ public class CompactNotation extends Notation {
// Use the default (non-compact) modifier.
// No need to take any action.
} else if (precomputedMods != null) {
// Build code path.
// Safe code path.
CompactModInfo info = precomputedMods.get(patternString);
info.mod.applyToMicros(micros, input);
numDigits = info.numDigits;
} else {
// Non-build code path.
// Unsafe code path.
// Overwrite the PatternInfo in the existing modMiddle
assert micros.modMiddle instanceof MurkyModifier;
PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString);
((MurkyModifier) micros.modMiddle).setPatternInfo(patternInfo);
numDigits = patternInfo.positive.totalIntegerDigits;
assert micros.modMiddle instanceof MutablePatternModifier;
ParsedPatternInfo patternInfo = PatternParser.parse(patternString);
((MutablePatternModifier) micros.modMiddle).setPatternInfo(patternInfo);
numDigits = patternInfo.positive.integerTotal;
}
// FIXME: Deal with numDigits == 0 (Awaiting a test case)

View File

@ -55,7 +55,7 @@ public class FormattedNumber {
fq.populateUFieldPosition(fieldPosition);
}
public AttributedCharacterIterator toAttributedCharacterIterator() {
public AttributedCharacterIterator getAttributes() {
return nsb.getIterator();
}

View File

@ -22,13 +22,13 @@ public abstract class FractionRounder extends Rounder {
* <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);
}
@ -46,13 +46,13 @@ public abstract class FractionRounder extends Rounder {
* This setting does not affect the number of trailing zeros. For example, with fixed fraction of 2, 123.4 would
* become "120.00".
*
* @param maxFigures
* @param maxSignificantDigits
* Round the number to no more than this number of significant figures.
* @return An immutable object for chaining.
*/
public Rounder withMaxFigures(int maxFigures) {
if (maxFigures > 0 && maxFigures <= MAX_VALUE) {
return constructFractionSignificant(this, -1, maxFigures);
public Rounder withMaxDigits(int maxSignificantDigits) {
if (maxSignificantDigits > 0 && maxSignificantDigits <= MAX_VALUE) {
return constructFractionSignificant(this, -1, maxSignificantDigits);
} else {
throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
}

View File

@ -3,7 +3,7 @@
package newapi;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
public class Grouper {
@ -64,11 +64,11 @@ public class Grouper {
}
}
static Grouper normalizeType(Grouper grouping, PatternParseResult patternInfo) {
static Grouper normalizeType(Grouper grouping, ParsedPatternInfo patternInfo) {
return grouping.withLocaleData(patternInfo);
}
Grouper withLocaleData(PatternParseResult patternInfo) {
Grouper withLocaleData(ParsedPatternInfo patternInfo) {
if (grouping1 != -2) {
return this;
}

View File

@ -11,28 +11,33 @@ import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.modifiers.SimpleModifier;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberFormat.Field;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter.UnitWidth;
import newapi.impl.MeasureData;
import newapi.impl.MicroProps;
import newapi.impl.QuantityChain;
import newapi.impl.MicroPropsGenerator;
class MurkyLongNameHandler implements QuantityChain {
class LongNameHandler implements MicroPropsGenerator {
private final Map<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();
@ -46,10 +51,10 @@ class MurkyLongNameHandler implements QuantityChain {
Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
result.put(plural, mod);
}
return new MurkyLongNameHandler(result);
return new LongNameHandler(result);
}
public static MurkyLongNameHandler getMeasureUnitModifiers(ULocale loc, MeasureUnit unit, FormatWidth width) {
public static LongNameHandler getMeasureUnitModifiers(ULocale loc, MeasureUnit unit, UnitWidth width) {
Map<StandardPlural, String> simpleFormats = MeasureData.getMeasureData(loc, unit, width);
Map<StandardPlural, Modifier> result = new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
StringBuilder sb = new StringBuilder();
@ -62,13 +67,28 @@ class MurkyLongNameHandler implements QuantityChain {
Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
result.put(plural, mod);
}
return new MurkyLongNameHandler(result);
return new LongNameHandler(result);
}
public QuantityChain withLocaleData(PluralRules rules, boolean safe, QuantityChain parent) {
/**
* Applies locale data and inserts a long-name handler into the quantity chain.
*
* @param rules
* The PluralRules instance to reference.
* @param safe
* If true, creates a new object to insert into the quantity chain. If false, re-uses <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;
@ -78,34 +98,12 @@ class MurkyLongNameHandler implements QuantityChain {
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
MicroProps micros = parent.withQuantity(quantity);
public MicroProps processQuantity(FormatQuantity quantity) {
MicroProps micros = parent.processQuantity(quantity);
// TODO: Avoid the copy here?
FormatQuantity copy = quantity.createCopy();
micros.rounding.apply(copy);
micros.modOuter = data.get(copy.getStandardPlural(rules));
return micros;
}
public static class ImmutableLongNameHandler implements QuantityChain {
final Map<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;
}
}
}

View File

@ -6,20 +6,20 @@ import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.AffixPatternUtils;
import com.ibm.icu.impl.number.AffixPatternUtils.SymbolProvider;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.LdmlPatternInfo;
import com.ibm.icu.impl.number.PatternParser;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
import com.ibm.icu.impl.number.modifiers.CurrencySpacingEnabledModifier;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnitWidth;
import newapi.impl.AffixPatternProvider;
import newapi.impl.MicroProps;
import newapi.impl.QuantityChain;
import newapi.impl.MicroPropsGenerator;
/**
* This class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's affixes in
@ -37,11 +37,12 @@ import newapi.impl.QuantityChain;
* <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;
@ -53,7 +54,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
// Symbol details
DecimalFormatSymbols symbols;
FormatWidth unitWidth;
UnitWidth unitWidth;
String currency1;
String currency2;
String[] currency3;
@ -64,7 +65,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
StandardPlural plural;
// QuantityChain details
QuantityChain parent;
MicroPropsGenerator parent;
// Transient CharSequence fields
boolean inCharSequenceMode;
@ -79,13 +80,13 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
* {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should be considered
* as non-strong.
*/
public MurkyModifier(boolean isStrong) {
public MutablePatternModifier(boolean isStrong) {
this.isStrong = isStrong;
}
/**
* Sets a reference to the parsed decimal format pattern, usually obtained from
* {@link LdmlPatternInfo#parse(String)}, but any implementation of {@link AffixPatternProvider} is accepted.
* {@link PatternParser#parse(String)}, but any implementation of {@link AffixPatternProvider} is accepted.
*/
public void setPatternInfo(AffixPatternProvider patternInfo) {
this.patternInfo = patternInfo;
@ -118,7 +119,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
* Required if the triple currency sign, "¤¤¤", appears in the pattern, which can be determined from the
* convenience method {@link #needsPlurals()}.
*/
public void setSymbols(DecimalFormatSymbols symbols, Currency currency, FormatWidth unitWidth, PluralRules rules) {
public void setSymbols(DecimalFormatSymbols symbols, Currency currency, UnitWidth unitWidth, PluralRules rules) {
assert (rules != null) == needsPlurals();
this.symbols = symbols;
this.unitWidth = unitWidth;
@ -182,7 +183,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
* The QuantityChain to which to chain this immutable.
* @return An immutable that supports both positive and negative numbers.
*/
public ImmutableMurkyModifier createImmutableAndChain(QuantityChain parent) {
public ImmutableMurkyModifier createImmutableAndChain(MicroPropsGenerator parent) {
NumberStringBuilder a = new NumberStringBuilder();
NumberStringBuilder b = new NumberStringBuilder();
if (needsPlurals()) {
@ -217,25 +218,25 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
}
}
public static interface ImmutableMurkyModifier extends QuantityChain {
public static interface ImmutableMurkyModifier extends MicroPropsGenerator {
public void applyToMicros(MicroProps micros, FormatQuantity quantity);
}
public static class ImmutableMurkyModifierWithoutPlurals implements ImmutableMurkyModifier {
final Modifier positive;
final Modifier negative;
final QuantityChain parent;
final MicroPropsGenerator parent;
public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative, QuantityChain parent) {
public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative, MicroPropsGenerator parent) {
this.positive = positive;
this.negative = negative;
this.parent = parent;
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
public MicroProps processQuantity(FormatQuantity quantity) {
assert parent != null;
MicroProps micros = parent.withQuantity(quantity);
MicroProps micros = parent.processQuantity(quantity);
applyToMicros(micros, quantity);
return micros;
}
@ -253,9 +254,9 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
public static class ImmutableMurkyModifierWithPlurals implements ImmutableMurkyModifier {
final Modifier[] mods;
final PluralRules rules;
final QuantityChain parent;
final MicroPropsGenerator parent;
public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules, QuantityChain parent) {
public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules, MicroPropsGenerator parent) {
assert mods.length == getModsLength();
assert rules != null;
this.mods = mods;
@ -272,9 +273,9 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
public MicroProps processQuantity(FormatQuantity quantity) {
assert parent != null;
MicroProps micros = parent.withQuantity(quantity);
MicroProps micros = parent.processQuantity(quantity);
applyToMicros(micros, quantity);
return micros;
}
@ -290,14 +291,14 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
}
}
public QuantityChain addToChain(QuantityChain parent) {
public MicroPropsGenerator addToChain(MicroPropsGenerator parent) {
this.parent = parent;
return this;
}
@Override
public MicroProps withQuantity(FormatQuantity fq) {
MicroProps micros = parent.withQuantity(fq);
public MicroProps processQuantity(FormatQuantity fq) {
MicroProps micros = parent.processQuantity(fq);
if (needsPlurals()) {
// TODO: Fix this. Avoid the copy.
FormatQuantity copy = fq.createCopy();
@ -321,7 +322,8 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
@Override
public int getPrefixLength() {
return insertPrefix(null, 0);
NumberStringBuilder dummy = new NumberStringBuilder();
return insertPrefix(dummy, 0);
}
@Override
@ -356,7 +358,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
return symbols.getPerMillString();
case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
// FormatWidth ISO overrides the singular currency symbol
if (unitWidth == FormatWidth.SHORT) {
if (unitWidth == UnitWidth.ISO_CODE) {
return currency2;
} else {
return currency1;
@ -387,7 +389,8 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
inCharSequenceMode = true;
// Should the output render '+' where '-' would normally appear in the pattern?
plusReplacesMinusSign = !isNegative && signDisplay == SignDisplay.ALWAYS
plusReplacesMinusSign = !isNegative
&& (signDisplay == SignDisplay.ALWAYS || signDisplay == SignDisplay.ACCOUNTING_ALWAYS)
&& patternInfo.positiveHasPlusSign() == false;
// Should we use the negative affix pattern? (If not, we will use the positive one)

View File

@ -4,48 +4,43 @@ package newapi;
import java.util.Locale;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
public final class NumberFormatter {
private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter();
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 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 DecimalMarkDisplay {
AUTO, ALWAYS,
}
public static enum SignDisplay {
AUTO,
ALWAYS,
NEVER,
}
public static enum SignDisplay {
AUTO, ALWAYS, NEVER, ACCOUNTING, ACCOUNTING_ALWAYS,
}
public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) {
// FIXME
throw new UnsupportedOperationException();
}
public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) {
// FIXME
throw new UnsupportedOperationException();
}
public static UnlocalizedNumberFormatter with() {
return BASE;
}
public static UnlocalizedNumberFormatter with() {
return BASE;
}
public static LocalizedNumberFormatter withLocale(Locale locale) {
return BASE.locale(locale);
}
public static LocalizedNumberFormatter withLocale(Locale locale) {
return BASE.locale(locale);
}
public static LocalizedNumberFormatter withLocale(ULocale locale) {
return BASE.locale(locale);
}
public static LocalizedNumberFormatter withLocale(ULocale locale) {
return BASE.locale(locale);
}
}

View File

@ -5,14 +5,22 @@ package newapi;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.NoUnit;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnitWidth;
import newapi.impl.MacroProps;
import newapi.impl.Padder;
/**
* An abstract base class for specifying settings related to number formatting. This class is implemented by
* {@link UnlocalizedNumberFormatter} and {@link LocalizedNumberFormatter}.
*/
public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<?>> {
static final int KEY_MACROS = 0;
@ -41,35 +49,292 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
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&nbsp;345,67"
* <li><em>de_CH</em> symbols: "12345.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);
}
@ -161,7 +426,7 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
break;
case KEY_UNIT_WIDTH:
if (macros.unitWidth == null) {
macros.unitWidth = (FormatWidth) current.value;
macros.unitWidth = (UnitWidth) current.value;
}
break;
case KEY_SIGN:

View File

@ -7,9 +7,9 @@ import java.math.MathContext;
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;
@ -44,7 +44,7 @@ public final class NumberPropertyMapper {
* 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);
}
@ -407,9 +407,9 @@ public final class NumberPropertyMapper {
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()));
}
}

View File

@ -33,65 +33,65 @@ public abstract class Rounder implements Cloneable {
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);
}

View File

@ -12,7 +12,7 @@ import newapi.NumberFormatter.SignDisplay;
import newapi.Rounder.SignificantRounderImpl;
import newapi.impl.MicroProps;
import newapi.impl.MultiplierProducer;
import newapi.impl.QuantityChain;
import newapi.impl.MicroPropsGenerator;
@SuppressWarnings("unused")
public class ScientificNotation extends Notation implements Cloneable {
@ -56,19 +56,19 @@ public class ScientificNotation extends Notation implements Cloneable {
}
}
/* package-private */ QuantityChain withLocaleData(DecimalFormatSymbols symbols, boolean build,
QuantityChain parent) {
/* package-private */ MicroPropsGenerator withLocaleData(DecimalFormatSymbols symbols, boolean build,
MicroPropsGenerator parent) {
return new MurkyScientificHandler(symbols, build, parent);
}
private class MurkyScientificHandler implements QuantityChain, MultiplierProducer, Modifier {
private class MurkyScientificHandler implements MicroPropsGenerator, MultiplierProducer, Modifier {
final DecimalFormatSymbols symbols;
final ImmutableScientificModifier[] precomputedMods;
final QuantityChain parent;
final MicroPropsGenerator parent;
/* unsafe */ int exponent;
private MurkyScientificHandler(DecimalFormatSymbols symbols, boolean safe, QuantityChain parent) {
private MurkyScientificHandler(DecimalFormatSymbols symbols, boolean safe, MicroPropsGenerator parent) {
this.symbols = symbols;
this.parent = parent;
@ -84,8 +84,8 @@ public class ScientificNotation extends Notation implements Cloneable {
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
MicroProps micros = parent.withQuantity(quantity);
public MicroProps processQuantity(FormatQuantity quantity) {
MicroProps micros = parent.processQuantity(quantity);
assert micros.rounding != null;
// Treat zero as if it had magnitude 0

View File

@ -8,15 +8,15 @@ import java.math.RoundingMode;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.Dimensionless;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.NoUnit;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnitWidth;
import newapi.Rounder.CurrencyRounderImpl;
import newapi.Rounder.FracSigRounderImpl;
import newapi.Rounder.FractionRounderImpl;
@ -228,7 +228,7 @@ final class SkeletonBuilder {
}
private static void unitToSkeleton(MeasureUnit value, StringBuilder sb) {
if (value.getType().equals("dimensionless")) {
if (value.getType().equals("none")) {
if (value.getSubtype().equals("percent")) {
sb.append('%');
} else if (value.getSubtype().equals("permille")) {
@ -255,12 +255,12 @@ final class SkeletonBuilder {
if (c0 == '%') {
char c = safeCharAt(skeleton, offset++);
if (c == '%') {
result = Dimensionless.PERCENT;
result = NoUnit.PERCENT;
} else {
result = Dimensionless.PERMILLE;
result = NoUnit.PERMILLE;
}
} else if (c0 == 'B') {
result = Dimensionless.BASE;
result = NoUnit.BASE;
} else if (c0 == '$') {
String currencyCode = skeleton.substring(offset, offset + 3);
offset += 3;
@ -360,10 +360,10 @@ final class SkeletonBuilder {
char c1 = skeleton.charAt(offset++);
if (c1 == '<') {
char c2 = skeleton.charAt(offset++);
result = temp.withMaxFigures(c2 - '0');
result = temp.withMaxDigits(c2 - '0');
} else if (c1 == '>') {
char c2 = skeleton.charAt(offset++);
result = temp.withMinFigures(c2 - '0');
result = temp.withMinDigits(c2 - '0');
} else {
result = temp;
}
@ -518,7 +518,7 @@ final class SkeletonBuilder {
}
}
private static void unitWidthToSkeleton(FormatWidth value, StringBuilder sb) {
private static void unitWidthToSkeleton(UnitWidth value, StringBuilder sb) {
sb.append(value.name());
}
@ -526,7 +526,7 @@ final class SkeletonBuilder {
int originalOffset = offset;
StringBuilder sb = new StringBuilder();
offset += consumeUntil(skeleton, offset, ' ', sb);
output.unitWidth = Enum.valueOf(FormatWidth.class, sb.toString());
output.unitWidth = Enum.valueOf(UnitWidth.class, sb.toString());
return offset - originalOffset;
}

View File

@ -3,122 +3,145 @@
package newapi;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.LdmlPatternInfo;
import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
import com.ibm.icu.impl.number.PatternParser;
import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.text.CompactDecimalFormat.CompactType;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.Dimensionless;
import com.ibm.icu.util.NoUnit;
import com.ibm.icu.util.ULocale;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnitWidth;
import newapi.impl.MacroProps;
import newapi.impl.MicroProps;
import newapi.impl.MicroPropsGenerator;
import newapi.impl.Padder;
import newapi.impl.QuantityChain;
public class Worker1 {
public static Worker1 fromMacros(MacroProps macros) {
return new Worker1(make(macros, true));
// Build a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly.
MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, true);
return new Worker1(microPropsGenerator);
}
public static MicroProps applyStatic(MacroProps macros, FormatQuantity inValue, NumberStringBuilder outString) {
MicroProps micros = make(macros, false).withQuantity(inValue);
applyStatic(micros, inValue, outString);
// Build an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once.
MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false);
MicroProps micros = microPropsGenerator.processQuantity(inValue);
microsToString(micros, inValue, outString);
return micros;
}
private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
final QuantityChain microsGenerator;
final MicroPropsGenerator microPropsGenerator;
private Worker1(QuantityChain microsGenerator) {
this.microsGenerator = microsGenerator;
private Worker1(MicroPropsGenerator microsGenerator) {
this.microPropsGenerator = microsGenerator;
}
public MicroProps apply(FormatQuantity inValue, NumberStringBuilder outString) {
MicroProps micros = microsGenerator.withQuantity(inValue);
applyStatic(micros, inValue, outString);
MicroProps micros = microPropsGenerator.processQuantity(inValue);
microsToString(micros, inValue, outString);
return micros;
}
//////////
private static QuantityChain make(MacroProps input, boolean build) {
/**
* Synthesizes the MacroProps into a MicroPropsGenerator. All information, including the locale, is encoded into the
* MicroPropsGenerator, except for the quantity itself, which is left abstract and must be provided to the returned
* MicroPropsGenerator instance.
*
* @see MicroPropsGenerator
* @param macros
* The {@link MacroProps} to consume. This method does not mutate the MacroProps instance.
* @param safe
* If true, the returned MicroPropsGenerator will be thread-safe. If false, the returned value will
* <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();
@ -130,23 +153,23 @@ public class Worker1 {
// Multiplier (compatibility mode value).
// An int magnitude multiplier is used when not in compatibility mode to
// reduce object creations.
if (input.multiplier != null) {
chain = input.multiplier.copyAndChain(chain);
if (macros.multiplier != null) {
chain = macros.multiplier.copyAndChain(chain);
}
// Rounding strategy
if (input.rounder != null) {
micros.rounding = Rounder.normalizeType(input.rounder, currency);
} else if (input.notation instanceof CompactNotation) {
if (macros.rounder != null) {
micros.rounding = Rounder.normalizeType(macros.rounder, currency);
} else if (macros.notation instanceof CompactNotation) {
micros.rounding = Rounder.COMPACT_STRATEGY;
} else {
micros.rounding = Rounder.normalizeType(defaultRounding, currency);
}
// Grouping strategy
if (input.grouper != null) {
micros.grouping = Grouper.normalizeType(input.grouper, patternInfo);
} else if (input.notation instanceof CompactNotation) {
if (macros.grouper != null) {
micros.grouping = Grouper.normalizeType(macros.grouper, patternInfo);
} else if (macros.notation instanceof CompactNotation) {
// Compact notation uses minGrouping by default since ICU 59
micros.grouping = Grouper.normalizeType(Grouper.min2(), patternInfo);
} else {
@ -154,8 +177,8 @@ public class Worker1 {
}
// Inner modifier (scientific notation)
if (input.notation instanceof ScientificNotation) {
chain = ((ScientificNotation) input.notation).withLocaleData(micros.symbols, build, chain);
if (macros.notation instanceof ScientificNotation) {
chain = ((ScientificNotation) macros.notation).withLocaleData(micros.symbols, safe, chain);
} else {
// No inner modifier required
micros.modInner = ConstantAffixModifier.EMPTY;
@ -163,39 +186,39 @@ public class Worker1 {
// Middle modifier (patterns, positive/negative, currency symbols, percent)
// The default middle modifier is weak (thus the false argument).
MurkyModifier murkyMod = new MurkyModifier(false);
murkyMod.setPatternInfo((input.affixProvider != null) ? input.affixProvider : patternInfo);
murkyMod.setPatternAttributes(micros.sign, perMille);
if (murkyMod.needsPlurals()) {
MutablePatternModifier patternMod = new MutablePatternModifier(false);
patternMod.setPatternInfo((macros.affixProvider != null) ? macros.affixProvider : patternInfo);
patternMod.setPatternAttributes(micros.sign, perMille);
if (patternMod.needsPlurals()) {
if (rules == null) {
// Lazily create PluralRules
rules = PluralRules.forLocale(input.loc);
rules = PluralRules.forLocale(macros.loc);
}
murkyMod.setSymbols(micros.symbols, currency, unitWidth, rules);
patternMod.setSymbols(micros.symbols, currency, unitWidth, rules);
} else {
murkyMod.setSymbols(micros.symbols, currency, unitWidth, null);
patternMod.setSymbols(micros.symbols, currency, unitWidth, null);
}
if (build) {
chain = murkyMod.createImmutableAndChain(chain);
if (safe) {
chain = patternMod.createImmutableAndChain(chain);
} else {
chain = murkyMod.addToChain(chain);
chain = patternMod.addToChain(chain);
}
// Outer modifier (CLDR units and currency long names)
if (longNames != null) {
if (rules == null) {
// Lazily create PluralRules
rules = PluralRules.forLocale(input.loc);
rules = PluralRules.forLocale(macros.loc);
}
chain = longNames.withLocaleData(rules, build, chain);
chain = longNames.withLocaleData(rules, safe, chain);
} else {
// No outer modifier required
micros.modOuter = ConstantAffixModifier.EMPTY;
}
// Padding strategy
if (input.padder != null) {
micros.padding = input.padder;
if (macros.padder != null) {
micros.padding = macros.padder;
} else {
micros.padding = Padder.none();
}
@ -203,14 +226,14 @@ public class Worker1 {
// Compact notation
// NOTE: Compact notation can (but might not) override the middle modifier and rounding.
// It therefore needs to go at the end of the chain.
if (input.notation instanceof CompactNotation) {
if (macros.notation instanceof CompactNotation) {
if (rules == null) {
// Lazily create PluralRules
rules = PluralRules.forLocale(input.loc);
rules = PluralRules.forLocale(macros.loc);
}
CompactType compactType = (input.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL;
chain = ((CompactNotation) input.notation).withLocaleData(input.loc, compactType, rules,
build ? murkyMod : null, chain);
CompactType compactType = (macros.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL;
chain = ((CompactNotation) macros.notation).withLocaleData(macros.loc, compactType, rules,
safe ? patternMod : null, chain);
}
return chain;
@ -218,58 +241,67 @@ public class Worker1 {
//////////
private static int applyStatic(MicroProps micros, FormatQuantity inValue, NumberStringBuilder outString) {
inValue.adjustMagnitude(micros.multiplier);
micros.rounding.apply(inValue);
/**
* Synthesizes the output string from a MicroProps and FormatQuantity.
*
* @param micros
* The MicroProps after the quantity has been consumed. Will not be mutated.
* @param quantity
* The FormatQuantity to be rendered. May be mutated.
* @param string
* The output string. Will be mutated.
*/
private static void microsToString(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) {
quantity.adjustMagnitude(micros.multiplier);
micros.rounding.apply(quantity);
if (micros.integerWidth.maxInt == -1) {
inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
quantity.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
} else {
inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
quantity.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
}
int length = writeNumber(micros, inValue, outString);
int length = writeNumber(micros, quantity, string);
// NOTE: When range formatting is added, these modifiers can bubble up.
// For now, apply them all here at once.
length += micros.padding.applyModsAndMaybePad(micros, outString, 0, length);
return length;
length += micros.padding.applyModsAndMaybePad(micros, string, 0, length);
}
private static int writeNumber(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
private static int writeNumber(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) {
int length = 0;
if (input.isInfinite()) {
if (quantity.isInfinite()) {
length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
} else if (input.isNaN()) {
} else if (quantity.isNaN()) {
length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
} else {
// Add the integer digits
length += writeIntegerDigits(micros, input, string);
length += writeIntegerDigits(micros, quantity, string);
// Add the decimal point
if (input.getLowerDisplayMagnitude() < 0 || micros.decimal == DecimalMarkDisplay.ALWAYS) {
if (quantity.getLowerDisplayMagnitude() < 0 || micros.decimal == DecimalMarkDisplay.ALWAYS) {
length += string.insert(length, micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString()
: micros.symbols.getDecimalSeparatorString(), NumberFormat.Field.DECIMAL_SEPARATOR);
}
// Add the fraction digits
length += writeFractionDigits(micros, input, string);
length += writeFractionDigits(micros, quantity, string);
}
return length;
}
private static int writeIntegerDigits(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
private static int writeIntegerDigits(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) {
int length = 0;
int integerCount = input.getUpperDisplayMagnitude() + 1;
int integerCount = quantity.getUpperDisplayMagnitude() + 1;
for (int i = 0; i < integerCount; i++) {
// Add grouping separator
if (micros.grouping.groupAtPosition(i, input)) {
if (micros.grouping.groupAtPosition(i, quantity)) {
length += string.insert(0, micros.useCurrency ? micros.symbols.getMonetaryGroupingSeparatorString()
: micros.symbols.getGroupingSeparatorString(), NumberFormat.Field.GROUPING_SEPARATOR);
}
// Get and append the next digit value
byte nextDigit = input.getDigit(i);
byte nextDigit = quantity.getDigit(i);
if (micros.symbols.getCodePointZero() != -1) {
length += string.insertCodePoint(0, micros.symbols.getCodePointZero() + nextDigit,
NumberFormat.Field.INTEGER);
@ -281,12 +313,12 @@ public class Worker1 {
return length;
}
private static int writeFractionDigits(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
private static int writeFractionDigits(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) {
int length = 0;
int fractionCount = -input.getLowerDisplayMagnitude();
int fractionCount = -quantity.getLowerDisplayMagnitude();
for (int i = 0; i < fractionCount; i++) {
// Get and append the next digit value
byte nextDigit = input.getDigit(-i - 1);
byte nextDigit = quantity.getDigit(-i - 1);
if (micros.symbols.getCodePointZero() != -1) {
length += string.appendCodePoint(micros.symbols.getCodePointZero() + nextDigit,
NumberFormat.Field.FRACTION);

View File

@ -4,7 +4,6 @@ package newapi.impl;
import java.util.Objects;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
@ -12,10 +11,10 @@ import com.ibm.icu.util.ULocale;
import newapi.Grouper;
import newapi.IntegerWidth;
import newapi.Notation;
import newapi.NumberFormatter;
import newapi.Rounder;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnitWidth;
import newapi.Rounder;
public class MacroProps implements Cloneable {
public Notation notation;
@ -25,7 +24,7 @@ public class MacroProps implements Cloneable {
public Padder padder;
public IntegerWidth integerWidth;
public Object symbols;
public FormatWidth unitWidth;
public UnitWidth unitWidth;
public SignDisplay sign;
public DecimalMarkDisplay decimal;
public AffixPatternProvider affixProvider; // not in API; for JDK compatibility mode only

View File

@ -9,11 +9,12 @@ import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
import newapi.NumberFormatter.UnitWidth;
public class MeasureData {
private static final class ShanesMeasureUnitSink extends UResource.Sink {
@ -42,14 +43,14 @@ public class MeasureData {
}
public static Map<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("/");

View File

@ -13,7 +13,7 @@ import newapi.Rounder;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.SignDisplay;
public class MicroProps implements Cloneable, QuantityChain {
public class MicroProps implements Cloneable, MicroPropsGenerator {
// Populated globally:
public SignDisplay sign;
public DecimalFormatSymbols symbols;
@ -41,7 +41,7 @@ public class MicroProps implements Cloneable, QuantityChain {
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
public MicroProps processQuantity(FormatQuantity quantity) {
if (immutable) {
return (MicroProps) this.clone();
} else {

View File

@ -0,0 +1,54 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.impl.number.FormatQuantity;
/**
* This interface is used when all number formatting settings, including the locale, are known, except for the quantity
* itself. The {@link #processQuantity} method performs the final step in the number processing pipeline: it uses the
* quantity to generate a finalized {@link MicroProps}, which can be used to render the number to output.
*
* <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;
* }
*
* &#64;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);
}

View File

@ -0,0 +1,13 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
/**
* @author sffc
*
*/
public interface MicroPropsMutator<T> {
public void mutateMicros(MicroProps micros, T value);
}

View File

@ -6,10 +6,10 @@ import java.math.BigDecimal;
import com.ibm.icu.impl.number.FormatQuantity;
public class MultiplierImpl implements QuantityChain, Cloneable {
public class MultiplierImpl implements MicroPropsGenerator, Cloneable {
final int magnitudeMultiplier;
final BigDecimal bigDecimalMultiplier;
final QuantityChain parent;
final MicroPropsGenerator parent;
public MultiplierImpl(int magnitudeMultiplier) {
this.magnitudeMultiplier = magnitudeMultiplier;
@ -23,19 +23,19 @@ public class MultiplierImpl implements QuantityChain, Cloneable {
parent = null;
}
private MultiplierImpl(MultiplierImpl base, QuantityChain parent) {
private MultiplierImpl(MultiplierImpl base, MicroPropsGenerator parent) {
this.magnitudeMultiplier = base.magnitudeMultiplier;
this.bigDecimalMultiplier = base.bigDecimalMultiplier;
this.parent = parent;
}
public QuantityChain copyAndChain(QuantityChain parent) {
public MicroPropsGenerator copyAndChain(MicroPropsGenerator parent) {
return new MultiplierImpl(this, parent);
}
@Override
public MicroProps withQuantity(FormatQuantity quantity) {
MicroProps micros = parent.withQuantity(quantity);
public MicroProps processQuantity(FormatQuantity quantity) {
MicroProps micros = parent.processQuantity(quantity);
quantity.adjustMagnitude(magnitudeMultiplier);
if (bigDecimalMultiplier != null) {
quantity.multiplyBy(bigDecimalMultiplier);

View File

@ -3,9 +3,46 @@
package newapi.impl;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
public class Padder {
public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space
public enum PadPosition {
BEFORE_PREFIX,
AFTER_PREFIX,
BEFORE_SUFFIX,
AFTER_SUFFIX;
public static PadPosition fromOld(int old) {
switch (old) {
case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX:
return PadPosition.BEFORE_PREFIX;
case com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX:
return PadPosition.AFTER_PREFIX;
case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX:
return PadPosition.BEFORE_SUFFIX;
case com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX:
return PadPosition.AFTER_SUFFIX;
default:
throw new IllegalArgumentException("Don't know how to map " + old);
}
}
public int toOld() {
switch (this) {
case BEFORE_PREFIX:
return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX;
case AFTER_PREFIX:
return com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX;
case BEFORE_SUFFIX:
return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX;
case AFTER_SUFFIX:
return com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX;
default:
return -1; // silence compiler errors
}
}
}
private static final Padder NONE = new Padder(null, -1, null);

View File

@ -1,10 +0,0 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package newapi.impl;
import com.ibm.icu.impl.number.FormatQuantity;
public interface QuantityChain {
//QuantityChain addToChain(QuantityChain parent);
MicroProps withQuantity(FormatQuantity quantity);
}

View File

@ -5,21 +5,21 @@ package newapi.impl;
import java.math.RoundingMode;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.Dimensionless;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.NoUnit;
import com.ibm.icu.util.ULocale;
import newapi.Grouper;
import newapi.Notation;
import newapi.NumberFormatter;
import newapi.Rounder;
import newapi.UnlocalizedNumberFormatter;
import newapi.NumberFormatter.DecimalMarkDisplay;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnitWidth;
import newapi.Rounder;
import newapi.UnlocalizedNumberFormatter;
public class demo {
public static void main(String[] args) {
@ -31,9 +31,9 @@ public class demo {
.notation(Notation.engineering().withMinExponentDigits(2))
.notation(Notation.simple())
.unit(Currency.getInstance("GBP"))
.unit(Dimensionless.PERCENT)
.unit(NoUnit.PERCENT)
.unit(MeasureUnit.CUBIC_METER)
.unitWidth(FormatWidth.SHORT)
.unitWidth(UnitWidth.SHORT)
// .rounding(Rounding.fixedSignificantDigits(3))
// .rounding(
// (BigDecimal input) -> {

View File

@ -17,6 +17,7 @@ import java.io.Serializable;
import java.lang.reflect.Field;
import java.text.FieldPosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@ -41,6 +42,7 @@ import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.NoUnit;
import com.ibm.icu.util.TimeUnit;
import com.ibm.icu.util.TimeUnitAmount;
import com.ibm.icu.util.ULocale;
@ -241,7 +243,6 @@ public class MeasureUnitTest extends TestFmwk {
}
}
/*
@Test
public void testZZZ() {
// various generateXXX calls go here, see
@ -254,7 +255,6 @@ public class MeasureUnitTest extends TestFmwk {
//generateCXXBackwardCompatibilityTest("59"); // for measfmttest.cpp, create TestCompatible59
//updateJAVAVersions("59"); // for MeasureUnitTest.java, JAVA_VERSIONS
}
*/
@Test
public void TestCompatible53() {
@ -2000,7 +2000,7 @@ public class MeasureUnitTest extends TestFmwk {
if (type.equals("currency")
|| type.equals("compound")
|| type.equals("coordinate")
|| type.equals("dimensionless")) {
|| type.equals("none")) {
continue;
}
for (MeasureUnit unit : MeasureUnit.getAvailable(type)) {
@ -2147,6 +2147,9 @@ public class MeasureUnitTest extends TestFmwk {
System.out.println("");
TreeMap<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()) {
@ -2246,7 +2249,7 @@ public class MeasureUnitTest extends TestFmwk {
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()) {

View File

@ -10,9 +10,8 @@ import org.junit.Test;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.PatternAndPropertyUtils;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.DecimalFormat_ICU58;
import com.ibm.icu.util.CurrencyAmount;
@ -20,6 +19,7 @@ import com.ibm.icu.util.ULocale;
import newapi.LocalizedNumberFormatter;
import newapi.NumberPropertyMapper;
import newapi.impl.Padder.PadPosition;
public class NumberFormatDataDrivenTest {
@ -509,8 +509,8 @@ public class NumberFormatDataDrivenTest {
}
if (tuple.localizedPattern != null) {
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(tuple.locale);
String converted = PatternString.convertLocalized(tuple.localizedPattern, symbols, false);
PatternString.parseToExistingProperties(converted, properties);
String converted = PatternAndPropertyUtils.convertLocalized(tuple.localizedPattern, symbols, false);
PatternAndPropertyUtils.parseToExistingProperties(converted, properties);
}
if (tuple.lenient != null) {
properties.setParseMode(tuple.lenient == 0 ? ParseMode.STRICT : ParseMode.LENIENT);
@ -548,11 +548,11 @@ public class NumberFormatDataDrivenTest {
String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
ULocale locale = (tuple.locale == null) ? ULocale.ENGLISH : tuple.locale;
Properties properties =
PatternString.parseToProperties(
PatternAndPropertyUtils.parseToProperties(
pattern,
tuple.currency != null
? PatternString.IGNORE_ROUNDING_ALWAYS
: PatternString.IGNORE_ROUNDING_NEVER);
? PatternAndPropertyUtils.IGNORE_ROUNDING_ALWAYS
: PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER);
propertiesFromTuple(tuple, properties);
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
LocalizedNumberFormatter fmt = NumberPropertyMapper.create(properties, symbols).locale(locale);

View File

@ -5188,7 +5188,7 @@ public class NumberFormatTest extends TestFmwk {
}
@Test
public void Test13113() {
public void Test13113_MalformedPatterns() {
String[][] cases = {
{"'", "quoted literal"},
{"ab#c'd", "quoted literal"},
@ -5197,7 +5197,8 @@ public class NumberFormatTest extends TestFmwk {
{".#0", "0 cannot follow #"},
{"@0", "Cannot mix @ and 0"},
{"0@", "Cannot mix 0 and @"},
{"#x#", "unquoted special character"}
{"#x#", "unquoted special character"},
{"@#@", "# inside of a run of @"},
};
for (String[] cas : cases) {
try {

View File

@ -15,6 +15,42 @@ import com.ibm.icu.util.ULocale;
public class AffixPatternUtilsTest {
private static final SymbolProvider DEFAULT_SYMBOL_PROVIDER =
new SymbolProvider() {
// ar_SA has an interesting percent sign and various Arabic letter marks
private final DecimalFormatSymbols SYMBOLS =
DecimalFormatSymbols.getInstance(new ULocale("ar_SA"));
@Override
public CharSequence getSymbol(int type) {
switch (type) {
case AffixPatternUtils.TYPE_MINUS_SIGN:
return "";
case AffixPatternUtils.TYPE_PLUS_SIGN:
return SYMBOLS.getPlusSignString();
case AffixPatternUtils.TYPE_PERCENT:
return SYMBOLS.getPercentString();
case AffixPatternUtils.TYPE_PERMILLE:
return SYMBOLS.getPerMillString();
case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
return "$";
case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
return "XXX";
case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
return "long name";
case AffixPatternUtils.TYPE_CURRENCY_QUAD:
return "\uFFFD";
case AffixPatternUtils.TYPE_CURRENCY_QUINT:
// TODO: Add support for narrow currency symbols here.
return "\uFFFD";
case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
return "\uFFFD";
default:
throw new AssertionError();
}
}
};
@Test
public void testEscape() {
Object[][] cases = {
@ -183,42 +219,6 @@ public class AffixPatternUtilsTest {
assertEquals("Symbol provider into middle", "abcd123efg", sb.toString());
}
private static final SymbolProvider DEFAULT_SYMBOL_PROVIDER =
new SymbolProvider() {
// ar_SA has an interesting percent sign and various Arabic letter marks
private final DecimalFormatSymbols SYMBOLS =
DecimalFormatSymbols.getInstance(new ULocale("ar_SA"));
@Override
public CharSequence getSymbol(int type) {
switch (type) {
case AffixPatternUtils.TYPE_MINUS_SIGN:
return "";
case AffixPatternUtils.TYPE_PLUS_SIGN:
return SYMBOLS.getPlusSignString();
case AffixPatternUtils.TYPE_PERCENT:
return SYMBOLS.getPercentString();
case AffixPatternUtils.TYPE_PERMILLE:
return SYMBOLS.getPerMillString();
case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
return "$";
case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
return "XXX";
case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
return "long name";
case AffixPatternUtils.TYPE_CURRENCY_QUAD:
return "\uFFFD";
case AffixPatternUtils.TYPE_CURRENCY_QUINT:
// TODO: Add support for narrow currency symbols here.
return "\uFFFD";
case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
return "\uFFFD";
default:
throw new AssertionError();
}
}
};
private static String unescapeWithDefaults(String input) {
NumberStringBuilder nsb = new NumberStringBuilder();
AffixPatternUtils.unescape(input, nsb, 0, DEFAULT_SYMBOL_PROVIDER);

View File

@ -0,0 +1,152 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
import com.ibm.icu.impl.number.modifiers.CurrencySpacingEnabledModifier;
import com.ibm.icu.impl.number.modifiers.SimpleModifier;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.util.ULocale;
public class ModifierTest {
@Test
public void testConstantAffixModifier() {
assertModifierEquals(ConstantAffixModifier.EMPTY, 0, false, "|", "n");
Modifier mod1 = new ConstantAffixModifier("a", "b", NumberFormat.Field.PERCENT, true);
assertModifierEquals(mod1, 1, true, "a|b", "%n%");
}
@Test
public void testConstantMultiFieldModifier() {
NumberStringBuilder prefix = new NumberStringBuilder();
NumberStringBuilder suffix = new NumberStringBuilder();
Modifier mod1 = new ConstantMultiFieldModifier(prefix, suffix, true);
assertModifierEquals(mod1, 0, true, "|", "n");
prefix.append("a", NumberFormat.Field.PERCENT);
suffix.append("b", NumberFormat.Field.CURRENCY);
Modifier mod2 = new ConstantMultiFieldModifier(prefix, suffix, true);
assertModifierEquals(mod2, 1, true, "a|b", "%n$");
// Make sure the first modifier is still the same (that it stayed constant)
assertModifierEquals(mod1, 0, true, "|", "n");
}
@Test
public void testSimpleModifier() {
String[] patterns = { "{0}", "X{0}Y", "XX{0}YYY", "{0}YY", "XXXX{0}" };
Object[][] outputs = { { "", 0, 0 }, { "abcde", 0, 0 }, { "abcde", 2, 2 }, { "abcde", 1, 3 } };
int[] prefixLens = { 0, 1, 2, 0, 4 };
String[][] expectedCharFields = { { "|", "n" }, { "X|Y", "%n%" }, { "XX|YYY", "%%n%%%" }, { "|YY", "n%%" },
{ "XXXX|", "%%%%n" } };
String[][] expecteds = { { "", "XY", "XXYYY", "YY", "XXXX" },
{ "abcde", "XYabcde", "XXYYYabcde", "YYabcde", "XXXXabcde" },
{ "abcde", "abXYcde", "abXXYYYcde", "abYYcde", "abXXXXcde" },
{ "abcde", "aXbcYde", "aXXbcYYYde", "abcYYde", "aXXXXbcde" } };
for (int i = 0; i < patterns.length; i++) {
String pattern = patterns[i];
String compiledPattern = SimpleFormatterImpl
.compileToStringMinMaxArguments(pattern, new StringBuilder(), 1, 1);
Modifier mod = new SimpleModifier(compiledPattern, NumberFormat.Field.PERCENT, false);
assertModifierEquals(mod, prefixLens[i], false, expectedCharFields[i][0], expectedCharFields[i][1]);
// Test strange insertion positions
for (int j = 0; j < outputs.length; j++) {
NumberStringBuilder output = new NumberStringBuilder();
output.append((String) outputs[j][0], null);
mod.apply(output, (Integer) outputs[j][1], (Integer) outputs[j][2]);
String expected = expecteds[j][i];
String actual = output.toString();
assertEquals(expected, actual);
}
}
}
@Test
public void testCurrencySpacingEnabledModifier() {
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
NumberStringBuilder prefix = new NumberStringBuilder();
NumberStringBuilder suffix = new NumberStringBuilder();
Modifier mod1 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols);
assertModifierEquals(mod1, 0, true, "|", "n");
prefix.append("USD", NumberFormat.Field.CURRENCY);
Modifier mod2 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols);
assertModifierEquals(mod2, 3, true, "USD|", "$$$n");
// Test the default currency spacing rules
NumberStringBuilder sb = new NumberStringBuilder();
sb.append("123", NumberFormat.Field.INTEGER);
NumberStringBuilder sb1 = new NumberStringBuilder(sb);
assertModifierEquals(mod2, sb1, 3, true, "USD\u00A0123", "$$$niii");
// Compare with the unsafe code path
NumberStringBuilder sb2 = new NumberStringBuilder(sb);
sb2.insert(0, "USD", NumberFormat.Field.CURRENCY);
CurrencySpacingEnabledModifier.applyCurrencySpacing(sb2, 0, 3, 6, 0, symbols);
assertTrue(sb1.toDebugString() + " vs " + sb2.toDebugString(), sb1.contentEquals(sb2));
// Test custom patterns
// The following line means that the last char of the number should be a | (rather than a digit)
symbols.setPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, true, "[|]");
suffix.append("XYZ", NumberFormat.Field.CURRENCY);
Modifier mod3 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols);
assertModifierEquals(mod3, 3, true, "USD|\u00A0XYZ", "$$$nn$$$");
}
@Test
public void testCurrencySpacingPatternStability() {
// This test checks for stability of the currency spacing patterns in CLDR.
// For efficiency, ICU caches the most common currency spacing UnicodeSets.
// If this test starts failing, please update the method #getUnicodeSet() in
// BOTH CurrencySpacingEnabledModifier.java AND in C++.
DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(new ULocale("en-US"));
assertEquals(
"[:^S:]",
dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, true));
assertEquals(
"[:^S:]",
dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, false));
assertEquals(
"[:digit:]",
dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, true));
assertEquals(
"[:digit:]",
dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, false));
}
private void assertModifierEquals(
Modifier mod,
int expectedPrefixLength,
boolean expectedStrong,
String expectedChars,
String expectedFields) {
NumberStringBuilder sb = new NumberStringBuilder();
sb.appendCodePoint('|', null);
assertModifierEquals(mod, sb, expectedPrefixLength, expectedStrong, expectedChars, expectedFields);
}
private void assertModifierEquals(
Modifier mod,
NumberStringBuilder sb,
int expectedPrefixLength,
boolean expectedStrong,
String expectedChars,
String expectedFields) {
mod.apply(sb, 0, sb.length());
assertEquals("Prefix length on " + sb, expectedPrefixLength, mod.getPrefixLength());
assertEquals("Strong on " + sb, expectedStrong, mod.isStrong());
assertEquals("<NumberStringBuilder [" + expectedChars + "] [" + expectedFields + "]>", sb.toDebugString());
}
}

View File

@ -6,27 +6,27 @@ import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.ibm.icu.impl.number.LdmlPatternInfo;
import com.ibm.icu.impl.number.PatternParser;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.ULocale;
import newapi.MurkyModifier;
import newapi.MutablePatternModifier;
import newapi.NumberFormatter.SignDisplay;
import newapi.NumberFormatter.UnitWidth;
public class MurkyModifierTest {
@Test
public void basic() {
MurkyModifier murky = new MurkyModifier(false);
murky.setPatternInfo(LdmlPatternInfo.parse("a0b"));
MutablePatternModifier murky = new MutablePatternModifier(false);
murky.setPatternInfo(PatternParser.parse("a0b"));
murky.setPatternAttributes(SignDisplay.AUTO, false);
murky.setSymbols(
DecimalFormatSymbols.getInstance(ULocale.ENGLISH),
Currency.getInstance("USD"),
FormatWidth.SHORT,
UnitWidth.SHORT,
null);
murky.setNumberProperties(false, null);
assertEquals("a", getPrefix(murky));
@ -41,7 +41,7 @@ public class MurkyModifierTest {
assertEquals("a", getPrefix(murky));
assertEquals("b", getSuffix(murky));
murky.setPatternInfo(LdmlPatternInfo.parse("a0b;c-0d"));
murky.setPatternInfo(PatternParser.parse("a0b;c-0d"));
murky.setPatternAttributes(SignDisplay.AUTO, false);
murky.setNumberProperties(false, null);
assertEquals("a", getPrefix(murky));
@ -57,13 +57,13 @@ public class MurkyModifierTest {
assertEquals("d", getSuffix(murky));
}
private static String getPrefix(MurkyModifier murky) {
private static String getPrefix(MutablePatternModifier murky) {
NumberStringBuilder nsb = new NumberStringBuilder();
murky.apply(nsb, 0, 0);
return nsb.subSequence(0, murky.getPrefixLength()).toString();
}
private static String getSuffix(MurkyModifier murky) {
private static String getSuffix(MutablePatternModifier murky) {
NumberStringBuilder nsb = new NumberStringBuilder();
murky.apply(nsb, 0, 0);
return nsb.subSequence(murky.getPrefixLength(), nsb.length()).toString();

View File

@ -183,6 +183,30 @@ public class NumberStringBuilderTest {
}
}
@Test
public void testCodePoints() {
NumberStringBuilder nsb = new NumberStringBuilder();
assertEquals("First is -1 on empty string", -1, nsb.getFirstCodePoint());
assertEquals("Last is -1 on empty string", -1, nsb.getLastCodePoint());
assertEquals("Length is 0 on empty string", 0, nsb.codePointCount());
nsb.append("q", null);
assertEquals("First is q", 'q', nsb.getFirstCodePoint());
assertEquals("Last is q", 'q', nsb.getLastCodePoint());
assertEquals("0th is q", 'q', nsb.codePointAt(0));
assertEquals("Before 1st is q", 'q', nsb.codePointBefore(1));
assertEquals("Code point count is 1", 1, nsb.codePointCount());
// 🚀 is two char16s
nsb.append("🚀", null);
assertEquals("First is still q", 'q', nsb.getFirstCodePoint());
assertEquals("Last is space ship", 128640, nsb.getLastCodePoint());
assertEquals("1st is space ship", 128640, nsb.codePointAt(1));
assertEquals("Before 1st is q", 'q', nsb.codePointBefore(1));
assertEquals("Before 3rd is space ship", 128640, nsb.codePointBefore(3));
assertEquals("Code point count is 2", 2, nsb.codePointCount());
}
private static void assertCharSequenceEquals(CharSequence a, CharSequence b) {
assertEquals(a.toString(), b.toString());

View File

@ -7,7 +7,7 @@ import static org.junit.Assert.fail;
import org.junit.Test;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.PatternAndPropertyUtils;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.ULocale;
@ -27,8 +27,8 @@ public class PatternStringTest {
String localized = ".'ab'c'b''a'''#,##0a0b'a%'";
String toStandard = "+-'ab'c'b''a'''#,##0.0%'a%'";
assertEquals(localized, PatternString.convertLocalized(standard, symbols, true));
assertEquals(toStandard, PatternString.convertLocalized(localized, symbols, false));
assertEquals(localized, PatternAndPropertyUtils.convertLocalized(standard, symbols, true));
assertEquals(toStandard, PatternAndPropertyUtils.convertLocalized(localized, symbols, false));
}
@Test
@ -60,8 +60,8 @@ public class PatternStringTest {
String input = cas[0];
String output = cas[1];
Properties properties = PatternString.parseToProperties(input);
String actual = PatternString.propertiesToString(properties);
Properties properties = PatternAndPropertyUtils.parseToProperties(input);
String actual = PatternAndPropertyUtils.propertiesToString(properties);
assertEquals(
"Failed on input pattern '" + input + "', properties " + properties, output, actual);
}
@ -90,7 +90,7 @@ public class PatternStringTest {
Properties input = (Properties) cas[0];
String output = (String) cas[1];
String actual = PatternString.propertiesToString(input);
String actual = PatternAndPropertyUtils.propertiesToString(input);
assertEquals("Failed on input properties " + input, output, actual);
}
}
@ -103,7 +103,7 @@ public class PatternStringTest {
for (String pattern : invalidPatterns) {
try {
PatternString.parseToProperties(pattern);
PatternAndPropertyUtils.parseToProperties(pattern);
fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern);
} catch (IllegalArgumentException e) {
}
@ -112,8 +112,8 @@ public class PatternStringTest {
@Test
public void testBug13117() {
Properties expected = PatternString.parseToProperties("0");
Properties actual = PatternString.parseToProperties("0;");
Properties expected = PatternAndPropertyUtils.parseToProperties("0");
Properties actual = PatternAndPropertyUtils.parseToProperties("0;");
assertEquals("Should not consume negative subpattern", expected, actual);
}
}

View File

@ -31,7 +31,7 @@ import org.junit.Test;
import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
import com.ibm.icu.impl.number.Parse.GroupingMode;
import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.PatternAndPropertyUtils;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
@ -310,7 +310,7 @@ public class PropertiesTest {
Properties props0 = new Properties();
// Write values to some of the fields
PatternString.parseToExistingProperties("A-**####,#00.00#b¤", props0);
PatternAndPropertyUtils.parseToExistingProperties("A-**####,#00.00#b¤", props0);
// Write to byte stream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@ -337,7 +337,7 @@ public class PropertiesTest {
public Object[] getTestObjects() {
return new Object[] {
new Properties(),
PatternString.parseToProperties("x#,##0.00%"),
PatternAndPropertyUtils.parseToProperties("x#,##0.00%"),
new Properties().setCompactStyle(CompactStyle.LONG).setMinimumExponentDigits(2)
};
}

View File

@ -827,7 +827,7 @@ public class SerializableTestUtility {
map.put("com.ibm.icu.text.PluralRules$FixedDecimal", new PluralRulesTest.FixedDecimalHandler());
map.put("com.ibm.icu.util.MeasureUnit", new MeasureUnitTest.MeasureUnitHandler());
map.put("com.ibm.icu.util.TimeUnit", new MeasureUnitTest.MeasureUnitHandler());
map.put("com.ibm.icu.util.Dimensionless", new MeasureUnitTest.MeasureUnitHandler());
map.put("com.ibm.icu.util.NoUnit", new MeasureUnitTest.MeasureUnitHandler());
map.put("com.ibm.icu.text.MeasureFormat", new MeasureUnitTest.MeasureFormatHandler());
map.put("com.ibm.icu.impl.number.Properties", new PropertiesTest.PropertiesHandler());