ICU-13177 Renaming classes and moving things around. No or very few behavior changes.
X-SVN-Rev: 40364
This commit is contained in:
parent
20e5b7b910
commit
c0f2ca5177
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
* "<NumberStringBuilder [-123.45] [-iii.ff]>"
|
||||
*
|
||||
* @return A string for debugging purposes.
|
||||
*/
|
||||
public String toDebugString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<NumberStringBuilder [");
|
||||
sb.append(this.toString());
|
||||
sb.append("] [");
|
||||
for (int i = zero; i < zero + length; i++) {
|
||||
if (fields[i] == null) {
|
||||
sb.append('n');
|
||||
} else {
|
||||
sb.append(fieldToDebugChar.get(fields[i]));
|
||||
}
|
||||
}
|
||||
sb.append("]>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** @return A new array containing the contents of this string builder. */
|
||||
public char[] toCharArray() {
|
||||
return Arrays.copyOfRange(chars, zero, zero + length);
|
||||
}
|
||||
|
||||
/** @return A new array containing the field values of this string builder. */
|
||||
public Field[] toFieldArray() {
|
||||
return Arrays.copyOfRange(fields, zero, zero + length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the contents and field values of this string builder are equal to the given
|
||||
* chars and fields.
|
||||
* @see #toCharArray
|
||||
* @see #toFieldArray
|
||||
*/
|
||||
public boolean contentEquals(char[] chars, Field[] fields) {
|
||||
if (chars.length != length) return false;
|
||||
if (fields.length != length) return false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (this.chars[zero + i] != chars[i]) return false;
|
||||
if (this.fields[zero + i] != fields[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param other The instance to compare.
|
||||
* @return Whether the contents of this instance is currently equal to the given instance.
|
||||
*/
|
||||
public boolean contentEquals(NumberStringBuilder other) {
|
||||
if (length != other.length) return false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (charAt(i) != other.charAt(i) || fieldAt(i) != other.fieldAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the given {@link FieldPosition} based on this string builder.
|
||||
*
|
||||
* @param fp The FieldPosition to populate.
|
||||
* @param offset An offset to add to the field position index; can be zero.
|
||||
*/
|
||||
public void populateFieldPosition(FieldPosition fp, int offset) {
|
||||
java.text.Format.Field rawField = fp.getFieldAttribute();
|
||||
|
||||
if (rawField == null) {
|
||||
// Backwards compatibility: read from fp.getField()
|
||||
if (fp.getField() == NumberFormat.INTEGER_FIELD) {
|
||||
rawField = NumberFormat.Field.INTEGER;
|
||||
} else if (fp.getField() == NumberFormat.FRACTION_FIELD) {
|
||||
rawField = NumberFormat.Field.FRACTION;
|
||||
} else {
|
||||
// No field is set
|
||||
return;
|
||||
}
|
||||
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 "<NumberStringBuilder
|
||||
* [-123.45] [-iii.ff]>"
|
||||
*
|
||||
* @return A string for debugging purposes.
|
||||
*/
|
||||
public String toDebugString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<NumberStringBuilder [");
|
||||
sb.append(this.toString());
|
||||
sb.append("] [");
|
||||
for (int i = zero; i < zero + length; i++) {
|
||||
if (fields[i] == null) {
|
||||
sb.append('n');
|
||||
} else {
|
||||
sb.append(fieldToDebugChar.get(fields[i]));
|
||||
}
|
||||
}
|
||||
sb.append("]>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** @return A new array containing the contents of this string builder. */
|
||||
public char[] toCharArray() {
|
||||
return Arrays.copyOfRange(chars, zero, zero + length);
|
||||
}
|
||||
|
||||
/** @return A new array containing the field values of this string builder. */
|
||||
public Field[] toFieldArray() {
|
||||
return Arrays.copyOfRange(fields, zero, zero + length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the contents and field values of this string builder are equal to the given chars and fields.
|
||||
* @see #toCharArray
|
||||
* @see #toFieldArray
|
||||
*/
|
||||
public boolean contentEquals(char[] chars, Field[] fields) {
|
||||
if (chars.length != length)
|
||||
return false;
|
||||
if (fields.length != length)
|
||||
return false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (this.chars[zero + i] != chars[i])
|
||||
return false;
|
||||
if (this.fields[zero + i] != fields[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param other
|
||||
* The instance to compare.
|
||||
* @return Whether the contents of this instance is currently equal to the given instance.
|
||||
*/
|
||||
public boolean contentEquals(NumberStringBuilder other) {
|
||||
if (length != other.length)
|
||||
return false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (charAt(i) != other.charAt(i) || fieldAt(i) != other.fieldAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the given {@link FieldPosition} based on this string builder.
|
||||
*
|
||||
* @param fp
|
||||
* The FieldPosition to populate.
|
||||
* @param offset
|
||||
* An offset to add to the field position index; can be zero.
|
||||
*/
|
||||
public void populateFieldPosition(FieldPosition fp, int offset) {
|
||||
java.text.Format.Field rawField = fp.getFieldAttribute();
|
||||
|
||||
if (rawField == null) {
|
||||
// Backwards compatibility: read from fp.getField()
|
||||
if (fp.getField() == NumberFormat.INTEGER_FIELD) {
|
||||
rawField = NumberFormat.Field.INTEGER;
|
||||
} else if (fp.getField() == NumberFormat.FRACTION_FIELD) {
|
||||
rawField = NumberFormat.Field.FRACTION;
|
||||
} else {
|
||||
// No field is set
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(rawField instanceof com.ibm.icu.text.NumberFormat.Field)) {
|
||||
throw new IllegalArgumentException(
|
||||
"You must pass an instance of com.ibm.icu.text.NumberFormat.Field as your FieldPosition attribute. You passed: "
|
||||
+ rawField.getClass().toString());
|
||||
}
|
||||
|
||||
/* com.ibm.icu.text.NumberFormat. */ Field field = (Field) rawField;
|
||||
|
||||
boolean seenStart = false;
|
||||
int fractionStart = -1;
|
||||
for (int i = zero; i <= zero + length; i++) {
|
||||
Field _field = (i < zero + length) ? fields[i] : null;
|
||||
if (seenStart && field != _field) {
|
||||
// Special case: GROUPING_SEPARATOR counts as an INTEGER.
|
||||
if (field == NumberFormat.Field.INTEGER && _field == NumberFormat.Field.GROUPING_SEPARATOR) {
|
||||
continue;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
19
icu4j/main/classes/core/src/com/ibm/icu/util/NoUnit.java
Normal file
19
icu4j/main/classes/core/src/com/ibm/icu/util/NoUnit.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -55,7 +55,7 @@ public class FormattedNumber {
|
||||
fq.populateUFieldPosition(fieldPosition);
|
||||
}
|
||||
|
||||
public AttributedCharacterIterator toAttributedCharacterIterator() {
|
||||
public AttributedCharacterIterator getAttributes() {
|
||||
return nsb.getIterator();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 345,67"
|
||||
* <li><em>de_CH</em> symbols: "12’345.67"
|
||||
* <li><em>my_MY</em> symbols: "၁၂,၃၄၅.၆၇"
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Pass this method an instance of {@link DecimalFormatSymbols}. For example:
|
||||
*
|
||||
* <pre>
|
||||
* NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("de_CH")))
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* <strong>Note:</strong> DecimalFormatSymbols automatically chooses the best numbering system based on the locale.
|
||||
* In the examples above, the first three are using the Latin numbering system, and the fourth is using the Myanmar
|
||||
* numbering system.
|
||||
*
|
||||
* <p>
|
||||
* <strong>Note:</strong> The instance of DecimalFormatSymbols will be copied: changes made to the symbols object
|
||||
* after passing it into the fluent chain will not be seen.
|
||||
*
|
||||
* <p>
|
||||
* <strong>Note:</strong> Calling this method will override the NumberingSystem previously specified in
|
||||
* {@link #symbols(NumberingSystem)}.
|
||||
*
|
||||
* <p>
|
||||
* The default is to choose the symbols based on the locale specified in the fluent chain.
|
||||
*
|
||||
* @param symbols
|
||||
* The DecimalFormatSymbols to use.
|
||||
* @return The fluent chain.
|
||||
* @see DecimalFormatSymbols
|
||||
* @provisional This API might change or be removed in a future release.
|
||||
* @draft ICU 60
|
||||
*/
|
||||
public T symbols(DecimalFormatSymbols symbols) {
|
||||
symbols = (DecimalFormatSymbols) symbols.clone();
|
||||
return create(KEY_SYMBOLS, symbols);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies that the given numbering system should be used when fetching symbols.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Latin numbering system: "12,345"
|
||||
* <li>Myanmar numbering system: "၁၂,၃၄၅"
|
||||
* <li>Math Sans Bold numbering system: "𝟭𝟮,𝟯𝟰𝟱"
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Pass this method an instance of {@link NumberingSystem}. For example, to force the locale to always use the Latin
|
||||
* alphabet numbering system (ASCII digits):
|
||||
*
|
||||
* <pre>
|
||||
* NumberFormatter.with().symbols(NumberingSystem.LATIN)
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* <strong>Note:</strong> Calling this method will override the DecimalFormatSymbols previously specified in
|
||||
* {@link #symbols(DecimalFormatSymbols)}.
|
||||
*
|
||||
* <p>
|
||||
* The default is to choose the best numbering system for the locale.
|
||||
*
|
||||
* @param ns
|
||||
* The NumberingSystem to use.
|
||||
* @return The fluent chain.
|
||||
* @see NumberingSystem
|
||||
* @provisional This API might change or be removed in a future release.
|
||||
* @draft ICU 60
|
||||
*/
|
||||
public T symbols(NumberingSystem ns) {
|
||||
return create(KEY_SYMBOLS, ns);
|
||||
}
|
||||
|
||||
public T unitWidth(FormatWidth style) {
|
||||
/**
|
||||
* Sets the width of the unit (measure unit or currency).
|
||||
*
|
||||
* <ul>
|
||||
* <li>Narrow: "$12.00", "12 m"
|
||||
* <li>Short: "12.00 USD", "12 m"
|
||||
* <li>Wide: "12.00 US dollars", "12 meters"
|
||||
* <li>Hidden: "12.00", "12"
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Pass an element from the {@link FormatWidth} enum to this setter. For example:
|
||||
*
|
||||
* <pre>
|
||||
* NumberFormatter.with().unitWidth(FormatWidth.SHORT)
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* The default is the narrow width.
|
||||
*
|
||||
* @param style
|
||||
* The with to use when rendering numbers.
|
||||
* @return The fluent chain
|
||||
* @see FormatWidth
|
||||
* @provisional This API might change or be removed in a future release.
|
||||
* @draft ICU 60
|
||||
*/
|
||||
public T unitWidth(UnitWidth style) {
|
||||
return create(KEY_UNIT_WIDTH, style);
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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("/");
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
* }
|
||||
*
|
||||
* @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);
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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) -> {
|
||||
|
@ -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()) {
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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());
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
@ -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());
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user