ICU-7467 Merging ICU4J decimal format overhaul from branch to trunk.

X-SVN-Rev: 39877
This commit is contained in:
Shane Carr 2017-03-18 03:32:40 +00:00
parent a59804f0ac
commit dbcae565a2
89 changed files with 25237 additions and 8616 deletions

1
.gitignore vendored
View File

@ -897,6 +897,7 @@ icu4j/.project
icu4j/build-local.properties
icu4j/demos/out
icu4j/doc
icu4j/eclipse
icu4j/eclipse-build/out
icu4j/lib/*.jar
icu4j/main/classes/charset/out

View File

@ -105,6 +105,85 @@ public class TextTrieMap<V> {
}
}
/**
* Creates an object that consumes code points one at a time and returns intermediate prefix
* matches. Returns null if no match exists.
*
* @return An instance of {@link ParseState}, or null if the starting code point is not a
* prefix for any entry in the trie.
*/
public ParseState openParseState(int startingCp) {
// Check to see whether this is a valid starting character. If not, return null.
if (_ignoreCase) {
startingCp = UCharacter.foldCase(startingCp, true);
}
int count = Character.charCount(startingCp);
char ch1 = (count == 1) ? (char) startingCp : Character.highSurrogate(startingCp);
if (!_root.hasChildFor(ch1)) {
return null;
}
return new ParseState(_root);
}
/**
* ParseState is mutable, not thread-safe, and intended to be used internally by parsers for
* consuming values from this trie.
*/
public class ParseState {
private Node node;
private int offset;
private Node.StepResult result;
ParseState(Node start) {
node = start;
offset = 0;
result = start.new StepResult();
}
/**
* Consumes a code point and walk to the next node in the trie.
*
* @param cp The code point to consume.
*/
public void accept(int cp) {
assert node != null;
if (_ignoreCase) {
cp = UCharacter.foldCase(cp, true);
}
int count = Character.charCount(cp);
char ch1 = (count == 1) ? (char) cp : Character.highSurrogate(cp);
node.takeStep(ch1, offset, result);
if (count == 2 && result.node != null) {
char ch2 = Character.lowSurrogate(cp);
result.node.takeStep(ch2, result.offset, result);
}
node = result.node;
offset = result.offset;
}
/**
* Gets the exact prefix matches for all code points that have been consumed so far.
*
* @return The matches.
*/
public Iterator<V> getCurrentMatches() {
if (node != null && offset == node.charCount()) {
return node.values();
}
return null;
}
/**
* Checks whether any more code points can be consumed.
*
* @return true if no more code points can be consumed; false otherwise.
*/
public boolean atEnd() {
return node == null || (node.charCount() == offset && node._children == null);
}
}
public static class CharIterator implements Iterator<Character> {
private boolean _ignoreCase;
private CharSequence _text;
@ -234,6 +313,21 @@ public class TextTrieMap<V> {
_children = children;
}
public int charCount() {
return _text == null ? 0 : _text.length;
}
public boolean hasChildFor(char ch) {
for (int i=0; _children != null && i < _children.size(); i++) {
Node child = _children.get(i);
if (ch < child._text[0]) break;
if (ch == child._text[0]) {
return true;
}
}
return false;
}
public Iterator<V> values() {
if (_values == null) {
return null;
@ -272,6 +366,37 @@ public class TextTrieMap<V> {
return match;
}
public class StepResult {
public Node node;
public int offset;
}
public void takeStep(char ch, int offset, StepResult result) {
assert offset <= charCount();
if (offset == charCount()) {
// Go to a child node
for (int i=0; _children != null && i < _children.size(); i++) {
Node child = _children.get(i);
if (ch < child._text[0]) break;
if (ch == child._text[0]) {
// Found a matching child node
result.node = child;
result.offset = 1;
return;
}
}
// No matching children; fall through
} else if (_text[offset] == ch) {
// Return to this node; increase offset
result.node = this;
result.offset = offset + 1;
return;
}
// No matches
result.node = null;
result.offset = -1;
return;
}
private void add(char[] text, int offset, V value) {
if (text.length == offset) {
_values = addValue(_values, value);

View File

@ -0,0 +1,524 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat.Field;
/**
* Performs manipulations on affix patterns: the prefix and suffix strings associated with a decimal
* format pattern. For example:
*
* <table>
* <tr><th>Affix Pattern</th><th>Example Unescaped (Formatted) String</th></tr>
* <tr><td>abc</td><td>abc</td></tr>
* <tr><td>ab-</td><td>ab</td></tr>
* <tr><td>ab'-'</td><td>ab-</td></tr>
* <tr><td>ab''</td><td>ab'</td></tr>
* </table>
*
* To manually iterate over tokens in a literal string, use the following pattern, which is designed
* to be efficient.
*
* <pre>
* long tag = 0L;
* while (AffixPatternUtils.hasNext(tag, patternString)) {
* tag = AffixPatternUtils.nextToken(tag, patternString);
* int typeOrCp = AffixPatternUtils.getTypeOrCp(tag);
* switch (typeOrCp) {
* case AffixPatternUtils.TYPE_MINUS_SIGN:
* // Current token is a minus sign.
* break;
* case AffixPatternUtils.TYPE_PLUS_SIGN:
* // Current token is a plus sign.
* break;
* case AffixPatternUtils.TYPE_PERCENT:
* // Current token is a percent sign.
* break;
* case AffixPatternUtils.TYPE_PERMILLE:
* // Current token is a permille sign.
* break;
* case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
* // Current token is a single currency sign.
* break;
* case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
* // Current token is a double currency sign.
* break;
* case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
* // Current token is a triple currency sign.
* break;
* case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
* // Current token has four or more currency signs.
* break;
* default:
* // Current token is an arbitrary code point.
* // The variable typeOrCp is the code point.
* break;
* }
* }
* </pre>
*/
public class AffixPatternUtils {
private static final int STATE_BASE = 0;
private static final int STATE_FIRST_QUOTE = 1;
private static final int STATE_INSIDE_QUOTE = 2;
private static final int STATE_AFTER_QUOTE = 3;
private static final int STATE_FIRST_CURR = 4;
private static final int STATE_SECOND_CURR = 5;
private static final int STATE_THIRD_CURR = 6;
private static final int STATE_OVERFLOW_CURR = 7;
private static final int TYPE_CODEPOINT = 0;
/** Represents a minus sign symbol '-'. */
public static final int TYPE_MINUS_SIGN = -1;
/** Represents a plus sign symbol '+'. */
public static final int TYPE_PLUS_SIGN = -2;
/** Represents a percent sign symbol '%'. */
public static final int TYPE_PERCENT = -3;
/** Represents a permille sign symbol '‰'. */
public static final int TYPE_PERMILLE = -4;
/** Represents a single currency symbol '¤'. */
public static final int TYPE_CURRENCY_SINGLE = -5;
/** Represents a double currency symbol '¤¤'. */
public static final int TYPE_CURRENCY_DOUBLE = -6;
/** Represents a triple currency symbol '¤¤¤'. */
public static final int TYPE_CURRENCY_TRIPLE = -7;
/** Represents a sequence of four or more currency symbols. */
public static final int TYPE_CURRENCY_OVERFLOW = -15;
/**
* Checks whether the specified affix pattern has any unquoted currency symbols ("¤").
*
* @param patternString The string to check for currency symbols.
* @return true if the literal has at least one unquoted currency symbol; false otherwise.
*/
public static boolean hasCurrencySymbols(CharSequence patternString) {
if (patternString == null) return false;
int offset = 0;
int state = STATE_BASE;
boolean result = false;
for (; offset < patternString.length(); ) {
int cp = Character.codePointAt(patternString, offset);
switch (state) {
case STATE_BASE:
if (cp == '¤') {
result = true;
} else if (cp == '\'') {
state = STATE_INSIDE_QUOTE;
}
break;
case STATE_INSIDE_QUOTE:
if (cp == '\'') {
state = STATE_BASE;
}
break;
default:
throw new AssertionError();
}
offset += Character.charCount(cp);
}
if (state == STATE_INSIDE_QUOTE) {
throw new IllegalArgumentException("Unterminated quote: \"" + patternString + "\"");
} else {
return result;
}
}
/**
* Estimates the number of code points present in an unescaped version of the affix pattern string
* (one that would be returned by {@link #unescape}), assuming that all interpolated symbols
* consume one code point and that currencies consume as many code points as their symbol width.
* Used for computing padding width.
*
* @param patternString The original string whose width will be estimated.
* @return The length of the unescaped string.
*/
public static int unescapedLength(CharSequence patternString) {
if (patternString == null) return 0;
int state = STATE_BASE;
int offset = 0;
int length = 0;
for (; offset < patternString.length(); ) {
int cp = Character.codePointAt(patternString, offset);
switch (state) {
case STATE_BASE:
if (cp == '\'') {
// First quote
state = STATE_FIRST_QUOTE;
} else {
// Unquoted symbol
length++;
}
break;
case STATE_FIRST_QUOTE:
if (cp == '\'') {
// Repeated quote
length++;
state = STATE_BASE;
} else {
// Quoted code point
length++;
state = STATE_INSIDE_QUOTE;
}
break;
case STATE_INSIDE_QUOTE:
if (cp == '\'') {
// End of quoted sequence
state = STATE_AFTER_QUOTE;
} else {
// Quoted code point
length++;
}
break;
case STATE_AFTER_QUOTE:
if (cp == '\'') {
// Double quote inside of quoted sequence
length++;
state = STATE_INSIDE_QUOTE;
} else {
// Unquoted symbol
length++;
}
break;
default:
throw new AssertionError();
}
offset += Character.charCount(cp);
}
switch (state) {
case STATE_FIRST_QUOTE:
case STATE_INSIDE_QUOTE:
throw new IllegalArgumentException("Unterminated quote: \"" + patternString + "\"");
default:
break;
}
return length;
}
/**
* Takes a string and escapes (quotes) characters that have special meaning in the affix pattern
* syntax. This function does not reverse-lookup symbols.
*
* <p>Example input: "-$x"; example output: "'-'$x"
*
* @param input The string to be escaped.
* @param output The string builder to which to append the escaped string.
* @return The number of chars (UTF-16 code units) appended to the output.
*/
public static int escape(CharSequence input, StringBuilder output) {
if (input == null) return 0;
int state = STATE_BASE;
int offset = 0;
int startLength = output.length();
for (; offset < input.length(); ) {
int cp = Character.codePointAt(input, offset);
switch (cp) {
case '\'':
output.append("''");
break;
case '-':
case '+':
case '%':
case '‰':
case '¤':
if (state == STATE_BASE) {
output.append('\'');
output.appendCodePoint(cp);
state = STATE_INSIDE_QUOTE;
} else {
output.appendCodePoint(cp);
}
break;
default:
if (state == STATE_INSIDE_QUOTE) {
output.append('\'');
output.appendCodePoint(cp);
state = STATE_BASE;
} else {
output.appendCodePoint(cp);
}
break;
}
offset += Character.charCount(cp);
}
if (state == STATE_INSIDE_QUOTE) {
output.append('\'');
}
return output.length() - startLength;
}
/**
* Executes the unescape state machine. Replaces the unquoted characters "-", "+", "%", and ""
* with their localized equivalents. Replaces "¤", "¤¤", and "¤¤¤" with the three argument
* strings.
*
* <p>Example input: "'-'¤x"; example output: "-$x"
*
* @param affixPattern The original string to be unescaped.
* @param symbols An instance of {@link DecimalFormatSymbols} for the locale of interest.
* @param currency1 The string to replace "¤".
* @param currency2 The string to replace "¤¤".
* @param currency3 The string to replace "¤¤¤".
* @param minusSign The string to replace "-". If null, symbols.getMinusSignString() is used.
* @param output The {@link NumberStringBuilder} to which the result will be appended.
*/
public static void unescape(
CharSequence affixPattern,
DecimalFormatSymbols symbols,
String currency1,
String currency2,
String currency3,
String minusSign,
NumberStringBuilder output) {
if (affixPattern == null || affixPattern.length() == 0) return;
if (minusSign == null) minusSign = symbols.getMinusSignString();
long tag = 0L;
while (hasNext(tag, affixPattern)) {
tag = nextToken(tag, affixPattern);
int typeOrCp = getTypeOrCp(tag);
switch (typeOrCp) {
case TYPE_MINUS_SIGN:
output.append(minusSign, Field.SIGN);
break;
case TYPE_PLUS_SIGN:
output.append(symbols.getPlusSignString(), Field.SIGN);
break;
case TYPE_PERCENT:
output.append(symbols.getPercentString(), Field.PERCENT);
break;
case TYPE_PERMILLE:
output.append(symbols.getPerMillString(), Field.PERMILLE);
break;
case TYPE_CURRENCY_SINGLE:
output.append(currency1, Field.CURRENCY);
break;
case TYPE_CURRENCY_DOUBLE:
output.append(currency2, Field.CURRENCY);
break;
case TYPE_CURRENCY_TRIPLE:
output.append(currency3, Field.CURRENCY);
break;
case TYPE_CURRENCY_OVERFLOW:
output.append("\uFFFD", Field.CURRENCY);
break;
default:
output.appendCodePoint(typeOrCp, null);
break;
}
}
}
/**
* Returns the next token from the affix pattern.
*
* @param tag A bitmask used for keeping track of state from token to token. The initial value
* should be 0L.
* @param patternString The affix pattern.
* @return The bitmask tag to pass to the next call of this method to retrieve the following
* token.
* @throws IllegalArgumentException If there are no tokens left or if there is a syntax error in
* the pattern string.
* @see #hasNext
*/
public static long nextToken(long tag, CharSequence patternString) {
int offset = getOffset(tag);
int state = getState(tag);
for (; offset < patternString.length(); ) {
int cp = Character.codePointAt(patternString, offset);
int count = Character.charCount(cp);
switch (state) {
case STATE_BASE:
switch (cp) {
case '\'':
state = STATE_FIRST_QUOTE;
offset += count;
// continue to the next code point
break;
case '-':
return makeTag(offset + count, TYPE_MINUS_SIGN, STATE_BASE, 0);
case '+':
return makeTag(offset + count, TYPE_PLUS_SIGN, STATE_BASE, 0);
case '%':
return makeTag(offset + count, TYPE_PERCENT, STATE_BASE, 0);
case '‰':
return makeTag(offset + count, TYPE_PERMILLE, STATE_BASE, 0);
case '¤':
state = STATE_FIRST_CURR;
offset += count;
// continue to the next code point
break;
default:
return makeTag(offset + count, TYPE_CODEPOINT, STATE_BASE, cp);
}
break;
case STATE_FIRST_QUOTE:
if (cp == '\'') {
return makeTag(offset + count, TYPE_CODEPOINT, STATE_BASE, cp);
} else {
return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
}
case STATE_INSIDE_QUOTE:
if (cp == '\'') {
state = STATE_AFTER_QUOTE;
offset += count;
// continue to the next code point
break;
} else {
return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
}
case STATE_AFTER_QUOTE:
if (cp == '\'') {
return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
} else {
state = STATE_BASE;
// re-evaluate this code point
break;
}
case STATE_FIRST_CURR:
if (cp == '¤') {
state = STATE_SECOND_CURR;
offset += count;
// continue to the next code point
break;
} else {
return makeTag(offset, TYPE_CURRENCY_SINGLE, STATE_BASE, 0);
}
case STATE_SECOND_CURR:
if (cp == '¤') {
state = STATE_THIRD_CURR;
offset += count;
// continue to the next code point
break;
} else {
return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
}
case STATE_THIRD_CURR:
if (cp == '¤') {
state = STATE_OVERFLOW_CURR;
offset += count;
// continue to the next code point
break;
} else {
return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
}
case STATE_OVERFLOW_CURR:
if (cp == '¤') {
offset += count;
// continue to the next code point and loop back to this state
break;
} else {
return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
}
default:
throw new AssertionError();
}
}
// End of string
switch (state) {
case STATE_BASE:
// We shouldn't get here if hasNext() was followed.
throw new IllegalArgumentException();
case STATE_FIRST_QUOTE:
case STATE_INSIDE_QUOTE:
// For consistent behavior with the JDK and ICU 58, throw an exception here.
throw new IllegalArgumentException(
"Unterminated quote in pattern affix: \"" + patternString + "\"");
case STATE_AFTER_QUOTE:
// We shouldn't get here if hasNext() was followed.
throw new IllegalArgumentException();
case STATE_FIRST_CURR:
return makeTag(offset, TYPE_CURRENCY_SINGLE, STATE_BASE, 0);
case STATE_SECOND_CURR:
return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
case STATE_THIRD_CURR:
return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
case STATE_OVERFLOW_CURR:
return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
default:
throw new AssertionError();
}
}
/**
* Returns whether the affix pattern string has any more tokens to be retrieved from a call to
* {@link #nextToken}.
*
* @param tag The bitmask tag of the previous token, as returned by {@link #nextToken}.
* @param string The affix pattern.
* @return true if there are more tokens to consume; false otherwise.
*/
public static boolean hasNext(long tag, CharSequence string) {
int state = getState(tag);
int offset = getOffset(tag);
// Special case: the last character in string is an end quote.
if (state == STATE_INSIDE_QUOTE
&& offset == string.length() - 1
&& string.charAt(offset) == '\'') {
return false;
} else if (state != STATE_BASE) {
return true;
} else {
return offset < string.length();
}
}
/**
* This function helps determine the identity of the token consumed by {@link #nextToken}.
* Converts from a bitmask tag, based on a call to {@link #nextToken}, to its corresponding symbol
* type or code point.
*
* @param tag The bitmask tag of the current token, as returned by {@link #nextToken}.
* @return If less than zero, a symbol type corresponding to one of the <code>TYPE_</code>
* constants, such as {@link #TYPE_MINUS_SIGN}. If greater than or equal to zero, a literal
* code point.
*/
public static int getTypeOrCp(long tag) {
int type = getType(tag);
return (type == 0) ? getCodePoint(tag) : -type;
}
private static long makeTag(int offset, int type, int state, int cp) {
long tag = 0L;
tag |= offset;
tag |= (-(long) type) << 32;
tag |= ((long) state) << 36;
tag |= ((long) cp) << 40;
return tag;
}
static int getOffset(long tag) {
return (int) (tag & 0xffffffff);
}
static int getType(long tag) {
return (int) ((tag >>> 32) & 0xf);
}
static int getState(long tag) {
return (int) ((tag >>> 36) & 0xf);
}
static int getCodePoint(long tag) {
return (int) (tag >>> 40);
}
}

View File

@ -0,0 +1,286 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import com.ibm.icu.impl.number.Format.BeforeTargetAfterFormat;
import com.ibm.icu.impl.number.Format.SingularFormat;
import com.ibm.icu.impl.number.Format.TargetFormat;
import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier;
import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
import com.ibm.icu.impl.number.formatters.CurrencyFormat;
import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier;
import com.ibm.icu.impl.number.formatters.MeasureFormat;
import com.ibm.icu.impl.number.formatters.PaddingFormat;
import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat;
import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
import com.ibm.icu.impl.number.formatters.RoundingFormat;
import com.ibm.icu.impl.number.formatters.ScientificFormat;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.ULocale;
public class Endpoint {
// public static Format from(DecimalFormatSymbols symbols, Properties properties)
// throws ParseException {
// Format format = new PositiveIntegerFormat(symbols, properties);
// // TODO: integer-only format
// format = new PositiveDecimalFormat((SelfContainedFormat) format, symbols, properties);
// if (properties.useCompactDecimalFormat()) {
// format = CompactDecimalFormat.getInstance((SingularFormat) format, symbols, properties);
// } else {
// format =
// PositiveNegativeAffixFormat.getInstance((SingularFormat) format, symbols, properties);
// }
// if (properties.useRoundingInterval()) {
// format = new IntervalRoundingFormat((SingularFormat) format, properties);
// } else if (properties.useSignificantDigits()) {
// format = new SignificantDigitsFormat((SingularFormat) format, properties);
// } else if (properties.useFractionFormat()) {
// format = new RoundingFormat((SingularFormat) format, properties);
// }
// return format;
// }
public static Format fromBTA(Properties properties) {
return fromBTA(properties, getSymbols());
}
public static SingularFormat fromBTA(Properties properties, Locale locale) {
return fromBTA(properties, getSymbols(locale));
}
public static SingularFormat fromBTA(Properties properties, ULocale uLocale) {
return fromBTA(properties, getSymbols(uLocale));
}
public static SingularFormat fromBTA(String pattern) {
return fromBTA(getProperties(pattern), getSymbols());
}
public static SingularFormat fromBTA(String pattern, Locale locale) {
return fromBTA(getProperties(pattern), getSymbols(locale));
}
public static SingularFormat fromBTA(String pattern, ULocale uLocale) {
return fromBTA(getProperties(pattern), getSymbols(uLocale));
}
public static SingularFormat fromBTA(String pattern, DecimalFormatSymbols symbols) {
return fromBTA(getProperties(pattern), symbols);
}
public static SingularFormat fromBTA(Properties properties, DecimalFormatSymbols symbols) {
if (symbols == null) throw new IllegalArgumentException("symbols must not be null");
// TODO: This fast track results in an improvement of about 10ns during formatting. See if
// there is a way to implement it more elegantly.
boolean canUseFastTrack = true;
PluralRules rules = getPluralRules(symbols.getULocale(), properties);
BeforeTargetAfterFormat format = new Format.BeforeTargetAfterFormat(rules);
TargetFormat target = new PositiveDecimalFormat(symbols, properties);
format.setTargetFormat(target);
// TODO: integer-only format?
if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) {
canUseFastTrack = false;
format.addBeforeFormat(MagnitudeMultiplier.getInstance(properties));
}
if (BigDecimalMultiplier.useMultiplier(properties)) {
canUseFastTrack = false;
format.addBeforeFormat(BigDecimalMultiplier.getInstance(properties));
}
if (MeasureFormat.useMeasureFormat(properties)) {
canUseFastTrack = false;
format.addBeforeFormat(MeasureFormat.getInstance(symbols, properties));
}
if (CurrencyFormat.useCurrency(properties)) {
canUseFastTrack = false;
if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties));
} else if (ScientificFormat.useScientificNotation(properties)) {
// TODO: Should the currency rounder or scientific rounder be used in this case?
// For now, default to using the scientific rounder.
format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties));
} else {
format.addBeforeFormat(CurrencyFormat.getCurrencyRounder(symbols, properties));
format.addBeforeFormat(CurrencyFormat.getCurrencyModifier(symbols, properties));
}
} else {
if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
canUseFastTrack = false;
format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties));
} else if (ScientificFormat.useScientificNotation(properties)) {
canUseFastTrack = false;
format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties));
} else {
format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
format.addBeforeFormat(RoundingFormat.getDefaultOrNoRounder(properties));
}
}
if (PaddingFormat.usePadding(properties)) {
canUseFastTrack = false;
format.addAfterFormat(PaddingFormat.getInstance(properties));
}
if (canUseFastTrack) {
return new Format.PositiveNegativeRounderTargetFormat(
PositiveNegativeAffixFormat.getInstance(symbols, properties),
RoundingFormat.getDefaultOrNoRounder(properties),
target);
} else {
return format;
}
}
public static String staticFormat(FormatQuantity input, Properties properties) {
return staticFormat(input, properties, getSymbols());
}
public static String staticFormat(FormatQuantity input, Properties properties, Locale locale) {
return staticFormat(input, properties, getSymbols(locale));
}
public static String staticFormat(FormatQuantity input, Properties properties, ULocale uLocale) {
return staticFormat(input, properties, getSymbols(uLocale));
}
public static String staticFormat(FormatQuantity input, String pattern) {
return staticFormat(input, getProperties(pattern), getSymbols());
}
public static String staticFormat(FormatQuantity input, String pattern, Locale locale) {
return staticFormat(input, getProperties(pattern), getSymbols(locale));
}
public static String staticFormat(FormatQuantity input, String pattern, ULocale uLocale) {
return staticFormat(input, getProperties(pattern), getSymbols(uLocale));
}
public static String staticFormat(
FormatQuantity input, String pattern, DecimalFormatSymbols symbols) {
return staticFormat(input, getProperties(pattern), symbols);
}
public static String staticFormat(
FormatQuantity input, Properties properties, DecimalFormatSymbols symbols) {
PluralRules rules = null;
ModifierHolder mods = Format.threadLocalModifierHolder.get().clear();
NumberStringBuilder sb = Format.threadLocalStringBuilder.get().clear();
int length = 0;
// Pre-processing
if (!input.isNaN()) {
if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) {
MagnitudeMultiplier.getInstance(properties).before(input, mods, rules);
}
if (BigDecimalMultiplier.useMultiplier(properties)) {
BigDecimalMultiplier.getInstance(properties).before(input, mods, rules);
}
if (MeasureFormat.useMeasureFormat(properties)) {
rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
MeasureFormat.getInstance(symbols, properties).before(input, mods, rules);
}
if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
CompactDecimalFormat.apply(input, mods, rules, symbols, properties);
} else if (CurrencyFormat.useCurrency(properties)) {
rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
CurrencyFormat.getCurrencyRounder(symbols, properties).before(input, mods, rules);
CurrencyFormat.getCurrencyModifier(symbols, properties).before(input, mods, rules);
} else if (ScientificFormat.useScientificNotation(properties)) {
// TODO: Is it possible to combine significant digits with currency?
PositiveNegativeAffixFormat.getInstance(symbols, properties).before(input, mods, rules);
ScientificFormat.getInstance(symbols, properties).before(input, mods, rules);
} else {
PositiveNegativeAffixFormat.apply(input, mods, symbols, properties);
RoundingFormat.getDefaultOrNoRounder(properties).before(input, mods, rules);
}
}
// Primary format step
length += new PositiveDecimalFormat(symbols, properties).target(input, sb, 0);
length += mods.applyStrong(sb, 0, length);
// Post-processing
if (PaddingFormat.usePadding(properties)) {
length += PaddingFormat.getInstance(properties).after(mods, sb, 0, length);
}
length += mods.applyAll(sb, 0, length);
return sb.toString();
}
private static final ThreadLocal<Map<ULocale, DecimalFormatSymbols>> threadLocalSymbolsCache =
new ThreadLocal<Map<ULocale, DecimalFormatSymbols>>() {
@Override
protected Map<ULocale, DecimalFormatSymbols> initialValue() {
return new HashMap<ULocale, DecimalFormatSymbols>();
}
};
private static DecimalFormatSymbols getSymbols() {
ULocale uLocale = ULocale.getDefault();
return getSymbols(uLocale);
}
private static DecimalFormatSymbols getSymbols(Locale locale) {
ULocale uLocale = ULocale.forLocale(locale);
return getSymbols(uLocale);
}
private static DecimalFormatSymbols getSymbols(ULocale uLocale) {
if (uLocale == null) uLocale = ULocale.getDefault();
DecimalFormatSymbols symbols = threadLocalSymbolsCache.get().get(uLocale);
if (symbols == null) {
symbols = DecimalFormatSymbols.getInstance(uLocale);
threadLocalSymbolsCache.get().put(uLocale, symbols);
}
return symbols;
}
private static final ThreadLocal<Map<String, Properties>> threadLocalPropertiesCache =
new ThreadLocal<Map<String, Properties>>() {
@Override
protected Map<String, Properties> initialValue() {
return new HashMap<String, Properties>();
}
};
private static Properties getProperties(String pattern) {
if (pattern == null) pattern = "#";
Properties properties = threadLocalPropertiesCache.get().get(pattern);
if (properties == null) {
properties = PatternString.parseToProperties(pattern);
threadLocalPropertiesCache.get().put(pattern.intern(), properties);
}
return properties;
}
private static final ThreadLocal<Map<ULocale, PluralRules>> threadLocalRulesCache =
new ThreadLocal<Map<ULocale, PluralRules>>() {
@Override
protected Map<ULocale, PluralRules> initialValue() {
return new HashMap<ULocale, PluralRules>();
}
};
private static PluralRules getPluralRules(ULocale uLocale, Properties properties) {
// Backwards compatibility: CurrencyPluralInfo wraps its own copy of PluralRules
if (properties.getCurrencyPluralInfo() != null) {
return properties.getCurrencyPluralInfo().getPluralRules();
}
if (uLocale == null) uLocale = ULocale.getDefault();
PluralRules rules = threadLocalRulesCache.get().get(uLocale);
if (rules == null) {
rules = PluralRules.forLocale(uLocale);
threadLocalRulesCache.get().put(uLocale, rules);
}
return rules;
}
}

View File

@ -0,0 +1,15 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
/**
* This is a small interface I made to assist with converting from a formatter pipeline object to a
* pattern string. It allows classes to "export" themselves to a property bag, which in turn can be
* passed to {@link PatternString#propertiesToString(Properties)} to generate the pattern string.
*
* <p>Depending on the new API we expose, this process might not be necessary if we persist the
* property bag in the current DecimalFormat shim.
*/
public interface Exportable {
public void export(Properties properties);
}

View File

@ -0,0 +1,277 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import com.ibm.icu.text.PluralRules;
// TODO: Get a better name for this base class.
public abstract class Format {
protected static final ThreadLocal<NumberStringBuilder> threadLocalStringBuilder =
new ThreadLocal<NumberStringBuilder>() {
@Override
protected NumberStringBuilder initialValue() {
return new NumberStringBuilder();
}
};
protected static final ThreadLocal<ModifierHolder> threadLocalModifierHolder =
new ThreadLocal<ModifierHolder>() {
@Override
protected ModifierHolder initialValue() {
return new ModifierHolder();
}
};
public String format(FormatQuantity... inputs) {
// Setup
Deque<FormatQuantity> inputDeque = new ArrayDeque<FormatQuantity>();
inputDeque.addAll(Arrays.asList(inputs));
ModifierHolder modDeque = threadLocalModifierHolder.get().clear();
NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
// Primary "recursion" step, calling the implementation's process method
int length = process(inputDeque, modDeque, sb, 0);
// Resolve remaining affixes
length += modDeque.applyAll(sb, 0, length);
return sb.toString();
}
/** A Format that works on only one number. */
public abstract static class SingularFormat extends Format implements Exportable {
public String format(FormatQuantity input) {
NumberStringBuilder sb = formatToStringBuilder(input);
return sb.toString();
}
public void format(FormatQuantity input, StringBuffer output) {
NumberStringBuilder sb = formatToStringBuilder(input);
output.append(sb);
}
public String format(FormatQuantity input, FieldPosition fp) {
NumberStringBuilder sb = formatToStringBuilder(input);
sb.populateFieldPosition(fp, 0);
return sb.toString();
}
public void format(FormatQuantity input, StringBuffer output, FieldPosition fp) {
NumberStringBuilder sb = formatToStringBuilder(input);
sb.populateFieldPosition(fp, output.length());
output.append(sb);
}
public AttributedCharacterIterator formatToCharacterIterator(FormatQuantity input) {
NumberStringBuilder sb = formatToStringBuilder(input);
return sb.getIterator();
}
private NumberStringBuilder formatToStringBuilder(FormatQuantity input) {
// Setup
ModifierHolder modDeque = threadLocalModifierHolder.get().clear();
NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
// Primary "recursion" step, calling the implementation's process method
int length = process(input, modDeque, sb, 0);
// Resolve remaining affixes
length += modDeque.applyAll(sb, 0, length);
return sb;
}
@Override
public int process(
Deque<FormatQuantity> input,
ModifierHolder mods,
NumberStringBuilder string,
int startIndex) {
return process(input.removeFirst(), mods, string, startIndex);
}
public abstract int process(
FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex);
}
public static class BeforeTargetAfterFormat extends SingularFormat {
// The formatters are kept as individual fields to avoid extra object creation overhead.
private BeforeFormat before1 = null;
private BeforeFormat before2 = null;
private BeforeFormat before3 = null;
private TargetFormat target = null;
private AfterFormat after1 = null;
private AfterFormat after2 = null;
private AfterFormat after3 = null;
private final PluralRules rules;
public BeforeTargetAfterFormat(PluralRules rules) {
this.rules = rules;
}
public void addBeforeFormat(BeforeFormat before) {
if (before1 == null) {
before1 = before;
} else if (before2 == null) {
before2 = before;
} else if (before3 == null) {
before3 = before;
} else {
throw new IllegalArgumentException("Only three BeforeFormats are allowed at a time");
}
}
public void setTargetFormat(TargetFormat target) {
this.target = target;
}
public void addAfterFormat(AfterFormat after) {
if (after1 == null) {
after1 = after;
} else if (after2 == null) {
after2 = after;
} else if (after3 == null) {
after3 = after;
} else {
throw new IllegalArgumentException("Only three AfterFormats are allowed at a time");
}
}
@Override
public String format(FormatQuantity input) {
ModifierHolder mods = threadLocalModifierHolder.get().clear();
NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
int length = process(input, mods, sb, 0);
length += mods.applyAll(sb, 0, length);
return sb.toString();
}
@Override
public int process(
FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex) {
// Special case: modifiers are skipped for NaN
int length = 0;
if (!input.isNaN()) {
if (before1 != null) {
before1.before(input, mods, rules);
}
if (before2 != null) {
before2.before(input, mods, rules);
}
if (before3 != null) {
before3.before(input, mods, rules);
}
}
length = target.target(input, string, startIndex);
length += mods.applyStrong(string, startIndex, startIndex + length);
if (after1 != null) {
length += after1.after(mods, string, startIndex, startIndex + length);
}
if (after2 != null) {
length += after2.after(mods, string, startIndex, startIndex + length);
}
if (after3 != null) {
length += after3.after(mods, string, startIndex, startIndex + length);
}
return length;
}
@Override
public void export(Properties properties) {
if (before1 != null) {
before1.export(properties);
}
if (before2 != null) {
before2.export(properties);
}
if (before3 != null) {
before3.export(properties);
}
target.export(properties);
if (after1 != null) {
after1.export(properties);
}
if (after2 != null) {
after2.export(properties);
}
if (after3 != null) {
after3.export(properties);
}
}
}
public static class PositiveNegativeRounderTargetFormat extends SingularFormat {
private final Modifier.PositiveNegativeModifier positiveNegative;
private final Rounder rounder;
private final TargetFormat target;
public PositiveNegativeRounderTargetFormat(
Modifier.PositiveNegativeModifier positiveNegative, Rounder rounder, TargetFormat target) {
this.positiveNegative = positiveNegative;
this.rounder = rounder;
this.target = target;
}
@Override
public String format(FormatQuantity input) {
NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
process(input, null, sb, 0);
return sb.toString();
}
@Override
public int process(
FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex) {
// Special case: modifiers are skipped for NaN
Modifier mod = null;
rounder.apply(input);
if (!input.isNaN() && positiveNegative != null) {
mod = positiveNegative.getModifier(input.isNegative());
}
int length = target.target(input, string, startIndex);
if (mod != null) {
length += mod.apply(string, 0, length);
}
return length;
}
@Override
public void export(Properties properties) {
rounder.export(properties);
positiveNegative.export(properties);
target.export(properties);
}
}
public abstract static class BeforeFormat implements Exportable {
protected abstract void before(FormatQuantity input, ModifierHolder mods);
@SuppressWarnings("unused")
public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
before(input, mods);
}
}
public static interface TargetFormat extends Exportable {
public abstract int target(FormatQuantity input, NumberStringBuilder string, int startIndex);
}
public static interface AfterFormat extends Exportable {
public abstract int after(
ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex);
}
// Instead of Dequeue<BigDecimal>, it could be Deque<Quantity> where
// we control the API of Quantity
public abstract int process(
Deque<FormatQuantity> inputs,
ModifierHolder outputMods,
NumberStringBuilder outputString,
int startIndex);
}

View File

@ -0,0 +1,180 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.math.MathContext;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.text.PluralRules;
/**
* An interface representing a number to be processed by the decimal formatting pipeline. Includes
* methods for rounding, plural rules, and decimal digit extraction.
*
* <p>By design, this is NOT IMMUTABLE and NOT THREAD SAFE. It is intended to be an intermediate
* object holding state during a pass through the decimal formatting pipeline.
*
* <p>Implementations of this interface are free to use any internal storage mechanism.
*
* <p>TODO: Should I change this to an abstract class so that logic for min/max digits doesn't need
* to be copied to every implementation?
*/
public interface FormatQuantity extends PluralRules.IFixedDecimal {
/**
* Sets the minimum and maximum digits that this {@link FormatQuantity} should generate. This
* method does not perform rounding.
*
* @param minInt The minimum number of integer digits.
* @param maxInt The maximum number of integer digits.
* @param minFrac The minimum number of fraction digits.
* @param maxFrac The maximum number of fraction digits.
*/
public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac);
/**
* Rounds the number to a specified interval, such as 0.05.
*
* <p>If rounding to a power of ten, use the more efficient {@link #roundToMagnitude} instead.
*
* @param roundingInterval The increment to which to round.
* @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior
* if null.
*/
public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext);
/**
* Rounds the number to a specified magnitude (power of ten).
*
* @param roundingMagnitude The power of ten to which to round. For example, a value of -2 will
* round to 2 decimal places.
* @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior
* if null.
*/
public void roundToMagnitude(int roundingMagnitude, MathContext mathContext);
/**
* Rounds the number to an infinite number of decimal points. This has no effect except for
* forcing the double in {@link FormatQuantityBCD} to adopt its exact representation.
*/
public void roundToInfinity();
/**
* Multiply the internal value.
*
* @param multiplicand The value by which to multiply.
*/
public void multiplyBy(BigDecimal multiplicand);
/**
* Scales the number by a power of ten. For example, if the value is currently "1234.56", calling
* this method with delta=-3 will change the value to "1.23456".
*
* @param delta The number of magnitudes of ten to change by.
*/
public void adjustMagnitude(int delta);
/**
* @return The power of ten corresponding to the most significant nonzero digit.
* @throws ArithmeticException If the value represented is zero.
*/
public int getMagnitude() throws ArithmeticException;
/** @return Whether the value represented by this {@link FormatQuantity} is zero. */
public boolean isZero();
/** @return Whether the value represented by this {@link FormatQuantity} is less than zero. */
public boolean isNegative();
/** @return Whether the value represented by this {@link FormatQuantity} is infinite. */
@Override
public boolean isInfinite();
/** @return Whether the value represented by this {@link FormatQuantity} is not a number. */
@Override
public boolean isNaN();
/** @return The value contained in this {@link FormatQuantity} approximated as a double. */
public double toDouble();
public BigDecimal toBigDecimal();
public int maxRepresentableDigits();
// TODO: Should this method be removed, since FormatQuantity implements IFixedDecimal now?
/**
* Computes the plural form for this number based on the specified set of rules.
*
* @param rules A {@link PluralRules} object representing the set of rules.
* @return The {@link StandardPlural} according to the PluralRules. If the plural form is not in
* the set of standard plurals, {@link StandardPlural#OTHER} is returned instead.
*/
public StandardPlural getStandardPlural(PluralRules rules);
// /**
// * @return The number of fraction digits, always in the closed interval [minFrac, maxFrac].
// * @see #setIntegerFractionLength(int, int, int, int)
// */
// public int fractionCount();
//
// /**
// * @return The number of integer digits, always in the closed interval [minInt, maxInt].
// * @see #setIntegerFractionLength(int, int, int, int)
// */
// public int integerCount();
//
// /**
// * @param index The index of the fraction digit relative to the decimal place, or 1 minus the
// * digit's power of ten.
// * @return The digit at the specified index. Undefined if index is greater than maxInt or less
// * than 0.
// * @see #fractionCount()
// */
// public byte getFractionDigit(int index);
//
// /**
// * @param index The index of the integer digit relative to the decimal place, or the digit's power
// * of ten.
// * @return The digit at the specified index. Undefined if index is greater than maxInt or less
// * than 0.
// * @see #integerCount()
// */
// public byte getIntegerDigit(int index);
/**
* Gets the digit at the specified magnitude. For example, if the represented number is 12.3,
* getDigit(-1) returns 3, since 3 is the digit corresponding to 10^-1.
*
* @param magnitude The magnitude of the digit.
* @return The digit at the specified magnitude.
*/
public byte getDigit(int magnitude);
/**
* Gets the largest power of ten that needs to be displayed. The value returned by this function
* will be bounded between minInt and maxInt.
*
* @return The highest-magnitude digit to be displayed.
*/
public int getUpperDisplayMagnitude();
/**
* Gets the smallest power of ten that needs to be displayed. The value returned by this function
* will be bounded between -minFrac and -maxFrac.
*
* @return The lowest-magnitude digit to be displayed.
*/
public int getLowerDisplayMagnitude();
public FormatQuantity clone();
public void copyFrom(FormatQuantity other);
/**
* This method is for internal testing only and should be removed before release.
*
* @internal
*/
public long getPositionFingerprint();
}

View File

@ -0,0 +1,856 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.text.PluralRules.Operand;
/**
* This is an older implementation of FormatQuantity. A newer, faster implementation is
* FormatQuantity2. I kept this implementation around because it was useful for testing purposes
* (being able to compare the output of one implementation with the other).
*
* <p>This class is NOT IMMUTABLE and NOT THREAD SAFE and is intended to be used by a single thread
* to format a number through a formatter, which is thread-safe.
*/
public class FormatQuantity1 implements FormatQuantity {
// Four positions: left optional '(', left required '[', right required ']', right optional ')'.
// These four positions determine which digits are displayed in the output string. They do NOT
// affect rounding. These positions are internal-only and can be specified only by the public
// endpoints like setFractionLength, setIntegerLength, and setSignificantDigits, among others.
//
// * Digits between lReqPos and rReqPos are in the "required zone" and are always displayed.
// * Digits between lOptPos and rOptPos but outside the required zone are in the "optional zone"
// and are displayed unless they are trailing off the left or right edge of the number and
// have a numerical value of zero. In order to be "trailing", the digits need to be beyond
// the decimal point in their respective directions.
// * Digits outside of the "optional zone" are never displayed.
//
// See the table below for illustrative examples.
//
// +---------+---------+---------+---------+------------+------------------------+--------------+
// | lOptPos | lReqPos | rReqPos | rOptPos | number | positions | en-US string |
// +---------+---------+---------+---------+------------+------------------------+--------------+
// | 5 | 2 | -1 | -5 | 1234.567 | ( 12[34.5]67 ) | 1,234.567 |
// | 3 | 2 | -1 | -5 | 1234.567 | 1(2[34.5]67 ) | 234.567 |
// | 3 | 2 | -1 | -2 | 1234.567 | 1(2[34.5]6)7 | 234.56 |
// | 6 | 4 | 2 | -5 | 123456789. | 123(45[67]89. ) | 456,789. |
// | 6 | 4 | 2 | 1 | 123456789. | 123(45[67]8)9. | 456,780. |
// | -1 | -1 | -3 | -4 | 0.123456 | 0.1([23]4)56 | .0234 |
// | 6 | 4 | -2 | -2 | 12.3 | ( [ 12.3 ]) | 0012.30 |
// +---------+---------+---------+---------+------------+------------------------+--------------+
//
private int lOptPos = Integer.MAX_VALUE;
private int lReqPos = 0;
private int rReqPos = 0;
private int rOptPos = Integer.MIN_VALUE;
// Internally, attempt to use a long to store the number. A long can hold numbers between 18 and
// 19 digits, covering the vast majority of use cases. We store three values: the long itself,
// the "scale" of the long (the power of 10 represented by the rightmost digit in the long), and
// the "precision" (the number of digits in the long). "primary" and "primaryScale" are the only
// two variables that are required for representing the number in memory. "primaryPrecision" is
// saved only for the sake of performance enhancements when performing certain operations. It can
// always be re-computed from "primary" and "primaryScale".
private long primary;
private int primaryScale;
private int primaryPrecision;
// If the decimal can't fit into the long, fall back to a BigDecimal.
private BigDecimal fallback;
// Other properties
private int flags;
private static final int NEGATIVE_FLAG = 1;
private static final int INFINITY_FLAG = 2;
private static final int NAN_FLAG = 4;
private static final long[] POWERS_OF_TEN = {
1L,
10L,
100L,
1000L,
10000L,
100000L,
1000000L,
10000000L,
100000000L,
1000000000L,
10000000000L,
100000000000L,
1000000000000L,
10000000000000L,
100000000000000L,
1000000000000000L,
10000000000000000L,
100000000000000000L,
1000000000000000000L
};
@Override
public int maxRepresentableDigits() {
return Integer.MAX_VALUE;
}
public FormatQuantity1(long input) {
if (input < 0) {
setNegative(true);
input *= -1;
}
primary = input;
primaryScale = 0;
primaryPrecision = computePrecision(primary);
fallback = null;
}
/**
* Creates a FormatQuantity from the given double value. Internally attempts several strategies
* for converting the double to an exact representation, falling back on a BigDecimal if it fails
* to do so.
*
* @param input The double to represent by this FormatQuantity.
*/
public FormatQuantity1(double input) {
if (input < 0) {
setNegative(true);
input *= -1;
}
// First try reading from IEEE bits. This is trivial only for doubles in [2^52, 2^64). If it
// fails, we wasted only a few CPU cycles.
long ieeeBits = Double.doubleToLongBits(input);
int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
if (exponent >= 52 && exponent <= 63) {
// We can convert this double directly to a long.
long mantissa = (ieeeBits & 0x000fffffffffffffL) + 0x0010000000000000L;
primary = (mantissa << (exponent - 52));
primaryScale = 0;
primaryPrecision = computePrecision(primary);
return;
}
// Now try parsing the string produced by Double.toString().
String temp = Double.toString(input);
try {
if (temp.length() == 3 && temp.equals("0.0")) {
// Case 1: Zero.
primary = 0L;
primaryScale = 0;
primaryPrecision = 0;
} else if (temp.indexOf('E') != -1) {
// Case 2: Exponential notation.
assert temp.indexOf('.') == 1;
int expPos = temp.indexOf('E');
primary = Long.parseLong(temp.charAt(0) + temp.substring(2, expPos));
primaryScale = Integer.parseInt(temp.substring(expPos + 1)) - (expPos - 1) + 1;
primaryPrecision = expPos - 1;
} else if (temp.charAt(0) == '0') {
// Case 3: Fraction-only number.
assert temp.indexOf('.') == 1;
primary = Long.parseLong(temp.substring(2)); // ignores leading zeros
primaryScale = 2 - temp.length();
primaryPrecision = computePrecision(primary);
} else if (temp.charAt(temp.length() - 1) == '0') {
// Case 4: Integer-only number.
assert temp.indexOf('.') == temp.length() - 2;
int rightmostNonzeroDigitIndex = temp.length() - 3;
while (temp.charAt(rightmostNonzeroDigitIndex) == '0') {
rightmostNonzeroDigitIndex -= 1;
}
primary = Long.parseLong(temp.substring(0, rightmostNonzeroDigitIndex + 1));
primaryScale = temp.length() - rightmostNonzeroDigitIndex - 3;
primaryPrecision = rightmostNonzeroDigitIndex + 1;
} else if (temp.equals("Infinity")) {
// Case 5: Infinity.
primary = 0;
setInfinity(true);
} else if (temp.equals("NaN")) {
// Case 6: NaN.
primary = 0;
setNaN(true);
} else {
// Case 7: Number with both a fraction and an integer.
int decimalPos = temp.indexOf('.');
primary = Long.parseLong(temp.substring(0, decimalPos) + temp.substring(decimalPos + 1));
primaryScale = decimalPos - temp.length() + 1;
primaryPrecision = temp.length() - 1;
}
} catch (NumberFormatException e) {
// The digits of the double can't fit into the long.
primary = -1;
fallback = new BigDecimal(temp);
}
}
static final double LOG_2_OF_TEN = 3.32192809489;
public FormatQuantity1(double input, boolean fast) {
if (input < 0) {
setNegative(true);
input *= -1;
}
// Our strategy is to read all digits that are *guaranteed* to be valid without delving into
// the IEEE rounding rules. This strategy might not end up with a perfect representation of
// the fractional part of the double.
long ieeeBits = Double.doubleToLongBits(input);
int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
long mantissa = (ieeeBits & 0x000fffffffffffffL) + 0x0010000000000000L;
if (exponent > 63) {
throw new IllegalArgumentException(); // FIXME
} else if (exponent >= 52) {
primary = (mantissa << (exponent - 52));
primaryScale = 0;
primaryPrecision = computePrecision(primary);
return;
} else if (exponent >= 0) {
int shift = 52 - exponent;
primary = (mantissa >> shift); // integer part
int fractionCount = (int) (shift / LOG_2_OF_TEN);
long fraction = (mantissa - (primary << shift)) + 1L; // TODO: Explain the +1L
primary *= POWERS_OF_TEN[fractionCount];
for (int i = 0; i < fractionCount; i++) {
long times10 = (fraction * 10L);
long digit = times10 >> shift;
assert digit >= 0 && digit < 10;
primary += digit * POWERS_OF_TEN[fractionCount - i - 1];
fraction = times10 & ((1L << shift) - 1);
}
primaryScale = -fractionCount;
primaryPrecision = computePrecision(primary);
} else {
throw new IllegalArgumentException(); // FIXME
}
}
public FormatQuantity1(BigDecimal decimal) {
if (decimal.compareTo(BigDecimal.ZERO) < 0) {
setNegative(true);
decimal = decimal.negate();
}
primary = -1;
if (decimal.compareTo(BigDecimal.ZERO) == 0) {
fallback = BigDecimal.ZERO;
} else {
fallback = decimal;
}
}
public FormatQuantity1(FormatQuantity1 other) {
copyFrom(other);
}
@Override
public FormatQuantity clone() {
return new FormatQuantity1(this);
}
/**
* Make the internal state of this FormatQuantity equal to another FormatQuantity.
*
* @param other The template FormatQuantity. All properties from this FormatQuantity will be
* copied into this FormatQuantity.
*/
@Override
public void copyFrom(FormatQuantity other) {
// TODO: Check before casting
FormatQuantity1 _other = (FormatQuantity1) other;
lOptPos = _other.lOptPos;
lReqPos = _other.lReqPos;
rReqPos = _other.rReqPos;
rOptPos = _other.rOptPos;
primary = _other.primary;
primaryScale = _other.primaryScale;
primaryPrecision = _other.primaryPrecision;
fallback = _other.fallback;
flags = _other.flags;
}
@Override
public long getPositionFingerprint() {
long fingerprint = 0;
fingerprint ^= lOptPos;
fingerprint ^= (lReqPos << 16);
fingerprint ^= ((long) rReqPos << 32);
fingerprint ^= ((long) rOptPos << 48);
return fingerprint;
}
/**
* Utility method to compute the number of digits ("precision") in a long.
*
* @param input The long (which can't contain more than 19 digits).
* @return The precision of the long.
*/
private static int computePrecision(long input) {
int precision = 0;
while (input > 0) {
input /= 10;
precision++;
}
return precision;
}
/**
* Changes the internal representation from a long to a BigDecimal. Used only for operations that
* don't support longs.
*/
private void convertToBigDecimal() {
if (primary == -1) {
return;
}
fallback = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
primary = -1;
}
@Override
public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
// Graceful failures for bogus input
minInt = Math.max(0, minInt);
maxInt = Math.max(0, maxInt);
minFrac = Math.max(0, minFrac);
maxFrac = Math.max(0, maxFrac);
// The minima must be less than or equal to the maxima
if (maxInt < minInt) {
minInt = maxInt;
}
if (maxFrac < minFrac) {
minFrac = maxFrac;
}
// Displaying neither integer nor fraction digits is not allowed
if (maxInt == 0 && maxFrac == 0) {
maxInt = Integer.MAX_VALUE;
maxFrac = Integer.MAX_VALUE;
}
// Save values into internal state
// Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
lOptPos = maxInt;
lReqPos = minInt;
rReqPos = -minFrac;
rOptPos = -maxFrac;
}
@Override
public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext) {
BigDecimal d =
(primary == -1) ? fallback : new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
if (isNegative()) d = d.negate();
d = d.divide(roundingInterval, 0, mathContext.getRoundingMode()).multiply(roundingInterval);
if (isNegative()) d = d.negate();
fallback = d;
primary = -1;
}
@Override
public void roundToMagnitude(int roundingMagnitude, MathContext mathContext) {
if (roundingMagnitude < -1000) {
roundToInfinity();
return;
}
if (primary == -1) {
if (isNegative()) fallback = fallback.negate();
fallback = fallback.setScale(-roundingMagnitude, mathContext.getRoundingMode());
if (isNegative()) fallback = fallback.negate();
// Enforce the math context.
fallback = fallback.round(mathContext);
} else {
int relativeScale = primaryScale - roundingMagnitude;
if (relativeScale < -18) {
// No digits will remain after rounding the number.
primary = 0L;
primaryScale = roundingMagnitude;
primaryPrecision = 0;
} else if (relativeScale < 0) {
// This is the harder case, when we need to perform the rounding logic.
// First check if the rightmost digits are already zero, where we can skip rounding.
if ((primary % POWERS_OF_TEN[0 - relativeScale]) == 0) {
// No rounding is necessary.
} else {
// TODO: Make this more efficient. Temporarily, convert to a BigDecimal and back again.
BigDecimal temp = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
if (isNegative()) temp = temp.negate();
temp = temp.setScale(-roundingMagnitude, mathContext.getRoundingMode());
if (isNegative()) temp = temp.negate();
temp = temp.scaleByPowerOfTen(-roundingMagnitude);
primary = temp.longValueExact(); // should never throw
primaryScale = roundingMagnitude;
primaryPrecision = computePrecision(primary);
}
} else {
// No rounding is necessary. All digits are to the left of the rounding magnitude.
}
// Enforce the math context.
primary = new BigDecimal(primary).round(mathContext).longValueExact();
primaryPrecision = computePrecision(primary);
}
}
@Override
public void roundToInfinity() {
// noop
}
/**
* Multiply the internal number by the specified multiplicand. This method forces the internal
* representation into a BigDecimal. If you are multiplying by a power of 10, use {@link
* #adjustMagnitude} instead.
*
* @param multiplicand The number to be passed to {@link BigDecimal#multiply}.
*/
@Override
public void multiplyBy(BigDecimal multiplicand) {
convertToBigDecimal();
fallback = fallback.multiply(multiplicand);
if (fallback.compareTo(BigDecimal.ZERO) < 0) {
setNegative(!isNegative());
fallback = fallback.negate();
}
}
/**
* Divide the internal number by the specified quotient. This method forces the internal
* representation into a BigDecimal. If you are dividing by a power of 10, use {@link
* #adjustMagnitude} instead.
*
* @param divisor The number to be passed to {@link BigDecimal#divide}.
* @param scale The scale of the final rounded number. More negative means more decimal places.
* @param mathContext The math context to use if rounding is necessary.
*/
@SuppressWarnings("unused")
private void divideBy(BigDecimal divisor, int scale, MathContext mathContext) {
convertToBigDecimal();
// Negate the scale because BigDecimal's scale is defined as the inverse of our scale
fallback = fallback.divide(divisor, -scale, mathContext.getRoundingMode());
if (fallback.compareTo(BigDecimal.ZERO) < 0) {
setNegative(!isNegative());
fallback = fallback.negate();
}
}
@Override
public boolean isZero() {
if (primary == -1) {
return fallback.compareTo(BigDecimal.ZERO) == 0;
} else {
return primary == 0;
}
}
/** @return The power of ten of the highest digit represented by this FormatQuantity */
@Override
public int getMagnitude() throws ArithmeticException {
int scale = (primary == -1) ? scaleBigDecimal(fallback) : primaryScale;
int precision = (primary == -1) ? precisionBigDecimal(fallback) : primaryPrecision;
if (precision == 0) {
throw new ArithmeticException("Magnitude is not well-defined for zero");
} else {
return scale + precision - 1;
}
}
/**
* Changes the magnitude of this FormatQuantity. If the indices of the represented digits had been
* previously specified, those indices are moved relative to the FormatQuantity.
*
* <p>This method does NOT perform rounding.
*
* @param delta The number of powers of ten to shift (positive shifts to the left).
*/
@Override
public void adjustMagnitude(int delta) {
if (primary == -1) {
fallback = fallback.scaleByPowerOfTen(delta);
} else {
primaryScale = addOrMaxValue(primaryScale, delta);
}
}
private static int addOrMaxValue(int a, int b) {
// Check for overflow, and return min/max value if overflow occurs.
if (b < 0 && a + b > a) {
return Integer.MIN_VALUE;
} else if (b > 0 && a + b < a) {
return Integer.MAX_VALUE;
}
return a + b;
}
/** @return If the number represented by this FormatQuantity is less than zero */
@Override
public boolean isNegative() {
return (flags & NEGATIVE_FLAG) != 0;
}
private void setNegative(boolean isNegative) {
flags = (flags & (~NEGATIVE_FLAG)) | (isNegative ? NEGATIVE_FLAG : 0);
}
@Override
public boolean isInfinite() {
return (flags & INFINITY_FLAG) != 0;
}
private void setInfinity(boolean isInfinity) {
flags = (flags & (~INFINITY_FLAG)) | (isInfinity ? INFINITY_FLAG : 0);
}
@Override
public boolean isNaN() {
return (flags & NAN_FLAG) != 0;
}
private void setNaN(boolean isNaN) {
flags = (flags & (~NAN_FLAG)) | (isNaN ? NAN_FLAG : 0);
}
/**
* Returns a representation of this FormatQuantity as a double, with possible loss of information.
*/
@Override
public double toDouble() {
double result;
if (primary == -1) {
result = fallback.doubleValue();
} else {
// TODO: Make this more efficient
result = primary;
for (int i = 0; i < primaryScale; i++) {
result *= 10.;
}
for (int i = 0; i > primaryScale; i--) {
result /= 10.;
}
}
return isNegative() ? -result : result;
}
@Override
public BigDecimal toBigDecimal() {
BigDecimal result;
if (primary != -1) {
result = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
} else {
result = fallback;
}
return isNegative() ? result.negate() : result;
}
@Override
public StandardPlural getStandardPlural(PluralRules rules) {
if (rules == null) {
// Fail gracefully if the user didn't provide a PluralRules
return StandardPlural.OTHER;
} else {
// TODO: Avoid converting to a double for the sake of PluralRules
String ruleString = rules.select(toDouble());
return StandardPlural.orOtherFromString(ruleString);
}
}
@Override
public double getPluralOperand(Operand operand) {
// TODO: This is a temporary hack.
return new PluralRules.FixedDecimal(toDouble()).getPluralOperand(operand);
}
public boolean hasNextFraction() {
if (rReqPos < 0) {
// We are in the required zone.
return true;
} else if (rOptPos >= 0) {
// We are in the forbidden zone.
return false;
} else {
// We are in the optional zone.
if (primary == -1) {
return fallback.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) > 0;
} else {
if (primaryScale <= -19) {
// The number is a fraction so small that it consists of only fraction digits.
return primary > 0;
} else if (primaryScale < 0) {
// Check if we have a fraction part.
long factor = POWERS_OF_TEN[0 - primaryScale];
return ((primary % factor) != 0);
} else {
// The lowest digit in the long has magnitude greater than -1.
return false;
}
}
}
}
public byte nextFraction() {
byte returnValue;
if (primary == -1) {
BigDecimal temp = fallback.multiply(BigDecimal.TEN);
returnValue = temp.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue();
fallback = fallback.setScale(0, RoundingMode.FLOOR).add(temp.remainder(BigDecimal.ONE));
} else {
if (primaryScale <= -20) {
// The number is a fraction so small that it has no first fraction digit.
primaryScale += 1;
returnValue = 0;
} else if (primaryScale < 0) {
// Extract the fraction digit out of the middle of the long.
long factor = POWERS_OF_TEN[0 - primaryScale - 1];
long temp1 = primary / factor;
long temp2 = primary % factor;
returnValue = (byte) (temp1 % 10); // not necessarily nonzero
primary = ((temp1 / 10) * factor) + temp2;
primaryScale += 1;
if (temp1 != 0) {
primaryPrecision -= 1;
}
} else {
// The lowest digit in the long has magnitude greater than -1.
returnValue = 0;
}
}
// Update digit brackets
if (lOptPos < 0) {
lOptPos += 1;
}
if (lReqPos < 0) {
lReqPos += 1;
}
if (rReqPos < 0) {
rReqPos += 1;
}
if (rOptPos < 0) {
rOptPos += 1;
}
assert returnValue >= 0;
return returnValue;
}
public boolean hasNextInteger() {
if (lReqPos > 0) {
// We are in the required zone.
return true;
} else if (lOptPos <= 0) {
// We are in the forbidden zone.
return false;
} else {
// We are in the optional zone.
if (primary == -1) {
return fallback.setScale(0, RoundingMode.FLOOR).compareTo(BigDecimal.ZERO) > 0;
} else {
if (primaryScale < -18) {
// The number is a fraction so small that it has no integer part.
return false;
} else if (primaryScale < 0) {
// Check if we have an integer part.
long factor = POWERS_OF_TEN[0 - primaryScale];
return ((primary % factor) != primary); // equivalent: ((primary / 10) != 0)
} else {
// The lowest digit in the long has magnitude of at least 0.
return primary != 0;
}
}
}
}
private int integerCount() {
int digitsRemaining;
if (primary == -1) {
digitsRemaining = precisionBigDecimal(fallback) + scaleBigDecimal(fallback);
} else {
digitsRemaining = primaryPrecision + primaryScale;
}
return Math.min(Math.max(digitsRemaining, lReqPos), lOptPos);
}
private int fractionCount() {
// TODO: This is temporary.
FormatQuantity1 copy = (FormatQuantity1) this.clone();
int fractionCount = 0;
while (copy.hasNextFraction()) {
copy.nextFraction();
fractionCount++;
}
return fractionCount;
}
@Override
public int getUpperDisplayMagnitude() {
return integerCount() - 1;
}
@Override
public int getLowerDisplayMagnitude() {
return -fractionCount();
}
// @Override
// public byte getIntegerDigit(int index) {
// return getDigitPos(index);
// }
//
// @Override
// public byte getFractionDigit(int index) {
// return getDigitPos(-index - 1);
// }
@Override
public byte getDigit(int magnitude) {
// TODO: This is temporary.
FormatQuantity1 copy = (FormatQuantity1) this.clone();
if (magnitude < 0) {
for (int p = -1; p > magnitude; p--) {
copy.nextFraction();
}
return copy.nextFraction();
} else {
for (int p = 0; p < magnitude; p++) {
copy.nextInteger();
}
return copy.nextInteger();
}
}
public byte nextInteger() {
byte returnValue;
if (primary == -1) {
returnValue = fallback.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue();
BigDecimal temp = fallback.divide(BigDecimal.TEN).setScale(0, RoundingMode.FLOOR);
fallback = fallback.remainder(BigDecimal.ONE).add(temp);
} else {
if (primaryScale < -18) {
// The number is a fraction so small that it has no integer part.
returnValue = 0;
} else if (primaryScale < 0) {
// Extract the integer digit out of the middle of the long. In many ways, this is the heart
// of the digit iterator algorithm.
long factor = POWERS_OF_TEN[0 - primaryScale];
if ((primary % factor) != primary) { // equivalent: ((primary / 10) != 0)
returnValue = (byte) ((primary / factor) % 10);
long temp = (primary / 10);
primary = temp - (temp % factor) + (primary % factor);
primaryPrecision -= 1;
} else {
returnValue = 0;
}
} else if (primaryScale == 0) {
// Fast-path for primaryScale == 0 (otherwise equivalent to previous step).
if (primary != 0) {
returnValue = (byte) (primary % 10);
primary /= 10;
primaryPrecision -= 1;
} else {
returnValue = 0;
}
} else {
// The lowest digit in the long has magnitude greater than 0.
primaryScale -= 1;
returnValue = 0;
}
}
// Update digit brackets
if (lOptPos > 0) {
lOptPos -= 1;
}
if (lReqPos > 0) {
lReqPos -= 1;
}
if (rReqPos > 0) {
rReqPos -= 1;
}
if (rOptPos > 0) {
rOptPos -= 1;
}
assert returnValue >= 0;
return returnValue;
}
/**
* Helper method to compute the precision of a BigDecimal by our definition of precision, which is
* that the number zero gets precision zero.
*
* @param decimal The BigDecimal whose precision to compute.
* @return The precision by our definition.
*/
private static int precisionBigDecimal(BigDecimal decimal) {
if (decimal.compareTo(BigDecimal.ZERO) == 0) {
return 0;
} else {
return decimal.precision();
}
}
/**
* Helper method to compute the scale of a BigDecimal by our definition of scale, which is that
* deeper fractions result in negative scales as opposed to positive scales.
*
* @param decimal The BigDecimal whose scale to compute.
* @return The scale by our definition.
*/
private static int scaleBigDecimal(BigDecimal decimal) {
return -decimal.scale();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("<FormatQuantity1 ");
if (primary == -1) {
sb.append(lOptPos > 1000 ? "max" : lOptPos);
sb.append(":");
sb.append(lReqPos);
sb.append(":");
sb.append(rReqPos);
sb.append(":");
sb.append(rOptPos < -1000 ? "min" : rOptPos);
sb.append(" ");
sb.append(fallback.toString());
} else {
String digits = Long.toString(primary);
int iDec = digits.length() + primaryScale;
int iLP = iDec - toRange(lOptPos, -1000, 1000);
int iLB = iDec - toRange(lReqPos, -1000, 1000);
int iRB = iDec - toRange(rReqPos, -1000, 1000);
int iRP = iDec - toRange(rOptPos, -1000, 1000);
iDec = Math.max(Math.min(iDec, digits.length() + 1), -1);
iLP = Math.max(Math.min(iLP, digits.length() + 1), -1);
iLB = Math.max(Math.min(iLB, digits.length() + 1), -1);
iRB = Math.max(Math.min(iRB, digits.length() + 1), -1);
iRP = Math.max(Math.min(iRP, digits.length() + 1), -1);
for (int i = -1; i <= digits.length() + 1; i++) {
if (i == iLP) sb.append('(');
if (i == iLB) sb.append('[');
if (i == iDec) sb.append('.');
if (i == iRB) sb.append(']');
if (i == iRP) sb.append(')');
if (i >= 0 && i < digits.length()) sb.append(digits.charAt(i));
else sb.append('\u00A0');
}
}
sb.append(">");
return sb.toString();
}
private static int toRange(int i, int lo, int hi) {
if (i < lo) {
return lo;
} else if (i > hi) {
return hi;
} else {
return i;
}
}
}

View File

@ -0,0 +1,173 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.math.BigInteger;
public final class FormatQuantity2 extends FormatQuantityBCD {
/**
* The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
* to one digit. For example, the number "12345" in BCD is "0x12345".
*
* <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
* like setting the digit to zero.
*/
private long bcd;
@Override
public int maxRepresentableDigits() {
return 16;
}
public FormatQuantity2(long input) {
setToLong(input);
}
public FormatQuantity2(int input) {
setToInt(input);
}
public FormatQuantity2(double input) {
setToDouble(input);
}
public FormatQuantity2(BigInteger input) {
setToBigInteger(input);
}
public FormatQuantity2(BigDecimal input) {
setToBigDecimal(input);
}
public FormatQuantity2(FormatQuantity2 other) {
copyFrom(other);
}
@Override
protected byte getDigitPos(int position) {
if (position < 0 || position >= 16) return 0;
return (byte) ((bcd >>> (position * 4)) & 0xf);
}
@Override
protected void setDigitPos(int position, byte value) {
assert position >= 0 && position < 16;
int shift = position * 4;
bcd = bcd & ~(0xfL << shift) | ((long) value << shift);
}
@Override
protected void shiftLeft(int numDigits) {
assert precision + numDigits <= 16;
bcd <<= (numDigits * 4);
scale -= numDigits;
precision += numDigits;
}
@Override
protected void shiftRight(int numDigits) {
bcd >>>= (numDigits * 4);
scale += numDigits;
precision -= numDigits;
}
@Override
protected void setBcdToZero() {
bcd = 0L;
scale = 0;
precision = 0;
isApproximate = false;
origDouble = 0;
origDelta = 0;
}
@Override
protected void readIntToBcd(int n) {
long result = 0L;
int i = 16;
for (; n != 0; n /= 10, i--) {
result = (result >>> 4) + (((long) n % 10) << 60);
}
// ints can't overflow the 16 digits in the BCD, so scale is always zero
bcd = result >>> (i * 4);
scale = 0;
precision = 16 - i;
}
@Override
protected void readLongToBcd(long n) {
long result = 0L;
int i = 16;
for (; n != 0L; n /= 10L, i--) {
result = (result >>> 4) + ((n % 10) << 60);
}
int adjustment = (i > 0) ? i : 0;
bcd = result >>> (adjustment * 4);
scale = (i < 0) ? -i : 0;
precision = 16 - i;
}
@Override
protected void readBigIntegerToBcd(BigInteger n) {
long result = 0L;
int i = 16;
for (; n.signum() != 0; i--) {
BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
result = (result >>> 4) + (temp[1].longValue() << 60);
n = temp[0];
}
int adjustment = (i > 0) ? i : 0;
bcd = result >>> (adjustment * 4);
scale = (i < 0) ? -i : 0;
}
@Override
protected BigDecimal bcdToBigDecimal() {
long tempLong = 0L;
for (int shift = (precision - 1); shift >= 0; shift--) {
tempLong = tempLong * 10 + getDigitPos(shift);
}
BigDecimal result = BigDecimal.valueOf(tempLong);
result = result.scaleByPowerOfTen(scale);
if (isNegative()) result = result.negate();
return result;
}
@Override
protected void compact() {
// Special handling for 0
if (bcd == 0L) {
scale = 0;
precision = 0;
return;
}
// Compact the number (remove trailing zeros)
int delta = Long.numberOfTrailingZeros(bcd) / 4;
bcd >>>= delta * 4;
scale += delta;
// Compute precision
precision = 16 - (Long.numberOfLeadingZeros(bcd) / 4);
}
@Override
protected void copyBcdFrom(FormatQuantity _other) {
FormatQuantity2 other = (FormatQuantity2) _other;
bcd = other.bcd;
}
@Override
public String toString() {
return String.format(
"<FormatQuantity2 %s:%d:%d:%s %016XE%d>",
(lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
lReqPos,
rReqPos,
(rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
bcd,
scale);
}
}

View File

@ -0,0 +1,222 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.math.BigInteger;
public final class FormatQuantity3 extends FormatQuantityBCD {
/**
* The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
* to one digit. For example, the number "12345" in BCD is "0x12345".
*
* <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
* like setting the digit to zero.
*/
private byte[] bcd = new byte[100];
@Override
public int maxRepresentableDigits() {
return Integer.MAX_VALUE;
}
public FormatQuantity3(long input) {
setToLong(input);
}
public FormatQuantity3(int input) {
setToInt(input);
}
public FormatQuantity3(double input) {
setToDouble(input);
}
public FormatQuantity3(BigInteger input) {
setToBigInteger(input);
}
public FormatQuantity3(BigDecimal input) {
setToBigDecimal(input);
}
public FormatQuantity3(FormatQuantity3 other) {
copyFrom(other);
}
@Override
protected byte getDigitPos(int position) {
if (position < 0 || position > precision) return 0;
return bcd[position];
}
@Override
protected void setDigitPos(int position, byte value) {
assert position >= 0;
ensureCapacity(position + 1);
bcd[position] = value;
}
@Override
protected void shiftLeft(int numDigits) {
ensureCapacity(precision + numDigits);
int i = precision + numDigits - 1;
for (; i >= numDigits; i--) {
bcd[i] = bcd[i - numDigits];
}
for (; i >= 0; i--) {
bcd[i] = 0;
}
scale -= numDigits;
precision += numDigits;
}
@Override
protected void shiftRight(int numDigits) {
int i = 0;
for (; i < precision - numDigits; i++) {
bcd[i] = bcd[i + numDigits];
}
for (; i < precision; i++) {
bcd[i] = 0;
}
scale += numDigits;
precision -= numDigits;
}
@Override
protected void setBcdToZero() {
for (int i = 0; i < precision; i++) {
bcd[i] = (byte) 0;
}
scale = 0;
precision = 0;
isApproximate = false;
origDouble = 0;
origDelta = 0;
}
@Override
protected void readIntToBcd(int n) {
int i = 0;
for (; n != 0L; n /= 10L, i++) {
bcd[i] = (byte) (n % 10);
}
scale = 0;
precision = i;
}
private static final byte[] LONG_MIN_VALUE =
new byte[] {8, 0, 8, 5, 7, 7, 4, 5, 8, 6, 3, 0, 2, 7, 3, 3, 2, 2, 9};
@Override
protected void readLongToBcd(long n) {
if (n == Long.MIN_VALUE) {
// Can't consume via the normal path.
System.arraycopy(LONG_MIN_VALUE, 0, bcd, 0, LONG_MIN_VALUE.length);
scale = 0;
precision = LONG_MIN_VALUE.length;
return;
}
int i = 0;
for (; n != 0L; n /= 10L, i++) {
bcd[i] = (byte) (n % 10);
}
scale = 0;
precision = i;
}
@Override
protected void readBigIntegerToBcd(BigInteger n) {
int i = 0;
for (; n.signum() != 0; i++) {
BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
ensureCapacity(i + 1);
bcd[i] = temp[1].byteValue();
n = temp[0];
}
scale = 0;
precision = i;
}
@Override
protected BigDecimal bcdToBigDecimal() {
// Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
return new BigDecimal(toDumbString());
}
private String toDumbString() {
StringBuilder sb = new StringBuilder();
if (isNegative()) sb.append('-');
if (precision == 0) {
sb.append('0');
return sb.toString();
}
for (int i = precision - 1; i >= 0; i--) {
sb.append(getDigitPos(i));
}
if (scale != 0) {
sb.append('E');
sb.append(scale);
}
return sb.toString();
}
@Override
protected void compact() {
// Special handling for 0
boolean isZero = true;
for (int i = 0; i < precision; i++) {
if (bcd[i] != 0) {
isZero = false;
break;
}
}
if (isZero) {
scale = 0;
precision = 0;
return;
}
// Compact the number (remove trailing zeros)
int delta = 0;
for (; bcd[delta] == 0; delta++) ;
shiftRight(delta);
// Compute precision
int leading = precision - 1;
for (; leading >= 0 && bcd[leading] == 0; leading--) ;
precision = leading + 1;
}
private void ensureCapacity(int capacity) {
if (bcd.length >= capacity) return;
byte[] bcd1 = new byte[capacity * 2];
System.arraycopy(bcd, 0, bcd1, 0, bcd.length);
bcd = bcd1;
}
@Override
protected void copyBcdFrom(FormatQuantity _other) {
FormatQuantity3 other = (FormatQuantity3) _other;
System.arraycopy(other.bcd, 0, bcd, 0, bcd.length);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 30; i >= 0; i--) {
sb.append(bcd[i]);
}
return String.format(
"<FormatQuantity3 %s:%d:%d:%s %s%s%d>",
(lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
lReqPos,
rReqPos,
(rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
sb,
"E",
scale);
}
}

View File

@ -0,0 +1,407 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.math.BigInteger;
public final class FormatQuantity4 extends FormatQuantityBCD {
/**
* The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
* to one digit. For example, the number "12345" in BCD is "0x12345".
*
* <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
* like setting the digit to zero.
*/
private byte[] bcdBytes;
private long bcdLong = 0L;
private boolean usingBytes = false;;
@Override
public int maxRepresentableDigits() {
return Integer.MAX_VALUE;
}
public FormatQuantity4() {
setBcdToZero();
}
public FormatQuantity4(long input) {
setToLong(input);
}
public FormatQuantity4(int input) {
setToInt(input);
}
public FormatQuantity4(double input) {
setToDouble(input);
}
public FormatQuantity4(BigInteger input) {
setToBigInteger(input);
}
public FormatQuantity4(BigDecimal input) {
setToBigDecimal(input);
}
public FormatQuantity4(FormatQuantity4 other) {
copyFrom(other);
}
public FormatQuantity4(Number number) {
if (number instanceof Long) {
setToLong(number.longValue());
} else if (number instanceof Integer) {
setToInt(number.intValue());
} else if (number instanceof Double) {
setToDouble(number.doubleValue());
} else if (number instanceof BigInteger) {
setToBigInteger((BigInteger) number);
} else if (number instanceof BigDecimal) {
setToBigDecimal((BigDecimal) number);
} else if (number instanceof com.ibm.icu.math.BigDecimal) {
setToBigDecimal(((com.ibm.icu.math.BigDecimal) number).toBigDecimal());
} else {
throw new IllegalArgumentException(
"Number is of an unsupported type: " + number.getClass().getName());
}
}
@Override
protected byte getDigitPos(int position) {
if (usingBytes) {
if (position < 0 || position > precision) return 0;
return bcdBytes[position];
} else {
if (position < 0 || position >= 16) return 0;
return (byte) ((bcdLong >>> (position * 4)) & 0xf);
}
}
@Override
protected void setDigitPos(int position, byte value) {
assert position >= 0;
if (usingBytes) {
ensureCapacity(position + 1);
bcdBytes[position] = value;
} else if (position >= 16) {
switchStorage();
ensureCapacity(position + 1);
bcdBytes[position] = value;
} else {
int shift = position * 4;
bcdLong = bcdLong & ~(0xfL << shift) | ((long) value << shift);
}
}
@Override
protected void shiftLeft(int numDigits) {
if (!usingBytes && precision + numDigits > 16) {
switchStorage();
}
if (usingBytes) {
ensureCapacity(precision + numDigits);
int i = precision + numDigits - 1;
for (; i >= numDigits; i--) {
bcdBytes[i] = bcdBytes[i - numDigits];
}
for (; i >= 0; i--) {
bcdBytes[i] = 0;
}
} else {
bcdLong <<= (numDigits * 4);
}
scale -= numDigits;
precision += numDigits;
}
@Override
protected void shiftRight(int numDigits) {
if (usingBytes) {
int i = 0;
for (; i < precision - numDigits; i++) {
bcdBytes[i] = bcdBytes[i + numDigits];
}
for (; i < precision; i++) {
bcdBytes[i] = 0;
}
} else {
bcdLong >>>= (numDigits * 4);
}
scale += numDigits;
precision -= numDigits;
}
@Override
protected void setBcdToZero() {
if (usingBytes) {
for (int i = 0; i < precision; i++) {
bcdBytes[i] = (byte) 0;
}
}
usingBytes = false;
bcdLong = 0L;
scale = 0;
precision = 0;
isApproximate = false;
origDouble = 0;
origDelta = 0;
}
@Override
protected void readIntToBcd(int n) {
// ints always fit inside the long implementation.
long result = 0L;
int i = 16;
for (; n != 0; n /= 10, i--) {
result = (result >>> 4) + (((long) n % 10) << 60);
}
usingBytes = false;
bcdLong = result >>> (i * 4);
scale = 0;
precision = 16 - i;
}
@Override
protected void readLongToBcd(long n) {
if (n >= 10000000000000000L) {
ensureCapacity();
int i = 0;
for (; n != 0L; n /= 10L, i++) {
bcdBytes[i] = (byte) (n % 10);
}
usingBytes = true;
scale = 0;
precision = i;
} else {
long result = 0L;
int i = 16;
for (; n != 0L; n /= 10L, i--) {
result = (result >>> 4) + ((n % 10) << 60);
}
assert i >= 0;
usingBytes = false;
bcdLong = result >>> (i * 4);
scale = 0;
precision = 16 - i;
}
}
@Override
protected void readBigIntegerToBcd(BigInteger n) {
ensureCapacity(); // allocate initial byte array
int i = 0;
for (; n.signum() != 0; i++) {
BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
ensureCapacity(i + 1);
bcdBytes[i] = temp[1].byteValue();
n = temp[0];
}
usingBytes = true;
scale = 0;
precision = i;
}
@Override
protected BigDecimal bcdToBigDecimal() {
if (usingBytes) {
// Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
StringBuilder sb = new StringBuilder();
if (isNegative()) sb.append('-');
assert precision > 0;
for (int i = precision - 1; i >= 0; i--) {
sb.append(getDigitPos(i));
}
if (scale != 0) {
sb.append('E');
sb.append(scale);
}
return new BigDecimal(sb.toString());
} else {
long tempLong = 0L;
for (int shift = (precision - 1); shift >= 0; shift--) {
tempLong = tempLong * 10 + getDigitPos(shift);
}
BigDecimal result = BigDecimal.valueOf(tempLong);
result = result.scaleByPowerOfTen(scale);
if (isNegative()) result = result.negate();
return result;
}
}
@Override
protected void compact() {
if (usingBytes) {
int delta = 0;
for (; delta < precision && bcdBytes[delta] == 0; delta++) ;
if (delta == precision) {
// Number is zero
setBcdToZero();
return;
} else {
// Remove trailing zeros
shiftRight(delta);
}
// Compute precision
int leading = precision - 1;
for (; leading >= 0 && bcdBytes[leading] == 0; leading--) ;
precision = leading + 1;
// Switch storage mechanism if possible
if (precision <= 16) {
switchStorage();
}
} else {
if (bcdLong == 0L) {
// Number is zero
setBcdToZero();
return;
}
// Compact the number (remove trailing zeros)
int delta = Long.numberOfTrailingZeros(bcdLong) / 4;
bcdLong >>>= delta * 4;
scale += delta;
// Compute precision
precision = 16 - (Long.numberOfLeadingZeros(bcdLong) / 4);
}
}
/** Ensure that a byte array of at least 40 digits is allocated. */
private void ensureCapacity() {
ensureCapacity(40);
}
private void ensureCapacity(int capacity) {
if (bcdBytes == null && capacity > 0) {
bcdBytes = new byte[capacity];
} else if (bcdBytes.length < capacity) {
byte[] bcd1 = new byte[capacity * 2];
System.arraycopy(bcdBytes, 0, bcd1, 0, bcdBytes.length);
bcdBytes = bcd1;
}
}
/** Switches the internal storage mechanism between the 64-bit long and the byte array. */
private void switchStorage() {
if (usingBytes) {
// Change from bytes to long
bcdLong = 0L;
for (int i = precision - 1; i >= 0; i--) {
bcdLong <<= 4;
bcdLong |= bcdBytes[i];
bcdBytes[i] = 0;
}
usingBytes = false;
} else {
// Change from long to bytes
ensureCapacity();
for (int i = 0; i < precision; i++) {
bcdBytes[i] = (byte) (bcdLong & 0xf);
bcdLong >>>= 4;
}
usingBytes = true;
}
}
@Override
protected void copyBcdFrom(FormatQuantity _other) {
FormatQuantity4 other = (FormatQuantity4) _other;
if (other.usingBytes) {
usingBytes = true;
ensureCapacity(other.precision);
System.arraycopy(other.bcdBytes, 0, bcdBytes, 0, other.precision);
} else {
usingBytes = false;
bcdLong = other.bcdLong;
}
}
/**
* Checks whether the bytes stored in this instance are all valid. For internal unit testing only.
*
* @return An error message if this instance is invalid, or null if this instance is healthy.
* @internal
* @deprecated This API is for ICU internal use only.
*/
@Deprecated
public String checkHealth() {
if (usingBytes) {
if (bcdLong != 0) return "Value in bcdLong but we are in byte mode";
if (precision == 0) return "Zero precision but we are in byte mode";
if (precision > bcdBytes.length) return "Precision exceeds length of byte array";
if (getDigitPos(precision - 1) == 0) return "Most significant digit is zero in byte mode";
if (getDigitPos(0) == 0) return "Least significant digit is zero in long mode";
for (int i = 0; i < precision; i++) {
if (getDigitPos(i) >= 10) return "Digit exceeding 10 in byte array";
if (getDigitPos(i) < 0) return "Digit below 0 in byte array";
}
for (int i = precision; i < bcdBytes.length; i++) {
if (getDigitPos(i) != 0) return "Nonzero digits outside of range in byte array";
}
} else {
if (bcdBytes != null) {
for (int i = 0; i < bcdBytes.length; i++) {
if (bcdBytes[i] != 0) return "Nonzero digits in byte array but we are in long mode";
}
}
if (precision == 0 && bcdLong != 0) return "Value in bcdLong even though precision is zero";
if (precision > 16) return "Precision exceeds length of long";
if (precision != 0 && getDigitPos(precision - 1) == 0)
return "Most significant digit is zero in long mode";
if (precision != 0 && getDigitPos(0) == 0)
return "Least significant digit is zero in long mode";
for (int i = 0; i < precision; i++) {
if (getDigitPos(i) >= 10) return "Digit exceeding 10 in long";
if (getDigitPos(i) < 0) return "Digit below 0 in long (?!)";
}
for (int i = precision; i < 16; i++) {
if (getDigitPos(i) != 0) return "Nonzero digits outside of range in long";
}
}
return null;
}
/**
* Checks whether this {@link FormatQuantity4} is using its internal byte array storage mechanism.
*
* @return true if an internal byte array is being used; false if a long is being used.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public boolean usingBytes() {
return usingBytes;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (usingBytes) {
for (int i = precision - 1; i >= 0; i--) {
sb.append(bcdBytes[i]);
}
} else {
sb.append(Long.toHexString(bcdLong));
}
return String.format(
"<FormatQuantity4 %s:%d:%d:%s %s %s%s%d>",
(lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
lReqPos,
rReqPos,
(rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
(usingBytes ? "bytes" : "long"),
sb,
"E",
scale);
}
}

View File

@ -0,0 +1,900 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.text.FieldPosition;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.text.PluralRules.Operand;
import com.ibm.icu.text.UFieldPosition;
/**
* Represents numbers and digit display properties using Binary Coded Decimal (BCD).
*
* @implements {@link FormatQuantity}
*/
public abstract class FormatQuantityBCD implements FormatQuantity {
/**
* The power of ten corresponding to the least significant digit in the BCD. For example, if this
* object represents the number "3.14", the BCD will be "0x314" and the scale will be -2.
*
* <p>Note that in {@link java.math.BigDecimal}, the scale is defined differently: the number of
* digits after the decimal place, which is the negative of our definition of scale.
*/
protected int scale;
/**
* The number of digits in the BCD. For example, "1007" has BCD "0x1007" and precision 4. The
* maximum precision is 16 since a long can hold only 16 digits.
*
* <p>This value must be re-calculated whenever the value in bcd changes by using {@link
* #computePrecisionAndCompact()}.
*/
protected int precision;
/**
* A bitmask of properties relating to the number represented by this object.
*
* @see #NEGATIVE_FLAG
* @see #INFINITY_FLAG
* @see #NAN_FLAG
*/
protected int flags;
protected static final int NEGATIVE_FLAG = 1;
protected static final int INFINITY_FLAG = 2;
protected static final int NAN_FLAG = 4;
/**
* The original number provided by the user and which is represented in BCD. Used when we need to
* re-compute the BCD for an exact double representation.
*/
protected double origDouble;
protected int origDelta;
protected boolean isApproximate;
// Four positions: left optional '(', left required '[', right required ']', right optional ')'.
// These four positions determine which digits are displayed in the output string. They do NOT
// affect rounding. These positions are internal-only and can be specified only by the public
// endpoints like setFractionLength, setIntegerLength, and setSignificantDigits, among others.
//
// * Digits between lReqPos and rReqPos are in the "required zone" and are always displayed.
// * Digits between lOptPos and rOptPos but outside the required zone are in the "optional zone"
// and are displayed unless they are trailing off the left or right edge of the number and
// have a numerical value of zero. In order to be "trailing", the digits need to be beyond
// the decimal point in their respective directions.
// * Digits outside of the "optional zone" are never displayed.
//
// See the table below for illustrative examples.
//
// +---------+---------+---------+---------+------------+------------------------+--------------+
// | lOptPos | lReqPos | rReqPos | rOptPos | number | positions | en-US string |
// +---------+---------+---------+---------+------------+------------------------+--------------+
// | 5 | 2 | -1 | -5 | 1234.567 | ( 12[34.5]67 ) | 1,234.567 |
// | 3 | 2 | -1 | -5 | 1234.567 | 1(2[34.5]67 ) | 234.567 |
// | 3 | 2 | -1 | -2 | 1234.567 | 1(2[34.5]6)7 | 234.56 |
// | 6 | 4 | 2 | -5 | 123456789. | 123(45[67]89. ) | 456,789. |
// | 6 | 4 | 2 | 1 | 123456789. | 123(45[67]8)9. | 456,780. |
// | -1 | -1 | -3 | -4 | 0.123456 | 0.1([23]4)56 | .0234 |
// | 6 | 4 | -2 | -2 | 12.3 | ( [ 12.3 ]) | 0012.30 |
// +---------+---------+---------+---------+------------+------------------------+--------------+
//
protected int lOptPos = Integer.MAX_VALUE;
protected int lReqPos = 0;
protected int rReqPos = 0;
protected int rOptPos = Integer.MIN_VALUE;
@Override
public void copyFrom(FormatQuantity _other) {
copyBcdFrom(_other);
FormatQuantityBCD other = (FormatQuantityBCD) _other;
lOptPos = other.lOptPos;
lReqPos = other.lReqPos;
rReqPos = other.rReqPos;
rOptPos = other.rOptPos;
scale = other.scale;
precision = other.precision;
flags = other.flags;
origDouble = other.origDouble;
origDelta = other.origDelta;
isApproximate = other.isApproximate;
}
public FormatQuantityBCD clear() {
lOptPos = Integer.MAX_VALUE;
lReqPos = 0;
rReqPos = 0;
rOptPos = Integer.MIN_VALUE;
flags = 0;
setBcdToZero(); // sets scale, precision, hasDouble, origDouble, origDelta, and BCD data
return this;
}
@Override
public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
// Graceful failures for bogus input
minInt = Math.max(0, minInt);
maxInt = Math.max(0, maxInt);
minFrac = Math.max(0, minFrac);
maxFrac = Math.max(0, maxFrac);
// The minima must be less than or equal to the maxima
if (maxInt < minInt) {
minInt = maxInt;
}
if (maxFrac < minFrac) {
minFrac = maxFrac;
}
// Displaying neither integer nor fraction digits is not allowed
if (maxInt == 0 && maxFrac == 0) {
maxInt = Integer.MAX_VALUE;
maxFrac = Integer.MAX_VALUE;
}
// Save values into internal state
// Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
lOptPos = maxInt;
lReqPos = minInt;
rReqPos = -minFrac;
rOptPos = -maxFrac;
}
@Override
public long getPositionFingerprint() {
long fingerprint = 0;
fingerprint ^= lOptPos;
fingerprint ^= (lReqPos << 16);
fingerprint ^= ((long) rReqPos << 32);
fingerprint ^= ((long) rOptPos << 48);
return fingerprint;
}
@Override
public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext) {
// TODO: Avoid converting back and forth to BigDecimal.
BigDecimal temp = toBigDecimal();
temp =
temp.divide(roundingInterval, 0, mathContext.getRoundingMode())
.multiply(roundingInterval)
.round(mathContext);
if (temp.signum() == 0) {
setBcdToZero(); // keeps negative flag for -0.0
} else {
setToBigDecimal(temp);
}
}
@Override
public void multiplyBy(BigDecimal multiplicand) {
BigDecimal temp = toBigDecimal();
temp = temp.multiply(multiplicand);
setToBigDecimal(temp);
}
@Override
public int getMagnitude() throws ArithmeticException {
if (precision == 0) {
throw new ArithmeticException("Magnitude is not well-defined for zero");
} else {
return scale + precision - 1;
}
}
@Override
public void adjustMagnitude(int delta) {
if (precision != 0) {
scale += delta;
origDelta += delta;
}
}
@Override
public StandardPlural getStandardPlural(PluralRules rules) {
if (rules == null) {
// Fail gracefully if the user didn't provide a PluralRules
return StandardPlural.OTHER;
} else {
@SuppressWarnings("deprecation")
String ruleString = rules.select(this);
return StandardPlural.orOtherFromString(ruleString);
}
}
@Override
public double getPluralOperand(Operand operand) {
switch (operand) {
case i:
return toLong();
case f:
return toFractionLong(true);
case t:
return toFractionLong(false);
case v:
return fractionCount();
case w:
return fractionCountWithoutTrailingZeros();
default:
return Math.abs(toDouble());
}
}
/**
* If the given {@link FieldPosition} is a {@link UFieldPosition}, populates it with the fraction
* length and fraction long value. If the argument is not a {@link UFieldPosition}, nothing
* happens.
*
* @param fp The {@link UFieldPosition} to populate.
*/
public void populateUFieldPosition(FieldPosition fp) {
if (fp instanceof UFieldPosition) {
((UFieldPosition) fp)
.setFractionDigits((int) getPluralOperand(Operand.v), (long) getPluralOperand(Operand.f));
}
}
@Override
public int getUpperDisplayMagnitude() {
int magnitude = scale + precision;
int result = (lReqPos > magnitude) ? lReqPos : (lOptPos < magnitude) ? lOptPos : magnitude;
return result - 1;
}
@Override
public int getLowerDisplayMagnitude() {
int magnitude = scale;
int result = (rReqPos < magnitude) ? rReqPos : (rOptPos > magnitude) ? rOptPos : magnitude;
return result;
}
@Override
public byte getDigit(int magnitude) {
return getDigitPos(magnitude - scale);
}
private int fractionCount() {
return -getLowerDisplayMagnitude();
}
private int fractionCountWithoutTrailingZeros() {
return Math.max(-scale, 0);
}
@Override
public boolean isNegative() {
return (flags & NEGATIVE_FLAG) != 0;
}
@Override
public boolean isInfinite() {
return (flags & INFINITY_FLAG) != 0;
}
@Override
public boolean isNaN() {
return (flags & NAN_FLAG) != 0;
}
@Override
public boolean isZero() {
return precision == 0;
}
@Override
public FormatQuantity clone() {
if (this instanceof FormatQuantity2) {
return new FormatQuantity2((FormatQuantity2) this);
} else if (this instanceof FormatQuantity3) {
return new FormatQuantity3((FormatQuantity3) this);
} else if (this instanceof FormatQuantity4) {
return new FormatQuantity4((FormatQuantity4) this);
} else {
throw new IllegalArgumentException("Don't know how to clone " + this.getClass());
}
}
public void setToInt(int n) {
setBcdToZero();
flags = 0;
if (n < 0) {
flags |= NEGATIVE_FLAG;
n = -n;
}
if (n != 0) {
_setToInt(n);
compact();
}
}
private void _setToInt(int n) {
if (n == Integer.MIN_VALUE) {
readLongToBcd(-(long) n);
} else {
readIntToBcd(n);
}
}
public void setToLong(long n) {
setBcdToZero();
flags = 0;
if (n < 0) {
flags |= NEGATIVE_FLAG;
n = -n;
}
if (n != 0) {
_setToLong(n);
compact();
}
}
private void _setToLong(long n) {
if (n == Long.MIN_VALUE) {
readBigIntegerToBcd(BigInteger.valueOf(n).negate());
} else if (n <= Integer.MAX_VALUE) {
readIntToBcd((int) n);
} else {
readLongToBcd(n);
}
}
public void setToBigInteger(BigInteger n) {
setBcdToZero();
flags = 0;
if (n.signum() == -1) {
flags |= NEGATIVE_FLAG;
n = n.negate();
}
if (n.signum() != 0) {
_setToBigInteger(n);
compact();
}
}
private void _setToBigInteger(BigInteger n) {
if (n.bitLength() < 32) {
readIntToBcd(n.intValue());
} else if (n.bitLength() < 64) {
readLongToBcd(n.longValue());
} else {
readBigIntegerToBcd(n);
}
}
/**
* Sets the internal BCD state to represent the value in the given double.
*
* @param n The value to consume.
*/
public void setToDouble(double n) {
setBcdToZero();
flags = 0;
// Double.compare() handles +0.0 vs -0.0
if (Double.compare(n, 0.0) < 0) {
flags |= NEGATIVE_FLAG;
n = -n;
}
if (Double.isNaN(n)) {
flags |= NAN_FLAG;
} else if (Double.isInfinite(n)) {
flags |= INFINITY_FLAG;
} else if (n != 0) {
_setToDoubleFast(n);
// TODO: Remove this when finished testing.
// isApproximate = true;
// origDouble = n;
// origDelta = 0;
// convertToAccurateDouble();
compact();
}
}
private static final double[] DOUBLE_MULTIPLIERS = {
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
1e17, 1e18, 1e19, 1e20, 1e21
};
/**
* Uses double multiplication and division to get the number into integer space before converting
* to digits. Since double arithmetic is inexact, the resulting digits may not be accurate.
*/
private void _setToDoubleFast(double n) {
long ieeeBits = Double.doubleToLongBits(n);
int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
// Not all integers can be represented exactly for exponent > 52
if (exponent <= 52 && (long) n == n) {
_setToLong((long) n);
return;
}
isApproximate = true;
origDouble = n;
origDelta = 0;
int fracLength = (int) ((52 - exponent) / 3.32192809489);
if (fracLength >= 0) {
int i = fracLength;
// 1e22 is the largest exact double.
for (; i >= 22; i -= 22) n *= 1e22;
n *= DOUBLE_MULTIPLIERS[i];
} else {
int i = fracLength;
// 1e22 is the largest exact double.
for (; i <= -22; i += 22) n /= 1e22;
n /= DOUBLE_MULTIPLIERS[-i];
}
_setToLong(Math.round(n));
scale -= fracLength;
}
/**
* Uses Double.toString() to obtain an exact accurate representation of the double, overwriting it
* into the BCD. This method can be called at any point after {@link #_setToDoubleFast} while
* {@link #isApproximate} is still true.
*/
private void convertToAccurateDouble() {
double n = origDouble;
assert n != 0;
int delta = origDelta;
setBcdToZero();
// Call the slow oracle function
String temp = Double.toString(n);
if (temp.indexOf('E') != -1) {
// Case 1: Exponential notation.
assert temp.indexOf('.') == 1;
int expPos = temp.indexOf('E');
_setToLong(Long.parseLong(temp.charAt(0) + temp.substring(2, expPos)));
scale += Integer.parseInt(temp.substring(expPos + 1)) - (expPos - 1) + 1;
} else if (temp.charAt(0) == '0') {
// Case 2: Fraction-only number.
assert temp.indexOf('.') == 1;
_setToLong(Long.parseLong(temp.substring(2)));
scale += 2 - temp.length();
} else if (temp.charAt(temp.length() - 1) == '0') {
// Case 3: Integer-only number.
// Note: this path should not normally happen, because integer-only numbers are captured
// before the approximate double logic is performed.
assert temp.indexOf('.') == temp.length() - 2;
assert temp.length() - 2 <= 18;
_setToLong(Long.parseLong(temp.substring(0, temp.length() - 2)));
// no need to adjust scale
} else {
// Case 4: Number with both a fraction and an integer.
int decimalPos = temp.indexOf('.');
_setToLong(Long.parseLong(temp.substring(0, decimalPos) + temp.substring(decimalPos + 1)));
scale += decimalPos - temp.length() + 1;
}
scale += delta;
compact();
explicitExactDouble = true;
}
/**
* Whether this {@link FormatQuantity4} has been explicitly converted to an exact double. true if
* backed by a double that was explicitly converted via convertToAccurateDouble; false otherwise.
* Used for testing.
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated public boolean explicitExactDouble = false;
/**
* Sets the internal BCD state to represent the value in the given BigDecimal.
*
* @param n The value to consume.
*/
public void setToBigDecimal(BigDecimal n) {
setBcdToZero();
flags = 0;
if (n.signum() == -1) {
flags |= NEGATIVE_FLAG;
n = n.negate();
}
if (n.signum() != 0) {
_setToBigDecimal(n);
compact();
}
}
private void _setToBigDecimal(BigDecimal n) {
int fracLength = n.scale();
n = n.scaleByPowerOfTen(fracLength);
BigInteger bi = n.toBigInteger();
_setToBigInteger(bi);
scale -= fracLength;
}
/**
* Returns a long approximating the internal BCD. A long can only represent the integral part of
* the number.
*
* @return A double representation of the internal BCD.
*/
protected long toLong() {
long result = 0L;
for (int magnitude = scale + precision - 1; magnitude >= 0; magnitude--) {
result = result * 10 + getDigitPos(magnitude - scale);
}
return result;
}
/**
* This returns a long representing the fraction digits of the number, as required by PluralRules.
* For example, if we represent the number "1.20" (including optional and required digits), then
* this function returns "20" if includeTrailingZeros is true or "2" if false.
*/
protected long toFractionLong(boolean includeTrailingZeros) {
long result = 0L;
int magnitude = -1;
for (;
(magnitude >= scale || (includeTrailingZeros && magnitude >= rReqPos))
&& magnitude >= rOptPos;
magnitude--) {
result = result * 10 + getDigitPos(magnitude - scale);
}
return result;
}
/**
* Returns a double approximating the internal BCD. The double may not retain all of the
* information encoded in the BCD if the BCD represents a number out of range of a double.
*
* @return A double representation of the internal BCD.
*/
@Override
public double toDouble() {
if (isApproximate) {
return toDoubleFromOriginal();
}
if (isNaN()) {
return Double.NaN;
} else if (isInfinite()) {
return isNegative() ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
}
long tempLong = 0L;
int lostDigits = precision - Math.min(precision, 17);
for (int shift = precision - 1; shift >= lostDigits; shift--) {
tempLong = tempLong * 10 + getDigitPos(shift);
}
double result = tempLong;
int _scale = scale + lostDigits;
if (_scale >= 0) {
int i = _scale;
for (; i >= 9; i -= 9) result *= 1000000000;
for (; i >= 3; i -= 3) result *= 1000;
for (; i >= 1; i -= 1) result *= 10;
} else {
int i = _scale;
for (; i <= -9; i += 9) result /= 1000000000;
for (; i <= -3; i += 3) result /= 1000;
for (; i <= -1; i += 1) result /= 10;
}
if (isNegative()) result = -result;
return result;
}
@Override
public BigDecimal toBigDecimal() {
if (isApproximate) {
// Converting to a BigDecimal requires Double.toString().
convertToAccurateDouble();
}
return bcdToBigDecimal();
}
protected double toDoubleFromOriginal() {
double result = origDouble;
double delta = origDelta;
if (delta >= 0) {
for (; delta >= 9; delta -= 9) result *= 1000000000;
for (; delta >= 3; delta -= 3) result *= 1000;
for (; delta >= 1; delta -= 1) result *= 10;
} else {
for (; delta <= -9; delta += 9) result /= 1000000000;
for (; delta <= -3; delta += 3) result /= 1000;
for (; delta <= -1; delta += 1) result /= 10;
}
if (isNegative()) result *= -1;
return result;
}
private static int safeSubtract(int a, int b) {
if (b < 0 && a - b < a) return Integer.MAX_VALUE;
if (b > 0 && a - b > a) return Integer.MIN_VALUE;
return a - b;
}
@Override
public void roundToMagnitude(int magnitude, MathContext mathContext) {
// The position in the BCD at which rounding will be performed; digits to the right of position
// will be rounded away.
// TODO: Andy: There was a test failure because of integer overflow here. Should I do
// "safe subtraction" everywhere in the code? What's the nicest way to do it?
int position = safeSubtract(magnitude, scale);
// Enforce the number of digits required by the MathContext.
int _mcPrecision = mathContext.getPrecision();
if (magnitude == Integer.MAX_VALUE
|| (_mcPrecision > 0 && precision - position > _mcPrecision)) {
position = precision - _mcPrecision;
}
if (position <= 0 && !isApproximate) {
// All digits are to the left of the rounding magnitude.
} else if (precision == 0) {
// No rounding for zero.
} else {
// Perform rounding logic.
// "leading" = most significant digit to the right of rounding
// "trailing" = least significant digit to the left of rounding
byte leadingDigit = getDigitPos(safeSubtract(position, 1));
byte trailingDigit = getDigitPos(position);
// Compute which section of the number we are in.
// EDGE means we are at the bottom or top edge, like 1.000 or 1.999 (used by doubles)
// LOWER means we are between the bottom edge and the midpoint, like 1.391
// MIDPOINT means we are exactly in the middle, like 1.500
// UPPER means we are between the midpoint and the top edge, like 1.916
int section = RoundingUtils.SECTION_MIDPOINT;
if (!isApproximate) {
if (leadingDigit < 5) {
section = RoundingUtils.SECTION_LOWER;
} else if (leadingDigit > 5) {
section = RoundingUtils.SECTION_UPPER;
} else {
for (int p = safeSubtract(position, 2); p >= 0; p--) {
if (getDigitPos(p) != 0) {
section = RoundingUtils.SECTION_UPPER;
break;
}
}
}
} else {
int p = safeSubtract(position, 2);
int minP = Math.max(0, precision - 14);
if (leadingDigit == 0) {
section = -1;
for (; p >= minP; p--) {
if (getDigitPos(p) != 0) {
section = RoundingUtils.SECTION_LOWER;
break;
}
}
} else if (leadingDigit == 4) {
for (; p >= minP; p--) {
if (getDigitPos(p) != 9) {
section = RoundingUtils.SECTION_LOWER;
break;
}
}
} else if (leadingDigit == 5) {
for (; p >= minP; p--) {
if (getDigitPos(p) != 0) {
section = RoundingUtils.SECTION_UPPER;
break;
}
}
} else if (leadingDigit == 9) {
section = -2;
for (; p >= minP; p--) {
if (getDigitPos(p) != 9) {
section = RoundingUtils.SECTION_UPPER;
break;
}
}
} else if (leadingDigit < 5) {
section = RoundingUtils.SECTION_LOWER;
} else {
section = RoundingUtils.SECTION_UPPER;
}
boolean roundsAtMidpoint =
RoundingUtils.roundsAtMidpoint(mathContext.getRoundingMode().ordinal());
if (safeSubtract(position, 1) < precision - 14
|| (roundsAtMidpoint && section == RoundingUtils.SECTION_MIDPOINT)
|| (!roundsAtMidpoint && section < 0 /* i.e. at upper or lower edge */)) {
// Oops! This means that we have to get the exact representation of the double, because
// the zone of uncertainty is along the rounding boundary.
convertToAccurateDouble();
roundToMagnitude(magnitude, mathContext); // start over
return;
}
// Turn off the approximate double flag, since the value is now confirmed to be exact.
isApproximate = false;
origDouble = 0.0;
origDelta = 0;
if (position <= 0) {
// All digits are to the left of the rounding magnitude.
return;
}
// Good to continue rounding.
if (section == -1) section = RoundingUtils.SECTION_LOWER;
if (section == -2) section = RoundingUtils.SECTION_UPPER;
}
boolean roundDown =
RoundingUtils.getRoundingDirection(
(trailingDigit % 2) == 0,
isNegative(),
section,
mathContext.getRoundingMode().ordinal(),
this);
// Perform truncation
if (position >= precision) {
setBcdToZero();
scale = magnitude;
} else {
shiftRight(position);
}
// Bubble the result to the higher digits
if (!roundDown) {
if (trailingDigit == 9) {
int bubblePos = 0;
// Note: in the long implementation, the most digits BCD can have at this point is 15,
// so bubblePos <= 15 and getDigitPos(bubblePos) is safe.
for (; getDigitPos(bubblePos) == 9; bubblePos++) {}
shiftRight(bubblePos); // shift off the trailing 9s
}
byte digit0 = getDigitPos(0);
assert digit0 != 9;
setDigitPos(0, (byte) (digit0 + 1));
precision += 1; // in case an extra digit got added
}
compact();
}
}
@Override
public void roundToInfinity() {
if (isApproximate) {
convertToAccurateDouble();
}
}
/**
* Appends a digit, optionally with one or more leading zeros, to the end of the value represented
* by this FormatQuantity.
*
* <p>The primary use of this method is to construct numbers during a parsing loop. It allows
* parsing to take advantage of the digit list infrastructure primarily designed for formatting.
*
* @param value The digit to append.
* @param leadingZeros The number of zeros to append before the digit. For example, if the value
* in this instance starts as 12.3, and you append a 4 with 1 leading zero, the value becomes
* 12.304.
* @param appendAsInteger If true, increase the magnitude of existing digits to make room for the
* new digit. If false, append to the end like a fraction digit. If true, there must not be
* any fraction digits already in the number.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public void appendDigit(byte value, int leadingZeros, boolean appendAsInteger) {
assert leadingZeros >= 0;
// Zero requires special handling to maintain the invariant that the least-significant digit
// in the BCD is nonzero.
if (value == 0) {
if (appendAsInteger && precision != 0) {
scale += leadingZeros + 1;
}
return;
}
// Deal with trailing zeros
if (scale > 0) {
leadingZeros += scale;
if (appendAsInteger) {
scale = 0;
}
}
// Append digit
shiftLeft(leadingZeros + 1);
setDigitPos(0, value);
// Fix scale if in integer mode
if (appendAsInteger) {
scale += leadingZeros + 1;
}
}
/**
* Returns a single digit from the BCD list. No internal state is changed by calling this method.
*
* @param position The position of the digit to pop, counted in BCD units from the least
* significant digit. If outside the range supported by the implementation, zero is returned.
* @return The digit at the specified location.
*/
protected abstract byte getDigitPos(int position);
/**
* Sets the digit in the BCD list. This method only sets the digit; it is the caller's
* responsibility to call {@link #compact} after setting the digit.
*
* @param position The position of the digit to pop, counted in BCD units from the least
* significant digit. If outside the range supported by the implementation, an AssertionError
* is thrown.
* @param value The digit to set at the specified location.
*/
protected abstract void setDigitPos(int position, byte value);
/**
* Adds zeros to the end of the BCD list. This will result in an invalid BCD representation; it is
* the caller's responsibility to do further manipulation and then call {@link #compact}.
*
* @param numDigits The number of zeros to add.
*/
protected abstract void shiftLeft(int numDigits);
protected abstract void shiftRight(int numDigits);
/**
* Sets the internal representation to zero. Clears any values stored in scale, precision,
* hasDouble, origDouble, origDelta, and BCD data.
*/
protected abstract void setBcdToZero();
/**
* Sets the internal BCD state to represent the value in the given int. The int is guaranteed to
* be either positive. The internal state is guaranteed to be empty when this method is called.
*
* @param n The value to consume.
*/
protected abstract void readIntToBcd(int input);
/**
* Sets the internal BCD state to represent the value in the given long. The long is guaranteed to
* be either positive. The internal state is guaranteed to be empty when this method is called.
*
* @param n The value to consume.
*/
protected abstract void readLongToBcd(long input);
/**
* Sets the internal BCD state to represent the value in the given BigInteger. The BigInteger is
* guaranteed to be positive, and it is guaranteed to be larger than Long.MAX_VALUE. The internal
* state is guaranteed to be empty when this method is called.
*
* @param n The value to consume.
*/
protected abstract void readBigIntegerToBcd(BigInteger input);
/**
* Returns a BigDecimal encoding the internal BCD value.
*
* @return A BigDecimal representation of the internal BCD.
*/
protected abstract BigDecimal bcdToBigDecimal();
protected abstract void copyBcdFrom(FormatQuantity _other);
/**
* Removes trailing zeros from the BCD (adjusting the scale as required) and then computes the
* precision. The precision is the number of digits in the number up through the greatest nonzero
* digit.
*
* <p>This method must always be called when bcd changes in order for assumptions to be correct in
* methods like {@link #fractionCount()}.
*/
protected abstract void compact();
}

View File

@ -0,0 +1,52 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.math.BigInteger;
/** @author sffc */
public class FormatQuantitySelector {
public static FormatQuantityBCD from(int input) {
return new FormatQuantity4(input);
}
public static FormatQuantityBCD from(long input) {
return new FormatQuantity4(input);
}
public static FormatQuantityBCD from(double input) {
return new FormatQuantity4(input);
}
public static FormatQuantityBCD from(BigInteger input) {
return new FormatQuantity4(input);
}
public static FormatQuantityBCD from(BigDecimal input) {
return new FormatQuantity4(input);
}
public static FormatQuantityBCD from(com.ibm.icu.math.BigDecimal input) {
return from(input.toBigDecimal());
}
public static FormatQuantityBCD from(Number number) {
if (number instanceof Long) {
return from(number.longValue());
} else if (number instanceof Integer) {
return from(number.intValue());
} else if (number instanceof Double) {
return from(number.doubleValue());
} else if (number instanceof BigInteger) {
return from((BigInteger) number);
} else if (number instanceof BigDecimal) {
return from((BigDecimal) number);
} else if (number instanceof com.ibm.icu.math.BigDecimal) {
return from((com.ibm.icu.math.BigDecimal) number);
} else {
throw new IllegalArgumentException(
"Number is of an unsupported type: " + number.getClass().getName());
}
}
}

View File

@ -0,0 +1,128 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
import com.ibm.icu.impl.number.modifiers.SimpleModifier;
/**
* A Modifier is an immutable object that can be passed through the formatting pipeline until it is
* finally applied to the string builder. A Modifier usually contains a prefix and a suffix that are
* applied, but it could contain something else, like a {@link com.ibm.icu.text.SimpleFormatter}
* pattern.
*
* @see PositiveNegativeAffixModifier
* @see ConstantAffixModifier
* @see GeneralPluralModifier
* @see SimpleModifier
*/
public interface Modifier {
/**
* Apply this Modifier to the string builder.
*
* @param output The string builder to which to apply this modifier.
* @param leftIndex The left index of the string within the builder. Equal to 0 when only one
* number is being formatted.
* @param rightIndex The right index of the string within the string builder. Equal to length-1
* when only one number is being formatted.
* @return The number of characters (UTF-16 code units) that were added to the string builder.
*/
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex);
/**
* The number of characters that {@link #apply} would add to the string builder.
*
* @return The number of characters (UTF-16 code units) that would be added to a string builder.
*/
public int length();
/**
* Whether this modifier is strong. If a modifier is strong, it should always be applied
* immediately and not allowed to bubble up. With regard to padding, strong modifiers are
* considered to be on the inside of the prefix and suffix.
*
* @return Whether the modifier is strong.
*/
public boolean isStrong();
/**
* Gets the prefix string associated with this modifier, defined as the string that will be
* inserted at leftIndex when {@link #apply} is called.
*
* @return The prefix string. Will not be null.
*/
public String getPrefix();
/**
* Gets the prefix string associated with this modifier, defined as the string that will be
* inserted at rightIndex when {@link #apply} is called.
*
* @return The suffix string. Will not be null.
*/
public String getSuffix();
/**
* An interface for a modifier that contains both a positive and a negative form. Note that a
* class implementing {@link PositiveNegativeModifier} is not necessarily a {@link Modifier}
* itself. Rather, it returns a {@link Modifier} when {@link #getModifier} is called.
*/
public static interface PositiveNegativeModifier extends Exportable {
/**
* Converts this {@link PositiveNegativeModifier} to a {@link Modifier} given the negative sign.
*
* @param isNegative true if the negative form of this modifier should be used; false if the
* positive form should be used.
* @return A Modifier corresponding to the negative sign.
*/
public Modifier getModifier(boolean isNegative);
}
/**
* An interface for a modifier that contains both a positive and a negative form for all six
* standard plurals. Note that a class implementing {@link PositiveNegativePluralModifier} is not
* necessarily a {@link Modifier} itself. Rather, it returns a {@link Modifier} when {@link
* #getModifier} is called.
*/
public static interface PositiveNegativePluralModifier extends Exportable {
/**
* Converts this {@link PositiveNegativePluralModifier} to a {@link Modifier} given the negative
* sign and the standard plural.
*
* @param plural The StandardPlural to use.
* @param isNegative true if the negative form of this modifier should be used; false if the
* positive form should be used.
* @return A Modifier corresponding to the negative sign.
*/
public Modifier getModifier(StandardPlural plural, boolean isNegative);
}
/**
* An interface for a modifier that is represented internally by a prefix string and a suffix
* string.
*/
public static interface AffixModifier extends Modifier {}
/**
* A starter implementation with defaults for some of the basic methods.
*
* <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 extends Format.BeforeFormat
implements Modifier, PositiveNegativeModifier {
@Override
public void before(FormatQuantity input, ModifierHolder mods) {
mods.add(this);
}
@Override
public Modifier getModifier(boolean isNegative) {
return this;
}
}
}

View File

@ -0,0 +1,106 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.util.ArrayDeque;
public class ModifierHolder {
private ArrayDeque<Modifier> mods = new ArrayDeque<Modifier>();
// Using five separate fields instead of the ArrayDeque saves about 10ns at the expense of
// worse code.
// TODO: Decide which implementation to use.
// private Modifier mod1 = null;
// private Modifier mod2 = null;
// private Modifier mod3 = null;
// private Modifier mod4 = null;
// private Modifier mod5 = null;
public ModifierHolder clear() {
// mod1 = null;
// mod2 = null;
// mod3 = null;
// mod4 = null;
// mod5 = null;
mods.clear();
return this;
}
public void add(Modifier modifier) {
// if (mod1 == null) {
// mod1 = modifier;
// } else if (mod2 == null) {
// mod2 = modifier;
// } else if (mod3 == null) {
// mod3 = modifier;
// } else if (mod4 == null) {
// mod4 = modifier;
// } else if (mod5 == null) {
// mod5 = modifier;
// } else {
// throw new IndexOutOfBoundsException();
// }
if (modifier != null) mods.addFirst(modifier);
}
public Modifier peekLast() {
return mods.peekLast();
}
public Modifier removeLast() {
return mods.removeLast();
}
public int applyAll(NumberStringBuilder string, int leftIndex, int rightIndex) {
int addedLength = 0;
// if (mod5 != null) {
// addedLength += mod5.apply(string, leftIndex, rightIndex + addedLength);
// mod5 = null;
// }
// if (mod4 != null) {
// addedLength += mod4.apply(string, leftIndex, rightIndex + addedLength);
// mod4 = null;
// }
// if (mod3 != null) {
// addedLength += mod3.apply(string, leftIndex, rightIndex + addedLength);
// mod3 = null;
// }
// if (mod2 != null) {
// addedLength += mod2.apply(string, leftIndex, rightIndex + addedLength);
// mod2 = null;
// }
// if (mod1 != null) {
// addedLength += mod1.apply(string, leftIndex, rightIndex + addedLength);
// mod1 = null;
// }
while (!mods.isEmpty()) {
Modifier mod = mods.removeFirst();
addedLength += mod.apply(string, leftIndex, rightIndex + addedLength);
}
return addedLength;
}
public int applyStrong(NumberStringBuilder string, int leftIndex, int rightIndex) {
int addedLength = 0;
while (!mods.isEmpty() && mods.peekFirst().isStrong()) {
Modifier mod = mods.removeFirst();
addedLength += mod.apply(string, leftIndex, rightIndex + addedLength);
}
return addedLength;
}
public int totalLength() {
int length = 0;
// if (mod1 != null) length += mod1.length();
// if (mod2 != null) length += mod2.length();
// if (mod3 != null) length += mod3.length();
// if (mod4 != null) length += mod4.length();
// if (mod5 != null) length += mod5.length();
for (Modifier mod : mods) {
if (mod == null) continue;
length += mod.length();
}
return length;
}
}

View File

@ -0,0 +1,411 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.FieldPosition;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberFormat.Field;
public class NumberStringBuilder implements CharSequence {
private char[] chars;
private Field[] fields;
private int zero;
private int length;
public NumberStringBuilder() {
this(40);
}
public NumberStringBuilder(int capacity) {
chars = new char[capacity];
fields = new Field[capacity];
zero = capacity / 2;
length = 0;
}
@Override
public int length() {
return length;
}
@Override
public char charAt(int index) {
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException();
}
return chars[zero + index];
}
/**
* Appends the specified codePoint to the end of the string.
*
* @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise.
*/
public int appendCodePoint(int codePoint, Field field) {
return insertCodePoint(length, codePoint, field);
}
/**
* Inserts the specified codePoint at the specified index in the string.
*
* @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise.
*/
public int insertCodePoint(int index, int codePoint, Field field) {
int count = Character.charCount(codePoint);
int position = prepareForInsert(index, count);
Character.toChars(codePoint, chars, position);
fields[position] = field;
if (count == 2) fields[position + 1] = field;
return count;
}
/**
* Appends the specified CharSequence to the end of the string.
*
* @return The number of chars added, which is the length of CharSequence.
*/
public int append(CharSequence sequence, Field field) {
return insert(length, sequence, field);
}
/**
* Inserts the specified CharSequence at the specified index in the string.
*
* @return The number of chars added, which is the length of CharSequence.
*/
public int insert(int index, CharSequence sequence, Field field) {
if (sequence.length() == 0) {
// Nothing to insert.
return 0;
} else if (sequence.length() == 1) {
// Fast path: on a single-char string, using insertCodePoint below is 70% faster than the
// CharSequence method: 12.2 ns versus 41.9 ns for five operations on my Linux x86-64.
return insertCodePoint(index, sequence.charAt(0), field);
} else {
return insert(index, sequence, 0, sequence.length(), field);
}
}
/**
* Inserts the specified CharSequence at the specified index in the string, reading from the
* CharSequence from start (inclusive) to end (exclusive).
*
* @return The number of chars added, which is the length of CharSequence.
*/
public int insert(int index, CharSequence sequence, int start, int end, Field field) {
int count = end - start;
int position = prepareForInsert(index, count);
for (int i = 0; i < count; i++) {
chars[position + i] = sequence.charAt(start + i);
fields[position + i] = field;
}
return count;
}
/**
* Appends the chars in the specified char array to the end of the string, and associates them
* with the fields in the specified field array, which must have the same length as chars.
*
* @return The number of chars added, which is the length of the char array.
*/
public int append(char[] chars, Field[] fields) {
return insert(length, chars, fields);
}
/**
* Inserts the chars in the specified char array at the specified index in the string, and
* associates them with the fields in the specified field array, which must have the same length
* as chars.
*
* @return The number of chars added, which is the length of the char array.
*/
public int insert(int index, char[] chars, Field[] fields) {
assert fields == null || chars.length == fields.length;
int count = chars.length;
if (count == 0) return 0; // nothing to insert
int position = prepareForInsert(index, count);
for (int i = 0; i < count; i++) {
this.chars[position + i] = chars[i];
this.fields[position + i] = fields == null ? null : fields[i];
}
return count;
}
/**
* Appends the contents of another {@link NumberStringBuilder} to the end of this instance.
*
* @return The number of chars added, which is the length of the other {@link
* NumberStringBuilder}.
*/
public int append(NumberStringBuilder other) {
return insert(length, other);
}
/**
* Inserts the contents of another {@link NumberStringBuilder} into this instance at the given
* index.
*
* @return The number of chars added, which is the length of the other {@link
* NumberStringBuilder}.
*/
public int insert(int index, NumberStringBuilder other) {
assert this != other;
int count = other.length;
if (count == 0) return 0; // nothing to insert
int position = prepareForInsert(index, count);
for (int i = 0; i < count; i++) {
this.chars[position + i] = other.chars[other.zero + i];
this.fields[position + i] = other.fields[other.zero + i];
}
return count;
}
/**
* Shifts around existing data if necessary to make room for new characters.
*
* @param index The location in the string where the operation is to take place.
* @param count The number of chars (UTF-16 code units) to be inserted at that location.
* @return The position in the char array to insert the chars.
*/
private int prepareForInsert(int index, int count) {
if (index == 0 && zero - count >= 0) {
// Append to start
zero -= count;
length += count;
return zero;
} else if (index == length && zero + length + count < chars.length) {
// Append to end
length += count;
return zero + length - count;
} else {
// Move chars around and/or allocate more space
return prepareForInsertHelper(index, count);
}
}
private int prepareForInsertHelper(int index, int count) {
// Keeping this code out of prepareForInsert() increases the speed of append operations.
if (length + count > chars.length) {
char[] newChars = new char[(length + count) * 2];
Field[] newFields = new Field[(length + count) * 2];
int newZero = newChars.length / 2 - (length + count) / 2;
System.arraycopy(chars, zero, newChars, newZero, index);
System.arraycopy(chars, zero + index, newChars, newZero + index + count, length - index);
System.arraycopy(fields, zero, newFields, newZero, index);
System.arraycopy(fields, zero + index, newFields, newZero + index + count, length - index);
chars = newChars;
fields = newFields;
zero = newZero;
length += count;
} else {
int newZero = chars.length / 2 - (length + count) / 2;
System.arraycopy(chars, zero, chars, newZero, length);
System.arraycopy(chars, newZero + index, chars, newZero + index + count, length - index);
System.arraycopy(fields, zero, fields, newZero, length);
System.arraycopy(fields, newZero + index, fields, newZero + index + count, length - index);
zero = newZero;
length += count;
}
return zero + index;
}
@Override
public CharSequence subSequence(int start, int end) {
if (start < 0 || end > length || end < start) {
throw new IndexOutOfBoundsException();
}
NumberStringBuilder other = this.clone();
other.zero = zero + start;
other.length = end - start;
return other;
}
/**
* Returns the string represented by the characters in this string builder.
*
* <p>For a string intended be used for debugging, use {@link #toDebugString}.
*/
@Override
public String toString() {
return new String(chars, zero, length);
}
private static final Map<Field, Character> fieldToDebugChar = new HashMap<Field, Character>();
static {
fieldToDebugChar.put(NumberFormat.Field.SIGN, '-');
fieldToDebugChar.put(NumberFormat.Field.INTEGER, 'i');
fieldToDebugChar.put(NumberFormat.Field.FRACTION, 'f');
fieldToDebugChar.put(NumberFormat.Field.EXPONENT, 'e');
fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SIGN, '+');
fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SYMBOL, 'E');
fieldToDebugChar.put(NumberFormat.Field.DECIMAL_SEPARATOR, '.');
fieldToDebugChar.put(NumberFormat.Field.GROUPING_SEPARATOR, ',');
fieldToDebugChar.put(NumberFormat.Field.PERCENT, '%');
fieldToDebugChar.put(NumberFormat.Field.PERMILLE, '‰');
fieldToDebugChar.put(NumberFormat.Field.CURRENCY, '$');
}
/**
* Returns a string that includes field information, for debugging purposes.
*
* <p>For example, if the string is "-12.345", the debug string will be something like
* "&lt;NumberStringBuilder [-123.45] [-iii.ff]&gt;"
*
* @return A string for debugging purposes.
*/
public String toDebugString() {
StringBuilder sb = new StringBuilder();
sb.append("<NumberStringBuilder [");
sb.append(this.toString());
sb.append("] [");
for (int i = zero; i < zero + length; i++) {
if (fields[i] == null) {
sb.append('n');
} else {
sb.append(fieldToDebugChar.get(fields[i]));
}
}
sb.append("]>");
return sb.toString();
}
/** @return A new array containing the contents of this string builder. */
public char[] toCharArray() {
return Arrays.copyOfRange(chars, zero, zero + length);
}
/** @return A new array containing the field values of this string builder. */
public Field[] toFieldArray() {
return Arrays.copyOfRange(fields, zero, zero + length);
}
/**
* @return Whether the contents and field values of this string builder are equal to the given
* chars and fields.
* @see #toCharArray
* @see #toFieldArray
*/
public boolean contentEquals(char[] chars, Field[] fields) {
if (chars.length != length) return false;
if (fields.length != length) return false;
for (int i = 0; i < length; i++) {
if (this.chars[zero + i] != chars[i]) return false;
if (this.fields[zero + i] != fields[i]) return false;
}
return true;
}
/**
* @param other The instance to compare.
* @return Whether the contents of this instance is currently equal to the given instance.
*/
public boolean contentEquals(NumberStringBuilder other) {
if (length != other.length) return false;
for (int i = 0; i < length; i++) {
if (chars[zero + i] != other.chars[other.zero + i]) return false;
if (fields[zero + i] != other.fields[other.zero + i]) return false;
}
return true;
}
/**
* Populates the given {@link FieldPosition} based on this string builder.
*
* @param fp The FieldPosition to populate.
* @param offset An offset to add to the field position index; can be zero.
*/
public void populateFieldPosition(FieldPosition fp, int offset) {
java.text.Format.Field rawField = fp.getFieldAttribute();
if (rawField == null) {
// Backwards compatibility: read from fp.getField()
if (fp.getField() == NumberFormat.INTEGER_FIELD) {
rawField = NumberFormat.Field.INTEGER;
} else if (fp.getField() == NumberFormat.FRACTION_FIELD) {
rawField = NumberFormat.Field.FRACTION;
} else {
// No field is set
return;
}
}
if (!(rawField instanceof com.ibm.icu.text.NumberFormat.Field)) {
throw new IllegalArgumentException(
"You must pass an instance of com.ibm.icu.text.NumberFormat.Field as your FieldPosition attribute. You passed: "
+ rawField.getClass().toString());
}
/* com.ibm.icu.text.NumberFormat. */ Field field = (Field) rawField;
boolean seenStart = false;
int fractionStart = -1;
for (int i = zero; i <= zero + length; i++) {
Field _field = (i < zero + length) ? fields[i] : null;
if (seenStart && field != _field) {
// Special case: GROUPING_SEPARATOR counts as an INTEGER.
if (field == NumberFormat.Field.INTEGER && _field == NumberFormat.Field.GROUPING_SEPARATOR)
continue;
fp.setEndIndex(i - zero + offset);
break;
} else if (!seenStart && field == _field) {
fp.setBeginIndex(i - zero + offset);
seenStart = true;
}
if (_field == NumberFormat.Field.INTEGER || _field == NumberFormat.Field.DECIMAL_SEPARATOR) {
fractionStart = i - zero + 1;
}
}
// Backwards compatibility: FRACTION needs to start after INTEGER if empty
if (field == NumberFormat.Field.FRACTION && !seenStart) {
fp.setBeginIndex(fractionStart);
fp.setEndIndex(fractionStart);
}
}
public AttributedCharacterIterator getIterator() {
AttributedString as = new AttributedString(toString());
Field current = null;
int currentStart = -1;
for (int i = 0; i < length; i++) {
Field field = fields[i + zero];
if (current == NumberFormat.Field.INTEGER && field == NumberFormat.Field.GROUPING_SEPARATOR) {
// Special case: GROUPING_SEPARATOR counts as an INTEGER.
as.addAttribute(
NumberFormat.Field.GROUPING_SEPARATOR, NumberFormat.Field.GROUPING_SEPARATOR, i, i + 1);
} else if (current != field) {
if (current != null) {
as.addAttribute(current, current, currentStart, i);
}
current = field;
currentStart = i;
}
}
if (current != null) {
as.addAttribute(current, current, currentStart, length);
}
return as.getIterator();
}
@Override
public NumberStringBuilder clone() {
NumberStringBuilder other = new NumberStringBuilder(chars.length);
other.zero = zero;
other.length = length;
System.arraycopy(chars, zero, other.chars, zero, length);
System.arraycopy(fields, zero, other.fields, zero, length);
return other;
}
public NumberStringBuilder clear() {
zero = chars.length / 2;
length = 0;
return this;
}
}

View File

@ -0,0 +1,296 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.number.Modifier.AffixModifier;
import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat.IProperties;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat.Field;
/**
* A class to convert from a bag of prefix/suffix properties into a positive and negative {@link
* Modifier}. This is a standard implementation used by {@link PositiveNegativeAffixFormat}, {@link
* CompactDecimalFormat}, {@link Parse}, and others.
*
* <p>This class is is intended to be an efficient generator for instances of Modifier by a single
* thread during construction of a formatter or during static formatting. It uses internal caching
* to avoid creating new Modifier objects when possible. It is NOT THREAD SAFE and NOT IMMUTABLE.
*
* <p>The thread-local instance of this class provided by {@link #getThreadLocalInstance} should be
* used in most cases instead of constructing a new instance of the object.
*
* <p>This class also handles the logic of assigning positive signs, negative signs, and currency
* signs according to the LDML specification.
*/
public class PNAffixGenerator {
public static class Result {
public AffixModifier positive = null;
public AffixModifier negative = null;
}
protected static final ThreadLocal<PNAffixGenerator> threadLocalInstance =
new ThreadLocal<PNAffixGenerator>() {
@Override
protected PNAffixGenerator initialValue() {
return new PNAffixGenerator();
}
};
public static PNAffixGenerator getThreadLocalInstance() {
return threadLocalInstance.get();
}
// These instances are used internally and cached to avoid object creation. The resultInstance
// also serves as a 1-element cache to avoid creating objects when subsequent calls have
// identical prefixes and suffixes. This happens, for example, when consuming CDF data.
private Result resultInstance = new Result();
private NumberStringBuilder sb1 = new NumberStringBuilder();
private NumberStringBuilder sb2 = new NumberStringBuilder();
private NumberStringBuilder sb3 = new NumberStringBuilder();
private NumberStringBuilder sb4 = new NumberStringBuilder();
/**
* Generates modifiers using default currency symbols.
*
* @param symbols The symbols to interpolate for minus, plus, percent, permille, and currency.
* @param properties The bag of properties to convert.
* @return The positive and negative {@link Modifier}.
*/
public Result getModifiers(
DecimalFormatSymbols symbols, PositiveNegativeAffixFormat.IProperties properties) {
// If this method is used, the user doesn't care about currencies. Default the currency symbols
// to the information we can get from the DecimalFormatSymbols instance.
return getModifiers(
symbols,
symbols.getCurrencySymbol(),
symbols.getInternationalCurrencySymbol(),
symbols.getCurrencySymbol(),
properties);
}
/**
* Generates modifiers using the specified currency symbol for all three lengths of currency
* placeholders in the pattern string.
*
* @param symbols The symbols to interpolate for minus, plus, percent, and permille.
* @param currencySymbol The currency symbol.
* @param properties The bag of properties to convert.
* @return The positive and negative {@link Modifier}.
*/
public Result getModifiers(
DecimalFormatSymbols symbols,
String currencySymbol,
PositiveNegativeAffixFormat.IProperties properties) {
// If this method is used, the user doesn't cares about currencies but doesn't care about
// supporting all three sizes of currency placeholders. Use the one provided string for all
// three sizes of placeholders.
return getModifiers(symbols, currencySymbol, currencySymbol, currencySymbol, properties);
}
/**
* Generates modifiers using the three specified strings to replace the three lengths of currency
* placeholders: "¤", "¤¤", and "¤¤¤".
*
* @param symbols The symbols to interpolate for minus, plus, percent, and permille.
* @param curr1 The string to replace "¤".
* @param curr2 The string to replace "¤¤".
* @param curr3 The string to replace "¤¤¤".
* @param properties The bag of properties to convert.
* @return The positive and negative {@link Modifier}.
*/
public Result getModifiers(
DecimalFormatSymbols symbols,
String curr1,
String curr2,
String curr3,
PositiveNegativeAffixFormat.IProperties properties) {
// Use a different code path for handling affixes with "always show plus sign"
if (properties.getPlusSignAlwaysShown()) {
return getModifiersWithPlusSign(symbols, curr1, curr2, curr3, properties);
}
CharSequence ppp = properties.getPositivePrefixPattern();
CharSequence psp = properties.getPositiveSuffixPattern();
CharSequence npp = properties.getNegativePrefixPattern();
CharSequence nsp = properties.getNegativeSuffixPattern();
// Set sb1/sb2 to the positive prefix/suffix.
sb1.clear();
sb2.clear();
AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1);
AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2);
setPositiveResult(sb1, sb2, properties);
// Set sb1/sb2 to the negative prefix/suffix.
if (npp == null && nsp == null) {
// Negative prefix defaults to positive prefix prepended with the minus sign.
// Negative suffix defaults to positive suffix.
sb1.insert(0, symbols.getMinusSignString(), Field.SIGN);
} else {
sb1.clear();
sb2.clear();
AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1);
AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2);
}
setNegativeResult(sb1, sb2, properties);
return resultInstance;
}
private Result getModifiersWithPlusSign(
DecimalFormatSymbols symbols,
String curr1,
String curr2,
String curr3,
IProperties properties) {
CharSequence ppp = properties.getPositivePrefixPattern();
CharSequence psp = properties.getPositiveSuffixPattern();
CharSequence npp = properties.getNegativePrefixPattern();
CharSequence nsp = properties.getNegativeSuffixPattern();
// There are three cases, listed below with their expected outcomes.
// TODO: Should we handle the cases when the positive subpattern has a '+' already?
//
// 1) No negative subpattern.
// Positive => Positive subpattern prepended with '+'
// Negative => Positive subpattern prepended with '-'
// 2) Negative subpattern does not have '-'.
// Positive => Positive subpattern prepended with '+'
// Negative => Negative subpattern
// 3) Negative subpattern has '-'.
// Positive => Negative subpattern with '+' substituted for '-'
// Negative => Negative subpattern
if (npp != null || nsp != null) {
// Case 2 or Case 3
sb1.clear();
sb2.clear();
sb3.clear();
sb4.clear();
AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1);
AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2);
AffixPatternUtils.unescape(
npp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb3);
AffixPatternUtils.unescape(
nsp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb4);
if (!charSequenceEquals(sb1, sb3) || !charSequenceEquals(sb2, sb4)) {
// Case 3. The plus sign substitution was successful.
setPositiveResult(sb3, sb4, properties);
setNegativeResult(sb1, sb2, properties);
return resultInstance;
} else {
// Case 2. There was no minus sign. Set the negative result and fall through.
setNegativeResult(sb1, sb2, properties);
}
}
// Case 1 or 2. Set sb1/sb2 to the positive prefix/suffix.
sb1.clear();
sb2.clear();
AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1);
AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2);
if (npp == null && nsp == null) {
// Case 1. Compute the negative result from the positive subpattern.
sb3.clear();
sb3.append(symbols.getMinusSignString(), Field.SIGN);
sb3.append(sb1);
setNegativeResult(sb3, sb2, properties);
}
// Case 1 or 2. Prepend a '+' sign to the positive prefix.
sb1.insert(0, symbols.getPlusSignString(), Field.SIGN);
setPositiveResult(sb1, sb2, properties);
return resultInstance;
}
private void setPositiveResult(
NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) {
if (properties.getPositivePrefix() != null || properties.getPositiveSuffix() != null) {
// Override with custom affixes
String _prefix = properties.getPositivePrefix();
String _suffix = properties.getPositiveSuffix();
if (_prefix == null) _prefix = "";
if (_suffix == null) _suffix = "";
if (_prefix.length() == 0 && _suffix.length() == 0) {
resultInstance.positive = ConstantAffixModifier.EMPTY;
return;
}
if (resultInstance.positive != null
&& (resultInstance.positive instanceof ConstantAffixModifier)
&& ((ConstantAffixModifier) resultInstance.positive).contentEquals(_prefix, _suffix)) {
// Use the cached modifier
return;
}
resultInstance.positive =
new ConstantAffixModifier(_prefix, _suffix, null, false);
} else {
// Use pattern affixes
if (prefix.length() == 0 && suffix.length() == 0) {
resultInstance.positive = ConstantAffixModifier.EMPTY;
return;
}
if (resultInstance.positive != null
&& (resultInstance.positive instanceof ConstantMultiFieldModifier)
&& ((ConstantMultiFieldModifier) resultInstance.positive).contentEquals(prefix, suffix)) {
// Use the cached modifier
return;
}
resultInstance.positive = new ConstantMultiFieldModifier(prefix, suffix, false);
}
}
private void setNegativeResult(
NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) {
if (properties.getNegativePrefix() != null || properties.getNegativeSuffix() != null) {
// Override with custom affixes
String _prefix = properties.getNegativePrefix();
String _suffix = properties.getNegativeSuffix();
if (_prefix == null) _prefix = "";
if (_suffix == null) _suffix = "";
if (_prefix.length() == 0 && _suffix.length() == 0) {
resultInstance.negative = ConstantAffixModifier.EMPTY;
return;
}
if (resultInstance.negative != null
&& (resultInstance.negative instanceof ConstantAffixModifier)
&& ((ConstantAffixModifier) resultInstance.negative).contentEquals(_prefix, _suffix)) {
// Use the cached modifier
return;
}
resultInstance.negative =
new ConstantAffixModifier(_prefix, _suffix, null, false);
} else {
// Use pattern affixes
if (prefix.length() == 0 && suffix.length() == 0) {
resultInstance.negative = ConstantAffixModifier.EMPTY;
return;
}
if (resultInstance.negative != null
&& (resultInstance.negative instanceof ConstantMultiFieldModifier)
&& ((ConstantMultiFieldModifier) resultInstance.negative).contentEquals(prefix, suffix)) {
// Use the cached modifier
return;
}
resultInstance.negative = new ConstantMultiFieldModifier(prefix, suffix, false);
}
}
/** A null-safe equals method for CharSequences. */
private static boolean charSequenceEquals(CharSequence a, CharSequence b) {
if (a == b) return true;
if (a == null || b == null) return false;
if (a.length() != b.length()) return false;
for (int i = 0; i < a.length(); i++) {
if (a.charAt(i) != b.charAt(i)) return false;
}
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,855 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import com.ibm.icu.impl.number.formatters.PaddingFormat;
import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
import com.ibm.icu.text.DecimalFormatSymbols;
/**
* Handles parsing and creation of the compact pattern string representation of a decimal format.
*/
public class PatternString {
/**
* Parses a pattern string into a new property bag.
*
* @param pattern The pattern string, like "#,##0.00"
* @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
* increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
* as CurrencyUsage, is to be used instead.
* @return A property bag object.
* @throws IllegalArgumentException If there is a syntax error in the pattern string.
*/
public static Properties parseToProperties(String pattern, boolean ignoreRounding) {
Properties properties = new Properties();
LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
return properties;
}
public static Properties parseToProperties(String pattern) {
return parseToProperties(pattern, false);
}
/**
* Parses a pattern string into an existing property bag. All properties that can be encoded into
* a pattern string will be overwritten with either their default value or with the value coming
* from the pattern string. Properties that cannot be encoded into a pattern string, such as
* rounding mode, are not modified.
*
* @param pattern The pattern string, like "#,##0.00"
* @param properties The property bag object to overwrite.
* @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
* increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
* as CurrencyUsage, is to be used instead.
* @throws IllegalArgumentException If there was a syntax error in the pattern string.
*/
public static void parseToExistingProperties(
String pattern, Properties properties, boolean ignoreRounding) {
LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
}
public static void parseToExistingProperties(String pattern, Properties properties) {
parseToExistingProperties(pattern, properties, false);
}
/**
* Creates a pattern string from a property bag.
*
* <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, Properties.DEFAULT_SECONDARY_GROUPING_SIZE)
&& firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZE)
&& groupingSize != firstGroupingSize) {
grouping = groupingSize;
grouping1 = groupingSize;
grouping2 = firstGroupingSize;
} else if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GROUPING_SIZE)) {
grouping = groupingSize;
grouping1 = 0;
grouping2 = groupingSize;
} else if (firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZE)) {
grouping = groupingSize;
grouping1 = 0;
grouping2 = firstGroupingSize;
} else {
grouping = 0;
grouping1 = 0;
grouping2 = 0;
}
int groupingLength = grouping1 + grouping2 + 1;
// Figure out the digits we need to put in the pattern.
BigDecimal roundingInterval = properties.getRoundingIncrement();
StringBuilder digitsString = new StringBuilder();
int digitsStringScale = 0;
if (maxSig != Math.min(dosMax, Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS)) {
// Significant Digits.
while (digitsString.length() < minSig) {
digitsString.append('@');
}
while (digitsString.length() < maxSig) {
digitsString.append('#');
}
} else if (roundingInterval != Properties.DEFAULT_ROUNDING_INCREMENT) {
// Rounding Interval.
digitsStringScale = -roundingInterval.scale();
// TODO: Check for DoS here?
String str = roundingInterval.scaleByPowerOfTen(roundingInterval.scale()).toPlainString();
if (str.charAt(0) == '\'') {
// TODO: Unsupported operation exception or fail silently?
digitsString.append(str, 1, str.length());
} else {
digitsString.append(str);
}
}
while (digitsString.length() + digitsStringScale < minInt) {
digitsString.insert(0, '0');
}
while (-digitsStringScale < minFrac) {
digitsString.append('0');
digitsStringScale--;
}
// Write the digits to the string builder
int m0 = Math.max(groupingLength, digitsString.length() + digitsStringScale);
m0 = (maxInt != dosMax) ? Math.max(maxInt, m0) - 1 : m0 - 1;
int mN = (maxFrac != dosMax) ? Math.min(-maxFrac, digitsStringScale) : digitsStringScale;
for (int magnitude = m0; magnitude >= mN; magnitude--) {
int di = digitsString.length() + digitsStringScale - magnitude - 1;
if (di < 0 || di >= digitsString.length()) {
sb.append('#');
} else {
sb.append(digitsString.charAt(di));
}
if (magnitude > grouping2 && grouping > 0 && (magnitude - grouping2) % grouping == 0) {
sb.append(',');
} else if (magnitude > 0 && magnitude == grouping2) {
sb.append(',');
} else if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
sb.append('.');
}
}
// Exponential notation
if (exponentDigits != Math.min(dosMax, Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS)) {
sb.append('E');
if (exponentShowPlusSign) {
sb.append('+');
}
for (int i = 0; i < exponentDigits; i++) {
sb.append('0');
}
}
// Suffixes
int beforeSuffixPos = sb.length();
if (psp != null) sb.append(psp);
AffixPatternUtils.escape(ps, sb);
// Resolve Padding
if (paddingWidth != Properties.DEFAULT_FORMAT_WIDTH) {
while (paddingWidth - sb.length() > 0) {
sb.insert(afterPrefixPos, '#');
beforeSuffixPos++;
}
int addedLength;
switch (paddingLocation) {
case BEFORE_PREFIX:
addedLength = escapePaddingString(paddingString, sb, 0);
sb.insert(0, '*');
afterPrefixPos += addedLength + 1;
beforeSuffixPos += addedLength + 1;
break;
case AFTER_PREFIX:
addedLength = escapePaddingString(paddingString, sb, afterPrefixPos);
sb.insert(afterPrefixPos, '*');
afterPrefixPos += addedLength + 1;
beforeSuffixPos += addedLength + 1;
break;
case BEFORE_SUFFIX:
escapePaddingString(paddingString, sb, beforeSuffixPos);
sb.insert(beforeSuffixPos, '*');
break;
case AFTER_SUFFIX:
sb.append('*');
escapePaddingString(paddingString, sb, sb.length());
break;
}
}
// Negative affixes
// Ignore if the negative prefix pattern is "-" and the negative suffix is empty
if (np != null
|| ns != null
|| (npp == null && nsp != null)
|| (npp != null && (npp.length() != 1 || npp.charAt(0) != '-' || nsp.length() != 0))) {
sb.append(';');
if (npp != null) sb.append(npp);
AffixPatternUtils.escape(np, sb);
// Copy the positive digit format into the negative.
// This is optional; the pattern is the same as if '#' were appended here instead.
sb.append(sb, afterPrefixPos, beforeSuffixPos);
if (nsp != null) sb.append(nsp);
AffixPatternUtils.escape(ns, sb);
}
return sb.toString();
}
/** @return The number of chars inserted. */
private static int escapePaddingString(CharSequence input, StringBuilder output, int startIndex) {
if (input == null || input.length() == 0) input = PaddingFormat.FALLBACK_PADDING_STRING;
int startLength = output.length();
if (input.length() == 1) {
if (input.equals("'")) {
output.insert(startIndex, "''");
} else {
output.insert(startIndex, input);
}
} else {
output.insert(startIndex, '\'');
int offset = 1;
for (int i = 0; i < input.length(); i++) {
// it's okay to deal in chars here because the quote mark is the only interesting thing.
char ch = input.charAt(i);
if (ch == '\'') {
output.insert(startIndex + offset, "''");
offset += 2;
} else {
output.insert(startIndex + offset, ch);
offset += 1;
}
}
output.insert(startIndex + offset, '\'');
}
return output.length() - startLength;
}
/**
* Converts a pattern between standard notation and localized notation. Localized notation means
* that instead of using generic placeholders in the pattern, you use the corresponding
* locale-specific characters instead. For example, in locale <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.
*
* @param input The pattern to convert.
* @param symbols The symbols corresponding to the localized pattern.
* @param toLocalized true to convert from standard to localized notation; false to convert from
* localized to standard notation.
* @return The pattern expressed in the other notation.
* @deprecated ICU 59 This method is provided for backwards compatibility and should not be used
* in any new code.
*/
@Deprecated
public static String convertLocalized(
CharSequence input, DecimalFormatSymbols symbols, boolean toLocalized) {
if (input == null) return null;
/// This is not the prettiest function in the world, but it gets the job done. ///
// Construct a table of code points to be converted between localized and standard.
int[][] table = new int[6][2];
int standIdx = toLocalized ? 0 : 1;
int localIdx = toLocalized ? 1 : 0;
table[0][standIdx] = '%';
table[0][localIdx] = symbols.getPercent();
table[1][standIdx] = '‰';
table[1][localIdx] = symbols.getPerMill();
table[2][standIdx] = '.';
table[2][localIdx] = symbols.getDecimalSeparator();
table[3][standIdx] = ',';
table[3][localIdx] = symbols.getGroupingSeparator();
table[4][standIdx] = '-';
table[4][localIdx] = symbols.getMinusSign();
table[5][standIdx] = '+';
table[5][localIdx] = symbols.getPlusSign();
// Special case: localIdx characters are NOT allowed to be quotes, like in de_CH.
// Use '' instead.
for (int i = 0; i < table.length; i++) {
if (table[i][localIdx] == '\'') {
table[i][localIdx] = '';
}
}
// Iterate through the string and convert
int offset = 0;
int state = 0;
StringBuilder result = new StringBuilder();
for (; offset < input.length(); ) {
int cp = Character.codePointAt(input, offset);
int cpToAppend = cp;
if (state == 1 || state == 3 || state == 4) {
// Inside user-specified quote
if (cp == '\'') {
if (state == 1) {
state = 0;
} else if (state == 3) {
state = 2;
cpToAppend = -1;
} else {
state = 2;
}
}
} else {
// Base state or inside special character quote
if (cp == '\'') {
if (state == 2 && offset + 1 < input.length()) {
int nextCp = Character.codePointAt(input, offset + 1);
if (nextCp == '\'') {
// escaped quote
state = 4;
} else {
// begin user-specified quote sequence
// we are already in a quote sequence, so omit the opening quote
state = 3;
cpToAppend = -1;
}
} else {
state = 1;
}
} else {
boolean needsSpecialQuote = false;
for (int i = 0; i < table.length; i++) {
if (table[i][0] == cp) {
cpToAppend = table[i][1];
needsSpecialQuote = false; // in case an earlier translation triggered it
break;
} else if (table[i][1] == cp) {
needsSpecialQuote = true;
}
}
if (state == 0 && needsSpecialQuote) {
state = 2;
result.appendCodePoint('\'');
} else if (state == 2 && !needsSpecialQuote) {
state = 0;
result.appendCodePoint('\'');
}
}
}
if (cpToAppend != -1) {
result.appendCodePoint(cpToAppend);
}
offset += Character.charCount(cp);
}
if (state == 2) {
result.appendCodePoint('\'');
}
return result.toString();
}
/** Implements a recursive descent parser for decimal format patterns. */
static class LdmlDecimalPatternParser {
/**
* An internal, intermediate data structure used for storing parse results before they are
* finalized into a DecimalFormatPattern.Builder.
*/
private static class PatternParseResult {
SubpatternParseResult positive = new SubpatternParseResult();
SubpatternParseResult negative = null;
/** Finalizes the temporary data stored in the PatternParseResult to the Builder. */
void saveToProperties(Properties properties, boolean ignoreRounding) {
// Translate from PatternState to Properties.
// Note that most data from "negative" is ignored per the specification of DecimalFormat.
// Grouping settings
if (positive.groupingSizes[1] != -1) {
properties.setGroupingSize(positive.groupingSizes[0]);
} else {
properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE);
}
if (positive.groupingSizes[2] != -1) {
properties.setSecondaryGroupingSize(positive.groupingSizes[1]);
} else {
properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE);
}
// For backwards compatibility, require that the pattern emit at least one min digit.
int minInt, minFrac;
if (positive.totalIntegerDigits == 0 && positive.maximumFractionDigits > 0) {
// patterns like ".##"
minInt = 0;
minFrac = Math.max(1, positive.minimumFractionDigits);
} else if (positive.minimumIntegerDigits == 0 && positive.minimumFractionDigits == 0) {
// patterns like "#.##"
minInt = 1;
minFrac = 0;
} else {
minInt = positive.minimumIntegerDigits;
minFrac = positive.minimumFractionDigits;
}
// Rounding settings
// Don't set basic rounding when there is a currency sign; defer to CurrencyUsage
if (positive.minimumSignificantDigits > 0) {
properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
properties.setMinimumSignificantDigits(positive.minimumSignificantDigits);
properties.setMaximumSignificantDigits(positive.maximumSignificantDigits);
} else if (!positive.rounding.isZero()) {
if (!ignoreRounding) {
properties.setMinimumFractionDigits(minFrac);
properties.setMaximumFractionDigits(positive.maximumFractionDigits);
properties.setRoundingIncrement(positive.rounding.toBigDecimal());
} else {
properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
}
properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
} else {
if (!ignoreRounding) {
properties.setMinimumFractionDigits(minFrac);
properties.setMaximumFractionDigits(positive.maximumFractionDigits);
properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
} else {
properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
}
properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
}
// If the pattern ends with a '.' then force the decimal point.
if (positive.hasDecimal && positive.maximumFractionDigits == 0) {
properties.setDecimalSeparatorAlwaysShown(true);
} else {
properties.setDecimalSeparatorAlwaysShown(false);
}
// Scientific notation settings
if (positive.exponentDigits > 0) {
properties.setExponentSignAlwaysShown(positive.exponentShowPlusSign);
properties.setMinimumExponentDigits(positive.exponentDigits);
if (positive.minimumSignificantDigits == 0) {
// patterns without '@' can define max integer digits, used for engineering notation
properties.setMinimumIntegerDigits(positive.minimumIntegerDigits);
properties.setMaximumIntegerDigits(positive.totalIntegerDigits);
} else {
// patterns with '@' cannot define max integer digits
properties.setMinimumIntegerDigits(1);
properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
}
} else {
properties.setExponentSignAlwaysShown(Properties.DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN);
properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
properties.setMinimumIntegerDigits(minInt);
properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
}
// Padding settings
if (positive.padding.length() > 0) {
// The width of the positive prefix and suffix templates are included in the padding
int paddingWidth =
positive.paddingWidth
+ AffixPatternUtils.unescapedLength(positive.prefix)
+ AffixPatternUtils.unescapedLength(positive.suffix);
properties.setFormatWidth(paddingWidth);
if (positive.padding.length() == 1) {
properties.setPadString(positive.padding.toString());
} else if (positive.padding.length() == 2) {
if (positive.padding.charAt(0) == '\'') {
properties.setPadString("'");
} else {
properties.setPadString(positive.padding.toString());
}
} else {
properties.setPadString(
positive.padding.subSequence(1, positive.padding.length() - 1).toString());
}
assert positive.paddingLocation != null;
properties.setPadPosition(positive.paddingLocation);
} else {
properties.setFormatWidth(Properties.DEFAULT_FORMAT_WIDTH);
properties.setPadString(Properties.DEFAULT_PAD_STRING);
properties.setPadPosition(Properties.DEFAULT_PAD_POSITION);
}
// Set the affixes
// Always call the setter, even if the prefixes are empty, especially in the case of the
// negative prefix pattern, to prevent default values from overriding the pattern.
properties.setPositivePrefixPattern(positive.prefix.toString());
properties.setPositiveSuffixPattern(positive.suffix.toString());
if (negative != null) {
properties.setNegativePrefixPattern(negative.prefix.toString());
properties.setNegativeSuffixPattern(negative.suffix.toString());
} else {
properties.setNegativePrefixPattern(null);
properties.setNegativeSuffixPattern(null);
}
// Set the magnitude multiplier
if (positive.hasPercentSign) {
properties.setMagnitudeMultiplier(2);
} else if (positive.hasPerMilleSign) {
properties.setMagnitudeMultiplier(3);
} else {
properties.setMagnitudeMultiplier(Properties.DEFAULT_MAGNITUDE_MULTIPLIER);
}
}
}
private static class SubpatternParseResult {
int[] groupingSizes = new int[] {0, -1, -1};
int minimumIntegerDigits = 0;
int totalIntegerDigits = 0;
int minimumFractionDigits = 0;
int maximumFractionDigits = 0;
int minimumSignificantDigits = 0;
int maximumSignificantDigits = 0;
boolean hasDecimal = false;
int paddingWidth = 0;
PadPosition paddingLocation = null;
FormatQuantity4 rounding = new FormatQuantity4();
boolean exponentShowPlusSign = false;
int exponentDigits = 0;
boolean hasPercentSign = false;
boolean hasPerMilleSign = false;
boolean hasCurrencySign = false;
StringBuilder padding = new StringBuilder();
StringBuilder prefix = new StringBuilder();
StringBuilder suffix = new StringBuilder();
}
/** An internal class used for tracking the cursor during parsing of a pattern string. */
private static class ParserState {
final String pattern;
int offset;
ParserState(String pattern) {
this.pattern = pattern;
this.offset = 0;
}
int peek() {
if (offset == pattern.length()) {
return -1;
} else {
return pattern.codePointAt(offset);
}
}
int next() {
int codePoint = peek();
offset += Character.charCount(codePoint);
return codePoint;
}
IllegalArgumentException toParseException(String message) {
StringBuilder sb = new StringBuilder();
sb.append("Unexpected character in decimal format pattern: '");
sb.append(pattern);
sb.append("': ");
sb.append(message);
sb.append(": ");
if (peek() == -1) {
sb.append("EOL");
} else {
sb.append("'");
sb.append(Character.toChars(peek()));
sb.append("'");
}
return new IllegalArgumentException(sb.toString());
}
}
static void parse(String pattern, Properties properties, boolean ignoreRounding) {
if (pattern == null || pattern.length() == 0) {
// Backwards compatibility requires that we reset to the default values.
// TODO: Only overwrite the properties that "saveToProperties" normally touches?
properties.clear();
return;
}
// TODO: Use whitespace characters from PatternProps
// TODO: Use thread locals here.
ParserState state = new ParserState(pattern);
PatternParseResult result = new PatternParseResult();
consumePattern(state, result);
result.saveToProperties(properties, ignoreRounding);
}
private static void consumePattern(ParserState state, PatternParseResult result) {
// pattern := subpattern (';' subpattern)?
consumeSubpattern(state, result.positive);
if (state.peek() == ';') {
state.next(); // consume the ';'
result.negative = new SubpatternParseResult();
consumeSubpattern(state, result.negative);
}
if (state.peek() != -1) {
throw state.toParseException("pattern");
}
}
private static void consumeSubpattern(ParserState state, SubpatternParseResult result) {
// subpattern := literals? number exponent? literals?
consumePadding(state, result, PadPosition.BEFORE_PREFIX);
consumeAffix(state, result, result.prefix);
consumePadding(state, result, PadPosition.AFTER_PREFIX);
consumeFormat(state, result);
consumeExponent(state, result);
consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
consumeAffix(state, result, result.suffix);
consumePadding(state, result, PadPosition.AFTER_SUFFIX);
}
private static void consumePadding(
ParserState state, SubpatternParseResult result, PadPosition paddingLocation) {
if (state.peek() != '*') {
return;
}
result.paddingLocation = paddingLocation;
state.next(); // consume the '*'
consumeLiteral(state, result.padding);
}
private static void consumeAffix(
ParserState state, SubpatternParseResult result, StringBuilder destination) {
// literals := { literal }
while (true) {
switch (state.peek()) {
case '#':
case '@':
case ';':
case '*':
case '.':
case ',':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case -1:
// Characters that cannot appear unquoted in a literal
return;
case '%':
result.hasPercentSign = true;
break;
case '‰':
result.hasPerMilleSign = true;
break;
case '¤':
result.hasCurrencySign = true;
break;
}
consumeLiteral(state, destination);
}
}
private static void consumeLiteral(ParserState state, StringBuilder destination) {
if (state.peek() == -1) {
throw state.toParseException("expected unquoted literal but found end of string");
} else if (state.peek() == '\'') {
destination.appendCodePoint(state.next()); // consume the starting quote
while (state.peek() != '\'') {
if (state.peek() == -1) {
throw state.toParseException("expected quoted literal but found end of string");
} else {
destination.appendCodePoint(state.next()); // consume a quoted character
}
}
destination.appendCodePoint(state.next()); // consume the ending quote
} else {
// consume a non-quoted literal character
destination.appendCodePoint(state.next());
}
}
private static void consumeFormat(ParserState state, SubpatternParseResult result) {
consumeIntegerFormat(state, result);
if (state.peek() == '.') {
state.next(); // consume the decimal point
result.hasDecimal = true;
result.paddingWidth += 1;
consumeFractionFormat(state, result);
}
}
private static void consumeIntegerFormat(ParserState state, SubpatternParseResult result) {
boolean seenSignificantDigitMarker = false;
boolean seenDigit = false;
while (true) {
switch (state.peek()) {
case ',':
result.paddingWidth += 1;
result.groupingSizes[2] = result.groupingSizes[1];
result.groupingSizes[1] = result.groupingSizes[0];
result.groupingSizes[0] = 0;
break;
case '#':
if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
result.paddingWidth += 1;
result.groupingSizes[0] += 1;
result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
// no change to result.minimumIntegerDigits
// no change to result.minimumSignificantDigits
result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
result.rounding.appendDigit((byte) 0, 0, true);
break;
case '@':
seenSignificantDigitMarker = true;
if (seenDigit) throw state.toParseException("Can't mix @ and 0 in pattern");
result.paddingWidth += 1;
result.groupingSizes[0] += 1;
result.totalIntegerDigits += 1;
// no change to result.minimumIntegerDigits
result.minimumSignificantDigits += 1;
result.maximumSignificantDigits += 1;
result.rounding.appendDigit((byte) 0, 0, true);
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
seenDigit = true;
if (seenSignificantDigitMarker)
throw state.toParseException("Can't mix @ and 0 in pattern");
// TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
result.paddingWidth += 1;
result.groupingSizes[0] += 1;
result.totalIntegerDigits += 1;
result.minimumIntegerDigits += 1;
// no change to result.minimumSignificantDigits
result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
break;
default:
return;
}
state.next(); // consume the symbol
}
}
private static void consumeFractionFormat(ParserState state, SubpatternParseResult result) {
int zeroCounter = 0;
boolean seenHash = false;
while (true) {
switch (state.peek()) {
case '#':
seenHash = true;
result.paddingWidth += 1;
// no change to result.minimumFractionDigits
result.maximumFractionDigits += 1;
zeroCounter++;
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (seenHash) throw state.toParseException("0 cannot follow # after decimal point");
result.paddingWidth += 1;
result.minimumFractionDigits += 1;
result.maximumFractionDigits += 1;
if (state.peek() == '0') {
zeroCounter++;
} else {
result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
zeroCounter = 0;
}
break;
default:
return;
}
state.next(); // consume the symbol
}
}
private static void consumeExponent(ParserState state, SubpatternParseResult result) {
if (state.peek() != 'E') {
return;
}
state.next(); // consume the E
result.paddingWidth++;
if (state.peek() == '+') {
state.next(); // consume the +
result.exponentShowPlusSign = true;
result.paddingWidth++;
}
while (state.peek() == '0') {
state.next(); // consume the 0
result.exponentDigits += 1;
result.paddingWidth++;
}
}
}
}

View File

@ -0,0 +1,994 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier;
import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
import com.ibm.icu.impl.number.formatters.CurrencyFormat;
import com.ibm.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle;
import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier;
import com.ibm.icu.impl.number.formatters.MeasureFormat;
import com.ibm.icu.impl.number.formatters.PaddingFormat;
import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat;
import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
import com.ibm.icu.impl.number.formatters.ScientificFormat;
import com.ibm.icu.impl.number.rounders.IncrementRounder;
import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CurrencyPluralInfo;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.MeasureUnit;
public class Properties
implements Cloneable,
Serializable,
PositiveDecimalFormat.IProperties,
PositiveNegativeAffixFormat.IProperties,
MagnitudeMultiplier.IProperties,
ScientificFormat.IProperties,
MeasureFormat.IProperties,
CompactDecimalFormat.IProperties,
PaddingFormat.IProperties,
BigDecimalMultiplier.IProperties,
CurrencyFormat.IProperties,
Parse.IProperties,
IncrementRounder.IProperties,
MagnitudeRounder.IProperties,
SignificantDigitsRounder.IProperties {
private static final Properties DEFAULT = new Properties();
/** Auto-generated. */
private static final long serialVersionUID = 4095518955889349243L;
// The setters in this class should NOT have any side-effects or perform any validation. It is
// up to the consumer of the property bag to deal with property validation.
// The fields are all marked "transient" because custom serialization is being used.
/*--------------------------------------------------------------------------------------------+/
/| IMPORTANT! |/
/| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(), |/
/| and in #_hashCode(). |/
/| |/
/| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(), |/
/| or #equals(), but it will NOT catch if you forget to add it to #hashCode(). |/
/+--------------------------------------------------------------------------------------------*/
private transient CompactStyle compactStyle;
private transient Currency currency;
private transient CurrencyPluralInfo currencyPluralInfo;
private transient CurrencyStyle currencyStyle;
private transient CurrencyUsage currencyUsage;
private transient boolean decimalPatternMatchRequired;
private transient boolean decimalSeparatorAlwaysShown;
private transient boolean exponentSignAlwaysShown;
private transient int formatWidth;
private transient int groupingSize;
private transient int magnitudeMultiplier;
private transient MathContext mathContext;
private transient int maximumFractionDigits;
private transient int maximumIntegerDigits;
private transient int maximumSignificantDigits;
private transient FormatWidth measureFormatWidth;
private transient MeasureUnit measureUnit;
private transient int minimumExponentDigits;
private transient int minimumFractionDigits;
private transient int minimumGroupingDigits;
private transient int minimumIntegerDigits;
private transient int minimumSignificantDigits;
private transient BigDecimal multiplier;
private transient String negativePrefix;
private transient String negativePrefixPattern;
private transient String negativeSuffix;
private transient String negativeSuffixPattern;
private transient PadPosition padPosition;
private transient String padString;
private transient boolean parseCaseSensitive;
private transient boolean parseNoExponent;
private transient boolean parseIntegerOnly;
private transient ParseMode parseMode;
private transient boolean parseToBigDecimal;
private transient boolean plusSignAlwaysShown;
private transient String positivePrefix;
private transient String positivePrefixPattern;
private transient String positiveSuffix;
private transient String positiveSuffixPattern;
private transient BigDecimal roundingIncrement;
private transient RoundingMode roundingMode;
private transient int secondaryGroupingSize;
private transient SignificantDigitsMode significantDigitsMode;
/*--------------------------------------------------------------------------------------------+/
/| IMPORTANT! |/
/| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(), |/
/| and in #_hashCode(). |/
/| |/
/| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(), |/
/| or #equals(), but it will NOT catch if you forget to add it to #hashCode(). |/
/+--------------------------------------------------------------------------------------------*/
public Properties() {
clear();
}
private Properties _clear() {
compactStyle = DEFAULT_COMPACT_STYLE;
currency = DEFAULT_CURRENCY;
currencyPluralInfo = DEFAULT_CURRENCY_PLURAL_INFO;
currencyStyle = DEFAULT_CURRENCY_STYLE;
currencyUsage = DEFAULT_CURRENCY_USAGE;
decimalPatternMatchRequired = DEFAULT_DECIMAL_PATTERN_MATCH_REQUIRED;
decimalSeparatorAlwaysShown = DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN;
exponentSignAlwaysShown = DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN;
formatWidth = DEFAULT_FORMAT_WIDTH;
groupingSize = DEFAULT_GROUPING_SIZE;
magnitudeMultiplier = DEFAULT_MAGNITUDE_MULTIPLIER;
mathContext = DEFAULT_MATH_CONTEXT;
maximumFractionDigits = DEFAULT_MAXIMUM_FRACTION_DIGITS;
maximumIntegerDigits = DEFAULT_MAXIMUM_INTEGER_DIGITS;
maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS;
measureFormatWidth = DEFAULT_MEASURE_FORMAT_WIDTH;
measureUnit = DEFAULT_MEASURE_UNIT;
minimumExponentDigits = DEFAULT_MINIMUM_EXPONENT_DIGITS;
minimumFractionDigits = DEFAULT_MINIMUM_FRACTION_DIGITS;
minimumGroupingDigits = DEFAULT_MINIMUM_GROUPING_DIGITS;
minimumIntegerDigits = DEFAULT_MINIMUM_INTEGER_DIGITS;
minimumSignificantDigits = DEFAULT_MINIMUM_SIGNIFICANT_DIGITS;
multiplier = DEFAULT_MULTIPLIER;
negativePrefix = DEFAULT_NEGATIVE_PREFIX;
negativePrefixPattern = DEFAULT_NEGATIVE_PREFIX_PATTERN;
negativeSuffix = DEFAULT_NEGATIVE_SUFFIX;
negativeSuffixPattern = DEFAULT_NEGATIVE_SUFFIX_PATTERN;
padPosition = DEFAULT_PAD_POSITION;
padString = DEFAULT_PAD_STRING;
parseCaseSensitive = DEFAULT_PARSE_CASE_SENSITIVE;
parseIntegerOnly = DEFAULT_PARSE_INTEGER_ONLY;
parseMode = DEFAULT_PARSE_MODE;
parseNoExponent = DEFAULT_PARSE_NO_EXPONENT;
parseToBigDecimal = DEFAULT_PARSE_TO_BIG_DECIMAL;
plusSignAlwaysShown = DEFAULT_PLUS_SIGN_ALWAYS_SHOWN;
positivePrefix = DEFAULT_POSITIVE_PREFIX;
positivePrefixPattern = DEFAULT_POSITIVE_PREFIX_PATTERN;
positiveSuffix = DEFAULT_POSITIVE_SUFFIX;
positiveSuffixPattern = DEFAULT_POSITIVE_SUFFIX_PATTERN;
roundingIncrement = DEFAULT_ROUNDING_INCREMENT;
roundingMode = DEFAULT_ROUNDING_MODE;
secondaryGroupingSize = DEFAULT_SECONDARY_GROUPING_SIZE;
significantDigitsMode = DEFAULT_SIGNIFICANT_DIGITS_MODE;
return this;
}
private Properties _copyFrom(Properties other) {
compactStyle = other.compactStyle;
currency = other.currency;
currencyPluralInfo = other.currencyPluralInfo;
currencyStyle = other.currencyStyle;
currencyUsage = other.currencyUsage;
decimalPatternMatchRequired = other.decimalPatternMatchRequired;
decimalSeparatorAlwaysShown = other.decimalSeparatorAlwaysShown;
exponentSignAlwaysShown = other.exponentSignAlwaysShown;
formatWidth = other.formatWidth;
groupingSize = other.groupingSize;
magnitudeMultiplier = other.magnitudeMultiplier;
mathContext = other.mathContext;
maximumFractionDigits = other.maximumFractionDigits;
maximumIntegerDigits = other.maximumIntegerDigits;
maximumSignificantDigits = other.maximumSignificantDigits;
measureFormatWidth = other.measureFormatWidth;
measureUnit = other.measureUnit;
minimumExponentDigits = other.minimumExponentDigits;
minimumFractionDigits = other.minimumFractionDigits;
minimumGroupingDigits = other.minimumGroupingDigits;
minimumIntegerDigits = other.minimumIntegerDigits;
minimumSignificantDigits = other.minimumSignificantDigits;
multiplier = other.multiplier;
negativePrefix = other.negativePrefix;
negativePrefixPattern = other.negativePrefixPattern;
negativeSuffix = other.negativeSuffix;
negativeSuffixPattern = other.negativeSuffixPattern;
padPosition = other.padPosition;
padString = other.padString;
parseCaseSensitive = other.parseCaseSensitive;
parseIntegerOnly = other.parseIntegerOnly;
parseMode = other.parseMode;
parseNoExponent = other.parseNoExponent;
parseToBigDecimal = other.parseToBigDecimal;
plusSignAlwaysShown = other.plusSignAlwaysShown;
positivePrefix = other.positivePrefix;
positivePrefixPattern = other.positivePrefixPattern;
positiveSuffix = other.positiveSuffix;
positiveSuffixPattern = other.positiveSuffixPattern;
roundingIncrement = other.roundingIncrement;
roundingMode = other.roundingMode;
secondaryGroupingSize = other.secondaryGroupingSize;
significantDigitsMode = other.significantDigitsMode;
return this;
}
private boolean _equals(Properties other) {
boolean eq = true;
eq = eq && _equalsHelper(compactStyle, other.compactStyle);
eq = eq && _equalsHelper(currency, other.currency);
eq = eq && _equalsHelper(currencyPluralInfo, other.currencyPluralInfo);
eq = eq && _equalsHelper(currencyStyle, other.currencyStyle);
eq = eq && _equalsHelper(currencyUsage, other.currencyUsage);
eq = eq && _equalsHelper(decimalPatternMatchRequired, other.decimalPatternMatchRequired);
eq = eq && _equalsHelper(decimalSeparatorAlwaysShown, other.decimalSeparatorAlwaysShown);
eq = eq && _equalsHelper(exponentSignAlwaysShown, other.exponentSignAlwaysShown);
eq = eq && _equalsHelper(formatWidth, other.formatWidth);
eq = eq && _equalsHelper(groupingSize, other.groupingSize);
eq = eq && _equalsHelper(magnitudeMultiplier, other.magnitudeMultiplier);
eq = eq && _equalsHelper(mathContext, other.mathContext);
eq = eq && _equalsHelper(maximumFractionDigits, other.maximumFractionDigits);
eq = eq && _equalsHelper(maximumIntegerDigits, other.maximumIntegerDigits);
eq = eq && _equalsHelper(maximumSignificantDigits, other.maximumSignificantDigits);
eq = eq && _equalsHelper(measureFormatWidth, other.measureFormatWidth);
eq = eq && _equalsHelper(measureUnit, other.measureUnit);
eq = eq && _equalsHelper(minimumExponentDigits, other.minimumExponentDigits);
eq = eq && _equalsHelper(minimumFractionDigits, other.minimumFractionDigits);
eq = eq && _equalsHelper(minimumGroupingDigits, other.minimumGroupingDigits);
eq = eq && _equalsHelper(minimumIntegerDigits, other.minimumIntegerDigits);
eq = eq && _equalsHelper(minimumSignificantDigits, other.minimumSignificantDigits);
eq = eq && _equalsHelper(multiplier, other.multiplier);
eq = eq && _equalsHelper(negativePrefix, other.negativePrefix);
eq = eq && _equalsHelper(negativePrefixPattern, other.negativePrefixPattern);
eq = eq && _equalsHelper(negativeSuffix, other.negativeSuffix);
eq = eq && _equalsHelper(negativeSuffixPattern, other.negativeSuffixPattern);
eq = eq && _equalsHelper(padPosition, other.padPosition);
eq = eq && _equalsHelper(padString, other.padString);
eq = eq && _equalsHelper(parseCaseSensitive, other.parseCaseSensitive);
eq = eq && _equalsHelper(parseIntegerOnly, other.parseIntegerOnly);
eq = eq && _equalsHelper(parseMode, other.parseMode);
eq = eq && _equalsHelper(parseNoExponent, other.parseNoExponent);
eq = eq && _equalsHelper(parseToBigDecimal, other.parseToBigDecimal);
eq = eq && _equalsHelper(plusSignAlwaysShown, other.plusSignAlwaysShown);
eq = eq && _equalsHelper(positivePrefix, other.positivePrefix);
eq = eq && _equalsHelper(positivePrefixPattern, other.positivePrefixPattern);
eq = eq && _equalsHelper(positiveSuffix, other.positiveSuffix);
eq = eq && _equalsHelper(positiveSuffixPattern, other.positiveSuffixPattern);
eq = eq && _equalsHelper(roundingIncrement, other.roundingIncrement);
eq = eq && _equalsHelper(roundingMode, other.roundingMode);
eq = eq && _equalsHelper(secondaryGroupingSize, other.secondaryGroupingSize);
eq = eq && _equalsHelper(significantDigitsMode, other.significantDigitsMode);
return eq;
}
private boolean _equalsHelper(boolean mine, boolean theirs) {
return mine == theirs;
}
private boolean _equalsHelper(int mine, int theirs) {
return mine == theirs;
}
private boolean _equalsHelper(Object mine, Object theirs) {
if (mine == theirs) return true;
if (mine == null) return false;
return mine.equals(theirs);
}
private int _hashCode() {
int hashCode = 0;
hashCode ^= _hashCodeHelper(compactStyle);
hashCode ^= _hashCodeHelper(currency);
hashCode ^= _hashCodeHelper(currencyPluralInfo);
hashCode ^= _hashCodeHelper(currencyStyle);
hashCode ^= _hashCodeHelper(currencyUsage);
hashCode ^= _hashCodeHelper(decimalPatternMatchRequired);
hashCode ^= _hashCodeHelper(decimalSeparatorAlwaysShown);
hashCode ^= _hashCodeHelper(exponentSignAlwaysShown);
hashCode ^= _hashCodeHelper(formatWidth);
hashCode ^= _hashCodeHelper(groupingSize);
hashCode ^= _hashCodeHelper(magnitudeMultiplier);
hashCode ^= _hashCodeHelper(mathContext);
hashCode ^= _hashCodeHelper(maximumFractionDigits);
hashCode ^= _hashCodeHelper(maximumIntegerDigits);
hashCode ^= _hashCodeHelper(maximumSignificantDigits);
hashCode ^= _hashCodeHelper(measureFormatWidth);
hashCode ^= _hashCodeHelper(measureUnit);
hashCode ^= _hashCodeHelper(minimumExponentDigits);
hashCode ^= _hashCodeHelper(minimumFractionDigits);
hashCode ^= _hashCodeHelper(minimumGroupingDigits);
hashCode ^= _hashCodeHelper(minimumIntegerDigits);
hashCode ^= _hashCodeHelper(minimumSignificantDigits);
hashCode ^= _hashCodeHelper(multiplier);
hashCode ^= _hashCodeHelper(negativePrefix);
hashCode ^= _hashCodeHelper(negativePrefixPattern);
hashCode ^= _hashCodeHelper(negativeSuffix);
hashCode ^= _hashCodeHelper(negativeSuffixPattern);
hashCode ^= _hashCodeHelper(padPosition);
hashCode ^= _hashCodeHelper(padString);
hashCode ^= _hashCodeHelper(parseCaseSensitive);
hashCode ^= _hashCodeHelper(parseIntegerOnly);
hashCode ^= _hashCodeHelper(parseMode);
hashCode ^= _hashCodeHelper(parseNoExponent);
hashCode ^= _hashCodeHelper(parseToBigDecimal);
hashCode ^= _hashCodeHelper(plusSignAlwaysShown);
hashCode ^= _hashCodeHelper(positivePrefix);
hashCode ^= _hashCodeHelper(positivePrefixPattern);
hashCode ^= _hashCodeHelper(positiveSuffix);
hashCode ^= _hashCodeHelper(positiveSuffixPattern);
hashCode ^= _hashCodeHelper(roundingIncrement);
hashCode ^= _hashCodeHelper(roundingMode);
hashCode ^= _hashCodeHelper(secondaryGroupingSize);
hashCode ^= _hashCodeHelper(significantDigitsMode);
return hashCode;
}
private int _hashCodeHelper(boolean value) {
return value ? 1 : 0;
}
private int _hashCodeHelper(int value) {
return value * 13;
}
private int _hashCodeHelper(Object value) {
if (value == null) return 0;
return value.hashCode();
}
public Properties clear() {
return _clear();
}
/** Creates and returns a shallow copy of the property bag. */
@Override
public Properties clone() {
// super.clone() returns a shallow copy.
try {
return (Properties) super.clone();
} catch (CloneNotSupportedException e) {
// Should never happen since super is Object
throw new UnsupportedOperationException(e);
}
}
/**
* Shallow-copies the properties from the given property bag into this property bag.
*
* @param other The property bag from which to copy and which will not be modified.
* @return The current property bag (the one modified by this operation), for chaining.
*/
public Properties copyFrom(Properties other) {
return _copyFrom(other);
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (this == other) return true;
if (!(other instanceof Properties)) return false;
return _equals((Properties) other);
}
@Override
public CompactStyle getCompactStyle() {
return compactStyle;
}
@Override
public Currency getCurrency() {
return currency;
}
/// BEGIN GETTERS/SETTERS ///
@Override
@Deprecated
public CurrencyPluralInfo getCurrencyPluralInfo() {
return currencyPluralInfo;
}
@Override
public CurrencyStyle getCurrencyStyle() {
return currencyStyle;
}
@Override
public CurrencyUsage getCurrencyUsage() {
return currencyUsage;
}
@Override
public boolean getDecimalPatternMatchRequired() {
return decimalPatternMatchRequired;
}
@Override
public boolean getDecimalSeparatorAlwaysShown() {
return decimalSeparatorAlwaysShown;
}
@Override
public boolean getExponentSignAlwaysShown() {
return exponentSignAlwaysShown;
}
@Override
public int getFormatWidth() {
return formatWidth;
}
@Override
public int getGroupingSize() {
return groupingSize;
}
@Override
public int getMagnitudeMultiplier() {
return magnitudeMultiplier;
}
@Override
public MathContext getMathContext() {
return mathContext;
}
@Override
public int getMaximumFractionDigits() {
return maximumFractionDigits;
}
@Override
public int getMaximumIntegerDigits() {
return maximumIntegerDigits;
}
@Override
public int getMaximumSignificantDigits() {
return maximumSignificantDigits;
}
@Override
public FormatWidth getMeasureFormatWidth() {
return measureFormatWidth;
}
@Override
public MeasureUnit getMeasureUnit() {
return measureUnit;
}
@Override
public int getMinimumExponentDigits() {
return minimumExponentDigits;
}
@Override
public int getMinimumFractionDigits() {
return minimumFractionDigits;
}
@Override
public int getMinimumGroupingDigits() {
return minimumGroupingDigits;
}
@Override
public int getMinimumIntegerDigits() {
return minimumIntegerDigits;
}
@Override
public int getMinimumSignificantDigits() {
return minimumSignificantDigits;
}
@Override
public BigDecimal getMultiplier() {
return multiplier;
}
@Override
public String getNegativePrefix() {
return negativePrefix;
}
@Override
public String getNegativePrefixPattern() {
return negativePrefixPattern;
}
@Override
public String getNegativeSuffix() {
return negativeSuffix;
}
@Override
public String getNegativeSuffixPattern() {
return negativeSuffixPattern;
}
@Override
public PadPosition getPadPosition() {
return padPosition;
}
@Override
public String getPadString() {
return padString;
}
@Override
public boolean getParseCaseSensitive() {
return parseCaseSensitive;
}
@Override
public boolean getParseIntegerOnly() {
return parseIntegerOnly;
}
@Override
public ParseMode getParseMode() {
return parseMode;
}
@Override
public boolean getParseNoExponent() {
return parseNoExponent;
}
@Override
public boolean getParseToBigDecimal() {
return parseToBigDecimal;
}
@Override
public boolean getPlusSignAlwaysShown() {
return plusSignAlwaysShown;
}
@Override
public String getPositivePrefix() {
return positivePrefix;
}
@Override
public String getPositivePrefixPattern() {
return positivePrefixPattern;
}
@Override
public String getPositiveSuffix() {
return positiveSuffix;
}
@Override
public String getPositiveSuffixPattern() {
return positiveSuffixPattern;
}
@Override
public BigDecimal getRoundingIncrement() {
return roundingIncrement;
}
@Override
public RoundingMode getRoundingMode() {
return roundingMode;
}
@Override
public int getSecondaryGroupingSize() {
return secondaryGroupingSize;
}
@Override
public SignificantDigitsMode getSignificantDigitsMode() {
return significantDigitsMode;
}
@Override
public int hashCode() {
return _hashCode();
}
/** Custom serialization: re-create object from serialized properties. */
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// Initialize to empty
clear();
// Extra int for possible future use
ois.readInt();
// 1) How many fields were serialized?
int count = ois.readInt();
// 2) Read each field by its name and value
for (int i=0; i<count; i++) {
String name = (String) ois.readObject();
Object value = ois.readObject();
// Get the field reference
Field field = null;
try {
field = Properties.class.getDeclaredField(name);
} catch (NoSuchFieldException e) {
// The field name does not exist! Possibly corrupted serialization. Ignore this entry.
continue;
} catch (SecurityException e) {
// Should not happen
throw new AssertionError(e);
}
// NOTE: If the type of a field were changed in the future, this would be the place to check:
// If the variable `value` is the old type, perform any conversions necessary.
// Save value into the field
try {
field.set(this, value);
} catch (IllegalArgumentException e) {
// Should not happen
throw new AssertionError(e);
} catch (IllegalAccessException e) {
// Should not happen
throw new AssertionError(e);
}
}
}
@Override
public Properties setCompactStyle(CompactStyle compactStyle) {
this.compactStyle = compactStyle;
return this;
}
@Override
public Properties setCurrency(Currency currency) {
this.currency = currency;
return this;
}
@Override
@Deprecated
public Properties setCurrencyPluralInfo(CurrencyPluralInfo currencyPluralInfo) {
// TODO: In order to maintain immutability, we have to perform a clone here.
// It would be better to just retire CurrencyPluralInfo entirely.
if (currencyPluralInfo != null) {
currencyPluralInfo = (CurrencyPluralInfo) currencyPluralInfo.clone();
}
this.currencyPluralInfo = currencyPluralInfo;
return this;
}
@Override
public Properties setCurrencyStyle(CurrencyStyle currencyStyle) {
this.currencyStyle = currencyStyle;
return this;
}
@Override
public Properties setCurrencyUsage(CurrencyUsage currencyUsage) {
this.currencyUsage = currencyUsage;
return this;
}
@Override
public Properties setDecimalPatternMatchRequired(boolean decimalPatternMatchRequired) {
this.decimalPatternMatchRequired = decimalPatternMatchRequired;
return this;
}
@Override
public Properties setDecimalSeparatorAlwaysShown(boolean alwaysShowDecimal) {
this.decimalSeparatorAlwaysShown = alwaysShowDecimal;
return this;
}
@Override
public Properties setExponentSignAlwaysShown(boolean exponentSignAlwaysShown) {
this.exponentSignAlwaysShown = exponentSignAlwaysShown;
return this;
}
@Override
public Properties setFormatWidth(int paddingWidth) {
this.formatWidth = paddingWidth;
return this;
}
@Override
public Properties setGroupingSize(int groupingSize) {
this.groupingSize = groupingSize;
return this;
}
@Override
public Properties setMagnitudeMultiplier(int magnitudeMultiplier) {
this.magnitudeMultiplier = magnitudeMultiplier;
return this;
}
@Override
public Properties setMathContext(MathContext mathContext) {
this.mathContext = mathContext;
return this;
}
@Override
public Properties setMaximumFractionDigits(int maximumFractionDigits) {
this.maximumFractionDigits = maximumFractionDigits;
return this;
}
@Override
public Properties setMaximumIntegerDigits(int maximumIntegerDigits) {
this.maximumIntegerDigits = maximumIntegerDigits;
return this;
}
@Override
public Properties setMaximumSignificantDigits(int maximumSignificantDigits) {
this.maximumSignificantDigits = maximumSignificantDigits;
return this;
}
@Override
public Properties setMeasureFormatWidth(FormatWidth measureFormatWidth) {
this.measureFormatWidth = measureFormatWidth;
return this;
}
@Override
public Properties setMeasureUnit(MeasureUnit measureUnit) {
this.measureUnit = measureUnit;
return this;
}
@Override
public Properties setMinimumExponentDigits(int exponentDigits) {
this.minimumExponentDigits = exponentDigits;
return this;
}
@Override
public Properties setMinimumFractionDigits(int minimumFractionDigits) {
this.minimumFractionDigits = minimumFractionDigits;
return this;
}
@Override
public Properties setMinimumGroupingDigits(int minimumGroupingDigits) {
this.minimumGroupingDigits = minimumGroupingDigits;
return this;
}
@Override
public Properties setMinimumIntegerDigits(int minimumIntegerDigits) {
this.minimumIntegerDigits = minimumIntegerDigits;
return this;
}
@Override
public Properties setMinimumSignificantDigits(int minimumSignificantDigits) {
this.minimumSignificantDigits = minimumSignificantDigits;
return this;
}
@Override
public Properties setMultiplier(BigDecimal multiplier) {
this.multiplier = multiplier;
return this;
}
@Override
public Properties setNegativePrefix(String negativePrefix) {
this.negativePrefix = negativePrefix;
return this;
}
@Override
public Properties setNegativePrefixPattern(String negativePrefixPattern) {
this.negativePrefixPattern = negativePrefixPattern;
return this;
}
@Override
public Properties setNegativeSuffix(String negativeSuffix) {
this.negativeSuffix = negativeSuffix;
return this;
}
@Override
public Properties setNegativeSuffixPattern(String negativeSuffixPattern) {
this.negativeSuffixPattern = negativeSuffixPattern;
return this;
}
@Override
public Properties setPadPosition(PadPosition paddingLocation) {
this.padPosition = paddingLocation;
return this;
}
@Override
public Properties setPadString(String paddingString) {
this.padString = paddingString;
return this;
}
@Override
public Properties setParseCaseSensitive(boolean parseCaseSensitive) {
this.parseCaseSensitive = parseCaseSensitive;
return this;
}
@Override
public Properties setParseIntegerOnly(boolean parseIntegerOnly) {
this.parseIntegerOnly = parseIntegerOnly;
return this;
}
@Override
public Properties setParseMode(ParseMode parseMode) {
this.parseMode = parseMode;
return this;
}
@Override
public Properties setParseNoExponent(boolean parseNoExponent) {
this.parseNoExponent = parseNoExponent;
return this;
}
@Override
public Properties setParseToBigDecimal(boolean parseToBigDecimal) {
this.parseToBigDecimal = parseToBigDecimal;
return this;
}
@Override
public Properties setPlusSignAlwaysShown(boolean plusSignAlwaysShown) {
this.plusSignAlwaysShown = plusSignAlwaysShown;
return this;
}
@Override
public Properties setPositivePrefix(String positivePrefix) {
this.positivePrefix = positivePrefix;
return this;
}
@Override
public Properties setPositivePrefixPattern(String positivePrefixPattern) {
this.positivePrefixPattern = positivePrefixPattern;
return this;
}
@Override
public Properties setPositiveSuffix(String positiveSuffix) {
this.positiveSuffix = positiveSuffix;
return this;
}
@Override
public Properties setPositiveSuffixPattern(String positiveSuffixPattern) {
this.positiveSuffixPattern = positiveSuffixPattern;
return this;
}
@Override
public Properties setRoundingIncrement(BigDecimal roundingIncrement) {
this.roundingIncrement = roundingIncrement;
return this;
}
@Override
public Properties setRoundingMode(RoundingMode roundingMode) {
this.roundingMode = roundingMode;
return this;
}
@Override
public Properties setSecondaryGroupingSize(int secondaryGroupingSize) {
this.secondaryGroupingSize = secondaryGroupingSize;
return this;
}
@Override
public Properties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode) {
this.significantDigitsMode = significantDigitsMode;
return this;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append("<Properties");
Field[] fields = Properties.class.getDeclaredFields();
for (Field field : fields) {
Object myValue, defaultValue;
try {
myValue = field.get(this);
defaultValue = field.get(DEFAULT);
} catch (IllegalArgumentException e) {
e.printStackTrace();
continue;
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
if (myValue == null && defaultValue == null) {
continue;
} else if (myValue == null || defaultValue == null) {
result.append(" " + field.getName() + ":" + myValue);
} else if (!myValue.equals(defaultValue)) {
result.append(" " + field.getName() + ":" + myValue);
}
}
result.append(">");
return result.toString();
}
/**
* Custom serialization: save fields along with their name, so that fields can be easily added in
* the future in any order. Only save fields that differ from their default value.
*/
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
// Extra int for possible future use
oos.writeInt(0);
ArrayList<Field> fieldsToSerialize = new ArrayList<Field>();
ArrayList<Object> valuesToSerialize = new ArrayList<Object>();
Field[] fields = Properties.class.getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
try {
Object myValue = field.get(this);
if (myValue == null) {
// All *Object* values default to null; no need to serialize.
continue;
}
Object defaultValue = field.get(DEFAULT);
if (!myValue.equals(defaultValue)) {
fieldsToSerialize.add(field);
valuesToSerialize.add(myValue);
}
} catch (IllegalArgumentException e) {
// Should not happen
throw new AssertionError(e);
} catch (IllegalAccessException e) {
// Should not happen
throw new AssertionError(e);
}
}
// 1) How many fields are to be serialized?
int count = fieldsToSerialize.size();
oos.writeInt(count);
// 2) Write each field with its name and value
for (int i = 0; i < count; i++) {
Field field = fieldsToSerialize.get(i);
Object value = valuesToSerialize.get(i);
oos.writeObject(field.getName());
oos.writeObject(value);
}
}
}

View File

@ -0,0 +1,265 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.MathContext;
import java.math.RoundingMode;
import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
import com.ibm.icu.impl.number.formatters.ScientificFormat;
/**
* The base class for a Rounder used by ICU Decimal Format.
*
* <p>A Rounder must implement the method {@link #apply}. An implementation must:
*
* <ol>
* <li>Either have the code <code>applyDefaults(input);</code> in its apply function, or otherwise
* ensure that minFrac, maxFrac, minInt, and maxInt are obeyed, paying special attention to
* the case when the input is zero.
* <li>Call one of {@link FormatQuantity#roundToIncrement}, {@link
* FormatQuantity#roundToMagnitude}, or {@link FormatQuantity#roundToInfinity} on the input.
* </ol>
*
* <p>In order to be used by {@link CompactDecimalFormat} and {@link ScientificFormat}, among
* others, your rounder must be stable upon <em>decreasing</em> the magnitude of the input number.
* For example, if your rounder converts "999" to "1000", it must also convert "99.9" to "100" and
* "0.999" to "1". (The opposite does not need to be the case: you can round "0.999" to "1" but keep
* "999" as "999".)
*
* @see com.ibm.icu.impl.number.rounders.MagnitudeRounder
* @see com.ibm.icu.impl.number.rounders.IncrementRounder
* @see com.ibm.icu.impl.number.rounders.SignificantDigitsRounder
* @see com.ibm.icu.impl.number.rounders.NoRounder
*/
public abstract class Rounder extends Format.BeforeFormat {
public static interface IBasicRoundingProperties {
static int DEFAULT_MINIMUM_INTEGER_DIGITS = -1;
/** @see #setMinimumIntegerDigits */
public int getMinimumIntegerDigits();
/**
* Sets the minimum number of digits to display before the decimal point. If the number has
* fewer than this number of digits, the number will be padded with zeros. The pattern "#00.0#",
* for example, corresponds to 2 minimum integer digits, and the number 5.3 would be formatted
* as "05.3" in locale <em>en-US</em>.
*
* @param minimumIntegerDigits The minimum number of integer digits to output.
* @return The property bag, for chaining.
*/
public IBasicRoundingProperties setMinimumIntegerDigits(int minimumIntegerDigits);
static int DEFAULT_MAXIMUM_INTEGER_DIGITS = -1;
/** @see #setMaximumIntegerDigits */
public int getMaximumIntegerDigits();
/**
* Sets the maximum number of digits to display before the decimal point. If the number has more
* than this number of digits, the extra digits will be truncated. For example, if maximum
* integer digits is 2, and you attempt to format the number 1970, you will get "70" in locale
* <em>en-US</em>. It is not possible to specify the maximum integer digits using a pattern
* string, except in the special case of a scientific format pattern.
*
* @param maximumIntegerDigits The maximum number of integer digits to output.
* @return The property bag, for chaining.
*/
public IBasicRoundingProperties setMaximumIntegerDigits(int maximumIntegerDigits);
static int DEFAULT_MINIMUM_FRACTION_DIGITS = -1;
/** @see #setMinimumFractionDigits */
public int getMinimumFractionDigits();
/**
* Sets the minimum number of digits to display after the decimal point. If the number has fewer
* than this number of digits, the number will be padded with zeros. The pattern "#00.0#", for
* example, corresponds to 1 minimum fraction digit, and the number 456 would be formatted as
* "456.0" in locale <em>en-US</em>.
*
* @param minimumFractionDigits The minimum number of fraction digits to output.
* @return The property bag, for chaining.
*/
public IBasicRoundingProperties setMinimumFractionDigits(int minimumFractionDigits);
static int DEFAULT_MAXIMUM_FRACTION_DIGITS = -1;
/** @see #setMaximumFractionDigits */
public int getMaximumFractionDigits();
/**
* Sets the maximum number of digits to display after the decimal point. If the number has fewer
* than this number of digits, the number will be rounded off using the rounding mode specified
* by {@link #setRoundingMode(RoundingMode)}. The pattern "#00.0#", for example, corresponds to
* 2 maximum fraction digits, and the number 456.789 would be formatted as "456.79" in locale
* <em>en-US</em> with the default rounding mode. Note that the number 456.999 would be
* formatted as "457.0" given the same configurations.
*
* @param maximumFractionDigits The maximum number of fraction digits to output.
* @return The property bag, for chaining.
*/
public IBasicRoundingProperties setMaximumFractionDigits(int maximumFractionDigits);
static RoundingMode DEFAULT_ROUNDING_MODE = null;
/** @see #setRoundingMode */
public RoundingMode getRoundingMode();
/**
* Sets the rounding mode, which determines under which conditions extra decimal places are
* rounded either up or down. See {@link RoundingMode} for details on the choices of rounding
* mode. The default if not set explicitly is {@link RoundingMode#HALF_EVEN}.
*
* <p>This setting is ignored if {@link #setMathContext} is used.
*
* @param roundingMode The rounding mode to use when rounding is required.
* @return The property bag, for chaining.
* @see RoundingMode
* @see #setMathContext
*/
public IBasicRoundingProperties setRoundingMode(RoundingMode roundingMode);
static MathContext DEFAULT_MATH_CONTEXT = null;
/** @see #setMathContext */
public MathContext getMathContext();
/**
* Sets the {@link MathContext} to be used during math and rounding operations. A MathContext
* encapsulates a RoundingMode and the number of significant digits in the output.
*
* @param mathContext The math context to use when rounding is required.
* @return The property bag, for chaining.
* @see MathContext
* @see #setRoundingMode
*/
public IBasicRoundingProperties setMathContext(MathContext mathContext);
}
public static interface MultiplierGenerator {
public int getMultiplier(int magnitude);
}
// Properties available to all rounding strategies
protected final MathContext mathContext;
protected final int minInt;
protected final int maxInt;
protected final int minFrac;
protected final int maxFrac;
/**
* Constructor that uses integer and fraction digit lengths from IBasicRoundingProperties.
*
* @param properties
*/
protected Rounder(IBasicRoundingProperties properties) {
mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
int _maxInt = properties.getMaximumIntegerDigits();
int _minInt = properties.getMinimumIntegerDigits();
int _maxFrac = properties.getMaximumFractionDigits();
int _minFrac = properties.getMinimumFractionDigits();
// Validate min/max int/frac.
// For backwards compatibility, minimum overrides maximum if the two conflict.
// The following logic ensures that there is always a minimum of at least one digit.
if (_minInt == 0 && _maxFrac != 0) {
// Force a digit to the right of the decimal point.
minFrac = _minFrac <= 0 ? 1 : _minFrac;
maxFrac = _maxFrac < 0 ? Integer.MAX_VALUE : _maxFrac < minFrac ? minFrac : _maxFrac;
minInt = 0;
maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt;
} else {
// Force a digit to the left of the decimal point.
minFrac = _minFrac < 0 ? 0 : _minFrac;
maxFrac = _maxFrac < 0 ? Integer.MAX_VALUE : _maxFrac < minFrac ? minFrac : _maxFrac;
minInt = _minInt <= 0 ? 1 : _minInt;
maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt < minInt ? minInt : _maxInt;
}
}
/**
* Perform rounding and specification of integer and fraction digit lengths on the input quantity.
* Calling this method will change the state of the FormatQuantity.
*
* @param input The {@link FormatQuantity} to be modified and rounded.
*/
public abstract void apply(FormatQuantity input);
/**
* Rounding can affect the magnitude. First we attempt to adjust according to the original
* magnitude, and if the magnitude changes, we adjust according to a magnitude one greater. Note
* that this algorithm assumes that increasing the multiplier never increases the number of digits
* that can be displayed.
*
* @param input The quantity to be rounded.
* @param mg The implementation that returns magnitude adjustment based on a given starting
* magnitude.
* @return The multiplier that was chosen to best fit the input.
*/
public int chooseMultiplierAndApply(FormatQuantity input, MultiplierGenerator mg) {
FormatQuantity copy = input.clone();
int magnitude = input.getMagnitude();
int multiplier = mg.getMultiplier(magnitude);
input.adjustMagnitude(multiplier);
apply(input);
if (input.getMagnitude() == magnitude + multiplier + 1) {
magnitude += 1;
input.copyFrom(copy);
multiplier = mg.getMultiplier(magnitude);
input.adjustMagnitude(multiplier);
assert input.getMagnitude() == magnitude + multiplier - 1;
apply(input);
assert input.getMagnitude() == magnitude + multiplier;
}
return multiplier;
}
/**
* Implementations can call this method to perform default logic for min/max digits. This method
* performs logic for handling of a zero input.
*
* @param input The digits being formatted.
*/
protected void applyDefaults(FormatQuantity input) {
input.setIntegerFractionLength(minInt, maxInt, minFrac, maxFrac);
}
private static final ThreadLocal<Properties> threadLocalProperties =
new ThreadLocal<Properties>() {
@Override
protected Properties initialValue() {
return new Properties();
}
};
/**
* Gets a thread-local property bag that can be used to deliver properties to a constructor.
* Rounders themselves are guaranteed to not internally use a copy of this property bag.
*
* @return A clean, thread-local property bag.
*/
public static Properties getThreadLocalProperties() {
return threadLocalProperties.get().clear();
}
@Override
public void before(FormatQuantity input, ModifierHolder mods) {
apply(input);
}
@Override
public void export(Properties properties) {
properties.setMathContext(mathContext);
properties.setRoundingMode(mathContext.getRoundingMode());
properties.setMinimumFractionDigits(minFrac);
properties.setMinimumIntegerDigits(minInt);
properties.setMaximumFractionDigits(maxFrac);
properties.setMaximumIntegerDigits(maxInt);
}
}

View File

@ -0,0 +1,165 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import com.ibm.icu.impl.number.Rounder.IBasicRoundingProperties;
/** @author sffc */
public class RoundingUtils {
public static final int SECTION_LOWER = 1;
public static final int SECTION_MIDPOINT = 2;
public static final int SECTION_UPPER = 3;
/**
* Converts a rounding mode and metadata about the quantity being rounded to a boolean determining
* whether the value should be rounded toward infinity or toward zero.
*
* <p>The parameters are of type int because benchmarks on an x86-64 processor against OpenJDK
* showed that ints were demonstrably faster than enums in switch statements.
*
* @param isEven Whether the digit immediately before the rounding magnitude is even.
* @param isNegative Whether the quantity is negative.
* @param section Whether the part of the quantity to the right of the rounding magnitude is
* exactly halfway between two digits, whether it is in the lower part (closer to zero), or
* whether it is in the upper part (closer to infinity). See {@link #SECTION_LOWER}, {@link
* #SECTION_MIDPOINT}, and {@link #SECTION_UPPER}.
* @param roundingMode The integer version of the {@link RoundingMode}, which you can get via
* {@link RoundingMode#ordinal}.
* @param reference A reference object to be used when throwing an ArithmeticException.
* @return true if the number should be rounded toward zero; false if it should be rounded toward
* infinity.
*/
public static boolean getRoundingDirection(
boolean isEven, boolean isNegative, int section, int roundingMode, Object reference) {
switch (roundingMode) {
case BigDecimal.ROUND_UP:
// round away from zero
return false;
case BigDecimal.ROUND_DOWN:
// round toward zero
return true;
case BigDecimal.ROUND_CEILING:
// round toward positive infinity
return isNegative;
case BigDecimal.ROUND_FLOOR:
// round toward negative infinity
return !isNegative;
case BigDecimal.ROUND_HALF_UP:
switch (section) {
case SECTION_MIDPOINT:
return false;
case SECTION_LOWER:
return true;
case SECTION_UPPER:
return false;
}
break;
case BigDecimal.ROUND_HALF_DOWN:
switch (section) {
case SECTION_MIDPOINT:
return true;
case SECTION_LOWER:
return true;
case SECTION_UPPER:
return false;
}
break;
case BigDecimal.ROUND_HALF_EVEN:
switch (section) {
case SECTION_MIDPOINT:
return isEven;
case SECTION_LOWER:
return true;
case SECTION_UPPER:
return false;
}
break;
}
// Rounding mode UNNECESSARY
throw new ArithmeticException("Rounding is required on " + reference.toString());
}
/**
* Gets whether the given rounding mode's rounding boundary is at the midpoint. The rounding
* boundary is the point at which a number switches from being rounded down to being rounded up.
* For example, with rounding mode HALF_EVEN, HALF_UP, or HALF_DOWN, the rounding boundary is at
* the midpoint, and this function would return true. However, for UP, DOWN, CEILING, and FLOOR,
* the rounding boundary is at the "edge", and this function would return false.
*
* @param roundingMode The integer version of the {@link RoundingMode}.
* @return true if rounding mode is HALF_EVEN, HALF_UP, or HALF_DOWN; false otherwise.
*/
public static boolean roundsAtMidpoint(int roundingMode) {
switch (roundingMode) {
case BigDecimal.ROUND_UP:
case BigDecimal.ROUND_DOWN:
case BigDecimal.ROUND_CEILING:
case BigDecimal.ROUND_FLOOR:
return false;
default:
return true;
}
}
private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED =
new MathContext[RoundingMode.values().length];
private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS =
new MathContext[RoundingMode.values().length];
static {
for (int i = 0; i < MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS.length; i++) {
MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[i] = new MathContext(0, RoundingMode.valueOf(i));
MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS[i] = new MathContext(16);
}
}
/**
* Gets the user-specified math context out of the property bag. If there is none, falls back to a
* math context with unlimited precision and the user-specified rounding mode, which defaults to
* HALF_EVEN (the IEEE 754R default).
*
* @param properties The property bag.
* @return A {@link MathContext}. Never null.
*/
public static MathContext getMathContextOrUnlimited(IBasicRoundingProperties properties) {
MathContext mathContext = properties.getMathContext();
if (mathContext == null) {
RoundingMode roundingMode = properties.getRoundingMode();
if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN;
mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[roundingMode.ordinal()];
}
return mathContext;
}
/**
* Gets the user-specified math context out of the property bag. If there is none, falls back to a
* math context with 16 digits of precision (the 64-bit IEEE 754R default) and the user-specified
* rounding mode, which defaults to HALF_EVEN (the IEEE 754R default).
*
* @param properties The property bag.
* @return A {@link MathContext}. Never null.
*/
public static MathContext getMathContextOr16Digits(IBasicRoundingProperties properties) {
MathContext mathContext = properties.getMathContext();
if (mathContext == null) {
RoundingMode roundingMode = properties.getRoundingMode();
if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN;
mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS[roundingMode.ordinal()];
}
return mathContext;
}
}

View File

@ -0,0 +1,123 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
import com.ibm.icu.impl.number.formatters.RangeFormat;
import com.ibm.icu.impl.number.modifiers.SimpleModifier;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.MeasureUnit;
public class demo {
public static void main(String[] args) throws ParseException {
SimpleModifier.testFormatAsPrefixSuffix();
System.out.println(new FormatQuantity1(3.14159));
System.out.println(new FormatQuantity1(3.14159, true));
System.out.println(new FormatQuantity2(3.14159));
System.out.println(
PatternString.propertiesToString(PatternString.parseToProperties("+**##,##,#00.05#%")));
ParsePosition ppos = new ParsePosition(0);
System.out.println(
Parse.parse(
"dd123",
ppos,
new Properties().setPositivePrefix("dd").setNegativePrefix("ddd"),
DecimalFormatSymbols.getInstance()));
System.out.println(ppos);
List<Format> formats = new ArrayList<Format>();
Properties properties = new Properties();
Format ndf = Endpoint.fromBTA(properties);
formats.add(ndf);
properties =
new Properties()
.setMinimumSignificantDigits(3)
.setMaximumSignificantDigits(3)
.setCompactStyle(CompactStyle.LONG);
Format cdf = Endpoint.fromBTA(properties);
formats.add(cdf);
properties =
new Properties().setFormatWidth(10).setPadPosition(PadPosition.AFTER_PREFIX);
Format pdf = Endpoint.fromBTA(properties);
formats.add(pdf);
properties =
new Properties()
.setMinimumExponentDigits(1)
.setMaximumIntegerDigits(3)
.setMaximumFractionDigits(1);
Format exf = Endpoint.fromBTA(properties);
formats.add(exf);
properties = new Properties().setRoundingIncrement(new BigDecimal("0.5"));
Format rif = Endpoint.fromBTA(properties);
formats.add(rif);
properties = new Properties().setMeasureUnit(MeasureUnit.HECTARE);
Format muf = Endpoint.fromBTA(properties);
formats.add(muf);
properties =
new Properties().setMeasureUnit(MeasureUnit.HECTARE).setCompactStyle(CompactStyle.LONG);
Format cmf = Endpoint.fromBTA(properties);
formats.add(cmf);
properties = PatternString.parseToProperties("#,##0.00 \u00a4");
Format ptf = Endpoint.fromBTA(properties);
formats.add(ptf);
RangeFormat rf = new RangeFormat(cdf, cdf, " to ");
System.out.println(rf.format(new FormatQuantity2(1234), new FormatQuantity2(2345)));
String[] cases = {
"1.0",
"2.01",
"1234.56",
"3000.0",
// "512.0000000000017",
// "4096.000000000001",
// "4096.000000000004",
// "4096.000000000005",
// "4096.000000000006",
// "4096.000000000007",
"0.00026418",
"0.01789261",
"468160.0",
"999000.0",
"999900.0",
"999990.0",
"0.0",
"12345678901.0",
// "789000000000000000000000.0",
// "789123123567853156372158.0",
"-5193.48",
};
for (String str : cases) {
System.out.println("----------");
System.out.println(str);
System.out.println(" NDF: " + ndf.format(new FormatQuantity2(Double.parseDouble(str))));
System.out.println(" CDF: " + cdf.format(new FormatQuantity2(Double.parseDouble(str))));
System.out.println(" PWD: " + pdf.format(new FormatQuantity2(Double.parseDouble(str))));
System.out.println(" EXF: " + exf.format(new FormatQuantity2(Double.parseDouble(str))));
System.out.println(" RIF: " + rif.format(new FormatQuantity2(Double.parseDouble(str))));
System.out.println(" MUF: " + muf.format(new FormatQuantity2(Double.parseDouble(str))));
System.out.println(" CMF: " + cmf.format(new FormatQuantity2(Double.parseDouble(str))));
System.out.println(" PTF: " + ptf.format(new FormatQuantity2(Double.parseDouble(str))));
}
}
}

View File

@ -0,0 +1,57 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import java.math.BigDecimal;
import com.ibm.icu.impl.number.Format.BeforeFormat;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.Properties;
public class BigDecimalMultiplier extends BeforeFormat {
public static interface IProperties {
static BigDecimal DEFAULT_MULTIPLIER = null;
/** @see #setMultiplier */
public BigDecimal getMultiplier();
/**
* Multiply all numbers by this amount before formatting.
*
* @param multiplier The amount to multiply by.
* @return The property bag, for chaining.
* @see MagnitudeMultiplier
*/
public IProperties setMultiplier(BigDecimal multiplier);
}
public static boolean useMultiplier(IProperties properties) {
return properties.getMultiplier() != IProperties.DEFAULT_MULTIPLIER;
}
private final BigDecimal multiplier;
public static BigDecimalMultiplier getInstance(IProperties properties) {
if (properties.getMultiplier() == null) {
throw new IllegalArgumentException("The multiplier must be present for MultiplierFormat");
}
// TODO: Intelligently fall back to a MagnitudeMultiplier if the multiplier is a power of ten?
return new BigDecimalMultiplier(properties);
}
private BigDecimalMultiplier(IProperties properties) {
this.multiplier = properties.getMultiplier();
}
@Override
public void before(FormatQuantity input, ModifierHolder mods) {
input.multiplyBy(multiplier);
}
@Override
public void export(Properties properties) {
properties.setMultiplier(multiplier);
}
}

View File

@ -0,0 +1,449 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import java.util.HashMap;
import java.util.Map;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.Modifier.PositiveNegativeModifier;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.PNAffixGenerator;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.Rounder;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
public class CompactDecimalFormat extends Format.BeforeFormat {
public static interface IProperties
extends RoundingFormat.IProperties, CurrencyFormat.ICurrencyProperties {
static CompactStyle DEFAULT_COMPACT_STYLE = null;
/** @see #setCompactStyle */
public CompactStyle getCompactStyle();
/**
* Use compact decimal formatting with the specified {@link CompactStyle}. CompactStyle.SHORT
* produces output like "10K" in locale <em>en-US</em>, whereas CompactStyle.LONG produces
* output like "10 thousand" in that locale.
*
* @param compactStyle The style of prefixes/suffixes to append.
* @return The property bag, for chaining.
*/
public IProperties setCompactStyle(CompactStyle compactStyle);
}
public static boolean useCompactDecimalFormat(IProperties properties) {
return properties.getCompactStyle() != IProperties.DEFAULT_COMPACT_STYLE;
}
static final int MAX_DIGITS = 15;
// Properties
private final CompactDecimalData data;
private final Rounder rounder;
private final PositiveNegativeModifier defaultMod;
private final CompactStyle style; // retained for exporting only
public static CompactDecimalFormat getInstance(
DecimalFormatSymbols symbols, IProperties properties) {
return new CompactDecimalFormat(symbols, properties);
}
private static Rounder getRounder(IProperties properties) {
// Use rounding settings if they were specified, or else use the default CDF rounder.
Rounder rounder = RoundingFormat.getDefaultOrNull(properties);
if (rounder == null) {
rounder =
SignificantDigitsRounder.getInstance(
SignificantDigitsRounder.getThreadLocalProperties()
.setMinimumSignificantDigits(1)
.setMaximumSignificantDigits(2)
.setSignificantDigitsMode(SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION));
}
return rounder;
}
protected static final ThreadLocal<Map<CompactDecimalFingerprint, CompactDecimalData>>
threadLocalDataCache =
new ThreadLocal<Map<CompactDecimalFingerprint, CompactDecimalData>>() {
@Override
protected Map<CompactDecimalFingerprint, CompactDecimalData> initialValue() {
return new HashMap<CompactDecimalFingerprint, CompactDecimalData>();
}
};
private static CompactDecimalData getData(
DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) {
// See if we already have a data object based on the fingerprint
CompactDecimalData data = threadLocalDataCache.get().get(fingerprint);
if (data != null) return data;
// Make data bundle object
data = new CompactDecimalData();
ULocale ulocale = symbols.getULocale();
CompactDecimalDataSink sink = new CompactDecimalDataSink(data, symbols, fingerprint);
String nsName = NumberingSystem.getInstance(ulocale).getName();
ICUResourceBundle r =
(ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, ulocale);
r.getAllItemsWithFallback("NumberElements/" + nsName, sink);
if (data.isEmpty() && !nsName.equals("latn")) {
r.getAllItemsWithFallback("NumberElements/latn", sink);
}
if (sink.exception != null) {
throw sink.exception;
}
threadLocalDataCache.get().put(fingerprint, data);
return data;
}
private static PositiveNegativeModifier getDefaultMod(
DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) {
ULocale uloc = symbols.getULocale();
String pattern;
if (fingerprint.compactType == CompactType.CURRENCY) {
pattern = NumberFormat.getPattern(uloc, NumberFormat.CURRENCYSTYLE);
} else {
pattern = NumberFormat.getPattern(uloc, NumberFormat.NUMBERSTYLE);
}
// TODO: Clean this up; avoid the extra object creations.
// TODO: Currency may also need to override grouping settings, not just affixes.
Properties properties = PatternString.parseToProperties(pattern);
PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
PNAffixGenerator.Result result =
pnag.getModifiers(symbols, fingerprint.currencySymbol, properties);
return new PositiveNegativeAffixModifier(result.positive, result.negative);
}
private CompactDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) {
CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties);
this.rounder = getRounder(properties);
this.data = getData(symbols, fingerprint);
this.defaultMod = getDefaultMod(symbols, fingerprint);
this.style = properties.getCompactStyle(); // for exporting only
}
@Override
public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
apply(input, mods, rules, rounder, data, defaultMod);
}
@Override
protected void before(FormatQuantity input, ModifierHolder mods) {
throw new UnsupportedOperationException();
}
public static void apply(
FormatQuantity input,
ModifierHolder mods,
PluralRules rules,
DecimalFormatSymbols symbols,
IProperties properties) {
CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties);
Rounder rounder = getRounder(properties);
CompactDecimalData data = getData(symbols, fingerprint);
PositiveNegativeModifier defaultMod = getDefaultMod(symbols, fingerprint);
apply(input, mods, rules, rounder, data, defaultMod);
}
private static void apply(
FormatQuantity input,
ModifierHolder mods,
PluralRules rules,
Rounder rounder,
CompactDecimalData data,
PositiveNegativeModifier defaultMod) {
// Treat zero as if it had magnitude 0
int magnitude;
if (input.isZero()) {
magnitude = 0;
rounder.apply(input);
} else {
int multiplier = rounder.chooseMultiplierAndApply(input, data);
magnitude = input.getMagnitude() - multiplier;
}
StandardPlural plural = input.getStandardPlural(rules);
boolean isNegative = input.isNegative();
Modifier mod = data.getModifier(magnitude, plural, isNegative);
if (mod == null) {
// Use the default (non-compact) modifier.
mod = defaultMod.getModifier(isNegative);
}
mods.add(mod);
}
@Override
public void export(Properties properties) {
properties.setCompactStyle(style);
rounder.export(properties);
}
static class CompactDecimalData implements Rounder.MultiplierGenerator {
// A dummy object used when a "0" compact decimal entry is encountered. This is necessary
// in order to prevent falling back to root.
private static final Modifier USE_FALLBACK = new ConstantAffixModifier();
final Modifier[] mods;
final byte[] multipliers;
boolean isEmpty;
int largestMagnitude;
CompactDecimalData() {
mods = new Modifier[(MAX_DIGITS + 1) * StandardPlural.COUNT * 2];
multipliers = new byte[MAX_DIGITS + 1];
isEmpty = true;
largestMagnitude = -1;
}
boolean isEmpty() {
return isEmpty;
}
@Override
public int getMultiplier(int magnitude) {
if (magnitude < 0) {
return 0;
}
if (magnitude > largestMagnitude) {
magnitude = largestMagnitude;
}
return multipliers[magnitude];
}
int setOrGetMultiplier(int magnitude, byte multiplier) {
if (multipliers[magnitude] != 0) {
return multipliers[magnitude];
}
multipliers[magnitude] = multiplier;
isEmpty = false;
if (magnitude > largestMagnitude) largestMagnitude = magnitude;
return multiplier;
}
Modifier getModifier(int magnitude, StandardPlural plural, boolean isNegative) {
if (magnitude < 0) {
return null;
}
if (magnitude > largestMagnitude) {
magnitude = largestMagnitude;
}
Modifier mod = mods[modIndex(magnitude, plural, isNegative)];
if (mod == null && plural != StandardPlural.OTHER) {
// Fall back to "other" plural variant
mod = mods[modIndex(magnitude, StandardPlural.OTHER, isNegative)];
}
if (mod == USE_FALLBACK) {
// Return null if USE_FALLBACK is present
mod = null;
}
return mod;
}
public boolean has(int magnitude, StandardPlural plural) {
// Return true if USE_FALLBACK is present
return mods[modIndex(magnitude, plural, false)] != null;
}
void setModifiers(Modifier positive, Modifier negative, int magnitude, StandardPlural plural) {
mods[modIndex(magnitude, plural, false)] = positive;
mods[modIndex(magnitude, plural, true)] = negative;
isEmpty = false;
if (magnitude > largestMagnitude) largestMagnitude = magnitude;
}
void setNoFallback(int magnitude, StandardPlural plural) {
setModifiers(USE_FALLBACK, USE_FALLBACK, magnitude, plural);
}
private static final int modIndex(int magnitude, StandardPlural plural, boolean isNegative) {
return magnitude * StandardPlural.COUNT * 2 + plural.ordinal() * 2 + (isNegative ? 1 : 0);
}
}
// Should this be public or internal?
static enum CompactType {
DECIMAL,
CURRENCY
}
static class CompactDecimalFingerprint {
// TODO: Add more stuff to the fingerprint, like the symbols used by PNAffixGenerator
final CompactStyle compactStyle;
final CompactType compactType;
final ULocale uloc;
final String currencySymbol;
CompactDecimalFingerprint(DecimalFormatSymbols symbols, IProperties properties) {
// CompactDecimalFormat does not need to worry about the same constraints as non-compact
// currency formatting needs to consider, like the currency rounding mode and the currency
// long names with plural forms.
if (properties.getCurrency() != CurrencyFormat.ICurrencyProperties.DEFAULT_CURRENCY) {
compactType = CompactType.CURRENCY;
currencySymbol = CurrencyFormat.getCurrencySymbol(symbols, properties);
} else {
compactType = CompactType.DECIMAL;
currencySymbol = symbols.getCurrencySymbol(); // fallback; should remain unused
}
compactStyle = properties.getCompactStyle();
uloc = symbols.getULocale();
}
@Override
public boolean equals(Object _other) {
if (_other == null) return false;
if (this == _other) return true;
CompactDecimalFingerprint other = (CompactDecimalFingerprint) _other;
if (compactStyle != other.compactStyle) return false;
if (compactType != other.compactType) return false;
if (currencySymbol != other.currencySymbol) {
// String comparison with null handling
if (currencySymbol == null || other.currencySymbol == null) return false;
if (!currencySymbol.equals(other.currencySymbol)) return false;
}
if (!uloc.equals(other.uloc)) return false;
return true;
}
@Override
public int hashCode() {
int hashCode = 0;
if (compactStyle != null) hashCode ^= compactStyle.hashCode();
if (compactType != null) hashCode ^= compactType.hashCode();
if (uloc != null) hashCode ^= uloc.hashCode();
if (currencySymbol != null) hashCode ^= currencySymbol.hashCode();
return hashCode;
}
}
private static final class CompactDecimalDataSink extends UResource.Sink {
final CompactDecimalData data;
final DecimalFormatSymbols symbols;
final CompactStyle compactStyle;
final CompactType compactType;
final String currencySymbol;
final PNAffixGenerator pnag;
IllegalArgumentException exception;
/*
* NumberElements{ <-- top (numbering system table)
* latn{ <-- patternsTable (one per numbering system)
* patternsLong{ <-- formatsTable (one per pattern)
* decimalFormat{ <-- powersOfTenTable (one per format)
* 1000{ <-- pluralVariantsTable (one per power of ten)
* one{"0 thousand"} <-- plural variant and template
*/
public CompactDecimalDataSink(
CompactDecimalData data,
DecimalFormatSymbols symbols,
CompactDecimalFingerprint fingerprint) {
this.data = data;
this.symbols = symbols;
compactType = fingerprint.compactType;
currencySymbol = fingerprint.currencySymbol;
compactStyle = fingerprint.compactStyle;
pnag = PNAffixGenerator.getThreadLocalInstance();
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
UResource.Table patternsTable = value.getTable();
for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
if (key.contentEquals("patternsShort") && compactStyle == CompactStyle.SHORT) {
} else if (key.contentEquals("patternsLong") && compactStyle == CompactStyle.LONG) {
} else {
continue;
}
// traverse into the table of formats
UResource.Table formatsTable = value.getTable();
for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
if (key.contentEquals("decimalFormat") && compactType == CompactType.DECIMAL) {
} else if (key.contentEquals("currencyFormat") && compactType == CompactType.CURRENCY) {
} else {
continue;
}
// traverse into the table of powers of ten
UResource.Table powersOfTenTable = value.getTable();
for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
try {
// Assumes that the keys are always of the form "10000" where the magnitude is the
// length of the key minus one
byte magnitude = (byte) (key.length() - 1);
// Silently ignore divisors that are too big.
if (magnitude >= MAX_DIGITS) continue;
// Iterate over the plural variants ("one", "other", etc)
UResource.Table pluralVariantsTable = value.getTable();
for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
// Skip this magnitude/plural if we already have it from a child locale.
StandardPlural plural = StandardPlural.fromString(key.toString());
if (data.has(magnitude, plural)) {
continue;
}
// The value "0" means that we need to use the default pattern and not fall back
// to parent locales. Example locale where this is relevant: 'it'.
String patternString = value.toString();
if (patternString.equals("0")) {
data.setNoFallback(magnitude, plural);
continue;
}
// The magnitude multiplier is the difference between the magnitude and the number
// of zeros in the pattern, getMinimumIntegerDigits.
Properties properties = PatternString.parseToProperties(patternString);
byte _multiplier = (byte) -(magnitude - properties.getMinimumIntegerDigits() + 1);
if (_multiplier != data.setOrGetMultiplier(magnitude, _multiplier)) {
throw new IllegalArgumentException(
String.format(
"Different number of zeros for same power of ten in compact decimal format data for locale '%s', style '%s', type '%s'",
symbols.getULocale().toString(),
compactStyle.toString(),
compactType.toString()));
}
PNAffixGenerator.Result result =
pnag.getModifiers(symbols, currencySymbol, properties);
data.setModifiers(result.positive, result.negative, magnitude, plural);
}
} catch (IllegalArgumentException e) {
exception = e;
continue;
}
}
// We want only one table of compact decimal formats, so if we get here, stop consuming.
// The data.isEmpty() check will prevent further bundles from being traversed.
return;
}
}
}
}
}

View File

@ -0,0 +1,299 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import java.math.BigDecimal;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.AffixPatternUtils;
import com.ibm.icu.impl.number.PNAffixGenerator;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.Rounder;
import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
import com.ibm.icu.impl.number.rounders.IncrementRounder;
import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
import com.ibm.icu.text.CurrencyPluralInfo;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
public class CurrencyFormat {
public enum CurrencyStyle {
SYMBOL,
ISO_CODE;
}
public static interface ICurrencyProperties {
static Currency DEFAULT_CURRENCY = null;
/** @see #setCurrency */
public Currency getCurrency();
/**
* Use the specified currency to substitute currency placeholders ('¤') in the pattern string.
*
* @param currency The currency.
* @return The property bag, for chaining.
*/
public IProperties setCurrency(Currency currency);
static CurrencyStyle DEFAULT_CURRENCY_STYLE = null;
/** @see #setCurrencyStyle */
public CurrencyStyle getCurrencyStyle();
/**
* Use the specified {@link CurrencyStyle} to replace currency placeholders ('¤').
* CurrencyStyle.SYMBOL will use the short currency symbol, like "$" or "", whereas
* CurrencyStyle.ISO_CODE will use the ISO 4217 currency code, like "USD" or "EUR".
*
* <p>For long currency names, use {@link MeasureFormat.IProperties#setMeasureUnit}.
*
* @param currencyStyle The currency style. Defaults to CurrencyStyle.SYMBOL.
* @return The property bag, for chaining.
*/
public IProperties setCurrencyStyle(CurrencyStyle currencyStyle);
/**
* An old enum that specifies how currencies should be rounded. It contains a subset of the
* functionality supported by RoundingInterval.
*/
static Currency.CurrencyUsage DEFAULT_CURRENCY_USAGE = null;
/** @see #setCurrencyUsage */
public Currency.CurrencyUsage getCurrencyUsage();
/**
* Use the specified {@link CurrencyUsage} instance, which provides default rounding rules for
* the currency in two styles, CurrencyUsage.CASH and CurrencyUsage.STANDARD.
*
* <p>The CurrencyUsage specified here will not be used unless there is a currency placeholder
* in the pattern.
*
* @param currencyUsage The currency usage. Defaults to CurrencyUsage.STANDARD.
* @return The property bag, for chaining.
*/
public IProperties setCurrencyUsage(Currency.CurrencyUsage currencyUsage);
static CurrencyPluralInfo DEFAULT_CURRENCY_PLURAL_INFO = null;
/** @see #setCurrencyPluralInfo */
@Deprecated
public CurrencyPluralInfo getCurrencyPluralInfo();
/**
* Use the specified {@link CurrencyPluralInfo} instance when formatting currency long names.
*
* @param currencyPluralInfo The currency plural info object.
* @return The property bag, for chaining.
* @deprecated Use {@link MeasureFormat.IProperties#setMeasureUnit} with a Currency instead.
*/
@Deprecated
public IProperties setCurrencyPluralInfo(CurrencyPluralInfo currencyPluralInfo);
public IProperties clone();
}
public static interface IProperties
extends ICurrencyProperties,
RoundingFormat.IProperties,
PositiveNegativeAffixFormat.IProperties {}
/**
* Returns true if the currency is set in The property bag or if currency symbols are present in
* the prefix/suffix pattern.
*/
public static boolean useCurrency(IProperties properties) {
return ((properties.getCurrency() != null)
|| properties.getCurrencyPluralInfo() != null
|| AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern())
|| AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern()));
}
/**
* Returns the effective currency symbol based on the input. If {@link
* ICurrencyProperties#setCurrencyStyle} was set to {@link CurrencyStyle#ISO_CODE}, the ISO Code
* will be returned; otherwise, the currency symbol, like "$", will be returned.
*
* @param symbols The current {@link DecimalFormatSymbols} instance
* @param properties The current property bag
* @return The currency symbol string, e.g., to substitute '¤' in a decimal pattern string.
*/
public static String getCurrencySymbol(
DecimalFormatSymbols symbols, ICurrencyProperties properties) {
// If the user asked for ISO Code, return the ISO Code instead of the symbol
CurrencyStyle style = properties.getCurrencyStyle();
if (style == CurrencyStyle.ISO_CODE) {
return getCurrencyIsoCode(symbols, properties);
}
// Get the currency symbol
Currency currency = properties.getCurrency();
if (currency == null) {
return symbols.getCurrencySymbol();
} else if (currency.equals(symbols.getCurrency())) {
// The user may have set a custom currency symbol in DecimalFormatSymbols.
return symbols.getCurrencySymbol();
} else {
// Use the canonical symbol.
return currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
}
}
/**
* Returns the currency ISO code based on the input, like "USD".
*
* @param symbols The current {@link DecimalFormatSymbols} instance
* @param properties The current property bag
* @return The currency ISO code string, e.g., to substitute '¤¤' in a decimal pattern string.
*/
public static String getCurrencyIsoCode(
DecimalFormatSymbols symbols, ICurrencyProperties properties) {
Currency currency = properties.getCurrency();
if (currency == null) {
// If a currency object was not provided, use the string from symbols
// Note: symbols.getCurrency().getCurrencyCode() won't work here because
// DecimalFormatSymbols#setInternationalCurrencySymbol() does not update the
// immutable internal currency instance.
return symbols.getInternationalCurrencySymbol();
} else if (currency.equals(symbols.getCurrency())) {
// The user may have set a custom currency symbol in DecimalFormatSymbols.
return symbols.getInternationalCurrencySymbol();
} else {
// Use the canonical currency code.
return currency.getCurrencyCode();
}
}
/**
* Returns the currency long name on the input, like "US dollars".
*
* @param symbols The current {@link DecimalFormatSymbols} instance
* @param properties The current property bag
* @param plural The plural form
* @return The currency long name string, e.g., to substitute '¤¤¤' in a decimal pattern string.
*/
public static String getCurrencyLongName(
DecimalFormatSymbols symbols, ICurrencyProperties properties, StandardPlural plural) {
// Attempt to get a currency object first from properties then from symbols
Currency currency = properties.getCurrency();
if (currency == null) {
currency = symbols.getCurrency();
}
// If no currency object is available, fall back to the currency symbol
if (currency == null) {
return getCurrencySymbol(symbols, properties);
}
// Get the long name
return currency.getName(
symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
}
public static GeneralPluralModifier getCurrencyModifier(
DecimalFormatSymbols symbols, IProperties properties) {
PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
String sym = getCurrencySymbol(symbols, properties);
String iso = getCurrencyIsoCode(symbols, properties);
// Previously, the user was also able to specify '¤¤' and '¤¤¤' directly into the prefix or
// suffix, which is how the user specified whether they wanted the ISO code or long name.
// For backwards compatibility support, that feature is implemented here.
CurrencyPluralInfo info = properties.getCurrencyPluralInfo();
GeneralPluralModifier mod = new GeneralPluralModifier();
Properties temp = new Properties();
for (StandardPlural plural : StandardPlural.VALUES) {
String longName = getCurrencyLongName(symbols, properties, plural);
PNAffixGenerator.Result result;
if (info == null) {
// CurrencyPluralInfo is not available.
result = pnag.getModifiers(symbols, sym, iso, longName, properties);
} else {
// CurrencyPluralInfo is available. Use it to generate affixes for long name support.
String pluralPattern = info.getCurrencyPluralPattern(plural.getKeyword());
PatternString.parseToExistingProperties(pluralPattern, temp, true);
result = pnag.getModifiers(symbols, sym, iso, longName, temp);
}
mod.put(plural, result.positive, result.negative);
}
return mod;
}
private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
public static void populateCurrencyRounderProperties(
Properties destination, DecimalFormatSymbols symbols, IProperties properties) {
Currency currency = properties.getCurrency();
if (currency == null) {
// Fall back to the DecimalFormatSymbols currency instance.
currency = symbols.getCurrency();
}
if (currency == null) {
// There is a currency symbol in the pattern, but we have no currency available to use.
// Use the default currency instead so that we can still apply currency usage rules.
currency = DEFAULT_CURRENCY;
}
Currency.CurrencyUsage currencyUsage = properties.getCurrencyUsage();
if (currencyUsage == null) {
currencyUsage = CurrencyUsage.STANDARD;
}
double incrementDouble = currency.getRoundingIncrement(currencyUsage);
int fractionDigits = currency.getDefaultFractionDigits(currencyUsage);
destination.setRoundingMode(properties.getRoundingMode());
destination.setMinimumIntegerDigits(properties.getMinimumIntegerDigits());
destination.setMaximumIntegerDigits(properties.getMaximumIntegerDigits());
int _minFrac = properties.getMinimumFractionDigits();
int _maxFrac = properties.getMaximumFractionDigits();
if (_minFrac >= 0 || _maxFrac >= 0) {
// User override
destination.setMinimumFractionDigits(_minFrac);
destination.setMaximumFractionDigits(_maxFrac);
} else {
destination.setMinimumFractionDigits(fractionDigits);
destination.setMaximumFractionDigits(fractionDigits);
}
if (incrementDouble > 0.0) {
BigDecimal incrementBigDecimal;
BigDecimal _roundingIncrement = properties.getRoundingIncrement();
if (_roundingIncrement != null) {
incrementBigDecimal = _roundingIncrement;
} else {
incrementBigDecimal = BigDecimal.valueOf(incrementDouble);
}
destination.setRoundingIncrement(incrementBigDecimal);
} else {
}
}
private static final ThreadLocal<Properties> threadLocalProperties =
new ThreadLocal<Properties>() {
@Override
protected Properties initialValue() {
return new Properties();
}
};
public static Rounder getCurrencyRounder(DecimalFormatSymbols symbols, IProperties properties) {
Properties cprops = threadLocalProperties.get().clear();
populateCurrencyRounderProperties(cprops, symbols, properties);
if (cprops.getRoundingIncrement() != null) {
return IncrementRounder.getInstance(cprops);
} else {
return MagnitudeRounder.getInstance(cprops);
}
}
}

View File

@ -0,0 +1,59 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.Format.BeforeFormat;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.Properties;
public class MagnitudeMultiplier extends Format.BeforeFormat {
private static final MagnitudeMultiplier DEFAULT = new MagnitudeMultiplier(0);
public static interface IProperties {
static int DEFAULT_MAGNITUDE_MULTIPLIER = 0;
/** @see #setMagnitudeMultiplier */
public int getMagnitudeMultiplier();
/**
* Multiply all numbers by this power of ten before formatting. Negative multipliers reduce the
* magnitude and make numbers smaller (closer to zero).
*
* @param magnitudeMultiplier The number of powers of ten to scale.
* @return The property bag, for chaining.
* @see BigDecimalMultiplier
*/
public IProperties setMagnitudeMultiplier(int magnitudeMultiplier);
}
public static boolean useMagnitudeMultiplier(IProperties properties) {
return properties.getMagnitudeMultiplier() != IProperties.DEFAULT_MAGNITUDE_MULTIPLIER;
}
// Properties
final int delta;
public static BeforeFormat getInstance(Properties properties) {
if (properties.getMagnitudeMultiplier() == 0) {
return DEFAULT;
}
return new MagnitudeMultiplier(properties.getMagnitudeMultiplier());
}
private MagnitudeMultiplier(int delta) {
this.delta = delta;
}
@Override
public void before(FormatQuantity input, ModifierHolder mods) {
input.adjustMagnitude(delta);
}
@Override
public void export(Properties properties) {
properties.setMagnitudeMultiplier(delta);
}
}

View File

@ -0,0 +1,73 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
import com.ibm.icu.impl.number.modifiers.SimpleModifier;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
public class MeasureFormat {
public static interface IProperties {
static MeasureUnit DEFAULT_MEASURE_UNIT = null;
/** @see #setMeasureUnit */
public MeasureUnit getMeasureUnit();
/**
* Apply prefixes and suffixes for the specified {@link MeasureUnit} to the formatted number.
*
* @param measureUnit The measure unit.
* @return The property bag, for chaining.
*/
public IProperties setMeasureUnit(MeasureUnit measureUnit);
static FormatWidth DEFAULT_MEASURE_FORMAT_WIDTH = null;
/** @see #setMeasureFormatWidth */
public FormatWidth getMeasureFormatWidth();
/**
* Use the specified {@link FormatWidth} when choosing the style of measure unit prefix/suffix.
*
* <p>Must be used in conjunction with {@link #setMeasureUnit}.
*
* @param measureFormatWidth The width style. Defaults to FormatWidth.WIDE.
* @return The property bag, for chaining.
*/
public IProperties setMeasureFormatWidth(FormatWidth measureFormatWidth);
}
public static boolean useMeasureFormat(IProperties properties) {
return properties.getMeasureUnit() != IProperties.DEFAULT_MEASURE_UNIT;
}
public static GeneralPluralModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) {
ULocale uloc = symbols.getULocale();
MeasureUnit unit = properties.getMeasureUnit();
FormatWidth width = properties.getMeasureFormatWidth();
if (unit == null) {
throw new IllegalArgumentException("A measure unit is required for MeasureFormat");
}
if (width == null) {
width = FormatWidth.WIDE;
}
// Temporarily, create a MeasureFormat instance for its data loading capability
// TODO: Move data loading directly into this class file
com.ibm.icu.text.MeasureFormat mf = com.ibm.icu.text.MeasureFormat.getInstance(uloc, width);
GeneralPluralModifier mod = new GeneralPluralModifier();
for (StandardPlural plural : StandardPlural.VALUES) {
String formatString = null;
mf.getPluralFormatter(unit, width, plural.ordinal());
mod.put(plural, new SimpleModifier(formatString, null, false));
}
return mod;
}
}

View File

@ -0,0 +1,173 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import com.ibm.icu.impl.number.Format.AfterFormat;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.Properties;
public class PaddingFormat implements AfterFormat {
public enum PadPosition {
BEFORE_PREFIX,
AFTER_PREFIX,
BEFORE_SUFFIX,
AFTER_SUFFIX;
public static PadPosition fromOld(int old) {
switch (old) {
case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX:
return PadPosition.BEFORE_PREFIX;
case com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX:
return PadPosition.AFTER_PREFIX;
case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX:
return PadPosition.BEFORE_SUFFIX;
case com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX:
return PadPosition.AFTER_SUFFIX;
default:
throw new IllegalArgumentException("Don't know how to map " + old);
}
}
public int toOld() {
switch (this) {
case BEFORE_PREFIX:
return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX;
case AFTER_PREFIX:
return com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX;
case BEFORE_SUFFIX:
return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX;
case AFTER_SUFFIX:
return com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX;
default:
return -1; // silence compiler errors
}
}
}
public static interface IProperties {
static int DEFAULT_FORMAT_WIDTH = 0;
/** @see #setFormatWidth */
public int getFormatWidth();
/**
* Sets the minimum width of the string output by the formatting pipeline. For example, if
* padding is enabled and paddingWidth is set to 6, formatting the number "3.14159" with the
* pattern "0.00" will result in "··3.14" if '·' is your padding string.
*
* <p>If the number is longer than your padding width, the number will display as if no padding
* width had been specified, which may result in strings longer than the padding width.
*
* <p>Width is counted in UTF-16 code units.
*
* @param formatWidth The output width.
* @return The property bag, for chaining.
* @see #setPadPosition
* @see #setPadString
*/
public IProperties setFormatWidth(int formatWidth);
static String DEFAULT_PAD_STRING = null;
/** @see #setPadString */
public String getPadString();
/**
* Sets the string used for padding. The string should contain a single character or grapheme
* cluster.
*
* <p>Must be used in conjunction with {@link #setFormatWidth}.
*
* @param paddingString The padding string. Defaults to an ASCII space (U+0020).
* @return The property bag, for chaining.
* @see #setFormatWidth
*/
public IProperties setPadString(String paddingString);
static PadPosition DEFAULT_PAD_POSITION = null;
/** @see #setPadPosition */
public PadPosition getPadPosition();
/**
* Sets the location where the padding string is to be inserted to maintain the padding width:
* one of BEFORE_PREFIX, AFTER_PREFIX, BEFORE_SUFFIX, or AFTER_SUFFIX.
*
* <p>Must be used in conjunction with {@link #setFormatWidth}.
*
* @param padPosition The output width.
* @return The property bag, for chaining.
* @see #setFormatWidth
*/
public IProperties setPadPosition(PadPosition padPosition);
}
public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space
public static boolean usePadding(IProperties properties) {
return properties.getFormatWidth() != IProperties.DEFAULT_FORMAT_WIDTH;
}
public static AfterFormat getInstance(IProperties properties) {
return new PaddingFormat(
properties.getFormatWidth(),
properties.getPadString(),
properties.getPadPosition());
}
// Properties
private final int paddingWidth;
private final String paddingString;
private final PadPosition paddingLocation;
private PaddingFormat(
int paddingWidth, String paddingString, PadPosition paddingLocation) {
this.paddingWidth = paddingWidth > 0 ? paddingWidth : 10; // TODO: Is this a sensible default?
this.paddingString = paddingString != null ? paddingString : FALLBACK_PADDING_STRING;
this.paddingLocation =
paddingLocation != null ? paddingLocation : PadPosition.BEFORE_PREFIX;
}
@Override
public int after(ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) {
// TODO: Count code points instead of code units?
int requiredPadding = paddingWidth - (rightIndex - leftIndex) - mods.totalLength();
if (requiredPadding <= 0) {
// Skip padding, but still apply modifiers to be consistent
return mods.applyAll(string, leftIndex, rightIndex);
}
int length = 0;
if (paddingLocation == PadPosition.AFTER_PREFIX) {
length += addPadding(requiredPadding, string, leftIndex);
} else if (paddingLocation == PadPosition.BEFORE_SUFFIX) {
length += addPadding(requiredPadding, string, rightIndex);
}
length += mods.applyAll(string, leftIndex, rightIndex + length);
if (paddingLocation == PadPosition.BEFORE_PREFIX) {
length += addPadding(requiredPadding, string, leftIndex);
} else if (paddingLocation == PadPosition.AFTER_SUFFIX) {
length += addPadding(requiredPadding, string, rightIndex + length);
}
return length;
}
private int addPadding(int requiredPadding, NumberStringBuilder string, int index) {
for (int i = 0; i < requiredPadding; i++) {
string.insert(index, paddingString, null);
}
return paddingString.length() * requiredPadding;
}
@Override
public void export(Properties properties) {
properties.setFormatWidth(paddingWidth);
properties.setPadString(paddingString);
properties.setPadPosition(paddingLocation);
}
}

View File

@ -0,0 +1,227 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberFormat.Field;
public class PositiveDecimalFormat implements Format.TargetFormat {
public static interface IProperties extends CurrencyFormat.IProperties {
static int DEFAULT_GROUPING_SIZE = -1;
/** @see #setGroupingSize */
public int getGroupingSize();
/**
* Sets the number of digits between grouping separators. For example, the <em>en-US</em> locale
* uses a grouping size of 3, so the number 1234567 would be formatted as "1,234,567". For
* locales whose grouping sizes vary with magnitude, see {@link #setSecondaryGroupingSize(int)}.
*
* @param groupingSize The primary grouping size.
* @return The property bag, for chaining.
*/
public IProperties setGroupingSize(int groupingSize);
static int DEFAULT_SECONDARY_GROUPING_SIZE = -1;
/** @see #setSecondaryGroupingSize */
public int getSecondaryGroupingSize();
/**
* Sets the number of digits between grouping separators higher than the least-significant
* grouping separator. For example, the locale <em>hi</em> uses a primary grouping size of 3 and
* a secondary grouping size of 2, so the number 1234567 would be formatted as "12,34,567".
*
* <p>The two levels of grouping separators can be specified in the pattern string. For example,
* the <em>hi</em> locale's default decimal format pattern is "#,##,##0.###".
*
* @param secondaryGroupingSize The secondary grouping size.
* @return The property bag, for chaining.
*/
public IProperties setSecondaryGroupingSize(int secondaryGroupingSize);
static boolean DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN = false;
/** @see #setDecimalSeparatorAlwaysShown */
public boolean getDecimalSeparatorAlwaysShown();
/**
* Sets whether to always show the decimal point, even if the number doesn't require one. For
* example, if always show decimal is true, the number 123 would be formatted as "123." in
* locale <em>en-US</em>.
*
* @param decimalSeparatorAlwaysShown Whether to show the decimal point when it is optional.
* @return The property bag, for chaining.
*/
public IProperties setDecimalSeparatorAlwaysShown(boolean decimalSeparatorAlwaysShown);
static int DEFAULT_MINIMUM_GROUPING_DIGITS = 1;
/** @see #setMinimumGroupingDigits */
public int getMinimumGroupingDigits();
/**
* Sets the minimum number of digits required to be beyond the first grouping separator in order
* to enable grouping. For example, if the minimum grouping digits is 2, then 1234 would be
* formatted as "1234" but 12345 would be formatted as "12,345" in <em>en-US</em>. Note that
* 1234567 would still be formatted as "1,234,567", not "1234,567".
*
* @param minimumGroupingDigits How many digits must appear before a grouping separator before
* enabling grouping.
* @return The property bag, for chaining.
*/
public IProperties setMinimumGroupingDigits(int minimumGroupingDigits);
}
public static boolean useGrouping(IProperties properties) {
return properties.getGroupingSize() != IProperties.DEFAULT_GROUPING_SIZE
|| properties.getSecondaryGroupingSize() != IProperties.DEFAULT_SECONDARY_GROUPING_SIZE;
}
public static boolean allowsDecimalPoint(IProperties properties) {
return properties.getDecimalSeparatorAlwaysShown() || properties.getMaximumFractionDigits() != 0;
}
// Properties
private final boolean alwaysShowDecimal;
private final int groupingSize;
private final int secondaryGroupingSize;
private final int minimumGroupingDigits;
// Symbols
private final String infinityString;
private final String nanString;
private final String groupingSeparator;
private final String decimalSeparator;
private final String[] digitStrings;
private final int codePointZero;
public PositiveDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) {
groupingSize =
(properties.getGroupingSize() < 0)
? properties.getSecondaryGroupingSize()
: properties.getGroupingSize();
secondaryGroupingSize =
(properties.getSecondaryGroupingSize() < 0)
? properties.getGroupingSize()
: properties.getSecondaryGroupingSize();
minimumGroupingDigits = properties.getMinimumGroupingDigits();
alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
infinityString = symbols.getInfinity();
nanString = symbols.getNaN();
if (CurrencyFormat.useCurrency(properties)) {
groupingSeparator = symbols.getMonetaryGroupingSeparatorString();
decimalSeparator = symbols.getMonetaryDecimalSeparatorString();
} else {
groupingSeparator = symbols.getGroupingSeparatorString();
decimalSeparator = symbols.getDecimalSeparatorString();
}
// Check to see if we can use code points instead of strings (~15% format performance boost)
int _codePointZero = -1;
String[] _digitStrings = symbols.getDigitStringsLocal();
for (int i = 0; i < _digitStrings.length; i++) {
int cp = Character.codePointAt(_digitStrings[i], 0);
int cc = Character.charCount(cp);
if (cc != _digitStrings[i].length()) {
_codePointZero = -1;
break;
} else if (i == 0) {
_codePointZero = cp;
} else if (cp != _codePointZero + i) {
_codePointZero = -1;
break;
}
}
if (_codePointZero != -1) {
digitStrings = null;
codePointZero = _codePointZero;
} else {
digitStrings = symbols.getDigitStrings(); // makes a copy
codePointZero = -1;
}
}
@Override
public int target(FormatQuantity input, NumberStringBuilder string, int startIndex) {
int length = 0;
if (input.isInfinite()) {
length += string.insert(startIndex, infinityString, NumberFormat.Field.INTEGER);
} else if (input.isNaN()) {
length += string.insert(startIndex, nanString, NumberFormat.Field.INTEGER);
} else {
// Add the integer digits
length += addIntegerDigits(input, string, startIndex);
// Add the decimal point
if (input.getLowerDisplayMagnitude() < 0 || alwaysShowDecimal) {
length += string.insert(startIndex + length, decimalSeparator, NumberFormat.Field.DECIMAL_SEPARATOR);
}
// Add the fraction digits
length += addFractionDigits(input, string, startIndex + length);
}
return length;
}
private int addIntegerDigits(FormatQuantity input, NumberStringBuilder string, int startIndex) {
int length = 0;
int integerCount = input.getUpperDisplayMagnitude() + 1;
for (int i = 0; i < integerCount; i++) {
// Add grouping separator
if (groupingSize > 0 && i == groupingSize && integerCount - i >= minimumGroupingDigits) {
length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR);
} else if (secondaryGroupingSize > 0
&& i > groupingSize
&& (i - groupingSize) % secondaryGroupingSize == 0) {
length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR);
}
// Get and append the next digit value
byte nextDigit = input.getDigit(i);
length += addDigit(nextDigit, string, startIndex, NumberFormat.Field.INTEGER);
}
return length;
}
private int addFractionDigits(FormatQuantity input, NumberStringBuilder string, int index) {
int length = 0;
int fractionCount = -input.getLowerDisplayMagnitude();
for (int i = 0; i < fractionCount; i++) {
// Get and append the next digit value
byte nextDigit = input.getDigit(-i - 1);
length += addDigit(nextDigit, string, index + length, NumberFormat.Field.FRACTION);
}
return length;
}
private int addDigit(byte digit, NumberStringBuilder outputString, int index, Field field) {
if (codePointZero != -1) {
return outputString.insertCodePoint(index, codePointZero + digit, field);
} else {
return outputString.insert(index, digitStrings[digit], field);
}
}
@Override
public void export(Properties properties) {
properties.setDecimalSeparatorAlwaysShown(alwaysShowDecimal);
properties.setGroupingSize(groupingSize);
properties.setSecondaryGroupingSize(secondaryGroupingSize);
properties.setMinimumGroupingDigits(minimumGroupingDigits);
}
}

View File

@ -0,0 +1,256 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.PNAffixGenerator;
import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
import com.ibm.icu.text.DecimalFormatSymbols;
/**
* The implementation of this class is a thin wrapper around {@link PNAffixGenerator}, a utility
* used by this and other classes, including {@link CompactDecimalFormat} and {@link Parse}, to
* efficiently convert from the abstract properties in the property bag to actual prefix and suffix
* strings.
*/
/**
* This class is responsible for adding the positive/negative prefixes and suffixes from the decimal
* format pattern. Properties are set using the following methods:
*
* <ul>
* <li>{@link IProperties#setPositivePrefix(String)}
* <li>{@link IProperties#setPositiveSuffix(String)}
* <li>{@link IProperties#setNegativePrefix(String)}
* <li>{@link IProperties#setNegativeSuffix(String)}
* <li>{@link IProperties#setPositivePrefixPattern(String)}
* <li>{@link IProperties#setPositiveSuffixPattern(String)}
* <li>{@link IProperties#setNegativePrefixPattern(String)}
* <li>{@link IProperties#setNegativeSuffixPattern(String)}
* </ul>
*
* If one of the first four methods is used (those of the form <code>setXxxYyy</code>), the value
* will be interpreted literally. If one of the second four methods is used (those of the form
* <code>setXxxYyyPattern</code>), locale-specific symbols for the plus sign, minus sign, percent
* sign, permille sign, and currency sign will be substituted into the string, according to Unicode
* Technical Standard #35 (LDML) section 3.2.
*
* <p>Literal characters can be used in the <code>setXxxYyyPattern</code> methods by using quotes;
* for example, to display a literal "%" sign, you can set the pattern <code>'%'</code>. To display
* a literal quote, use two quotes in a row, like <code>''</code>.
*
* <p>If a value is set in both a <code>setXxxYyy</code> method and in the corresponding <code>
* setXxxYyyPattern</code> method, the one set in <code>setXxxYyy</code> takes precedence.
*
* <p>For more information on formatting currencies, see {@link CurrencyFormat}.
*
* <p>The parameter is taken by reference by these methods into the property bag, meaning that if a
* mutable object like StringBuilder is passed, changes to the StringBuilder will be reflected in
* the property bag. However, upon creation of a finalized formatter object, all prefixes and
* suffixes will be converted to strings and will stop reflecting changes in the property bag.
*/
public class PositiveNegativeAffixFormat {
public static interface IProperties {
static String DEFAULT_POSITIVE_PREFIX = null;
/** @see #setPositivePrefix */
public String getPositivePrefix();
/**
* Sets the prefix to prepend to positive numbers. The prefix will be interpreted literally. For
* example, if you set a positive prefix of <code>p</code>, then the number 123 will be
* formatted as "p123" in the locale <em>en-US</em>.
*
* <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
*
* @param positivePrefix The CharSequence to prepend to positive numbers.
* @return The property bag, for chaining.
* @see PositiveNegativeAffixFormat
* @see #setPositivePrefixPattern
*/
public IProperties setPositivePrefix(String positivePrefix);
static String DEFAULT_POSITIVE_SUFFIX = null;
/** @see #setPositiveSuffix */
public String getPositiveSuffix();
/**
* Sets the suffix to append to positive numbers. The suffix will be interpreted literally. For
* example, if you set a positive suffix of <code>p</code>, then the number 123 will be
* formatted as "123p" in the locale <em>en-US</em>.
*
* <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
*
* @param positiveSuffix The CharSequence to append to positive numbers.
* @return The property bag, for chaining.
* @see PositiveNegativeAffixFormat
* @see #setPositiveSuffixPattern
*/
public IProperties setPositiveSuffix(String positiveSuffix);
static String DEFAULT_NEGATIVE_PREFIX = null;
/** @see #setNegativePrefix */
public String getNegativePrefix();
/**
* Sets the prefix to prepend to negative numbers. The prefix will be interpreted literally. For
* example, if you set a negative prefix of <code>n</code>, then the number -123 will be
* formatted as "n123" in the locale <em>en-US</em>. Note that if the negative prefix is left unset,
* the locale's minus sign is used.
*
* <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
*
* @param negativePrefix The CharSequence to prepend to negative numbers.
* @return The property bag, for chaining.
* @see PositiveNegativeAffixFormat
* @see #setNegativePrefixPattern
*/
public IProperties setNegativePrefix(String negativePrefix);
static String DEFAULT_NEGATIVE_SUFFIX = null;
/** @see #setNegativeSuffix */
public String getNegativeSuffix();
/**
* Sets the suffix to append to negative numbers. The suffix will be interpreted literally. For
* example, if you set a suffix prefix of <code>n</code>, then the number -123 will be formatted
* as "-123n" in the locale <em>en-US</em>. Note that the minus sign is prepended by default unless
* otherwise specified in either the pattern string or in one of the {@link #setNegativePrefix}
* methods.
*
* <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
*
* @param negativeSuffix The CharSequence to append to negative numbers.
* @return The property bag, for chaining.
* @see PositiveNegativeAffixFormat
* @see #setNegativeSuffixPattern
*/
public IProperties setNegativeSuffix(String negativeSuffix);
static String DEFAULT_POSITIVE_PREFIX_PATTERN = null;
/** @see #setPositivePrefixPattern */
public String getPositivePrefixPattern();
/**
* Sets the prefix to prepend to positive numbers. Locale-specific symbols will be substituted
* into the string according to Unicode Technical Standard #35 (LDML).
*
* <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
*
* @param positivePrefixPattern The CharSequence to prepend to positive numbers after locale
* symbol substitutions take place.
* @return The property bag, for chaining.
* @see PositiveNegativeAffixFormat
* @see #setPositivePrefix
*/
public IProperties setPositivePrefixPattern(String positivePrefixPattern);
static String DEFAULT_POSITIVE_SUFFIX_PATTERN = null;
/** @see #setPositiveSuffixPattern */
public String getPositiveSuffixPattern();
/**
* Sets the suffix to append to positive numbers. Locale-specific symbols will be substituted
* into the string according to Unicode Technical Standard #35 (LDML).
*
* <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
*
* @param positiveSuffixPattern The CharSequence to append to positive numbers after locale
* symbol substitutions take place.
* @return The property bag, for chaining.
* @see PositiveNegativeAffixFormat
* @see #setPositiveSuffix
*/
public IProperties setPositiveSuffixPattern(String positiveSuffixPattern);
static String DEFAULT_NEGATIVE_PREFIX_PATTERN = null;
/** @see #setNegativePrefixPattern */
public String getNegativePrefixPattern();
/**
* Sets the prefix to prepend to negative numbers. Locale-specific symbols will be substituted
* into the string according to Unicode Technical Standard #35 (LDML).
*
* <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
*
* @param negativePrefixPattern The CharSequence to prepend to negative numbers after locale
* symbol substitutions take place.
* @return The property bag, for chaining.
* @see PositiveNegativeAffixFormat
* @see #setNegativePrefix
*/
public IProperties setNegativePrefixPattern(String negativePrefixPattern);
static String DEFAULT_NEGATIVE_SUFFIX_PATTERN = null;
/** @see #setNegativeSuffixPattern */
public String getNegativeSuffixPattern();
/**
* Sets the suffix to append to negative numbers. Locale-specific symbols will be substituted
* into the string according to Unicode Technical Standard #35 (LDML).
*
* <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
*
* @param negativeSuffixPattern The CharSequence to append to negative numbers after locale
* symbol substitutions take place.
* @return The property bag, for chaining.
* @see PositiveNegativeAffixFormat
* @see #setNegativeSuffix
*/
public IProperties setNegativeSuffixPattern(String negativeSuffixPattern);
static boolean DEFAULT_PLUS_SIGN_ALWAYS_SHOWN = false;
/** @see #setPlusSignAlwaysShown */
public boolean getPlusSignAlwaysShown();
/**
* Sets whether to always display of a plus sign on positive numbers.
*
* <p>If the location of the negative sign is specified by the decimal format pattern (or by the
* negative prefix/suffix pattern methods), a plus sign is substituted into that location, in
* accordance with Unicode Technical Standard #35 (LDML) section 3.2.1. Otherwise, the plus sign
* is prepended to the number. For example, if the decimal format pattern <code>#;#-</code> is
* used, then formatting 123 would result in "123+" in the locale <em>en-US</em>.
*
* <p>This method should be used <em>instead of</em> setting the positive prefix/suffix. The
* behavior is undefined if alwaysShowPlusSign is set but the positive prefix/suffix already
* contains a plus sign.
*
* @param plusSignAlwaysShown Whether positive numbers should display a plus sign.
* @return The property bag, for chaining.
*/
public IProperties setPlusSignAlwaysShown(boolean plusSignAlwaysShown);
}
public static PositiveNegativeAffixModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) {
PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties);
return new PositiveNegativeAffixModifier(result.positive, result.negative);
}
// TODO: Investigate static interface methods (Java 8 only?)
public static void apply(
FormatQuantity input,
ModifierHolder mods,
DecimalFormatSymbols symbols,
IProperties properties) {
PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties);
if (input.isNegative()) {
mods.add(result.negative);
} else {
mods.add(result.positive);
}
}
}

View File

@ -0,0 +1,58 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
// THIS CLASS IS A PROOF OF CONCEPT ONLY.
// IT REQUIRES ADDITIONAL DISCUSION ABOUT ITS DESIGN AND IMPLEMENTATION.
package com.ibm.icu.impl.number.formatters;
import java.util.Deque;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.NumberStringBuilder;
public class RangeFormat extends Format {
// Primary settings
private final String separator;
// Child formatters
private final Format left;
private final Format right;
public RangeFormat(Format left, Format right, String separator) {
this.separator = separator; // TODO: This would be loaded from locale data.
this.left = left;
this.right = right;
if (left == null || right == null) {
throw new IllegalArgumentException("Both child formatters are required for RangeFormat");
}
}
@Override
public int process(
Deque<FormatQuantity> inputs,
ModifierHolder mods,
NumberStringBuilder string,
int startIndex) {
ModifierHolder lMods = new ModifierHolder();
ModifierHolder rMods = new ModifierHolder();
int lLen = left.process(inputs, lMods, string, startIndex);
int rLen = right.process(inputs, rMods, string, startIndex + lLen);
// Bubble up any modifiers that are shared between the two sides
while (lMods.peekLast() != null && lMods.peekLast() == rMods.peekLast()) {
mods.add(lMods.removeLast());
rMods.removeLast();
}
// Apply the remaining modifiers
lLen += lMods.applyAll(string, startIndex, startIndex + lLen);
rLen += rMods.applyAll(string, startIndex + lLen, startIndex + lLen + rLen);
int sLen = string.insert(startIndex + lLen, separator, null);
return lLen + sLen + rLen;
}
}

View File

@ -0,0 +1,41 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import com.ibm.icu.impl.number.Rounder;
import com.ibm.icu.impl.number.Rounder.IBasicRoundingProperties;
import com.ibm.icu.impl.number.rounders.IncrementRounder;
import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
import com.ibm.icu.impl.number.rounders.NoRounder;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
// TODO: Figure out a better place to put these methods.
public class RoundingFormat {
public static interface IProperties
extends IBasicRoundingProperties,
IncrementRounder.IProperties,
MagnitudeRounder.IProperties,
SignificantDigitsRounder.IProperties {}
public static Rounder getDefaultOrNoRounder(IProperties properties) {
Rounder candidate = getDefaultOrNull(properties);
if (candidate == null) {
candidate = NoRounder.getInstance(properties);
}
return candidate;
}
public static Rounder getDefaultOrNull(IProperties properties) {
if (SignificantDigitsRounder.useSignificantDigits(properties)) {
return SignificantDigitsRounder.getInstance(properties);
} else if (IncrementRounder.useRoundingIncrement(properties)) {
return IncrementRounder.getInstance(properties);
} else if (MagnitudeRounder.useFractionFormat(properties)) {
return MagnitudeRounder.getInstance(properties);
} else {
return null;
}
}
}

View File

@ -0,0 +1,233 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.FormatQuantitySelector;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.Rounder;
import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
import com.ibm.icu.impl.number.rounders.IncrementRounder;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
public class ScientificFormat extends Format.BeforeFormat implements Rounder.MultiplierGenerator {
public static interface IProperties
extends RoundingFormat.IProperties, CurrencyFormat.IProperties {
static boolean DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN = false;
/** @see #setExponentSignAlwaysShown */
public boolean getExponentSignAlwaysShown();
/**
* Sets whether to show the plus sign in the exponent part of numbers with a zero or positive
* exponent. For example, the number "1200" with the pattern "0.0E0" would be formatted as
* "1.2E+3" instead of "1.2E3" in <em>en-US</em>.
*
* @param exponentSignAlwaysShown Whether to show the plus sign in positive exponents.
* @return The property bag, for chaining.
*/
public IProperties setExponentSignAlwaysShown(boolean exponentSignAlwaysShown);
static int DEFAULT_MINIMUM_EXPONENT_DIGITS = -1;
/** @see #setMinimumExponentDigits */
public int getMinimumExponentDigits();
/**
* Sets the minimum number of digits to display in the exponent. For example, the number "1200"
* with the pattern "0.0E00", which has 2 exponent digits, would be formatted as "1.2E03" in
* <em>en-US</em>.
*
* @param minimumExponentDigits The minimum number of digits to display in the exponent field.
* @return The property bag, for chaining.
*/
public IProperties setMinimumExponentDigits(int minimumExponentDigits);
@Override
public IProperties clone();
}
public static boolean useScientificNotation(IProperties properties) {
return properties.getMinimumExponentDigits() != IProperties.DEFAULT_MINIMUM_EXPONENT_DIGITS;
}
private static final ThreadLocal<Properties> threadLocalProperties =
new ThreadLocal<Properties>() {
@Override
protected Properties initialValue() {
return new Properties();
}
};
public static ScientificFormat getInstance(DecimalFormatSymbols symbols, IProperties properties) {
// If significant digits or rounding interval are specified through normal means, we use those.
// Otherwise, we use the special significant digit rules for scientific notation.
Rounder rounder;
if (IncrementRounder.useRoundingIncrement(properties)) {
rounder = IncrementRounder.getInstance(properties);
} else if (SignificantDigitsRounder.useSignificantDigits(properties)) {
rounder = SignificantDigitsRounder.getInstance(properties);
} else {
Properties rprops = threadLocalProperties.get().clear();
int minInt = properties.getMinimumIntegerDigits();
int maxInt = properties.getMaximumIntegerDigits();
int minFrac = properties.getMinimumFractionDigits();
int maxFrac = properties.getMaximumFractionDigits();
// If currency is in use, pull information from CurrencyUsage.
if (CurrencyFormat.useCurrency(properties)) {
// Use rprops as the vehicle (it is still clean)
CurrencyFormat.populateCurrencyRounderProperties(rprops, symbols, properties);
minFrac = rprops.getMinimumFractionDigits();
maxFrac = rprops.getMaximumFractionDigits();
rprops.clear();
}
// TODO: Mark/Andy, take a look at this logic and see if it makes sense to you.
// I fiddled with the settings and fallbacks to make the unit tests pass, but I
// don't feel that it's the "right way" to do things.
if (minInt < 0) minInt = 0;
if (maxInt < minInt) maxInt = minInt;
if (minFrac < 0) minFrac = 0;
if (maxFrac < minFrac) maxFrac = minFrac;
rprops.setRoundingMode(properties.getRoundingMode());
if (minInt == 0 && maxFrac == 0) {
// Special case for the pattern "#E0" with no significant digits specified.
rprops.setMinimumSignificantDigits(1);
rprops.setMaximumSignificantDigits(Integer.MAX_VALUE);
} else if (minInt == 0 && minFrac == 0) {
// Special case for patterns like "#.##E0" with no significant digits specified.
rprops.setMinimumSignificantDigits(1);
rprops.setMaximumSignificantDigits(1 + maxFrac);
} else {
rprops.setMinimumSignificantDigits(minInt + minFrac);
rprops.setMaximumSignificantDigits(minInt + maxFrac);
}
rprops.setMinimumIntegerDigits(maxInt == 0 ? 0 : Math.max(1, minInt + minFrac - maxFrac));
rprops.setMaximumIntegerDigits(maxInt);
rprops.setMinimumFractionDigits(Math.max(0, minFrac + minInt - maxInt));
rprops.setMaximumFractionDigits(maxFrac);
rounder = SignificantDigitsRounder.getInstance(rprops);
}
return new ScientificFormat(symbols, properties, rounder);
}
public static ScientificFormat getInstance(
DecimalFormatSymbols symbols, IProperties properties, Rounder rounder) {
return new ScientificFormat(symbols, properties, rounder);
}
// Properties
private final boolean exponentShowPlusSign;
private final int exponentDigits;
private final int minInt;
private final int maxInt;
private final int interval;
private final Rounder rounder;
private final ConstantAffixModifier separatorMod;
private final PositiveNegativeAffixModifier signMod;
// Symbols
private final String[] digitStrings;
private ScientificFormat(DecimalFormatSymbols symbols, IProperties properties, Rounder rounder) {
exponentShowPlusSign = properties.getExponentSignAlwaysShown();
exponentDigits = Math.max(1, properties.getMinimumExponentDigits());
int _maxInt = properties.getMaximumIntegerDigits();
int _minInt = properties.getMinimumIntegerDigits();
// Special behavior:
if (_maxInt > 8) {
_maxInt = _minInt;
}
maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt;
minInt = _minInt < 0 ? 0 : _minInt < maxInt ? _minInt : maxInt;
interval = Math.max(1, maxInt);
this.rounder = rounder;
digitStrings = symbols.getDigitStrings(); // makes a copy
separatorMod =
new ConstantAffixModifier(
"", symbols.getExponentSeparator(), NumberFormat.Field.EXPONENT_SYMBOL, true);
signMod =
new PositiveNegativeAffixModifier(
new ConstantAffixModifier(
"",
exponentShowPlusSign ? symbols.getPlusSignString() : "",
NumberFormat.Field.EXPONENT_SIGN,
true),
new ConstantAffixModifier(
"", symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN, true));
}
private static final ThreadLocal<StringBuilder> threadLocalStringBuilder =
new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return new StringBuilder();
}
};
@Override
public void before(FormatQuantity input, ModifierHolder mods) {
// Treat zero as if it had magnitude 0
int exponent;
if (input.isZero()) {
rounder.apply(input);
exponent = 0;
} else {
exponent = -rounder.chooseMultiplierAndApply(input, this);
}
// Format the exponent part of the scientific format.
// Insert digits starting from the left so that append can be used.
// TODO: Use thread locals here.
FormatQuantity exponentQ = FormatQuantitySelector.from(exponent);
StringBuilder exponentSB = threadLocalStringBuilder.get();
exponentSB.setLength(0);
exponentQ.setIntegerFractionLength(exponentDigits, Integer.MAX_VALUE, 0, 0);
for (int i = exponentQ.getUpperDisplayMagnitude(); i >= 0; i--) {
exponentSB.append(digitStrings[exponentQ.getDigit(i)]);
}
// Add modifiers from the outside in.
mods.add(
new ConstantAffixModifier("", exponentSB.toString(), NumberFormat.Field.EXPONENT, true));
mods.add(signMod.getModifier(exponent < 0));
mods.add(separatorMod);
}
@Override
public int getMultiplier(int magnitude) {
int digitsShown = ((magnitude % interval + interval) % interval) + 1;
if (digitsShown < minInt) {
digitsShown = minInt;
} else if (digitsShown > maxInt) {
digitsShown = maxInt;
}
int retval = digitsShown - magnitude - 1;
return retval;
}
@Override
public void export(Properties properties) {
properties.setMinimumExponentDigits(exponentDigits);
properties.setExponentSignAlwaysShown(exponentShowPlusSign);
// Set the transformed object into the property bag. This may result in a pattern string that
// uses different syntax from the original, but it will be functionally equivalent.
rounder.export(properties);
}
}

View File

@ -0,0 +1,48 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.formatters;
import java.util.Deque;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.Properties;
// TODO: This class isn't currently being used anywhere. Consider removing it.
/** Attaches all prefixes and suffixes at this point in the render tree without bubbling up. */
public class StrongAffixFormat extends Format implements Format.AfterFormat {
private final Format child;
public StrongAffixFormat(Format child) {
this.child = child;
if (child == null) {
throw new IllegalArgumentException("A child formatter is required for StrongAffixFormat");
}
}
@Override
public int process(
Deque<FormatQuantity> inputs,
ModifierHolder mods,
NumberStringBuilder string,
int startIndex) {
int length = child.process(inputs, mods, string, startIndex);
length += mods.applyAll(string, startIndex, startIndex + length);
return length;
}
@Override
public int after(
ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) {
return mods.applyAll(string, leftIndex, rightIndex);
}
@Override
public void export(Properties properties) {
// Nothing to do.
}
}

View File

@ -0,0 +1,105 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.Modifier.AffixModifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.NumberFormat.Field;
/** The canonical implementation of {@link Modifier}, containing a prefix and suffix string. */
public class ConstantAffixModifier extends Modifier.BaseModifier implements AffixModifier {
// TODO: Avoid making a new instance by default if prefix and suffix are empty
public static final AffixModifier EMPTY = new ConstantAffixModifier();
private final String prefix;
private final String suffix;
private final Field field;
private final boolean strong;
/**
* Constructs an instance with the given strings.
*
* <p>The arguments need to be Strings, not CharSequences, because Strings are immutable but
* CharSequences are not.
*
* @param prefix The prefix string.
* @param suffix The suffix string.
* @param field The field type to be associated with this modifier. Can be null.
* @param strong Whether this modifier should be strongly applied.
* @see Field
*/
public ConstantAffixModifier(String prefix, String suffix, Field field, boolean strong) {
// Use an empty string instead of null if we are given null
// TODO: Consider returning a null modifier if both prefix and suffix are empty.
this.prefix = (prefix == null ? "" : prefix);
this.suffix = (suffix == null ? "" : suffix);
this.field = field;
this.strong = strong;
}
/**
* Constructs a new instance with an empty prefix, suffix, and field.
*/
public ConstantAffixModifier() {
prefix = "";
suffix = "";
field = null;
strong = false;
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
// Insert the suffix first since inserting the prefix will change the rightIndex
int length = output.insert(rightIndex, suffix, field);
length += output.insert(leftIndex, prefix, field);
return length;
}
@Override
public int length() {
return prefix.length() + suffix.length();
}
@Override
public boolean isStrong() {
return strong;
}
@Override
public String getPrefix() {
return prefix;
}
@Override
public String getSuffix() {
return suffix;
}
public boolean contentEquals(CharSequence _prefix, CharSequence _suffix) {
if (_prefix == null && !prefix.isEmpty()) return false;
if (_suffix == null && !suffix.isEmpty()) return false;
if (prefix.length() != _prefix.length()) return false;
if (suffix.length() != _suffix.length()) return false;
for (int i = 0; i < prefix.length(); i++) {
if (prefix.charAt(i) != _prefix.charAt(i)) return false;
}
for (int i = 0; i < suffix.length(); i++) {
if (suffix.charAt(i) != _suffix.charAt(i)) return false;
}
return true;
}
@Override
public String toString() {
return String.format(
"<ConstantAffixModifier(%d) prefix:'%s' suffix:'%s'>", length(), prefix, suffix);
}
@Override
public void export(Properties properties) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,93 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.Modifier.AffixModifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.NumberFormat.Field;
/**
* An implementation of {@link Modifier} that allows for multiple types of fields in the same
* modifier. Constructed based on the contents of two {@link NumberStringBuilder} instances (one for
* the prefix, one for the suffix).
*/
public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements AffixModifier {
// TODO: Avoid making a new instance by default if prefix and suffix are empty
public static final ConstantMultiFieldModifier EMPTY = new ConstantMultiFieldModifier();
private final char[] prefixChars;
private final char[] suffixChars;
private final Field[] prefixFields;
private final Field[] suffixFields;
private final String prefix;
private final String suffix;
private final boolean strong;
public ConstantMultiFieldModifier(
NumberStringBuilder prefix, NumberStringBuilder suffix, boolean strong) {
prefixChars = prefix.toCharArray();
suffixChars = suffix.toCharArray();
prefixFields = prefix.toFieldArray();
suffixFields = suffix.toFieldArray();
this.prefix = new String(prefixChars);
this.suffix = new String(suffixChars);
this.strong = strong;
}
private ConstantMultiFieldModifier() {
prefixChars = new char[0];
suffixChars = new char[0];
prefixFields = new Field[0];
suffixFields = new Field[0];
prefix = "";
suffix = "";
strong = false;
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
// Insert the suffix first since inserting the prefix will change the rightIndex
int length = output.insert(rightIndex, suffixChars, suffixFields);
length += output.insert(leftIndex, prefixChars, prefixFields);
return length;
}
@Override
public int length() {
return prefixChars.length + suffixChars.length;
}
@Override
public boolean isStrong() {
return strong;
}
@Override
public String getPrefix() {
return prefix;
}
@Override
public String getSuffix() {
return suffix;
}
public boolean contentEquals(NumberStringBuilder prefix, NumberStringBuilder suffix) {
return prefix.contentEquals(prefixChars, prefixFields)
&& suffix.contentEquals(suffixChars, suffixFields);
}
@Override
public String toString() {
return String.format(
"<ConstantMultiFieldModifier(%d) prefix:'%s' suffix:'%s'>", length(), prefix, suffix);
}
@Override
public void export(Properties properties) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,76 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.PluralRules;
// TODO: Is it okay that this class is not completely immutable? Right now it is internal-only.
// Freezable or Builder could be used if necessary.
/**
* A basic implementation of {@link com.ibm.icu.impl.number.Modifier.PositiveNegativePluralModifier}
* that is built on the fly using its <code>put</code> methods.
*/
public class GeneralPluralModifier extends Format.BeforeFormat
implements Modifier.PositiveNegativePluralModifier {
/**
* A single array for modifiers. Even elements are positive; odd elements are negative. The
* elements 2i and 2i+1 belong to the StandardPlural with ordinal i.
*/
private final Modifier[] mods;
public GeneralPluralModifier() {
this.mods = new Modifier[StandardPlural.COUNT * 2];
}
/** Adds a positive/negative-agnostic modifier for the specified plural form. */
public void put(StandardPlural plural, Modifier modifier) {
put(plural, modifier, modifier);
}
/** Adds a positive and a negative modifier for the specified plural form. */
public void put(StandardPlural plural, Modifier positive, Modifier negative) {
assert mods[plural.ordinal() * 2] == null;
assert mods[plural.ordinal() * 2 + 1] == null;
assert positive != null;
assert negative != null;
mods[plural.ordinal() * 2] = positive;
mods[plural.ordinal() * 2 + 1] = negative;
}
@Override
public Modifier getModifier(StandardPlural plural, boolean isNegative) {
Modifier mod = mods[plural.ordinal() * 2 + (isNegative ? 1 : 0)];
if (mod == null) {
mod = mods[StandardPlural.OTHER.ordinal()*2 + (isNegative ? 1 : 0)];
}
if (mod == null) {
throw new UnsupportedOperationException();
}
return mod;
}
@Override
public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
mods.add(getModifier(input.getStandardPlural(rules), input.isNegative()));
}
@Override
public void before(FormatQuantity input, ModifierHolder mods) {
throw new UnsupportedOperationException();
}
@Override
public void export(Properties properties) {
// Since we can export only one affix pair, do the one for "other".
Modifier positive = getModifier(StandardPlural.OTHER, false);
Modifier negative = getModifier(StandardPlural.OTHER, true);
PositiveNegativeAffixModifier.exportPositiveNegative(properties, positive, negative);
}
}

View File

@ -0,0 +1,53 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.Modifier.AffixModifier;
import com.ibm.icu.impl.number.ModifierHolder;
import com.ibm.icu.impl.number.Properties;
/** A class containing a positive form and a negative form of {@link ConstantAffixModifier}. */
public class PositiveNegativeAffixModifier extends Format.BeforeFormat
implements Modifier.PositiveNegativeModifier {
private final AffixModifier positive;
private final AffixModifier negative;
/**
* Constructs an instance using the two {@link ConstantMultiFieldModifier} classes for positive
* and negative.
*
* @param positive The positive-form Modifier.
* @param negative The negative-form Modifier.
*/
public PositiveNegativeAffixModifier(AffixModifier positive, AffixModifier negative) {
this.positive = positive;
this.negative = negative;
}
@Override
public Modifier getModifier(boolean isNegative) {
return isNegative ? negative : positive;
}
@Override
public void before(FormatQuantity input, ModifierHolder mods) {
Modifier mod = getModifier(input.isNegative());
mods.add(mod);
}
@Override
public void export(Properties properties) {
exportPositiveNegative(properties, positive, negative);
}
/** Internal method used to export a positive and negative modifier to a property bag. */
static void exportPositiveNegative(Properties properties, Modifier positive, Modifier negative) {
properties.setPositivePrefix(positive.getPrefix().isEmpty() ? null : positive.getPrefix());
properties.setPositiveSuffix(positive.getSuffix().isEmpty() ? null : positive.getSuffix());
properties.setNegativePrefix(negative.getPrefix().isEmpty() ? null : negative.getPrefix());
properties.setNegativeSuffix(negative.getSuffix().isEmpty() ? null : negative.getSuffix());
}
}

View File

@ -0,0 +1,130 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.modifiers;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.NumberFormat.Field;
/**
* The second primary implementation of {@link Modifier}, this one consuming a {@link
* com.ibm.icu.text.SimpleFormatter} pattern.
*/
public class SimpleModifier extends Modifier.BaseModifier {
private final String compiledPattern;
private final Field field;
private final boolean strong;
/** Creates a modifier that uses the SimpleFormatter string formats. */
public SimpleModifier(String compiledPattern, Field field, boolean strong) {
this.compiledPattern = (compiledPattern == null) ? "\u0001\u0000" : compiledPattern;
this.field = field;
this.strong = strong;
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
return formatAsPrefixSuffix(compiledPattern, output, leftIndex, rightIndex, field);
}
@Override
public int length() {
// TODO: Make a separate method for computing the length only?
return formatAsPrefixSuffix(compiledPattern, null, -1, -1, field);
}
@Override
public boolean isStrong() {
return strong;
}
@Override
public String getPrefix() {
// TODO: Implement this when MeasureFormat is ready.
throw new UnsupportedOperationException();
}
@Override
public String getSuffix() {
// TODO: Implement this when MeasureFormat is ready.
throw new UnsupportedOperationException();
}
/**
* TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is
* because DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it
* should not depend on it.
*
* <p>Formats a value that is already stored inside the StringBuilder <code>result</code> between
* the indices <code>startIndex</code> and <code>endIndex</code> by inserting characters before
* the start index and after the end index.
*
* <p>This is well-defined only for patterns with exactly one argument.
*
* @param compiledPattern Compiled form of a pattern string.
* @param result The StringBuilder containing the value argument.
* @param startIndex The left index of the value within the string builder.
* @param endIndex The right index of the value within the string builder.
* @return The number of characters (UTF-16 code points) that were added to the StringBuilder.
*/
public static int formatAsPrefixSuffix(
String compiledPattern,
NumberStringBuilder result,
int startIndex,
int endIndex,
Field field) {
assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 1;
int ARG_NUM_LIMIT = 0x100;
int length = 0, offset = 2;
if (compiledPattern.charAt(1) != '\u0000') {
int prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT;
if (result != null) {
result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field);
}
length += prefixLength;
offset = 3 + prefixLength;
}
if (offset < compiledPattern.length()) {
int suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT;
if (result != null) {
result.insert(
endIndex + length, compiledPattern, offset + 1, offset + suffixLength + 1, field);
}
length += suffixLength;
}
return length;
}
/** TODO: Move this to a test file somewhere, once we figure out what to do with the method. */
public static void testFormatAsPrefixSuffix() {
String[] patterns = {"{0}", "X{0}Y", "XX{0}YYY", "{0}YY", "XXXX{0}"};
Object[][] outputs = {{"", 0, 0}, {"abcde", 0, 0}, {"abcde", 2, 2}, {"abcde", 1, 3}};
String[][] expecteds = {
{"", "XY", "XXYYY", "YY", "XXXX"},
{"abcde", "XYabcde", "XXYYYabcde", "YYabcde", "XXXXabcde"},
{"abcde", "abXYcde", "abXXYYYcde", "abYYcde", "abXXXXcde"},
{"abcde", "aXbcYde", "aXXbcYYYde", "abcYYde", "aXXXXbcde"}
};
for (int i = 0; i < patterns.length; i++) {
for (int j = 0; j < outputs.length; j++) {
String pattern = patterns[i];
String compiledPattern =
SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, new StringBuilder(), 1, 1);
NumberStringBuilder output = new NumberStringBuilder();
output.append((String) outputs[j][0], null);
formatAsPrefixSuffix(
compiledPattern, output, (Integer) outputs[j][1], (Integer) outputs[j][2], null);
String expected = expecteds[j][i];
String actual = output.toString();
assert expected.equals(actual);
}
}
}
@Override
public void export(Properties properties) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,67 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.rounders;
import java.math.BigDecimal;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.Rounder;
public class IncrementRounder extends Rounder {
public static interface IProperties extends IBasicRoundingProperties {
static BigDecimal DEFAULT_ROUNDING_INCREMENT = null;
/** @see #setRoundingIncrement */
public BigDecimal getRoundingIncrement();
/**
* Sets the increment to which to round numbers. For example, with a rounding interval of 0.05,
* the number 11.17 would be formatted as "11.15" in locale <em>en-US</em> with the default
* rounding mode.
*
* <p>You can use either a rounding increment or significant digits, but not both at the same
* time.
*
* <p>The rounding increment can be specified in a pattern string. For example, the pattern
* "#,##0.05" corresponds to a rounding interval of 0.05 with 1 minimum integer digit and a
* grouping size of 3.
*
* @param roundingIncrement The interval to which to round.
* @return The property bag, for chaining.
*/
public IProperties setRoundingIncrement(BigDecimal roundingIncrement);
}
public static boolean useRoundingIncrement(IProperties properties) {
return properties.getRoundingIncrement() != IProperties.DEFAULT_ROUNDING_INCREMENT;
}
private final BigDecimal roundingIncrement;
public static IncrementRounder getInstance(IProperties properties) {
return new IncrementRounder(properties);
}
private IncrementRounder(IProperties properties) {
super(properties);
if (properties.getRoundingIncrement().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Rounding interval must be greater than zero");
}
roundingIncrement = properties.getRoundingIncrement();
}
@Override
public void apply(FormatQuantity input) {
input.roundToIncrement(roundingIncrement, mathContext);
applyDefaults(input);
}
@Override
public void export(Properties properties) {
super.export(properties);
properties.setRoundingIncrement(roundingIncrement);
}
}

View File

@ -0,0 +1,30 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.rounders;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Rounder;
public class MagnitudeRounder extends Rounder {
public static interface IProperties extends IBasicRoundingProperties {}
public static boolean useFractionFormat(IProperties properties) {
return properties.getMinimumFractionDigits() != IProperties.DEFAULT_MINIMUM_FRACTION_DIGITS
|| properties.getMaximumFractionDigits() != IProperties.DEFAULT_MAXIMUM_FRACTION_DIGITS;
}
public static MagnitudeRounder getInstance(IBasicRoundingProperties properties) {
return new MagnitudeRounder(properties);
}
private MagnitudeRounder(IBasicRoundingProperties properties) {
super(properties);
}
@Override
public void apply(FormatQuantity input) {
input.roundToMagnitude(-maxFrac, mathContext);
applyDefaults(input);
}
}

View File

@ -0,0 +1,24 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.rounders;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Rounder;
/** Sets the integer and fraction length based on the properties, but does not perform rounding. */
public final class NoRounder extends Rounder {
public static NoRounder getInstance(IBasicRoundingProperties properties) {
return new NoRounder(properties);
}
private NoRounder(IBasicRoundingProperties properties) {
super(properties);
}
@Override
public void apply(FormatQuantity input) {
applyDefaults(input);
input.roundToInfinity();
}
}

View File

@ -0,0 +1,210 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.rounders;
import java.math.RoundingMode;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.Rounder;
public class SignificantDigitsRounder extends Rounder {
public static enum SignificantDigitsMode {
OVERRIDE_MAXIMUM_FRACTION,
RESPECT_MAXIMUM_FRACTION,
ENSURE_MINIMUM_SIGNIFICANT
};
public static interface IProperties extends IBasicRoundingProperties {
static int DEFAULT_MINIMUM_SIGNIFICANT_DIGITS = -1;
/** @see #setMinimumSignificantDigits */
public int getMinimumSignificantDigits();
/**
* Sets the minimum number of significant digits to display. If, after rounding to the number of
* significant digits specified by {@link #setMaximumSignificantDigits}, the number of remaining
* significant digits is less than the minimum, the number will be padded with zeros. For
* example, if minimum significant digits is 3, the number 5.8 will be formatted as "5.80" in
* locale <em>en-US</em>. Note that minimum significant digits is relevant only when numbers
* have digits after the decimal point.
*
* <p>If both minimum significant digits and minimum integer/fraction digits are set at the same
* time, both values will be respected, and the one that results in the greater number of
* padding zeros will be used. For example, formatting the number 73 with 3 minimum significant
* digits and 2 minimum fraction digits will produce "73.00".
*
* <p>The number of significant digits can be specified in a pattern string using the '@'
* character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3
* significant digits.
*
* @param minimumSignificantDigits The minimum number of significant digits to display.
* @return The property bag, for chaining.
*/
public IProperties setMinimumSignificantDigits(int minimumSignificantDigits);
static int DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = -1;
/** @see #setMaximumSignificantDigits */
public int getMaximumSignificantDigits();
/**
* Sets the maximum number of significant digits to display. The number of significant digits is
* equal to the number of digits counted from the leftmost nonzero digit through the rightmost
* nonzero digit; for example, the number "2010" has 3 significant digits. If the number has
* more significant digits than specified here, the extra significant digits will be rounded off
* using the rounding mode specified by {@link #setRoundingMode(RoundingMode)}. For example, if
* maximum significant digits is 3, the number 1234.56 will be formatted as "1230" in locale
* <em>en-US</em> with the default rounding mode.
*
* <p>If both maximum significant digits and maximum integer/fraction digits are set at the same
* time, the behavior is undefined.
*
* <p>The number of significant digits can be specified in a pattern string using the '@'
* character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3
* significant digits.
*
* @param maximumSignificantDigits The maximum number of significant digits to display.
* @return The property bag, for chaining.
*/
public IProperties setMaximumSignificantDigits(int maximumSignificantDigits);
static SignificantDigitsMode DEFAULT_SIGNIFICANT_DIGITS_MODE = null;
/** @see #setSignificantDigitsMode */
public SignificantDigitsMode getSignificantDigitsMode();
/**
* Sets the strategy used when reconciling significant digits versus integer and fraction
* lengths.
*
* @param significantDigitsMode One of the options from {@link SignificantDigitsMode}.
* @return The property bag, for chaining.
*/
public IProperties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode);
}
public static boolean useSignificantDigits(IProperties properties) {
return properties.getMinimumSignificantDigits()
!= IProperties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS
|| properties.getMaximumSignificantDigits()
!= IProperties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS
|| properties.getSignificantDigitsMode() != IProperties.DEFAULT_SIGNIFICANT_DIGITS_MODE;
}
public static SignificantDigitsRounder getInstance(IProperties properties) {
return new SignificantDigitsRounder(properties);
}
private final int minSig;
private final int maxSig;
private final SignificantDigitsMode mode;
private SignificantDigitsRounder(IProperties properties) {
super(properties);
int _minSig = properties.getMinimumSignificantDigits();
int _maxSig = properties.getMaximumSignificantDigits();
minSig = _minSig < 1 ? 1 : _minSig > 1000 ? 1000 : _minSig;
maxSig = _maxSig < 0 ? 1000 : _maxSig < minSig ? minSig : _maxSig > 1000 ? 1000 : _maxSig;
SignificantDigitsMode _mode = properties.getSignificantDigitsMode();
mode = _mode == null ? SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION : _mode;
}
@Override
public void apply(FormatQuantity input) {
int magnitude, effectiveMag, magMinSig, magMaxSig;
if (input.isZero()) {
// Treat zero as if magnitude corresponded to the minimum number of zeros
magnitude = minInt - 1;
} else {
magnitude = input.getMagnitude();
}
effectiveMag = Math.min(magnitude + 1, maxInt);
magMinSig = effectiveMag - minSig;
magMaxSig = effectiveMag - maxSig;
// Step 1: pick the rounding magnitude and apply.
int roundingMagnitude;
switch (mode) {
case OVERRIDE_MAXIMUM_FRACTION:
// Always round to maxSig.
// Of the six possible orders:
// Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
// Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
// Case 3: minSig, minFrac, maxFrac, maxSig -- maxSig wins
// Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
// Case 5: minFrac, minSig, maxFrac, maxSig -- maxSig wins
// Case 6: minFrac, maxFrac, minSig, maxSig -- maxSig wins
roundingMagnitude = magMaxSig;
break;
case RESPECT_MAXIMUM_FRACTION:
// Round to the strongest of maxFrac, maxInt, and maxSig.
// Of the six possible orders:
// Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
// Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
// Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default
// Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
// Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default
// Case 6: minFrac, maxFrac, minSig, maxSig -- maxFrac wins --> differs from default
//
// Math.max() picks the rounding magnitude farthest to the left (most significant).
// Math.min() picks the rounding magnitude farthest to the right (least significant).
roundingMagnitude = Math.max(-maxFrac, magMaxSig);
break;
case ENSURE_MINIMUM_SIGNIFICANT:
// Round to the strongest of maxFrac and maxSig, and always ensure minSig.
// Of the six possible orders:
// Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
// Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
// Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default
// Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
// Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default
// Case 6: minFrac, maxFrac, minSig, maxSig -- minSig wins --> differs from default
roundingMagnitude = Math.min(magMinSig, Math.max(-maxFrac, magMaxSig));
break;
default:
throw new AssertionError();
}
input.roundToMagnitude(roundingMagnitude, mathContext);
// In case magnitude changed:
if (input.isZero()) {
magnitude = minInt - 1;
} else {
magnitude = input.getMagnitude();
}
effectiveMag = Math.min(magnitude + 1, maxInt);
magMinSig = effectiveMag - minSig;
magMaxSig = effectiveMag - maxSig;
// Step 2: pick the number of visible digits.
switch (mode) {
case OVERRIDE_MAXIMUM_FRACTION:
// Ensure minSig is always displayed.
input.setIntegerFractionLength(
minInt, maxInt, Math.max(minFrac, -magMinSig), Integer.MAX_VALUE);
break;
case RESPECT_MAXIMUM_FRACTION:
// Ensure minSig is displayed, unless doing so is in violation of maxFrac.
input.setIntegerFractionLength(
minInt, maxInt, Math.min(maxFrac, Math.max(minFrac, -magMinSig)), maxFrac);
break;
case ENSURE_MINIMUM_SIGNIFICANT:
// Follow minInt/minFrac, but ensure all digits are allowed to be visible.
input.setIntegerFractionLength(minInt, maxInt, minFrac, Integer.MAX_VALUE);
break;
}
}
@Override
public void export(Properties properties) {
super.export(properties);
properties.setMinimumSignificantDigits(minSig);
properties.setMaximumSignificantDigits(maxSig);
properties.setSignificantDigitsMode(mode);
}
}

View File

@ -1,524 +0,0 @@
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
*******************************************************************************
* Copyright (C) 2012-2016, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.text;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import com.ibm.icu.impl.ICUCache;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.text.DecimalFormat.Unit;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* A cache containing data by locale for {@link CompactDecimalFormat}
*
* @author Travis Keep
*/
class CompactDecimalDataCache {
private static final String SHORT_STYLE = "short";
private static final String LONG_STYLE = "long";
private static final String SHORT_CURRENCY_STYLE = "shortCurrency";
private static final String NUMBER_ELEMENTS = "NumberElements";
private static final String PATTERNS_LONG = "patternsLong";
private static final String PATTERNS_SHORT = "patternsShort";
private static final String DECIMAL_FORMAT = "decimalFormat";
private static final String CURRENCY_FORMAT = "currencyFormat";
private static final String LATIN_NUMBERING_SYSTEM = "latn";
private static enum PatternsTableKey { PATTERNS_LONG, PATTERNS_SHORT };
private static enum FormatsTableKey { DECIMAL_FORMAT, CURRENCY_FORMAT };
public static final String OTHER = "other";
/**
* We can specify prefixes or suffixes for values with up to 15 digits,
* less than 10^15.
*/
static final int MAX_DIGITS = 15;
private final ICUCache<ULocale, DataBundle> cache =
new SimpleCache<ULocale, DataBundle>();
/**
* Data contains the compact decimal data for a particular locale. Data consists
* of one array and two hashmaps. The index of the divisors array as well
* as the arrays stored in the values of the two hashmaps correspond
* to log10 of the number being formatted, so when formatting 12,345, the 4th
* index of the arrays should be used. Divisors contain the number to divide
* by before doing formatting. In the case of english, <code>divisors[4]</code>
* is 1000. So to format 12,345, divide by 1000 to get 12. Then use
* PluralRules with the current locale to figure out which of the 6 plural variants
* 12 matches: "zero", "one", "two", "few", "many", or "other." Prefixes and
* suffixes are maps whose key is the plural variant and whose values are
* arrays of strings with indexes corresponding to log10 of the original number.
* these arrays contain the prefix or suffix to use.
*
* Each array in data is 15 in length, and every index is filled.
*
* @author Travis Keep
*
*/
static class Data {
long[] divisors;
Map<String, DecimalFormat.Unit[]> units;
boolean fromFallback;
Data(long[] divisors, Map<String, DecimalFormat.Unit[]> units)
{
this.divisors = divisors;
this.units = units;
}
public boolean isEmpty() {
return units == null || units.isEmpty();
}
}
/**
* DataBundle contains compact decimal data for all the styles in a particular
* locale. Currently available styles are short and long for decimals, and
* short only for currencies.
*
* @author Travis Keep
*/
static class DataBundle {
Data shortData;
Data longData;
Data shortCurrencyData;
private DataBundle(Data shortData, Data longData, Data shortCurrencyData) {
this.shortData = shortData;
this.longData = longData;
this.shortCurrencyData = shortCurrencyData;
}
private static DataBundle createEmpty() {
return new DataBundle(
new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>())
);
}
}
/**
* Sink for enumerating all of the compact decimal format patterns.
*
* More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
* Only store a value if it is still missing, that is, it has not been overridden.
*/
private static final class CompactDecimalDataSink extends UResource.Sink {
private DataBundle dataBundle; // Where to save values when they are read
private ULocale locale; // The locale we are traversing (for exception messages)
private boolean isLatin; // Whether or not we are traversing the Latin table
private boolean isFallback; // Whether or not we are traversing the Latin table as fallback
/*
* NumberElements{ <-- top (numbering system table)
* latn{ <-- patternsTable (one per numbering system)
* patternsLong{ <-- formatsTable (one per pattern)
* decimalFormat{ <-- powersOfTenTable (one per format)
* 1000{ <-- pluralVariantsTable (one per power of ten)
* one{"0 thousand"} <-- plural variant and template
*/
public CompactDecimalDataSink(DataBundle dataBundle, ULocale locale) {
this.dataBundle = dataBundle;
this.locale = locale;
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
// SPECIAL CASE: Don't consume root in the non-Latin numbering system
if (isRoot && !isLatin) { return; }
UResource.Table patternsTable = value.getTable();
for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
// patterns table: check for patternsShort or patternsLong
PatternsTableKey patternsTableKey;
if (key.contentEquals(PATTERNS_SHORT)) {
patternsTableKey = PatternsTableKey.PATTERNS_SHORT;
} else if (key.contentEquals(PATTERNS_LONG)) {
patternsTableKey = PatternsTableKey.PATTERNS_LONG;
} else {
continue;
}
// traverse into the table of formats
UResource.Table formatsTable = value.getTable();
for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
// formats table: check for decimalFormat or currencyFormat
FormatsTableKey formatsTableKey;
if (key.contentEquals(DECIMAL_FORMAT)) {
formatsTableKey = FormatsTableKey.DECIMAL_FORMAT;
} else if (key.contentEquals(CURRENCY_FORMAT)) {
formatsTableKey = FormatsTableKey.CURRENCY_FORMAT;
} else {
continue;
}
// Set the current style and destination based on the lvl1 and lvl2 keys
String style = null;
Data destination = null;
if (patternsTableKey == PatternsTableKey.PATTERNS_LONG
&& formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
style = LONG_STYLE;
destination = dataBundle.longData;
} else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
&& formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
style = SHORT_STYLE;
destination = dataBundle.shortData;
} else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
&& formatsTableKey == FormatsTableKey.CURRENCY_FORMAT) {
style = SHORT_CURRENCY_STYLE;
destination = dataBundle.shortCurrencyData;
} else {
// Silently ignore this case
continue;
}
// SPECIAL CASE: RULES FOR WHETHER OR NOT TO CONSUME THIS TABLE:
// 1) Don't consume longData if shortData was consumed from the non-Latin
// locale numbering system
// 2) Don't consume longData for the first time if this is the root bundle and
// shortData is already populated from a more specific locale. Note that if
// both longData and shortData are both only in root, longData will be
// consumed since it is alphabetically before shortData in the bundle.
if (isFallback
&& style == LONG_STYLE
&& !dataBundle.shortData.isEmpty()
&& !dataBundle.shortData.fromFallback) {
continue;
}
if (isRoot
&& style == LONG_STYLE
&& dataBundle.longData.isEmpty()
&& !dataBundle.shortData.isEmpty()) {
continue;
}
// Set the "fromFallback" flag on the data object
destination.fromFallback = isFallback;
// traverse into the table of powers of ten
UResource.Table powersOfTenTable = value.getTable();
for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
// This value will always be some even power of 10. e.g 10000.
long power10 = Long.parseLong(key.toString());
int log10Value = (int) Math.log10(power10);
// Silently ignore divisors that are too big.
if (log10Value >= MAX_DIGITS) continue;
// Iterate over the plural variants ("one", "other", etc)
UResource.Table pluralVariantsTable = value.getTable();
for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
// TODO: Use StandardPlural rather than String.
String pluralVariant = key.toString();
String template = value.toString();
// Copy the data into the in-memory data bundle (do not overwrite
// existing values)
int numZeros = populatePrefixSuffix(
pluralVariant, log10Value, template, locale, style, destination, false);
// If populatePrefixSuffix returns -1, it means that this key has been
// encountered already.
if (numZeros < 0) {
continue;
}
// Set the divisor, which is based on the number of zeros in the template
// string. If the divisor from here is different from the one previously
// stored, it means that the number of zeros in different plural variants
// differs; throw an exception.
long divisor = calculateDivisor(power10, numZeros);
if (destination.divisors[log10Value] != 0L
&& destination.divisors[log10Value] != divisor) {
throw new IllegalArgumentException("Plural variant '" + pluralVariant
+ "' template '" + template
+ "' for 10^" + log10Value
+ " has wrong number of zeros in " + localeAndStyle(locale, style));
}
destination.divisors[log10Value] = divisor;
}
}
}
}
}
}
/**
* Fetch data for a particular locale. Clients must not modify any part of the returned data. Portions of returned
* data may be shared so modifying it will have unpredictable results.
*/
DataBundle get(ULocale locale) {
DataBundle result = cache.get(locale);
if (result == null) {
result = load(locale);
cache.put(locale, result);
}
return result;
}
private static DataBundle load(ULocale ulocale) throws MissingResourceException {
DataBundle dataBundle = DataBundle.createEmpty();
String nsName = NumberingSystem.getInstance(ulocale).getName();
ICUResourceBundle r = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
ulocale);
CompactDecimalDataSink sink = new CompactDecimalDataSink(dataBundle, ulocale);
sink.isFallback = false;
// First load the number elements data from nsName if nsName is not Latin.
if (!nsName.equals(LATIN_NUMBERING_SYSTEM)) {
sink.isLatin = false;
try {
r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + nsName, sink);
} catch (MissingResourceException e) {
// Silently ignore and use Latin
}
// Set the "isFallback" flag for when we read Latin
sink.isFallback = true;
}
// Now load Latin, which will fill in things that were left out from above.
sink.isLatin = true;
r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + LATIN_NUMBERING_SYSTEM, sink);
// If longData is empty, default it to be equal to shortData
if (dataBundle.longData.isEmpty()) {
dataBundle.longData = dataBundle.shortData;
}
// Check for "other" variants in each of the three data classes
checkForOtherVariants(dataBundle.longData, ulocale, LONG_STYLE);
checkForOtherVariants(dataBundle.shortData, ulocale, SHORT_STYLE);
checkForOtherVariants(dataBundle.shortCurrencyData, ulocale, SHORT_CURRENCY_STYLE);
// Resolve missing elements
fillInMissing(dataBundle.longData);
fillInMissing(dataBundle.shortData);
fillInMissing(dataBundle.shortCurrencyData);
// Return the data bundle
return dataBundle;
}
/**
* Populates prefix and suffix information for a particular plural variant
* and index (log10 value).
* @param pluralVariant e.g "one", "other"
* @param idx the index (log10 value of the number) 0 <= idx < MAX_DIGITS
* @param template e.g "00K"
* @param locale the locale
* @param style the style
* @param destination Extracted prefix and suffix stored here.
* @return number of zeros found before any decimal point in template, or -1 if it was not saved.
*/
private static int populatePrefixSuffix(
String pluralVariant, int idx, String template, ULocale locale, String style,
Data destination, boolean overwrite) {
int firstIdx = template.indexOf("0");
int lastIdx = template.lastIndexOf("0");
if (firstIdx == -1) {
throw new IllegalArgumentException(
"Expect at least one zero in template '" + template +
"' for variant '" +pluralVariant + "' for 10^" + idx +
" in " + localeAndStyle(locale, style));
}
String prefix = template.substring(0, firstIdx);
String suffix = template.substring(lastIdx + 1);
// Save the unit, and return -1 if it was not saved
boolean saved = saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, destination.units, overwrite);
if (!saved) {
return -1;
}
// If there is effectively no prefix or suffix, ignore the actual
// number of 0's and act as if the number of 0's matches the size
// of the number
if (prefix.trim().length() == 0 && suffix.trim().length() == 0) {
return idx + 1;
}
// Calculate number of zeros before decimal point.
int i = firstIdx + 1;
while (i <= lastIdx && template.charAt(i) == '0') {
i++;
}
return i - firstIdx;
}
/**
* Calculate a divisor based on the magnitude and number of zeros in the
* template string.
* @param power10
* @param numZeros
* @return
*/
private static long calculateDivisor(long power10, int numZeros) {
// We craft our divisor such that when we divide by it, we get a
// number with the same number of digits as zeros found in the
// plural variant templates. If our magnitude is 10000 and we have
// two 0's in our plural variants, then we want a divisor of 1000.
// Note that if we have 43560 which is of same magnitude as 10000.
// When we divide by 1000 we a quotient which rounds to 44 (2 digits)
long divisor = power10;
for (int i = 1; i < numZeros; i++) {
divisor /= 10;
}
return divisor;
}
/**
* Returns locale and style. Used to form useful messages in thrown exceptions.
*
* Note: This is not covered by unit tests since no exceptions are thrown on the default CLDR data. It is too
* cumbersome to cover via reflection.
*
* @param locale the locale
* @param style the style
*/
private static String localeAndStyle(ULocale locale, String style) {
return "locale '" + locale + "' style '" + style + "'";
}
/**
* Checks to make sure that an "other" variant is present in all powers of 10.
* @param data
*/
private static void checkForOtherVariants(Data data, ULocale locale, String style) {
DecimalFormat.Unit[] otherByBase = data.units.get(OTHER);
if (otherByBase == null) {
throw new IllegalArgumentException("No 'other' plural variants defined in "
+ localeAndStyle(locale, style));
}
// Check all other plural variants, and make sure that if any of them are populated, then
// other is also populated
for (Map.Entry<String, Unit[]> entry : data.units.entrySet()) {
if (entry.getKey() == OTHER) continue;
DecimalFormat.Unit[] variantByBase = entry.getValue();
for (int log10Value = 0; log10Value < MAX_DIGITS; log10Value++) {
if (variantByBase[log10Value] != null && otherByBase[log10Value] == null) {
throw new IllegalArgumentException(
"No 'other' plural variant defined for 10^" + log10Value
+ " but a '" + entry.getKey() + "' variant is defined"
+ " in " +localeAndStyle(locale, style));
}
}
}
}
/**
* After reading information from resource bundle into a Data object, there
* is guarantee that it is complete.
*
* This method fixes any incomplete data it finds within <code>result</code>.
* It looks at each log10 value applying the two rules.
* <p>
* If no prefix is defined for the "other" variant, use the divisor, prefixes and
* suffixes for all defined variants from the previous log10. For log10 = 0,
* use all empty prefixes and suffixes and a divisor of 1.
* </p><p>
* Otherwise, examine each plural variant defined for the given log10 value.
* If it has no prefix and suffix for a particular variant, use the one from the
* "other" variant.
* </p>
*
* @param result this instance is fixed in-place.
*/
private static void fillInMissing(Data result) {
// Initially we assume that previous divisor is 1 with no prefix or suffix.
long lastDivisor = 1L;
for (int i = 0; i < result.divisors.length; i++) {
if (result.units.get(OTHER)[i] == null) {
result.divisors[i] = lastDivisor;
copyFromPreviousIndex(i, result.units);
} else {
lastDivisor = result.divisors[i];
propagateOtherToMissing(i, result.units);
}
}
}
private static void propagateOtherToMissing(
int idx, Map<String, DecimalFormat.Unit[]> units) {
DecimalFormat.Unit otherVariantValue = units.get(OTHER)[idx];
for (DecimalFormat.Unit[] byBase : units.values()) {
if (byBase[idx] == null) {
byBase[idx] = otherVariantValue;
}
}
}
private static void copyFromPreviousIndex(int idx, Map<String, DecimalFormat.Unit[]> units) {
for (DecimalFormat.Unit[] byBase : units.values()) {
if (idx == 0) {
byBase[idx] = DecimalFormat.NULL_UNIT;
} else {
byBase[idx] = byBase[idx - 1];
}
}
}
private static boolean saveUnit(
DecimalFormat.Unit unit, String pluralVariant, int idx,
Map<String, DecimalFormat.Unit[]> units,
boolean overwrite) {
DecimalFormat.Unit[] byBase = units.get(pluralVariant);
if (byBase == null) {
byBase = new DecimalFormat.Unit[MAX_DIGITS];
units.put(pluralVariant, byBase);
}
// Don't overwrite a pre-existing value unless the "overwrite" flag is true.
if (!overwrite && byBase[idx] != null) {
return false;
}
// Save the value and return
byBase[idx] = unit;
return true;
}
/**
* Fetches a prefix or suffix given a plural variant and log10 value. If it
* can't find the given variant, it falls back to "other".
* @param prefixOrSuffix the prefix or suffix map
* @param variant the plural variant
* @param base log10 value. 0 <= base < MAX_DIGITS.
* @return the prefix or suffix.
*/
static DecimalFormat.Unit getUnit(
Map<String, DecimalFormat.Unit[]> units, String variant, int base) {
DecimalFormat.Unit[] byBase = units.get(variant);
if (byBase == null) {
byBase = units.get(CompactDecimalDataCache.OTHER);
}
return byBase[base];
}
}

View File

@ -18,44 +18,37 @@ import java.math.BigInteger;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import com.ibm.icu.text.CompactDecimalDataCache.Data;
import com.ibm.icu.text.PluralRules.FixedDecimal;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.Output;
import com.ibm.icu.impl.number.FormatQuantity4;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.util.ULocale;
/**
* The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will limited real estate.
* For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will be appropriate for the given language,
* such as "1,2 Mrd." for German.
* <p>
* For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be short for supported
* languages. However, the result may sometimes exceed 7 characters, such as when there are combining marks or thin
* characters. In such cases, the visual width in fonts should still be short.
* <p>
* By default, there are 2 significant digits. After creation, if more than three significant digits are set (with
* setMaximumSignificantDigits), or if a fixed number of digits are set (with setMaximumIntegerDigits or
* setMaximumFractionDigits), then result may be wider.
* <p>
* The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of "$1,200,000.00" (English) or
* "5,3 Mio. €" instead of "5.300.000,00 €" (German). Localized data concerning longer formats is not available yet in
* the Unicode CLDR. Because of this, attempting to format a currency amount using the "long" style will produce
* an UnsupportedOperationException.
* The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will
* limited real estate. For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will
* be appropriate for the given language, such as "1,2 Mrd." for German.
*
* At this time, negative numbers and parsing are not supported, and will produce an UnsupportedOperationException.
* Resetting the pattern prefixes or suffixes is not supported; the method calls are ignored.
* <p>
* Note that important methods, like setting the number of decimals, will be moved up from DecimalFormat to
* NumberFormat.
* <p>For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be
* short for supported languages. However, the result may sometimes exceed 7 characters, such as
* when there are combining marks or thin characters. In such cases, the visual width in fonts
* should still be short.
*
* <p>By default, there are 2 significant digits. After creation, if more than three significant
* digits are set (with setMaximumSignificantDigits), or if a fixed number of digits are set (with
* setMaximumIntegerDigits or setMaximumFractionDigits), then result may be wider.
*
* <p>The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of
* "$1,200,000.00" (English) or "5,3 Mio. €" instead of "5.300.000,00 €" (German). Localized data
* concerning longer formats is not available yet in the Unicode CLDR. Because of this, attempting
* to format a currency amount using the "long" style will produce an UnsupportedOperationException.
*
* <p>At this time, negative numbers and parsing are not supported, and will produce an
* UnsupportedOperationException. Resetting the pattern prefixes or suffixes is not supported; the
* method calls are ignored.
*
* <p>Note that important methods, like setting the number of decimals, will be moved up from
* DecimalFormat to NumberFormat.
*
* @author markdavis
* @stable ICU 49
@ -64,39 +57,29 @@ public class CompactDecimalFormat extends DecimalFormat {
private static final long serialVersionUID = 4716293295276629682L;
// private static final int POSITIVE_PREFIX = 0, POSITIVE_SUFFIX = 1, AFFIX_SIZE = 2;
private static final CompactDecimalDataCache cache = new CompactDecimalDataCache();
private final Map<String, DecimalFormat.Unit[]> units;
private final Map<String, DecimalFormat.Unit[]> currencyUnits;
private final long[] divisor;
private final long[] currencyDivisor;
private final Map<String, Unit> pluralToCurrencyAffixes;
private CompactStyle style;
// null if created internally using explicit prefixes and suffixes.
private final PluralRules pluralRules;
/**
* Style parameter for CompactDecimalFormat.
*
* @stable ICU 50
*/
public enum CompactStyle {
/**
* Short version, like "1.2T"
*
* @stable ICU 50
*/
SHORT,
/**
* Longer version, like "1.2 trillion", if available. May return same result as SHORT if not.
*
* @stable ICU 50
*/
LONG
}
/**
* Create a CompactDecimalFormat appropriate for a locale. The result may
* be affected by the number system in the locale, such as ar-u-nu-latn.
* Create a CompactDecimalFormat appropriate for a locale. The result may be affected by the
* number system in the locale, such as ar-u-nu-latn.
*
* @param locale the desired locale
* @param style the compact style
@ -107,8 +90,8 @@ public class CompactDecimalFormat extends DecimalFormat {
}
/**
* Create a CompactDecimalFormat appropriate for a locale. The result may
* be affected by the number system in the locale, such as ar-u-nu-latn.
* Create a CompactDecimalFormat appropriate for a locale. The result may be affected by the
* number system in the locale, such as ar-u-nu-latn.
*
* @param locale the desired locale
* @param style the compact style
@ -121,201 +104,130 @@ public class CompactDecimalFormat extends DecimalFormat {
/**
* The public mechanism is CompactDecimalFormat.getInstance().
*
* @param locale
* the desired locale
* @param style
* the compact style
* @param locale the desired locale
* @param style the compact style
*/
CompactDecimalFormat(ULocale locale, CompactStyle style) {
this.pluralRules = PluralRules.forLocale(locale);
DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(locale);
CompactDecimalDataCache.Data data = getData(locale, style);
CompactDecimalDataCache.Data currencyData = getCurrencyData(locale);
this.units = data.units;
this.divisor = data.divisors;
this.currencyUnits = currencyData.units;
this.currencyDivisor = currencyData.divisors;
this.style = style;
pluralToCurrencyAffixes = null;
// DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
// // TODO fix to use plural-dependent affixes
// Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix());
// pluralToCurrencyAffixes = new HashMap<String,Unit>();
// for (String key : pluralRules.getKeywords()) {
// pluralToCurrencyAffixes.put(key, currency);
// }
// // TODO fix to get right symbol for the count
finishInit(style, format.toPattern(), format.getDecimalFormatSymbols());
}
/**
* Create a short number "from scratch". Intended for internal use. The prefix, suffix, and divisor arrays are
* parallel, and provide the information for each power of 10. When formatting a value, the correct power of 10 is
* found, then the value is divided by the divisor, and the prefix and suffix are set (using
* setPositivePrefix/Suffix).
*
* @param pattern
* A number format pattern. Note that the prefix and suffix are discarded, and the decimals are
* overridden by default.
* @param formatSymbols
* Decimal format symbols, typically from a locale.
* @param style
* compact style.
* @param divisor
* An array of prefix values, one for each power of 10 from 0 to 14
* @param pluralAffixes
* A map from plural categories to affixes.
* @param currencyAffixes
* A map from plural categories to currency affixes.
* @param debugCreationErrors
* A collection of strings for debugging. If null on input, then any errors found will be added to that
* collection instead of throwing exceptions.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols,
CompactStyle style, PluralRules pluralRules,
long[] divisor, Map<String,String[][]> pluralAffixes, Map<String, String[]> currencyAffixes,
Collection<String> debugCreationErrors) {
this.pluralRules = pluralRules;
this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
this.currencyUnits = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
if (!pluralRules.getKeywords().equals(this.units.keySet())) {
debugCreationErrors.add("Missmatch in pluralCategories, should be: " + pluralRules.getKeywords() + ", was actually " + this.units.keySet());
}
this.divisor = divisor.clone();
this.currencyDivisor = divisor.clone();
if (currencyAffixes == null) {
pluralToCurrencyAffixes = null;
} else {
pluralToCurrencyAffixes = new HashMap<String,Unit>();
for (Entry<String, String[]> s : currencyAffixes.entrySet()) {
String[] pair = s.getValue();
pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1]));
}
}
finishInit(style, pattern, formatSymbols);
}
private void finishInit(CompactStyle style, String pattern, DecimalFormatSymbols formatSymbols) {
applyPattern(pattern);
setDecimalFormatSymbols(formatSymbols);
setMaximumSignificantDigits(2); // default significant digits
setSignificantDigitsUsed(true);
// Use the locale's default pattern
String pattern = getPattern(locale, 0);
symbols = DecimalFormatSymbols.getInstance(locale);
properties = new Properties();
properties.setCompactStyle(style);
exportedProperties = new Properties();
setPropertiesFromPattern(pattern, true);
if (style == CompactStyle.SHORT) {
setGroupingUsed(false);
// TODO: This was setGroupingUsed(false) in ICU 58. Is it okay that I changed it for ICU 59?
properties.setMinimumGroupingDigits(2);
}
setCurrency(null);
refreshFormatter();
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (!super.equals(obj))
return false; // super does class check
CompactDecimalFormat other = (CompactDecimalFormat) obj;
return mapsAreEqual(units, other.units)
&& Arrays.equals(divisor, other.divisor)
&& (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes
|| pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes))
&& pluralRules.equals(other.pluralRules);
}
private boolean mapsAreEqual(
Map<String, DecimalFormat.Unit[]> lhs, Map<String, DecimalFormat.Unit[]> rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
// For each MapEntry in lhs, see if there is a matching one in rhs.
for (Map.Entry<String, DecimalFormat.Unit[]> entry : lhs.entrySet()) {
DecimalFormat.Unit[] value = rhs.get(entry.getKey());
if (value == null || !Arrays.equals(entry.getValue(), value)) {
return false;
}
}
return true;
return super.equals(obj);
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
return format(number, null, toAppendTo, pos);
FormatQuantity4 fq = new FormatQuantity4(number);
formatter.format(fq, toAppendTo, pos);
fq.populateUFieldPosition(pos);
return toAppendTo;
}
/**
* {@inheritDoc}
*
* @stable ICU 50
*/
@Override
public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
if (!(obj instanceof Number)) {
throw new IllegalArgumentException();
}
if (!(obj instanceof Number)) throw new IllegalArgumentException();
Number number = (Number) obj;
Amount amount = toAmount(number.doubleValue(), null, null);
return super.formatToCharacterIterator(amount.getQty(), amount.getUnit());
FormatQuantity4 fq = new FormatQuantity4(number);
AttributedCharacterIterator result = formatter.formatToCharacterIterator(fq);
return result;
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
return format((double) number, toAppendTo, pos);
FormatQuantity4 fq = new FormatQuantity4(number);
formatter.format(fq, toAppendTo, pos);
fq.populateUFieldPosition(pos);
return toAppendTo;
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
return format(number.doubleValue(), toAppendTo, pos);
FormatQuantity4 fq = new FormatQuantity4(number);
formatter.format(fq, toAppendTo, pos);
fq.populateUFieldPosition(pos);
return toAppendTo;
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
return format(number.doubleValue(), toAppendTo, pos);
FormatQuantity4 fq = new FormatQuantity4(number);
formatter.format(fq, toAppendTo, pos);
fq.populateUFieldPosition(pos);
return toAppendTo;
}
/**
* {@inheritDoc}
*
* @stable ICU 49
*/
@Override
public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
return format(number.doubleValue(), toAppendTo, pos);
}
/**
* {@inheritDoc}
* @internal ICU 57 technology preview
* @deprecated This API might change or be removed in a future release.
*/
@Override
@Deprecated
public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
return format(currAmt.getNumber().doubleValue(), currAmt.getCurrency(), toAppendTo, pos);
public StringBuffer format(
com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
FormatQuantity4 fq = new FormatQuantity4(number.toBigDecimal());
formatter.format(fq, toAppendTo, pos);
fq.populateUFieldPosition(pos);
return toAppendTo;
}
// /**
// * {@inheritDoc}
// *
// * @internal ICU 57 technology preview
// * @deprecated This API might change or be removed in a future release.
// */
// @Override
// @Deprecated
// public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
// // TODO(sffc)
// throw new UnsupportedOperationException();
// }
/**
* Parsing is currently unsupported, and throws an UnsupportedOperationException.
*
* @stable ICU 49
*/
@Override
@ -332,245 +244,4 @@ public class CompactDecimalFormat extends DecimalFormat {
private void readObject(ObjectInputStream in) throws IOException {
throw new NotSerializableException();
}
/* INTERNALS */
private StringBuffer format(double number, Currency curr, StringBuffer toAppendTo, FieldPosition pos) {
if (curr != null && style == CompactStyle.LONG) {
throw new UnsupportedOperationException("CompactDecimalFormat does not support LONG style for currency.");
}
// Compute the scaled amount, prefix, and suffix appropriate for the number's magnitude.
Output<Unit> currencyUnit = new Output<Unit>();
Amount amount = toAmount(number, curr, currencyUnit);
Unit unit = amount.getUnit();
// Note that currencyUnit is a remnant. In almost all cases, it will be null.
StringBuffer prefix = new StringBuffer();
StringBuffer suffix = new StringBuffer();
if (currencyUnit.value != null) {
currencyUnit.value.writePrefix(prefix);
}
unit.writePrefix(prefix);
unit.writeSuffix(suffix);
if (currencyUnit.value != null) {
currencyUnit.value.writeSuffix(suffix);
}
if (curr == null) {
// Prevent locking when not formatting a currency number.
toAppendTo.append(escape(prefix.toString()));
super.format(amount.getQty(), toAppendTo, pos);
toAppendTo.append(escape(suffix.toString()));
} else {
// To perform the formatting, we set this DecimalFormat's pattern to have the correct prefix, suffix,
// and currency, and then reset it back to what it was before.
// This has to be synchronized since this information is held in the state of the DecimalFormat object.
synchronized(this) {
String originalPattern = this.toPattern();
Currency originalCurrency = this.getCurrency();
StringBuffer newPattern = new StringBuffer();
// Write prefixes and suffixes to the pattern. Note that we have to apply it to both halves of a
// positive/negative format (separated by ';')
int semicolonPos = originalPattern.indexOf(';');
newPattern.append(prefix);
if (semicolonPos != -1) {
newPattern.append(originalPattern, 0, semicolonPos);
newPattern.append(suffix);
newPattern.append(';');
newPattern.append(prefix);
}
newPattern.append(originalPattern, semicolonPos + 1, originalPattern.length());
newPattern.append(suffix);
// Overwrite the pattern and currency.
setCurrency(curr);
applyPattern(newPattern.toString());
// Actually perform the formatting.
super.format(amount.getQty(), toAppendTo, pos);
// Reset the pattern and currency.
setCurrency(originalCurrency);
applyPattern(originalPattern);
}
}
return toAppendTo;
}
private static final Pattern UNESCAPE_QUOTE = Pattern.compile("((?<!'))'");
private static String escape(String string) {
if (string.indexOf('\'') >= 0) {
return UNESCAPE_QUOTE.matcher(string).replaceAll("$1");
}
return string;
}
private Amount toAmount(double number, Currency curr, Output<Unit> currencyUnit) {
// We do this here so that the prefix or suffix we choose is always consistent
// with the rounding we do. This way, 999999 -> 1M instead of 1000K.
boolean negative = isNumberNegative(number);
number = adjustNumberAsInFormatting(number);
int base = number <= 1.0d ? 0 : (int) Math.log10(number);
if (base >= CompactDecimalDataCache.MAX_DIGITS) {
base = CompactDecimalDataCache.MAX_DIGITS - 1;
}
if (curr != null) {
number /= currencyDivisor[base];
} else {
number /= divisor[base];
}
String pluralVariant = getPluralForm(getFixedDecimal(number, toDigitList(number)));
if (pluralToCurrencyAffixes != null && currencyUnit != null) {
currencyUnit.value = pluralToCurrencyAffixes.get(pluralVariant);
}
if (negative) {
number = -number;
}
if ( curr != null ) {
return new Amount(number, CompactDecimalDataCache.getUnit(currencyUnits, pluralVariant, base));
} else {
return new Amount(number, CompactDecimalDataCache.getUnit(units, pluralVariant, base));
}
}
private void recordError(Collection<String> creationErrors, String errorMessage) {
if (creationErrors == null) {
throw new IllegalArgumentException(errorMessage);
}
creationErrors.add(errorMessage);
}
/**
* Manufacture the unit list from arrays
*/
private Map<String, DecimalFormat.Unit[]> otherPluralVariant(Map<String, String[][]> pluralCategoryToPower10ToAffix,
long[] divisor, Collection<String> debugCreationErrors) {
// check for bad divisors
if (divisor.length < CompactDecimalDataCache.MAX_DIGITS) {
recordError(debugCreationErrors, "Must have at least " + CompactDecimalDataCache.MAX_DIGITS + " prefix items.");
}
long oldDivisor = 0;
for (int i = 0; i < divisor.length; ++i) {
// divisor must be a power of 10, and must be less than or equal to 10^i
int log = (int) Math.log10(divisor[i]);
if (log > i) {
recordError(debugCreationErrors, "Divisor[" + i + "] must be less than or equal to 10^" + i
+ ", but is: " + divisor[i]);
}
long roundTrip = (long) Math.pow(10.0d, log);
if (roundTrip != divisor[i]) {
recordError(debugCreationErrors, "Divisor[" + i + "] must be a power of 10, but is: " + divisor[i]);
}
if (divisor[i] < oldDivisor) {
recordError(debugCreationErrors, "Bad divisor, the divisor for 10E" + i + "(" + divisor[i]
+ ") is less than the divisor for the divisor for 10E" + (i - 1) + "(" + oldDivisor + ")");
}
oldDivisor = divisor[i];
}
Map<String, DecimalFormat.Unit[]> result = new HashMap<String, DecimalFormat.Unit[]>();
Map<String,Integer> seen = new HashMap<String,Integer>();
String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other");
for (Entry<String, String[][]> pluralCategoryAndPower10ToAffix : pluralCategoryToPower10ToAffix.entrySet()) {
String pluralCategory = pluralCategoryAndPower10ToAffix.getKey();
String[][] power10ToAffix = pluralCategoryAndPower10ToAffix.getValue();
// we can't have one of the arrays be of different length
if (power10ToAffix.length != divisor.length) {
recordError(debugCreationErrors, "Prefixes & suffixes must be present for all divisors " + pluralCategory);
}
DecimalFormat.Unit[] units = new DecimalFormat.Unit[power10ToAffix.length];
for (int i = 0; i < power10ToAffix.length; i++) {
String[] pair = power10ToAffix[i];
if (pair == null) {
pair = defaultPower10ToAffix[i];
}
// we can't have bad pair
if (pair.length != 2 || pair[0] == null || pair[1] == null) {
recordError(debugCreationErrors, "Prefix or suffix is null for " + pluralCategory + ", " + i + ", " + Arrays.asList(pair));
continue;
}
// we can't have two different indexes with the same display
int log = (int) Math.log10(divisor[i]);
String key = pair[0] + "\uFFFF" + pair[1] + "\uFFFF" + (i - log);
Integer old = seen.get(key);
if (old == null) {
seen.put(key, i);
} else if (old != i) {
recordError(debugCreationErrors, "Collision between values for " + i + " and " + old
+ " for [prefix/suffix/index-log(divisor)" + key.replace('\uFFFF', ';'));
}
units[i] = new Unit(pair[0], pair[1]);
}
result.put(pluralCategory, units);
}
return result;
}
private String getPluralForm(FixedDecimal fixedDecimal) {
if (pluralRules == null) {
return CompactDecimalDataCache.OTHER;
}
return pluralRules.select(fixedDecimal);
}
/**
* Gets the data for a particular locale and style. If style is unrecognized,
* we just return data for CompactStyle.SHORT.
* @param locale The locale.
* @param style The style.
* @return The data which must not be modified.
*/
private Data getData(ULocale locale, CompactStyle style) {
CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
switch (style) {
case SHORT:
return bundle.shortData;
case LONG:
return bundle.longData;
default:
return bundle.shortData;
}
}
/**
* Gets the currency data for a particular locale.
* Currently only short currency format is supported, since that is
* the only form in CLDR.
* @param locale The locale.
* @return The data which must not be modified.
*/
private Data getCurrencyData(ULocale locale) {
CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
return bundle.shortCurrencyData;
}
private static class Amount {
private final double qty;
private final Unit unit;
public Amount(double qty, Unit unit) {
this.qty = qty;
this.unit = unit;
}
public double getQty() {
return qty;
}
public Unit getUnit() {
return unit;
}
}
}

View File

@ -162,6 +162,10 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
* Set currency plural patterns. These are initially set in the constructor based on the
* locale, and usually do not need to be changed.
*
* The decimal digits part of the pattern cannot be specified via this method. All plural
* forms will use the same decimal pattern as set in the constructor of DecimalFormat. For
* example, you can't set "0.0" for plural "few" but "0.00" for plural "many".
*
* @param pluralCount the plural count for which the currency pattern will
* be overridden.
* @param pattern the new currency plural pattern
@ -188,6 +192,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
*
* @stable ICU 4.2
*/
@Override
public Object clone() {
try {
CurrencyPluralInfo other = (CurrencyPluralInfo) super.clone();
@ -213,6 +218,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
*
* @stable ICU 4.2
*/
@Override
public boolean equals(Object a) {
if (a instanceof CurrencyPluralInfo) {
CurrencyPluralInfo other = (CurrencyPluralInfo)a;
@ -223,15 +229,17 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
}
/**
* Mock implementation of hashCode(). This implementation always returns a constant
* value. When Java assertion is enabled, this method triggers an assertion failure.
* Override hashCode
*
* @internal
* @deprecated This API is ICU internal only.
*/
@Override
@Deprecated
public int hashCode() {
assert false : "hashCode not designed";
return 42;
return pluralCountToCurrencyUnitPattern.hashCode()
^ pluralRules.hashCode()
^ ulocale.hashCode();
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -232,8 +232,11 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
* Returns the array of strings used as digits, in order from 0 through 9
* Package private method - doesn't create a defensively copy.
* @return the array of digit strings
* @internal
* @deprecated This API is ICU internal only.
*/
String[] getDigitStringsLocal() {
@Deprecated
public String[] getDigitStringsLocal() {
return digitStrings;
}
@ -1318,9 +1321,9 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
setMonetaryGroupingSeparatorString(numberElements[11]);
setExponentMultiplicationSign(numberElements[12]);
digit = DecimalFormat.PATTERN_DIGIT; // Localized pattern character no longer in CLDR
padEscape = DecimalFormat.PATTERN_PAD_ESCAPE;
sigDigit = DecimalFormat.PATTERN_SIGNIFICANT_DIGIT;
digit = '#'; // Localized pattern character no longer in CLDR
padEscape = '*';
sigDigit = '@';
CurrencyDisplayInfo info = CurrencyData.provider.getInstance(locale, true);
@ -1448,8 +1451,8 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
exponential = 'E';
}
if (serialVersionOnStream < 2) {
padEscape = DecimalFormat.PATTERN_PAD_ESCAPE;
plusSign = DecimalFormat.PATTERN_PLUS_SIGN;
padEscape = '*';
plusSign = '+';
exponentSeparator = String.valueOf(exponential);
// Although we read the exponential field on stream to create the
// exponentSeparator, we don't do the reverse, since scientific
@ -1527,7 +1530,7 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
groupingSeparatorString = String.valueOf(groupingSeparator);
}
if (percentString == null) {
percentString = String.valueOf(percentString);
percentString = String.valueOf(percent);
}
if (perMillString == null) {
perMillString = String.valueOf(perMill);

File diff suppressed because it is too large Load Diff

View File

@ -1020,7 +1020,12 @@ public class MeasureFormat extends UFormat {
return pattern;
}
private String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
if (index != StandardPlural.OTHER_INDEX) {
String pattern = getFormatterOrNull(unit, width, index);
if (pattern != null) {
@ -1171,6 +1176,7 @@ public class MeasureFormat extends UFormat {
suffix = pattern.substring(pos+3);
}
}
@Override
public String toString() {
return prefix + "; " + suffix;
}
@ -1404,6 +1410,7 @@ public class MeasureFormat extends UFormat {
public MeasureProxy() {
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeByte(0); // version
out.writeUTF(locale.toLanguageTag());
@ -1413,6 +1420,7 @@ public class MeasureFormat extends UFormat {
out.writeObject(keyValues);
}
@Override
@SuppressWarnings("unchecked")
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
in.readByte(); // version.

View File

@ -38,7 +38,7 @@ import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.MessagePattern.ArgType;
import com.ibm.icu.text.MessagePattern.Part;
import com.ibm.icu.text.PluralRules.FixedDecimal;
import com.ibm.icu.text.PluralRules.IFixedDecimal;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.ULocale;
@ -2108,7 +2108,7 @@ public class MessageFormat extends UFormat {
assert context.number.doubleValue() == number; // argument number minus the offset
context.numberString = context.formatter.format(context.number);
if(context.formatter instanceof DecimalFormat) {
FixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
IFixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
return rules.select(dec);
} else {
return rules.select(number);

View File

@ -497,8 +497,13 @@ public abstract class NumberFormat extends UFormat {
}
/**
* {@icu} Sets whether strict parsing is in effect. When this is true, the
* following conditions cause a parse failure (examples use the pattern "#,##0.#"):<ul>
* {@icu} Sets whether strict parsing is in effect. When this is true, the string
* is required to be a stronger match to the pattern than when lenient parsing is in
* effect. More specifically, the following conditions cause a parse failure relative
* to lenient mode (examples use the pattern "#,##0.#"):<ul>
* <li>The presence and position of special symbols, including currency, must match the
* pattern.<br>
* '123-' fails (the minus sign is expected in the prefix, not suffix)</li>
* <li>Leading or doubled grouping separators<br>
* ',123' and '1,,234" fail</li>
* <li>Groups of incorrect length when grouping is used<br>
@ -1113,8 +1118,13 @@ public abstract class NumberFormat extends UFormat {
/**
* Returns the maximum number of digits allowed in the integer portion of a
* number. The default value is 40, which subclasses can override.
* When formatting, the exact behavior when this value is exceeded is
* subclass-specific. When parsing, this has no effect.
*
* When formatting, if the number of digits exceeds this value, the highest-
* significance digits are truncated until the limit is reached, in accordance
* with UTS#35.
*
* This setting has no effect on parsing.
*
* @return the maximum number of integer digits
* @see #setMaximumIntegerDigits
* @stable ICU 2.0
@ -1415,10 +1425,12 @@ public abstract class NumberFormat extends UFormat {
f.setDecimalSeparatorAlwaysShown(false);
f.setParseIntegerOnly(true);
}
if (choice == CASHCURRENCYSTYLE) {
f.setCurrencyUsage(CurrencyUsage.CASH);
}
if (choice == PLURALCURRENCYSTYLE) {
f.setCurrencyPluralInfo(CurrencyPluralInfo.getInstance(desiredLocale));
}
format = f;
}
// TODO: the actual locale of the *pattern* may differ from that
@ -1449,8 +1461,11 @@ public abstract class NumberFormat extends UFormat {
* @param choice the pattern format.
* @return the pattern
* @stable ICU 3.2
* @internal
* @deprecated This API is ICU internal only.
*/
protected static String getPattern(ULocale forLocale, int choice) {
@Deprecated
public static String getPattern(ULocale forLocale, int choice) {
/* for ISOCURRENCYSTYLE and PLURALCURRENCYSTYLE,
* the pattern is the same as the pattern of CURRENCYSTYLE
* but by replacing the single currency sign with
@ -1460,6 +1475,7 @@ public abstract class NumberFormat extends UFormat {
switch (choice) {
case NUMBERSTYLE:
case INTEGERSTYLE:
case PLURALCURRENCYSTYLE:
patternKey = "decimalFormat";
break;
case CURRENCYSTYLE:
@ -1469,7 +1485,6 @@ public abstract class NumberFormat extends UFormat {
break;
case CASHCURRENCYSTYLE:
case ISOCURRENCYSTYLE:
case PLURALCURRENCYSTYLE:
case STANDARDCURRENCYSTYLE:
patternKey = "currencyFormat";
break;

View File

@ -18,6 +18,8 @@ import java.util.Map;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.PluralRules.FixedDecimal;
import com.ibm.icu.text.PluralRules.IFixedDecimal;
import com.ibm.icu.text.PluralRules.Operand;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
@ -554,8 +556,8 @@ public class PluralFormat extends UFormat {
private final class PluralSelectorAdapter implements PluralSelector {
@Override
public String select(Object context, double number) {
FixedDecimal dec = (FixedDecimal) context;
assert dec.source == (dec.isNegative ? -number : number);
IFixedDecimal dec = (IFixedDecimal) context;
assert dec.getPluralOperand(Operand.n) == Math.abs(number);
return pluralRules.select(dec);
}
}
@ -618,7 +620,7 @@ public class PluralFormat extends UFormat {
} else {
numberString = numberFormat.format(numberMinusOffset);
}
FixedDecimal dec;
IFixedDecimal dec;
if(numberFormat instanceof DecimalFormat) {
dec = ((DecimalFormat) numberFormat).getFixedDecimal(numberMinusOffset);
} else {

View File

@ -356,7 +356,7 @@ public class PluralRules implements Serializable {
private static final long serialVersionUID = 9163464945387899416L;
@Override
public boolean isFulfilled(FixedDecimal n) {
public boolean isFulfilled(IFixedDecimal n) {
return true;
}
@ -412,11 +412,21 @@ public class PluralRules implements Serializable {
*/
public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE));
private enum Operand {
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static enum Operand {
/** The double value of the entire number. */
n,
/** The integer value, with the fraction digits truncated off. */
i,
/** All visible fraction digits as an integer, including trailing zeros. */
f,
/** Visible fraction digits, not including trailing zeros. */
t,
/** Number of visible fraction digits. */
v,
w,
/* deprecated */
@ -428,7 +438,18 @@ public class PluralRules implements Serializable {
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static class FixedDecimal extends Number implements Comparable<FixedDecimal> {
public static interface IFixedDecimal {
public double getPluralOperand(Operand operand);
public boolean isNaN();
public boolean isInfinite();
}
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static class FixedDecimal extends Number implements Comparable<FixedDecimal>, IFixedDecimal {
private static final long serialVersionUID = -4756200506571685661L;
/**
* @internal
@ -726,8 +747,9 @@ public class PluralRules implements Serializable {
* @internal
* @deprecated This API is ICU internal only.
*/
@Override
@Deprecated
public double get(Operand operand) {
public double getPluralOperand(Operand operand) {
switch(operand) {
default: return source;
case i: return integerValue;
@ -881,6 +903,22 @@ public class PluralRules implements Serializable {
) throws IOException, ClassNotFoundException {
throw new NotSerializableException();
}
/* (non-Javadoc)
* @see com.ibm.icu.text.PluralRules.IFixedDecimal#isNaN()
*/
@Override
public boolean isNaN() {
return Double.isNaN(source);
}
/* (non-Javadoc)
* @see com.ibm.icu.text.PluralRules.IFixedDecimal#isInfinite()
*/
@Override
public boolean isInfinite() {
return Double.isInfinite(source);
}
}
/**
@ -1106,7 +1144,7 @@ public class PluralRules implements Serializable {
* Returns true if the number fulfills the constraint.
* @param n the number to test, >= 0.
*/
boolean isFulfilled(FixedDecimal n);
boolean isFulfilled(IFixedDecimal n);
/*
* Returns false if an unlimited number of values fulfills the
@ -1463,10 +1501,10 @@ public class PluralRules implements Serializable {
}
@Override
public boolean isFulfilled(FixedDecimal number) {
double n = number.get(operand);
public boolean isFulfilled(IFixedDecimal number) {
double n = number.getPluralOperand(operand);
if ((integersOnly && (n - (long)n) != 0.0
|| operand == Operand.j && number.visibleDecimalDigitCount != 0)) {
|| operand == Operand.j && number.getPluralOperand(Operand.v) != 0)) {
return !inRange;
}
if (mod != 0) {
@ -1566,7 +1604,7 @@ public class PluralRules implements Serializable {
}
@Override
public boolean isFulfilled(FixedDecimal n) {
public boolean isFulfilled(IFixedDecimal n) {
return a.isFulfilled(n)
&& b.isFulfilled(n);
}
@ -1594,7 +1632,7 @@ public class PluralRules implements Serializable {
}
@Override
public boolean isFulfilled(FixedDecimal n) {
public boolean isFulfilled(IFixedDecimal n) {
return a.isFulfilled(n)
|| b.isFulfilled(n);
}
@ -1645,7 +1683,7 @@ public class PluralRules implements Serializable {
return keyword;
}
public boolean appliesTo(FixedDecimal n) {
public boolean appliesTo(IFixedDecimal n) {
return constraint.isFulfilled(n);
}
@ -1708,7 +1746,7 @@ public class PluralRules implements Serializable {
return this;
}
private Rule selectRule(FixedDecimal n) {
private Rule selectRule(IFixedDecimal n) {
for (Rule rule : rules) {
if (rule.appliesTo(n)) {
return rule;
@ -1717,8 +1755,8 @@ public class PluralRules implements Serializable {
return null;
}
public String select(FixedDecimal n) {
if (Double.isInfinite(n.source) || Double.isNaN(n.source)) {
public String select(IFixedDecimal n) {
if (n.isInfinite() || n.isNaN()) {
return KEYWORD_OTHER;
}
Rule r = selectRule(n);
@ -1780,7 +1818,7 @@ public class PluralRules implements Serializable {
return null;
}
public boolean select(FixedDecimal sample, String keyword) {
public boolean select(IFixedDecimal sample, String keyword) {
for (Rule rule : rules) {
if (rule.getKeyword().equals(keyword) && rule.appliesTo(sample)) {
return true;
@ -1800,9 +1838,9 @@ public class PluralRules implements Serializable {
}
@SuppressWarnings("unused")
private boolean addConditional(Set<FixedDecimal> toAddTo, Set<FixedDecimal> others, double trial) {
private boolean addConditional(Set<IFixedDecimal> toAddTo, Set<IFixedDecimal> others, double trial) {
boolean added;
FixedDecimal toAdd = new FixedDecimal(trial);
IFixedDecimal toAdd = new FixedDecimal(trial);
if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) {
others.add(toAdd);
added = true;
@ -1969,7 +2007,7 @@ public class PluralRules implements Serializable {
* @deprecated This API is ICU internal only.
*/
@Deprecated
public String select(FixedDecimal number) {
public String select(IFixedDecimal number) {
return rules.select(number);
}

View File

@ -1267,7 +1267,7 @@ public class RuleBasedNumberFormat extends NumberFormat {
public StringBuffer format(com.ibm.icu.math.BigDecimal number,
StringBuffer toAppendTo,
FieldPosition pos) {
if (MIN_VALUE.compareTo(number) >= 0 || MAX_VALUE.compareTo(number) <= 0) {
if (MIN_VALUE.compareTo(number) > 0 || MAX_VALUE.compareTo(number) < 0) {
// We're outside of our normal range that this framework can handle.
// The DecimalFormat will provide more accurate results.
return getDecimalFormat().format(number, toAppendTo, pos);

View File

@ -13,6 +13,7 @@ import java.text.AttributedCharacterIterator.Attribute;
import java.text.CharacterIterator;
import java.util.Map;
import com.ibm.icu.impl.number.Parse;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.util.ULocale;
@ -229,14 +230,14 @@ public final class ScientificNumberFormatter {
int start = iterator.getRunStart(NumberFormat.Field.EXPONENT_SIGN);
int limit = iterator.getRunLimit(NumberFormat.Field.EXPONENT_SIGN);
int aChar = char32AtAndAdvance(iterator);
if (DecimalFormat.minusSigns.contains(aChar)) {
if (Parse.UNISET_MINUS.contains(aChar)) {
append(
iterator,
copyFromOffset,
start,
result);
result.append(SUPERSCRIPT_MINUS_SIGN);
} else if (DecimalFormat.plusSigns.contains(aChar)) {
} else if (Parse.UNISET_PLUS.contains(aChar)) {
append(
iterator,
copyFromOffset,

View File

@ -674,19 +674,7 @@ public class Currency extends MeasureUnit {
*/
@Deprecated
public static String parse(ULocale locale, String text, int type, ParsePosition pos) {
List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale);
if (currencyTrieVec == null) {
TextTrieMap<CurrencyStringInfo> currencyNameTrie =
new TextTrieMap<CurrencyStringInfo>(true);
TextTrieMap<CurrencyStringInfo> currencySymbolTrie =
new TextTrieMap<CurrencyStringInfo>(false);
currencyTrieVec = new ArrayList<TextTrieMap<CurrencyStringInfo>>();
currencyTrieVec.add(currencySymbolTrie);
currencyTrieVec.add(currencyNameTrie);
setupCurrencyTrieVec(locale, currencyTrieVec);
CURRENCY_NAME_CACHE.put(locale, currencyTrieVec);
}
List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = getCurrencyTrieVec(locale);
int maxLength = 0;
String isoResult = null;
@ -711,6 +699,37 @@ public class Currency extends MeasureUnit {
return isoResult;
}
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static TextTrieMap<CurrencyStringInfo>.ParseState openParseState(
ULocale locale, int startingCp, int type) {
List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = getCurrencyTrieVec(locale);
if (type == Currency.LONG_NAME) {
return currencyTrieVec.get(0).openParseState(startingCp);
} else {
return currencyTrieVec.get(1).openParseState(startingCp);
}
}
private static List<TextTrieMap<CurrencyStringInfo>> getCurrencyTrieVec(ULocale locale) {
List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale);
if (currencyTrieVec == null) {
TextTrieMap<CurrencyStringInfo> currencyNameTrie =
new TextTrieMap<CurrencyStringInfo>(true);
TextTrieMap<CurrencyStringInfo> currencySymbolTrie =
new TextTrieMap<CurrencyStringInfo>(false);
currencyTrieVec = new ArrayList<TextTrieMap<CurrencyStringInfo>>();
currencyTrieVec.add(currencySymbolTrie);
currencyTrieVec.add(currencyNameTrie);
setupCurrencyTrieVec(locale, currencyTrieVec);
CURRENCY_NAME_CACHE.put(locale, currencyTrieVec);
}
return currencyTrieVec;
}
private static void setupCurrencyTrieVec(ULocale locale,
List<TextTrieMap<CurrencyStringInfo>> trieVec) {
@ -734,7 +753,12 @@ public class Currency extends MeasureUnit {
}
}
private static final class CurrencyStringInfo {
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static final class CurrencyStringInfo {
private String isoCode;
private String currencyString;

View File

@ -43,7 +43,7 @@ public class Measure {
*/
public Measure(Number number, MeasureUnit unit) {
if (number == null || unit == null) {
throw new NullPointerException();
throw new NullPointerException("Number and MeasureUnit must not be null");
}
this.number = number;
this.unit = unit;

View File

@ -2,17 +2,17 @@
<launchConfiguration type="org.eclipse.ant.AntLaunchConfigurationType">
<booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/icu4j-collate-tests/build.xml"/>
<listEntry value="/icu4j-core-tests"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
<listEntry value="4"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="icu4j-core-tests"/>
<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<mapAttribute key="org.eclipse.ui.externaltools.ATTR_ANT_PROPERTIES">
<mapEntry key="eclipse.running" value="true"/>
<mapEntry key="build.compiler" value="org.eclipse.jdt.core.JDTCompilerAdapter"/>
<mapEntry key="eclipse.running" value="true"/>
</mapAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_ANT_PROPERTY_FILES" value="${workspace_loc:/icu4j-shared/build/locations-eclipse.properties},"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/icu4j-collate-tests/build.xml}"/>

View File

@ -5,18 +5,18 @@
<booleanAttribute key="org.eclipse.ant.ui.ATTR_TARGETS_UPDATED" value="true"/>
<booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/icu4j-core-tests/build.xml"/>
<listEntry value="/icu4j-core-tests"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
<listEntry value="4"/>
</listAttribute>
<booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="true"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="icu4j-core-tests"/>
<mapAttribute key="org.eclipse.ui.externaltools.ATTR_ANT_PROPERTIES">
<mapEntry key="eclipse.running" value="true"/>
<mapEntry key="build.compiler" value="org.eclipse.jdt.core.JDTCompilerAdapter"/>
<mapEntry key="eclipse.running" value="true"/>
</mapAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_ANT_PROPERTY_FILES" value="${workspace_loc:/icu4j-shared/build/locations-eclipse.properties},"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/icu4j-core-tests/build.xml}"/>

View File

@ -2,17 +2,17 @@
<launchConfiguration type="org.eclipse.ant.AntLaunchConfigurationType">
<booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/icu4j-core-tests/build.xml"/>
<listEntry value="/icu4j-core-tests"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
<listEntry value="4"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="icu4j-core-tests"/>
<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<mapAttribute key="org.eclipse.ui.externaltools.ATTR_ANT_PROPERTIES">
<mapEntry key="eclipse.running" value="true"/>
<mapEntry key="build.compiler" value="org.eclipse.jdt.core.JDTCompilerAdapter"/>
<mapEntry key="eclipse.running" value="true"/>
</mapAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_ANT_PROPERTY_FILES" value="${workspace_loc:/icu4j-shared/build/locations-eclipse.properties},"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/icu4j-core-tests/build.xml}"/>

View File

@ -17,8 +17,8 @@ set locale ar
set pattern +0;-#
begin
format output breaks
6 \u200F+\u0666 JK
-6 \u200F-\u0666 JK
6 \u061C+\u0666 JK
-6 \u061C-\u0666 K
test basic patterns
set locale fr_FR
@ -54,6 +54,75 @@ format output
12345 2345.000
72.1234 72.1234
test patterns with no '0' symbols
set locale en_US
begin
pattern format output breaks
# 514.23 514
# 0.23 0
# 0 0
# 1 1
##.# 514.23 514.2
##.# 0.23 0.2
##.# 0 0
##.# 1 1
#.# 514.23 514.2
#.# 0.23 0.2
#.# 0 0
#.# 1 1
.# 514.23 514.2
.# 0.23 .2
.# 0 .0
.# 1 1.0
#. 514.23 514.
#. 0.23 0.
#. 0 0.
#. 1 1.
. 514.23 514.
. 0.23 0.
. 0 0.
. 1 1.
test behavior on numbers approaching zero
set locale en
begin
pattern format output breaks
#.## 0.01 0.01
#.## 0.001 0
#.## 0 0
#.00 0.01 .01
#.00 0.001 .00
#.00 0 .00
0.00 0.01 0.01
0.00 0.001 0.00
0.00 0 0.00
// Not in official spec, but needed for backwards compatibility
test patterns with leading grouping separator
set locale en_US
begin
pattern format output breaks
,##0 1234.56 1,235
'#',## 3456 #34,56
test patterns with valid and invalid quote marks
set locale et
begin
pattern format output breaks
'# 1 fail
''# 1 '1
'''# 1 fail
''''# 1 ''1
'''''# 1 fail
'-''-'# 1 -'-1
// K doesn't know the locale symbol for et
-'-'# 1 -1 K
'#'# 1 #1
''#'' 1 '1'
''#- 1 '1 K
'-'#- 1 -1 K
-#'-' 1 1- K
test int64
set locale en
begin
@ -113,12 +182,36 @@ pattern format output breaks
0.05E0 12301.2 1,25E4 JK
##0.000#E0 0.17 170,0E-3
// JDK doesn't support significant digits in exponents
@@@E0 6.235 6,24E0 K
@@@E0 6235 6,24E3 K
@@@#E0 6200 6,20E3 K
@@@#E0 6201 6,201E3 K
@@@#E0 6201.7 6,202E3 K
@@@#E00 6201.7 6,202E03 K
@@@#E+00 6201.7 6,202E+03 K
// If no zeros are specified, significant digits is fraction length plus 1
#.##E0 52413 5,24E4
###.##E0 52413 52,4E3 K
#E0 52413 5,2413E4 K
0E0 52413 5E4
test scientific with grouping
set locale en
set pattern #,##0.000E0
begin
format output breaks
// J throws an IllegalArgumentException when parsing the pattern.
1 1.000E0 J
11 11.00E0 J
111 111.0E0 J
// K doesn't print the grouping separator ("1111E0")
1111 1,111E0 JK
// K prints too many digits ("1.1111E4")
11111 1.111E4 JK
111111 11.11E4 JK
1111111 111.1E4 JK
11111111 1,111E4 JK
111111111 1.111E8 JK
test percents
set locale fr
@ -165,7 +258,7 @@ $**####,##0 1234 $***1\u00a0234 K
// In J ICU adds padding as if 'EUR' is only 2 chars (2 * 0xa4)
\u00a4\u00a4 **####0.00 433.0 EUR *433,00 JK
// In J ICU adds padding as if 'EUR' is only 2 chars (2 * 0xa4)
\u00a4\u00a4 **#######0 433.0 EUR *433,00 JK
\u00a4\u00a4 **#######0 433.0 EUR ****433 JK
test padding and currencies
begin
@ -235,13 +328,16 @@ set pattern #E0
set format 299792458.0
begin
minIntegerDigits maxIntegerDigits minFractionDigits maxFractionDigits output breaks
// JDK gives 2.99792458E8 (maxInt + maxFrac instead of minInt + maxFrac)
1 1000 0 5 2.99792E8 K
// JDK gives .3E9 instead of unlimited precision.
0 1 0 0 2.99792458E8 K
1 1 0 0 3E8
// JDK gives E0 instead of allowing for unlimited precision
0 0 0 0 2.99792458E8 K
// JDK gives .299792E9
0 1 0 5 2.9979E8 K
// S obeys the maximum integer digits and returns .299792458E9
0 0 0 0 2.99792458E8 KS
// JDK and S give .299792E9
0 1 0 5 2.9979E8 KS
// JDK gives 300E6
0 3 0 0 299.792458E6 K
// JDK gives 299.8E6 (maybe maxInt + maxFrac instead of minInt + maxFrac)?
@ -256,11 +352,14 @@ minIntegerDigits maxIntegerDigits minFractionDigits maxFractionDigits output bre
4 4 0 0 2998E5
0 0 1 5 .29979E9
// JDK gives E0
0 0 1 0 2.99792458E8 K
// S obeys the maximum integer digits
0 0 1 0 2.99792458E8 KS
// JDK gives .2998E9
0 0 0 4 2.998E8 K
0 0 0 4 2.998E8 KS
// S correctly formats this as 29.979246E7.
// JDK uses 8 + 6 for significant digits instead of 2 + 6
2 8 1 6 2.9979246E8 K
// J and C return 2.9979246E8.
2 8 1 6 29.979246E7 CJK
// Treat max int digits > 8 as being the same as min int digits.
// This behavior is not spelled out in the specification.
// JDK fails here because it tries to use 9 + 6 = 15 sig digits.
@ -290,7 +389,8 @@ set format 29979245.0
begin
minIntegerDigits maxIntegerDigits minFractionDigits maxFractionDigits output breaks
// JDK gives E0
0 0 0 0 2.9979245E7 K
// S obeys the max integer digits and prints 0.299...
0 0 0 0 2.9979245E7 KS
// JDK gives .3E8
0 1 0 0 2.9979245E7 K
// JDK gives 2998E4.
@ -300,23 +400,27 @@ test ticket 11524
set locale en
set pattern #,##0.###
begin
format maxIntegerDigits output
format maxIntegerDigits output breaks
123 1 3
123 -2147483648 0
0 0 0
// S ignores max integer if it is less than zero and prints "123"
123 -2147483648 0 S
12345 1 5
12345 -2147483648 0
12345 -2147483648 0 S
5.3 1 5.3
5.3 -2147483648 .3
5.3 -2147483648 .3 S
test patterns with zero
set locale en
set format 0
begin
pattern output
pattern output breaks
#.# 0
#. 0.
.# .0
# 0
#,##0.00 0.00
#,###.00 .00
00.000E00 00.000E00
0.####E0 0E0
##0.######E000 0E000
@ -334,8 +438,8 @@ format output breaks
0.001234 0.001234 K
0.0012345 0.0012345 K
0.00123456 0.0012346 K
-43 -43.0 K
-43.7 -43.7 K
-43 -43.0
-43.7 -43.7
-43.76 -43.76 K
-43.762 -43.762 K
-43.7626 -43.763 K
@ -360,7 +464,7 @@ output grouping breaks grouping2 minGroupingDigits
1,2345,6789 4
1,23,45,6789 4 K 2
1,23,45,6789 4 K 2 2
123,456789 6 K 6 3
123,456789 6 6 3
123456789 6 JK 6 4
test multiplier setters
@ -370,7 +474,7 @@ format multiplier output breaks
23 -12 -276
23 -1 -23
// ICU4J and JDK throw exception on zero multiplier. ICU4C does not.
23 0 23 JK
23 0 23 JKS
23 1 23
23 12 276
-23 12 -276
@ -394,7 +498,7 @@ set pattern bill0
set format 1357
begin
padCharacter formatWidth output breaks
* 8 bill1357 K
* 8 bill1357
* 9 *bill1357 K
^ 10 ^^bill1357 K
@ -406,7 +510,7 @@ begin
output breaks useScientific
186283.00
1.86E5 K 1
186283.00 K 0
186283.00 0
test rounding mode setters
set locale en_US
@ -423,19 +527,43 @@ format roundingMode output breaks
-1.49 down -1 K
1.01 up 1.5 K
1.49 down 1 K
-1.01 ceiling -1 K
-1.49 floor -1.5 K
-1.01 ceiling -1
-1.49 floor -1.5
test currency usage setters
// TODO: Find a country and currency where standard and cash differ
set locale CH
set currency CHF
set pattern \u00a4\u00a4 0
begin
format currencyUsage output breaks
0.37 standard CHF 0.37 K
// TODO: Get the rounding data into ICU4C and ICU4J
0.37 cash CHF 0.35 CJK
format currency currencyUsage output breaks
0.37 CHF standard CHF 0.37 K
0.37 CHF cash CHF 0.35 CK
1.234 CZK standard CZK 1.23 K
1.234 CZK cash CZK 1
test currency usage to pattern
set locale en
begin
currency currencyUsage toPattern breaks
// These work in J, but it prepends an extra hash sign to the pattern.
// K does not support this feature.
USD standard 0.00 JK
CHF standard 0.00 JK
CZK standard 0.00 JK
USD cash 0.00 JK
CHF cash 0.05 JK
CZK cash 0 JK
test currency rounding
set locale en
set currency USD
begin
pattern format output breaks
# 123 123 S
// Currency rounding should always override the pattern.
// K prints the currency in ISO format for some reason.
\u00a4# 123 $123.00 K
\u00a4#.000 123 $123.00 K
\u00a4#.## 123 $123.00 K
test exponent parameter setters
set locale en_US
@ -445,12 +573,10 @@ begin
decimalSeparatorAlwaysShown exponentSignAlwaysShown minimumExponentDigits output breaks
0 0 2 3E08 K
0 1 3 3E+008 K
// ICU DecimalFormat J does not honor decimalSeparatorAlwaysShown
// for scientific notation. But JDK DecimalFormat does honor
// decimalSeparatorAlwaysShown K=JDK; C=ICU4C; J=ICU4J
// See ticket 11621
1 0 2 3.E08 JK
1 1 3 3.E+008 JK
1 0 2 3.E08 K
1 1 3 3.E+008 K
1 0 1 3.E8
0 0 1 3E8
@ -462,7 +588,7 @@ format output breaks decimalSeparatorAlwaysShown
// decimalSeparatorAlwaysShown off by default
299792458 3E8
299000000 2.99E8
299792458 3.E8 J 1
299792458 3.E8 1
test pad position setters
set locale en_US
@ -505,7 +631,7 @@ set locale en_US
set pattern [0.00];(#)
begin
format output breaks
Inf [\u221e] K
Inf [\u221e]
-Inf (\u221e) K
NaN NaN K
@ -539,36 +665,38 @@ begin
locale pattern format output breaks
en #0% 0.4376 44%
// This next test breaks JDK. JDK doesn't multiply by 100.
// It also is now broken in ICU4J until #10368 is fixed.
fa \u0025\u00a0\u0023\u0030 0.4376 \u200e\u066a\u00a0\u06f4\u06f4 JK
fa \u0025\u00a0\u0023\u0030 0.4376 \u200e\u066a\u00a0\u06f4\u06f4 K
test toPattern
set locale en
begin
pattern toPattern breaks
// All of the "S" failures in this section are because of functionally equivalent patterns
// JDK doesn't support any patterns with padding or both negative prefix and suffix
// Breaks ICU4J See ticket 11671
**0,000 **0,000 JK
**##0,000 **##0,000 K
**###0,000 **###0,000 K
**####0,000 **#,##0,000 K
**####0,000 **#,##0,000 KS
###,000. #,000.
0,000 #0,000
0,000 #0,000 S
.00 #.00
000 #000
000,000 #,000,000
000 #000 S
000,000 #,000,000 S
pp#,000 pp#,000
00.## #00.##
00.## #00.## S
#,#00.025 #,#00.025
// No secondary grouping in JDK
#,##,###.02500 #,##,###.02500 K
pp#,000;(#) pp#,000;(#,000) K
**####,##,##0.0##;(#) **#,##,##,##0.0##;**(##,##,##0.0##) K
**####,##,##0.0##;(#) **#,##,##,##0.0##;**(##,##,##0.0##) KS
// No significant digits in JDK
@@### @@### K
@,@#,### @,@#,### K
0.00E0 0.00E0
@@@##E0 @@@##E0 K
// The following one works in JDK, probably because
// it just returns the same string
@@@##E0 @@@##E0
###0.00#E0 ###0.00#E0
##00.00#E0 ##00.00#E0
0.00E+00 0.00E+00 K
@ -589,7 +717,8 @@ parse output breaks
// J requires prefix and suffix for lenient parsing, but C doesn't
5,347.25 5347.25 JK
(5,347.25 -5347.25 J
-5,347.25 fail
// S is successful at parsing this as -5347.25 in lenient mode
-5,347.25 fail S
+3.52E4 35200
(34.8E-3) -0.0348
// JDK stops parsing at the spaces. JDK doesn't see space as a grouping separator
@ -598,7 +727,7 @@ parse output breaks
// J doesn't allow trailing separators before E but C does
(34,,25,E-1) -342.5 J
(34 25 E-1) -342.5 JK
(34,,25 E-1) -3425 J
(34,,25 E-1) -342.5 JK
// Spaces are not allowed after exponent symbol
// C parses up to the E but J bails
(34 25E -1) -3425 JK
@ -648,16 +777,16 @@ set locale en
set pattern #,##0.0###+;#-
begin
parse output breaks
// C sees this as -3426, don't understand why
3426 -3426 JK
// C sees this as -3426, don't understand why.
// J and K just bail.
3426 3426 JKC
3426+ 3426
// J bails, but JDK will parse up to the space and get 34.
// C sees -34
34 d1+ -34 JK
// J bails; C and K see -34
34 d1+ 34 JKC
// JDK sees this as -1234 for some reason
// J bails b/c of trailing separators
// C parses until trailing separators, but sees -1234
1,234,,,+ -1234 JK
1,234,,,+ 1234 JKC
1,234- -1234
// J bails because of trailing separators
1,234,- -1234 J
@ -668,55 +797,70 @@ parse output breaks
test parse strict
set locale en
set pattern +#,##0.0###;(#)
set pattern +#,##,##0.0###;(#)
set lenient 0
set minGroupingDigits 2
begin
parse output breaks
+123d5 123
+5347.25 5347.25
// separators in wrong place cause failure, no separators ok.
+5,347.25 5347.25
(5347.25) -5347.25
(5,347.25) -5347.25
+65,347.25 65347.25
(65347.25) -65347.25
(65,347.25) -65347.25
// JDK does allow separators in the wrong place and parses as -5347.25
(53,47.25) fail K
// strict requires prefix or suffix
5,347.25 fail
65,347.25 fail
+3.52E4 35200
(34.8E-3) -0.0348
(3425E-1) -342.5
// Strict doesn't allow separators in sci notation.
(3,425) -3425
// JDK allows separators in sci notation and parses as -342.5
(3,425E-1) fail K
(63,425) -63425
// JDK and S allow separators in sci notation and parses as -342.5
(63,425E-1) fail KS
// Both prefix and suffix needed for strict.
// JDK accepts this and parses as -342.5
(3425E-1 fail K
+3.52EE4 3.52
+1,234,567.8901 1234567.8901
+12,34,567.8901 1234567.8901
// With strict digit separators don't have to be the right type
// JDK doesn't acknowledge space as a separator
+1 234 567.8901 1234567.8901 K
+12 34 567.8901 1234567.8901 K
// In general the grouping separators have to match their expected
// location exactly. The only exception is when string being parsed
// have no separators at all.
+1,234,567.8901 1234567.8901
// JDK doesn't require separators to be in the right place
+12,345.67 12345.67
// JDK doesn't require separators to be in the right place.
+1,23,4567.8901 fail K
+1,234,567.8901 fail K
+1234,567.8901 fail K
+1,234567.8901 fail K
+1234567.8901 1234567.8901
// Minimum grouping is not satisfied below, but that's ok
// because minimum grouping is optional.
+1,234.5 1234.5
// Comma after decimal means parse to a comma
+123,456.78,9 123456.78
// A decimal after a decimal means bail
// JDK parses as 123456.78
+123,456.78.9 fail K
+1,23,456.78,9 123456.78
// J fails upon seeing the second decimal point
+1,23,456.78.9 123456.78 J
+79 79
+79 79
+ 79 fail
// JDK parses as -1945
(1,945d1) fail K
test parse strict without prefix/suffix
set locale en
set pattern #
set lenient 0
begin
parse output breaks
12.34 12.34
-12.34 -12.34
+12.34 12.34 JK
$12.34 fail
test parse integer only
set locale en
set pattern 0.00
@ -724,7 +868,8 @@ set parseIntegerOnly 1
begin
parse output breaks
35 35
+35 fail
// S accepts leading plus signs
+35 35 CJK
-35 -35
2.63 2
-39.99 -39
@ -746,8 +891,8 @@ set pattern 0
set locale en
begin
parse output outputCurrency breaks
// See ticket 11735
53.45 fail USD J
// Fixed in ticket 11735
53.45 fail USD
test parse strange prefix
set locale en
@ -775,12 +920,13 @@ set negativePrefix
set negativeSuffix 9N
begin
parse output breaks
// S is the only implementation that passes these cases.
// C consumes the '9' as a digit and assumes number is negative
// J and JDK bail
// 6549K 654 CJK
6549K 654 CJK
// C consumes the '9' as a digit and assumes number is negative
// J and JDK bail
// 6549N -654 CJK
6549N -654 CJK
test really strange prefix
set locale en
@ -791,6 +937,39 @@ parse output
8245 45
2845 -45
test parse pattern with quotes
set locale en
set pattern '-'#y
begin
parse output
-45y 45
test parse with locale symbols
// The grouping separator in it_CH is an apostrophe
set locale it_CH
set pattern #
begin
parse output breaks
१३ 13
१३.३१‍ 13.31
// J and K stop parsing at the apostrophe
123'456 123456 JK
524'1.3 5241.3 JK
३'१‍ 31 JK
test parse with European-style comma/period
set locale pt
set pattern #
begin
parse output breaks
// J and K get 123
123.456 123456 JK
123,456 123.456
987,654.321 987.654
987,654 321 987.654
// J and K get 987
987.654,321 987654.321 JK
test select
set locale sr
begin
@ -811,25 +990,28 @@ NaN 0.0 other
test parse currency ISO
set pattern 0.00 \u00a4\u00a4;(#) \u00a4\u00a4
set locale en_US
set locale en_GB
begin
parse output outputCurrency breaks
$53.45 53.45 USD
53.45 fail GBP
£53.45 53.45 GBP
$53.45 fail USD
53.45 USD 53.45 USD
53.45 GBP 53.45 GBP
USD 53.45 53.45 USD J
53.45USD fail USD
53.45USD 53.45 USD CJ
USD53.45 53.45 USD
(7.92) USD -7.92 USD
(7.92) EUR -7.92 EUR
(7.92) GBP -7.92 GBP
(7.926) USD -7.926 USD
(7.926 USD) fail USD
(USD 7.926) fail USD
USD (7.926) fail USD
USD (7.92) fail USD
(7.92)USD fail USD
USD(7.92) fail USD
(7.926 USD) -7.926 USD CJ
(USD 7.926) -7.926 USD CJ
USD (7.926) -7.926 USD CJ
USD (7.92) -7.92 USD CJ
(7.92)USD -7.92 USD CJ
USD(7.92) -7.92 USD CJ
(8) USD -8 USD
-8 USD fail USD
-8 USD -8 USD CJ
67 USD 67 USD
53.45$ fail USD
US Dollars 53.45 53.45 USD J
@ -837,37 +1019,41 @@ US Dollars 53.45 53.45 USD J
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
53.45US Dollars fail USD
53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
US Dollat53.45 fail USD
53.45US Dollar fail USD
US Dollars (53.45) fail USD
53.45US Dollar 53.45 USD CJ
US Dollars (53.45) -53.45 USD CJ
(53.45) US Dollars -53.45 USD
US Dollar (53.45) fail USD
(53.45) Euros -53.45 EUR
US Dollar (53.45) -53.45 USD CJ
(53.45) US Dollar -53.45 USD
US Dollars(53.45) fail USD
(53.45)US Dollars fail USD
US Dollar(53.45) fail USD
US Dollars(53.45) -53.45 USD CJ
(53.45)US Dollars -53.45 USD CJ
US Dollar(53.45) -53.45 USD CJ
US Dollat(53.45) fail USD
(53.45)US Dollar fail USD
(53.45)US Dollar -53.45 USD CJ
test parse currency ISO negative
set pattern 0.00 \u00a4\u00a4;-# \u00a4\u00a4
set locale en_US
set locale en_GB
begin
parse output outputCurrency breaks
$53.45 53.45 USD
53.45 fail GBP
£53.45 53.45 GBP
$53.45 fail USD
53.45 USD 53.45 USD
53.45 GBP 53.45 GBP
USD 53.45 53.45 USD J
53.45USD fail USD
53.45USD 53.45 USD CJ
USD53.45 53.45 USD
-7.92 USD -7.92 USD
-7.92 EUR -7.92 EUR
-7.92 GBP -7.92 GBP
-7.926 USD -7.926 USD
USD -7.926 fail USD
-7.92USD fail USD
USD-7.92 fail USD
USD -7.926 -7.926 USD CJ
-7.92USD -7.92 USD CJ
USD-7.92 -7.92 USD CJ
-8 USD -8 USD
67 USD 67 USD
53.45$ fail USD
@ -876,70 +1062,75 @@ US Dollars 53.45 53.45 USD J
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
53.45US Dollars fail USD
53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
US Dollat53.45 fail USD
53.45US Dollar fail USD
53.45US Dollar 53.45 USD CJ
test parse currency long
set pattern 0.00 \u00a4\u00a4\u00a4;(#) \u00a4\u00a4\u00a4
set locale en_US
set locale en_GB
begin
parse output outputCurrency breaks
$53.45 53.45 USD
// J throws a NullPointerException on the first case
53.45 fail GBP
£53.45 53.45 GBP
$53.45 fail USD
53.45 USD 53.45 USD
53.45 GBP 53.45 GBP
USD 53.45 53.45 USD J
// See ticket 11735
53.45USD fail USD J
53.45USD 53.45 USD CJ
USD53.45 53.45 USD
(7.92) USD -7.92 USD
(7.92) GBP -7.92 GBP
(7.926) USD -7.926 USD
(7.926 USD) fail USD
(USD 7.926) fail USD
USD (7.926) fail USD
USD (7.92) fail USD
(7.92)USD fail USD
USD(7.92) fail USD
(7.926 USD) -7.926 USD CJ
(USD 7.926) -7.926 USD CJ
USD (7.926) -7.926 USD CJ
USD (7.92) -7.92 USD CJ
(7.92)USD -7.92 USD CJ
USD(7.92) -7.92 USD CJ
(8) USD -8 USD
// See ticket 11735
-8 USD fail USD J
-8 USD -8 USD CJ
67 USD 67 USD
// See ticket 11735
53.45$ fail USD J
// J throws a NullPointerException on the next case
53.45$ fail USD
US Dollars 53.45 53.45 USD J
53.45 US Dollars 53.45 USD
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
// See ticket 11735
53.45US Dollars fail USD J
53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
US Dollat53.45 fail USD
// See ticket 11735
53.45US Dollar fail USD J
53.45US Dollar 53.45 USD CJ
test parse currency short
set pattern 0.00 \u00a4;(#) \u00a4
set locale en_US
set locale en_GB
begin
parse output outputCurrency breaks
$53.45 53.45 USD
53.45 fail GBP
£53.45 53.45 GBP
$53.45 fail USD
53.45 USD 53.45 USD
53.45 GBP 53.45 GBP
USD 53.45 53.45 USD J
53.45USD fail USD
53.45USD 53.45 USD CJ
USD53.45 53.45 USD
(7.92) USD -7.92 USD
(7.92) GBP -7.92 GBP
(7.926) USD -7.926 USD
(7.926 USD) fail USD
(USD 7.926) fail USD
USD (7.926) fail USD
USD (7.92) fail USD
(7.92)USD fail USD
USD(7.92) fail USD
(7.926 USD) -7.926 USD CJ
(USD 7.926) -7.926 USD CJ
USD (7.926) -7.926 USD CJ
USD (7.92) -7.92 USD CJ
(7.92)USD -7.92 USD CJ
USD(7.92) -7.92 USD CJ
(8) USD -8 USD
-8 USD fail USD
-8 USD -8 USD CJ
67 USD 67 USD
53.45$ fail USD
US Dollars 53.45 53.45 USD J
@ -947,45 +1138,51 @@ US Dollars 53.45 53.45 USD J
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
53.45US Dollars fail USD
53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
US Dollat53.45 fail USD
53.45US Dollar fail USD
53.45US Dollar 53.45 USD CJ
test parse currency short prefix
set pattern \u00a40.00;(\u00a4#)
set locale en_US
set locale en_GB
begin
parse output outputCurrency breaks
$53.45 53.45 USD
53.45 USD fail USD
53.45 fail GBP
£53.45 53.45 GBP
$53.45 fail USD
53.45 USD 53.45 USD CJ
53.45 GBP 53.45 GBP CJ
USD 53.45 53.45 USD J
53.45USD fail USD
53.45USD 53.45 USD CJ
USD53.45 53.45 USD
(7.92) USD fail USD
(7.926) USD fail USD
(7.926 USD) fail USD
// S fails these because '(' is an incomplete prefix.
(7.92) USD -7.92 USD CJS
(7.92) GBP -7.92 GBP CJS
(7.926) USD -7.926 USD CJS
(7.926 USD) -7.926 USD CJS
(USD 7.926) -7.926 USD J
USD (7.926) fail USD
USD (7.92) fail USD
(7.92)USD fail USD
USD(7.92) fail USD
(8) USD fail USD
-8 USD fail USD
67 USD fail USD
USD (7.926) -7.926 USD CJS
USD (7.92) -7.92 USD CJS
(7.92)USD -7.92 USD CJS
USD(7.92) -7.92 USD CJS
(8) USD -8 USD CJS
-8 USD -8 USD CJ
67 USD 67 USD CJ
53.45$ fail USD
US Dollars 53.45 53.45 USD J
53.45 US Dollars 53.45 USD
US Dollar 53.45 53.45 USD J
53.45 US Dollar 53.45 USD
US Dollars53.45 53.45 USD
53.45US Dollars fail USD
53.45US Dollars 53.45 USD CJ
US Dollar53.45 53.45 USD
53.45US Dollar fail USD
53.45US Dollar 53.45 USD CJ
test format foreign currency
set locale fa_IR
set currency IRR
begin
pattern format output breaks
\u00a4\u00a4\u00a4 0.00;\u00a4\u00a4\u00a4 # 1235 \u0631\u06cc\u0627\u0644 \u0627\u06cc\u0631\u0627\u0646 \u06F1\u06F2\u06F3\u06F5 K
@ -1058,6 +1255,22 @@ EUR 7.82 7.82 EUR
Euro 7.82 7.82 EUR
Euros 7.82 7.82 EUR
test parse currency without currency mode
// Should accept a symbol associated with the currency specified by the API,
// but should not traverse the full currency data.
set locale en_US
set pattern \u00a4#,##0.00
begin
parse currency output breaks
$52.41 USD 52.41
USD52.41 USD 52.41 K
\u20ac52.41 USD fail
EUR52.41 USD fail
$52.41 EUR fail
USD52.41 EUR fail
\u20ac52.41 EUR 52.41 K
EUR52.41 EUR 52.41
test parse currency ISO strict
set pattern 0.00 \u00a4\u00a4;(#) \u00a4\u00a4
set locale en_US
@ -1110,3 +1323,107 @@ begin
format output breaks
-0.99 -0 JK
test parse decimalPatternMatchRequired
set locale en
set decimalPatternMatchRequired 1
begin
pattern parse output breaks
// K doesn't support this feature.
0 123 123
0 123. fail JK
0 1.23 fail JK
0 -513 -513
0 -513. fail JK
0 -5.13 fail JK
0.0 123 fail K
0.0 123. 123
0.0 1.23 1.23
0.0 -513 fail K
0.0 -513. -513
0.0 -5.13 -5.13
test parse minus sign
set locale en
set pattern #
begin
parse output breaks
-123 -123
- 123 -123 JK
-123 -123 JK
- 123 -123 JK
123- -123 JKS
123 - -123 JKS
test parse case sensitive
set locale en
set lenient 1
set pattern Aa#
begin
parse parseCaseSensitive output breaks
Aa1.23 1 1.23
Aa1.23 0 1.23
AA1.23 1 fail
// J and K do not support case-insensitive parsing for prefix/suffix.
// J supports it for the exponent separator, but not K.
AA1.23 0 1.23 JK
aa1.23 1 fail
aa1.23 0 1.23 JK
Aa1.23E3 1 1230
Aa1.23E3 0 1230
Aa1.23e3 1 1.23 J
Aa1.23e3 0 1230 K
NaN 1 NaN K
NaN 0 NaN K
nan 1 fail
nan 0 NaN JK
test parse infinity and scientific notation overflow
set locale en
begin
parse output breaks
NaN NaN K
// JDK returns zero
1E999999999999999 Inf K
-1E999999999999999 -Inf K
1E-99999999999999 0.0
// Note: The test suite code doesn't properly check for 0.0 vs. -0.0
-1E-99999999999999 -0.0
1E2147483648 Inf K
1E2147483647 Inf K
1E2147483646 1E2147483646
1E-2147483649 0
1E-2147483648 0
// S returns zero here
1E-2147483647 1E-2147483647 S
1E-2147483646 1E-2147483646
test format push limits
set locale en
set minFractionDigits 2
set roundingMode halfDown
begin
maxFractionDigits format output breaks
100 987654321987654321 987654321987654321.00
100 987654321.987654321 987654321.987654321
100 9999999999999.9950000000001 9999999999999.9950000000001
2 9999999999999.9950000000001 10000000000000.00
2 9999999.99499999 9999999.99
// K doesn't support halfDowm rounding mode?
2 9999999.995 9999999.99 K
2 9999999.99500001 10000000.00
100 56565656565656565656565656565656565656565656565656565656565656 56565656565656565656565656565656565656565656565656565656565656.00
100 454545454545454545454545454545.454545454545454545454545454545 454545454545454545454545454545.454545454545454545454545454545
100 0.0000000000000000000123 0.0000000000000000000123
100 -78787878787878787878787878787878 -78787878787878787878787878787878.00
100 -8989898989898989898989.8989898989898989 -8989898989898989898989.8989898989898989
test ticket 11230
set locale en
set pattern ###
begin
parse output breaks
// K and J return null; S returns 99
9 9 9 JKS
// K and J return null
9 999 9999 JK

View File

@ -48,11 +48,11 @@ public class BigNumberFormatTest extends TestFmwk {
DecimalFormatSymbols US = new DecimalFormatSymbols(Locale.US);
DecimalFormat f = new DecimalFormat("#,##,###", US);
expect(f, new Long(123456789), "12,34,56,789");
expectPat(f, "#,##,###");
expectPat(f, "#,##,##0");
f.applyPattern("#,###");
f.setSecondaryGroupingSize(4);
expect(f, new Long(123456789), "12,3456,789");
expectPat(f, "#,####,###");
expectPat(f, "#,####,##0");
// On Sun JDK 1.2-1.3, the hi_IN locale uses '0' for a zero digit,
// but on IBM JDK 1.2-1.3, the locale uses U+0966.
@ -144,7 +144,7 @@ public class BigNumberFormatTest extends TestFmwk {
fmt.setFormatWidth(16);
// 12 34567890123456
expectPat(fmt, "AA*^#,###,##0.00ZZ");
expectPat(fmt, "AA*^#####,##0.00ZZ");
}
private void expectPat(DecimalFormat fmt, String exp) {

View File

@ -20,20 +20,15 @@ import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import org.junit.Test;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.text.CompactDecimalFormat;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.DecimalFormat;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ULocale;
@ -61,11 +56,13 @@ public class CompactDecimalFormatTest extends TestFmwk {
{1234567890123f, "1.2T"},
{12345678901234f, "12T"},
{123456789012345f, "120T"},
{12345678901234567890f, "12000000T"},
{12345678901234567890f, "12,000,000T"},
};
Object[][] SerbianTestDataShort = {
{1234, "1,2\u00A0\u0445\u0438\u0459."},
{1, "1"},
{12, "12"},
{123, "120"},
{12345, "12\u00a0хиљ."},
{20789, "21\u00a0хиљ."},
{123456, "120\u00a0хиљ."},
@ -82,6 +79,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
};
Object[][] SerbianTestDataLong = {
{1, "1"},
{12, "12"},
{123, "120"},
{1234, "1,2 хиљаде"},
{12345, "12 хиљада"},
{21789, "22 хиљаде"},
@ -102,6 +102,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
};
Object[][] SerbianTestDataLongNegative = {
{-1, "-1"},
{-12, "-12"},
{-123, "-120"},
{-1234, "-1,2 хиљаде"},
{-12345, "-12 хиљада"},
{-21789, "-22 хиљаде"},
@ -122,6 +125,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
};
Object[][] JapaneseTestData = {
{1f, "1"},
{12f, "12"},
{123f, "120"},
{1234f, "1200"},
{12345f, "1.2万"},
{123456f, "12万"},
@ -137,9 +143,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
};
Object[][] ChineseCurrencyTestData = {
// The first one should really have a in front, but the CLDR data is
// incorrect. See http://unicode.org/cldr/trac/ticket/9298 and update
// this test case when the CLDR ticket is fixed.
{new CurrencyAmount(1f, Currency.getInstance("CNY")), "¥1"},
{new CurrencyAmount(12f, Currency.getInstance("CNY")), "¥12"},
{new CurrencyAmount(123f, Currency.getInstance("CNY")), "¥120"},
{new CurrencyAmount(1234f, Currency.getInstance("CNY")), "¥1.2千"},
{new CurrencyAmount(12345f, Currency.getInstance("CNY")), "¥1.2万"},
{new CurrencyAmount(123456f, Currency.getInstance("CNY")), "¥12万"},
@ -154,6 +160,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
{new CurrencyAmount(123456789012345f, Currency.getInstance("CNY")), "¥120兆"},
};
Object[][] GermanCurrencyTestData = {
{new CurrencyAmount(1f, Currency.getInstance("EUR")), "1 "},
{new CurrencyAmount(12f, Currency.getInstance("EUR")), "12 "},
{new CurrencyAmount(123f, Currency.getInstance("EUR")), "120 "},
{new CurrencyAmount(1234f, Currency.getInstance("EUR")), "1,2 Tsd. €"},
{new CurrencyAmount(12345f, Currency.getInstance("EUR")), "12 Tsd. €"},
{new CurrencyAmount(123456f, Currency.getInstance("EUR")), "120 Tsd. €"},
@ -168,6 +177,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
{new CurrencyAmount(123456789012345f, Currency.getInstance("EUR")), "120 Bio. €"},
};
Object[][] EnglishCurrencyTestData = {
{new CurrencyAmount(1f, Currency.getInstance("USD")), "$1"},
{new CurrencyAmount(12f, Currency.getInstance("USD")), "$12"},
{new CurrencyAmount(123f, Currency.getInstance("USD")), "$120"},
{new CurrencyAmount(1234f, Currency.getInstance("USD")), "$1.2K"},
{new CurrencyAmount(12345f, Currency.getInstance("USD")), "$12K"},
{new CurrencyAmount(123456f, Currency.getInstance("USD")), "$120K"},
@ -183,6 +195,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
};
Object[][] SwahiliTestData = {
{1f, "1"},
{12f, "12"},
{123f, "120"},
{1234f, "elfu\u00a01.2"},
{12345f, "elfu\u00a012"},
{123456f, "elfu\u00A0120"},
@ -194,10 +209,13 @@ public class CompactDecimalFormatTest extends TestFmwk {
{123456789012f, "B120"},
{1234567890123f, "T1.2"},
{12345678901234f, "T12"},
{12345678901234567890f, "T12000000"},
{12345678901234567890f, "T12,000,000"},
};
Object[][] CsTestDataShort = {
{1, "1"},
{12, "12"},
{123, "120"},
{1000, "1\u00a0tis."},
{1500, "1,5\u00a0tis."},
{5000, "5\u00a0tis."},
@ -221,6 +239,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
};
Object[][] SwahiliTestDataNegative = {
{-1f, "-1"},
{-12f, "-12"},
{-123f, "-120"},
{-1234f, "elfu\u00a0-1.2"},
{-12345f, "elfu\u00a0-12"},
{-123456f, "elfu\u00A0-120"},
@ -232,7 +253,7 @@ public class CompactDecimalFormatTest extends TestFmwk {
{-123456789012f, "B-120"},
{-1234567890123f, "T-1.2"},
{-12345678901234f, "T-12"},
{-12345678901234567890f, "T-12000000"},
{-12345678901234567890f, "T-12,000,000"},
};
Object[][] TestACoreCompactFormatList = {
@ -249,74 +270,78 @@ public class CompactDecimalFormatTest extends TestFmwk {
{2000, "2Ks$s"},
};
@Test
public void TestACoreCompactFormat() {
Map<String,String[][]> affixes = new HashMap();
affixes.put("one", new String[][] {
{"","",}, {"","",}, {"","",},
{"","K"}, {"","K"}, {"","K"},
{"","M"}, {"","M"}, {"","M"},
{"","B"}, {"","B"}, {"","B"},
{"","T"}, {"","T"}, {"","T"},
});
affixes.put("other", new String[][] {
{"","",}, {"","",}, {"","",},
{"","Ks"}, {"","Ks"}, {"","Ks"},
{"","Ms"}, {"","Ms"}, {"","Ms"},
{"","Bs"}, {"","Bs"}, {"","Bs"},
{"","Ts"}, {"","Ts"}, {"","Ts"},
});
// TODO(sffc): Re-write these tests for the new CompactDecimalFormat pipeline
Map<String,String[]> currencyAffixes = new HashMap();
currencyAffixes.put("one", new String[] {"", "$"});
currencyAffixes.put("other", new String[] {"", "$s"});
// @Test
// public void TestACoreCompactFormat() {
// Map<String,String[][]> affixes = new HashMap();
// affixes.put("one", new String[][] {
// {"","",}, {"","",}, {"","",},
// {"","K"}, {"","K"}, {"","K"},
// {"","M"}, {"","M"}, {"","M"},
// {"","B"}, {"","B"}, {"","B"},
// {"","T"}, {"","T"}, {"","T"},
// });
// affixes.put("other", new String[][] {
// {"","",}, {"","",}, {"","",},
// {"","Ks"}, {"","Ks"}, {"","Ks"},
// {"","Ms"}, {"","Ms"}, {"","Ms"},
// {"","Bs"}, {"","Bs"}, {"","Bs"},
// {"","Ts"}, {"","Ts"}, {"","Ts"},
// });
//
// Map<String,String[]> currencyAffixes = new HashMap();
// currencyAffixes.put("one", new String[] {"", "$"});
// currencyAffixes.put("other", new String[] {"", "$s"});
//
// long[] divisors = new long[] {
// 0,0,0,
// 1000, 1000, 1000,
// 1000000, 1000000, 1000000,
// 1000000000L, 1000000000L, 1000000000L,
// 1000000000000L, 1000000000000L, 1000000000000L};
// long[] divisors_err = new long[] {
// 0,0,0,
// 13, 13, 13,
// 1000000, 1000000, 1000000,
// 1000000000L, 1000000000L, 1000000000L,
// 1000000000000L, 1000000000000L, 1000000000000L};
// checkCore(affixes, null, divisors, TestACoreCompactFormatList);
// checkCore(affixes, currencyAffixes, divisors, TestACoreCompactFormatListCurrency);
// try {
// checkCore(affixes, null, divisors_err, TestACoreCompactFormatList);
// } catch(AssertionError e) {
// // Exception expected, thus return.
// return;
// }
// fail("Error expected but passed");
// }
long[] divisors = new long[] {
0,0,0,
1000, 1000, 1000,
1000000, 1000000, 1000000,
1000000000L, 1000000000L, 1000000000L,
1000000000000L, 1000000000000L, 1000000000000L};
long[] divisors_err = new long[] {
0,0,0,
13, 13, 13,
1000000, 1000000, 1000000,
1000000000L, 1000000000L, 1000000000L,
1000000000000L, 1000000000000L, 1000000000000L};
checkCore(affixes, null, divisors, TestACoreCompactFormatList);
checkCore(affixes, currencyAffixes, divisors, TestACoreCompactFormatListCurrency);
try {
checkCore(affixes, null, divisors_err, TestACoreCompactFormatList);
} catch(AssertionError e) {
// Exception expected, thus return.
return;
}
fail("Error expected but passed");
}
private void checkCore(Map<String, String[][]> affixes, Map<String, String[]> currencyAffixes, long[] divisors, Object[][] testItems) {
Collection<String> debugCreationErrors = new LinkedHashSet();
CompactDecimalFormat cdf = new CompactDecimalFormat(
"#,###.00",
DecimalFormatSymbols.getInstance(new ULocale("fr")),
CompactStyle.SHORT, PluralRules.createRules("one: j is 1 or f is 1"),
divisors, affixes, currencyAffixes,
debugCreationErrors
);
if (debugCreationErrors.size() != 0) {
for (String s : debugCreationErrors) {
errln("Creation error: " + s);
}
} else {
checkCdf("special cdf ", cdf, testItems);
}
}
// private void checkCore(Map<String, String[][]> affixes, Map<String, String[]> currencyAffixes, long[] divisors, Object[][] testItems) {
// Collection<String> debugCreationErrors = new LinkedHashSet();
// CompactDecimalFormat cdf = new CompactDecimalFormat(
// "#,###.00",
// DecimalFormatSymbols.getInstance(new ULocale("fr")),
// CompactStyle.SHORT, PluralRules.createRules("one: j is 1 or f is 1"),
// divisors, affixes, currencyAffixes,
// debugCreationErrors
// );
// if (debugCreationErrors.size() != 0) {
// for (String s : debugCreationErrors) {
// errln("Creation error: " + s);
// }
// } else {
// checkCdf("special cdf ", cdf, testItems);
// }
// }
@Test
public void TestDefaultSignificantDigits() {
// We are expecting two significant digits as default.
// We are expecting two significant digits for compact formats with one or two zeros,
// and rounded to the unit for compact formats with three or more zeros.
CompactDecimalFormat cdf =
CompactDecimalFormat.getInstance(ULocale.ENGLISH, CompactStyle.SHORT);
assertEquals("Default significant digits", "120K", cdf.format(123456));
assertEquals("Default significant digits", "12K", cdf.format(12345));
assertEquals("Default significant digits", "1.2K", cdf.format(1234));
assertEquals("Default significant digits", "120", cdf.format(123));
@ -432,10 +457,11 @@ public class CompactDecimalFormatTest extends TestFmwk {
CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(
ULocale.ENGLISH, CompactStyle.LONG);
BigInteger source_int = new BigInteger("31415926535897932384626433");
assertEquals("BigInteger format wrong: ", "31,000,000,000,000 trillion",
cdf.setMaximumFractionDigits(0);
assertEquals("BigInteger format wrong: ", "31,415,926,535,898 trillion",
cdf.format(source_int));
BigDecimal source_dec = new BigDecimal(source_int);
assertEquals("BigDecimal format wrong: ", "31,000,000,000,000 trillion",
assertEquals("BigDecimal format wrong: ", "31,415,926,535,898 trillion",
cdf.format(source_dec));
}
@ -548,7 +574,7 @@ public class CompactDecimalFormatTest extends TestFmwk {
result = cdf.format(new CurrencyAmount(43000f, Currency.getInstance("USD")));
assertEquals("CDF should correctly format 43000 with currency in 'ar'", "US$ ٤٣ ألف", result);
result = cdf.format(new CurrencyAmount(-43000f, Currency.getInstance("USD")));
assertEquals("CDF should correctly format -43000 with currency in 'ar'", "US$ ؜-٤٣ ألف", result);
assertEquals("CDF should correctly format -43000 with currency in 'ar'", "؜-US$ ٤٣ ألف", result);
// Extra locale with different positive/negative formats
cdf = CompactDecimalFormat.getInstance(new ULocale("fi"), CompactDecimalFormat.CompactStyle.SHORT);
@ -590,4 +616,42 @@ public class CompactDecimalFormatTest extends TestFmwk {
result = cdf.format(new CurrencyAmount(123000, Currency.getInstance("EUR")));
assertEquals("CDF should correctly format 123000 with currency in 'it'", "120000 €", result);
}
@Test
public void TestBug11319() {
if (logKnownIssue("11319", "CDF does not fall back from zh-Hant-HK to zh-Hant")) {
return;
}
CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(new ULocale("zh-Hant-HK"), CompactStyle.SHORT);
String result = cdf.format(958000000L);
assertEquals("CDF should correctly format 958 million in zh-Hant-HK", "9.6億", result);
}
@Test
public void TestBug12975() {
ULocale locale = new ULocale("it");
CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(locale, CompactStyle.SHORT);
String resultCdf = cdf.format(120000);
DecimalFormat df = (DecimalFormat) DecimalFormat.getInstance(locale);
String resultDefault = df.format(120000);
assertEquals("CompactDecimalFormat should use default pattern when compact pattern is unavailable",
resultDefault, resultCdf);
}
@Test
public void TestBug11534() {
ULocale locale = new ULocale("pt_PT");
CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(locale, CompactStyle.SHORT);
String result = cdf.format(1000);
assertEquals("pt_PT should fall back to pt", "1 mil", result);
}
@Test
public void TestBug12181() {
ULocale loc = ULocale.ENGLISH;
CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(loc, CompactStyle.SHORT);
String s = cdf.format(-1500);
assertEquals("Should work with negative numbers", "-1.5K", s);
}
}

View File

@ -50,7 +50,7 @@ import com.ibm.icu.util.ULocale;
*
* @author rocketman
*/
public class NumberFormatTestData {
public class DataDrivenNumberFormatTestData {
/**
* The locale.
@ -117,6 +117,7 @@ public class NumberFormatTestData {
public String plural = null;
public Integer parseIntegerOnly = null;
public Integer decimalPatternMatchRequired = null;
public Integer parseCaseSensitive = null;
public Integer parseNoExponent = null;
public String outputCurrency = null;
@ -409,6 +410,10 @@ public class NumberFormatTestData {
parseIntegerOnly = Integer.valueOf(value);
}
public void setParseCaseSensitive(String value) {
parseCaseSensitive = Integer.valueOf(value);
}
public void setDecimalPatternMatchRequired(String value) {
decimalPatternMatchRequired = Integer.valueOf(value);
}
@ -482,6 +487,7 @@ public class NumberFormatTestData {
}
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append("{");

View File

@ -9,7 +9,9 @@
package com.ibm.icu.dev.test.format;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
@ -44,7 +46,8 @@ public class DataDrivenNumberFormatTestUtility {
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String format(NumberFormatTestData tuple) {
public String format(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
@ -54,7 +57,8 @@ public class DataDrivenNumberFormatTestUtility {
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String toPattern(NumberFormatTestData tuple) {
public String toPattern(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
@ -64,7 +68,8 @@ public class DataDrivenNumberFormatTestUtility {
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String parse(NumberFormatTestData tuple) {
public String parse(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
@ -74,7 +79,8 @@ public class DataDrivenNumberFormatTestUtility {
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String parseCurrency(NumberFormatTestData tuple) {
public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
@ -84,7 +90,8 @@ public class DataDrivenNumberFormatTestUtility {
* Subclasses should override.
* @param tuple contains the parameters of the format test.
*/
public String select(NumberFormatTestData tuple) {
public String select(DataDrivenNumberFormatTestData tuple) {
if (tuple.output != null && tuple.output.equals("fail")) return "fail";
return null;
}
}
@ -98,7 +105,7 @@ public class DataDrivenNumberFormatTestUtility {
private String fileLine = null;
private int fileLineNumber = 0;
private String fileTestName = "";
private NumberFormatTestData tuple = new NumberFormatTestData();
private DataDrivenNumberFormatTestData tuple = new DataDrivenNumberFormatTestData();
/**
* Runs all the tests in the data driven test suite against codeUnderTest.
@ -107,7 +114,7 @@ public class DataDrivenNumberFormatTestUtility {
* @param codeUnderTest the code under test
*/
static void runSuite(
public static void runSuite(
String fileName, CodeUnderTest codeUnderTest) {
new DataDrivenNumberFormatTestUtility(codeUnderTest)
.run(fileName, RunMode.SKIP_KNOWN_FAILURES);
@ -115,13 +122,14 @@ public class DataDrivenNumberFormatTestUtility {
/**
* Runs every format test in data driven test suite including those
* that are known to fail.
* that are known to fail. If a test is supposed to fail but actually
* passes, an error is printed.
*
* @param fileName The name of the test file. A relative file name under
* com/ibm/icu/dev/data such as "data.txt"
* @param codeUnderTest the code under test
*/
static void runFormatSuiteIncludingKnownFailures(
public static void runFormatSuiteIncludingKnownFailures(
String fileName, CodeUnderTest codeUnderTest) {
new DataDrivenNumberFormatTestUtility(codeUnderTest)
.run(fileName, RunMode.INCLUDE_KNOWN_FAILURES);
@ -166,7 +174,7 @@ public class DataDrivenNumberFormatTestUtility {
if (state == 0) {
if (fileLine.startsWith("test ")) {
fileTestName = fileLine;
tuple = new NumberFormatTestData();
tuple = new DataDrivenNumberFormatTestData();
} else if (fileLine.startsWith("set ")) {
if (!setTupleField()) {
return;
@ -196,19 +204,41 @@ public class DataDrivenNumberFormatTestUtility {
return;
}
}
if (runMode == RunMode.INCLUDE_KNOWN_FAILURES
|| !breaks(codeUnderTestId)) {
String errorMessage = isPass(tuple);
if (errorMessage != null) {
if (runMode == RunMode.INCLUDE_KNOWN_FAILURES || !breaks(codeUnderTestId)) {
String errorMessage;
Exception err = null;
boolean shouldFail = (tuple.output != null && tuple.output.equals("fail"))
? !breaks(codeUnderTestId)
: breaks(codeUnderTestId);
try {
errorMessage = isPass(tuple);
} catch (Exception e) {
err = e;
errorMessage = "Exception: " + e + ": " + e.getCause();
}
if (shouldFail && errorMessage == null) {
showError("Expected failure, but passed");
} else if (!shouldFail && errorMessage != null) {
if (err != null) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(os);
err.printStackTrace(ps);
String stackTrace = os.toString();
showError(errorMessage + " Stack trace: " + stackTrace.substring(0, 500));
} else {
showError(errorMessage);
}
}
}
}
fileLine = null;
}
} catch (Exception e) {
showError(e.toString());
e.printStackTrace();
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(os);
e.printStackTrace(ps);
String stackTrace = os.toString();
showError("MAJOR ERROR: " + e.toString() + " Stack trace: " + stackTrace.substring(0,500));
} finally {
try {
if (in != null) {
@ -302,7 +332,7 @@ public class DataDrivenNumberFormatTestUtility {
return true;
}
private String isPass(NumberFormatTestData tuple) {
private String isPass(DataDrivenNumberFormatTestData tuple) {
StringBuilder result = new StringBuilder();
if (tuple.format != null && tuple.output != null) {
String errorMessage = codeUnderTest.format(tuple);

View File

@ -298,7 +298,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
DecimalFormat decfmt = new DecimalFormat();
MathContext resultICU;
MathContext comp1 = new MathContext(0, MathContext.PLAIN);
MathContext comp1 = new MathContext(0, MathContext.PLAIN, false, MathContext.ROUND_HALF_EVEN);
resultICU = decfmt.getMathContextICU();
if ((comp1.getDigits() != resultICU.getDigits()) ||
(comp1.getForm() != resultICU.getForm()) ||
@ -309,7 +309,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
" / expected: " + comp1.toString());
}
MathContext comp2 = new MathContext(5, MathContext.ENGINEERING);
MathContext comp2 = new MathContext(5, MathContext.ENGINEERING, false, MathContext.ROUND_HALF_EVEN);
decfmt.setMathContextICU(comp2);
resultICU = decfmt.getMathContextICU();
if ((comp2.getDigits() != resultICU.getDigits()) ||

View File

@ -228,7 +228,7 @@ public class IntlTestDecimalFormatAPIC extends com.ibm.icu.dev.test.TestFmwk {
s2 = pat.toPattern();
logln("Extracted pattern is " + s2);
if (!s2.equals(p1)) {
errln("ERROR: toPattern() result did not match pattern applied");
errln("ERROR: toPattern() result did not match pattern applied: " + p1 + " vs " + s2);
}
String p2 = new String("#,##0.0# FF;(#,##0.0# FF)");
@ -237,9 +237,7 @@ public class IntlTestDecimalFormatAPIC extends com.ibm.icu.dev.test.TestFmwk {
String s3;
s3 = pat.toLocalizedPattern();
logln("Extracted pattern is " + s3);
if (!s3.equals(p2)) {
errln("ERROR: toLocalizedPattern() result did not match pattern applied");
}
assertEquals("ERROR: toLocalizedPattern() result did not match pattern applied", p2, s3);
// ======= Test getStaticClassID()

View File

@ -124,7 +124,7 @@ public class IntlTestDecimalFormatSymbolsC extends com.ibm.icu.dev.test.TestFmwk
sym.setPercent('P');
verify(34.5, "00 %", sym, "3450 P");
sym.setCurrencySymbol("D");
verify(34.5, "\u00a4##.##", sym, "D34.5");
verify(34.5, "\u00a4##.##", sym, "D34.50");
sym.setGroupingSeparator('|');
verify(3456.5, "0,000.##", sym, "3|456S5");
}

View File

@ -30,6 +30,7 @@ import java.util.TreeMap;
import org.junit.Test;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.dev.test.serializable.FormatHandler;
import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
import com.ibm.icu.impl.Pair;
import com.ibm.icu.impl.Utility;
@ -2035,6 +2036,13 @@ public class MeasureUnitTest extends TestFmwk {
}
}
@Test
public void testBug11966() {
Locale locale = new Locale("en", "AU");
MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE);
// Should not throw an exception.
}
// DO NOT DELETE THIS FUNCTION! It may appear as dead code, but we use this to generate code
// for MeasureFormat during the release process.
static Map<MeasureUnit, Pair<MeasureUnit, MeasureUnit>> getUnitsToPerParts() {
@ -2528,6 +2536,8 @@ public class MeasureUnitTest extends TestFmwk {
public static class MeasureFormatHandler implements SerializableTestUtility.Handler
{
FormatHandler.NumberFormatHandler nfh = new FormatHandler.NumberFormatHandler();
@Override
public Object[] getTestObjects()
{
@ -2547,8 +2557,7 @@ public class MeasureUnitTest extends TestFmwk {
MeasureFormat b1 = (MeasureFormat) b;
return a1.getLocale().equals(b1.getLocale())
&& a1.getWidth().equals(b1.getWidth())
&& a1.getNumberFormat().equals(b1.getNumberFormat())
;
&& nfh.hasSameBehavior(a1.getNumberFormat(), b1.getNumberFormat());
}
}
}

View File

@ -0,0 +1,434 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.format;
import java.math.BigDecimal;
import java.text.ParsePosition;
import org.junit.Test;
import com.ibm.icu.dev.test.number.ShanesDataDrivenTestUtility;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.DecimalFormat_ICU58;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ULocale;
public class NumberFormatDataDrivenTest {
private static ULocale EN = new ULocale("en");
private static Number toNumber(String s) {
if (s.equals("NaN")) {
return Double.NaN;
} else if (s.equals("-Inf")) {
return Double.NEGATIVE_INFINITY;
} else if (s.equals("Inf")) {
return Double.POSITIVE_INFINITY;
}
return new BigDecimal(s);
}
private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU58 =
new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
@Override
public Character Id() {
return 'J';
}
@Override
public String format(DataDrivenNumberFormatTestData tuple) {
DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
String actual = fmt.format(toNumber(tuple.format));
String expected = tuple.output;
if (!expected.equals(actual)) {
return "Expected " + expected + ", got " + actual;
}
return null;
}
@Override
public String toPattern(DataDrivenNumberFormatTestData tuple) {
DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
StringBuilder result = new StringBuilder();
if (tuple.toPattern != null) {
String expected = tuple.toPattern;
String actual = fmt.toPattern();
if (!expected.equals(actual)) {
result.append("Expected toPattern=" + expected + ", got " + actual);
}
}
if (tuple.toLocalizedPattern != null) {
String expected = tuple.toLocalizedPattern;
String actual = fmt.toLocalizedPattern();
if (!expected.equals(actual)) {
result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
}
}
return result.length() == 0 ? null : result.toString();
}
@Override
public String parse(DataDrivenNumberFormatTestData tuple) {
DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
ParsePosition ppos = new ParsePosition(0);
Number actual = fmt.parse(tuple.parse, ppos);
if (ppos.getIndex() == 0) {
return "Parse failed; got " + actual + ", but expected " + tuple.output;
}
if (tuple.output.equals("fail")) {
return null;
}
Number expected = toNumber(tuple.output);
// number types cannot be compared, this is the best we can do.
if (expected.doubleValue() != actual.doubleValue()
&& !Double.isNaN(expected.doubleValue())
&& !Double.isNaN(expected.doubleValue())) {
return "Expected: " + expected + ", got: " + actual;
}
return null;
}
@Override
public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
ParsePosition ppos = new ParsePosition(0);
CurrencyAmount currAmt = fmt.parseCurrency(tuple.parse, ppos);
if (ppos.getIndex() == 0) {
return "Parse failed; got " + currAmt + ", but expected " + tuple.output;
}
if (tuple.output.equals("fail")) {
return null;
}
Number expected = toNumber(tuple.output);
Number actual = currAmt.getNumber();
// number types cannot be compared, this is the best we can do.
if (expected.doubleValue() != actual.doubleValue()
&& !Double.isNaN(expected.doubleValue())
&& !Double.isNaN(expected.doubleValue())) {
return "Expected: " + expected + ", got: " + actual;
}
if (!tuple.outputCurrency.equals(currAmt.getCurrency().toString())) {
return "Expected currency: " + tuple.outputCurrency + ", got: " + currAmt.getCurrency();
}
return null;
}
/**
* @param tuple
* @return
*/
private DecimalFormat_ICU58 createDecimalFormat(DataDrivenNumberFormatTestData tuple) {
DecimalFormat_ICU58 fmt =
new DecimalFormat_ICU58(
tuple.pattern == null ? "0" : tuple.pattern,
new DecimalFormatSymbols(tuple.locale == null ? EN : tuple.locale));
adjustDecimalFormat(tuple, fmt);
return fmt;
}
/**
* @param tuple
* @param fmt
*/
private void adjustDecimalFormat(
DataDrivenNumberFormatTestData tuple, DecimalFormat_ICU58 fmt) {
if (tuple.minIntegerDigits != null) {
fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
}
if (tuple.maxIntegerDigits != null) {
fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
}
if (tuple.minFractionDigits != null) {
fmt.setMinimumFractionDigits(tuple.minFractionDigits);
}
if (tuple.maxFractionDigits != null) {
fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
}
if (tuple.currency != null) {
fmt.setCurrency(tuple.currency);
}
if (tuple.minGroupingDigits != null) {
// Oops we don't support this.
}
if (tuple.useSigDigits != null) {
fmt.setSignificantDigitsUsed(tuple.useSigDigits != 0);
}
if (tuple.minSigDigits != null) {
fmt.setMinimumSignificantDigits(tuple.minSigDigits);
}
if (tuple.maxSigDigits != null) {
fmt.setMaximumSignificantDigits(tuple.maxSigDigits);
}
if (tuple.useGrouping != null) {
fmt.setGroupingUsed(tuple.useGrouping != 0);
}
if (tuple.multiplier != null) {
fmt.setMultiplier(tuple.multiplier);
}
if (tuple.roundingIncrement != null) {
fmt.setRoundingIncrement(tuple.roundingIncrement.doubleValue());
}
if (tuple.formatWidth != null) {
fmt.setFormatWidth(tuple.formatWidth);
}
if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
fmt.setPadCharacter(tuple.padCharacter.charAt(0));
}
if (tuple.useScientific != null) {
fmt.setScientificNotation(tuple.useScientific != 0);
}
if (tuple.grouping != null) {
fmt.setGroupingSize(tuple.grouping);
}
if (tuple.grouping2 != null) {
fmt.setSecondaryGroupingSize(tuple.grouping2);
}
if (tuple.roundingMode != null) {
fmt.setRoundingMode(tuple.roundingMode);
}
if (tuple.currencyUsage != null) {
fmt.setCurrencyUsage(tuple.currencyUsage);
}
if (tuple.minimumExponentDigits != null) {
fmt.setMinimumExponentDigits(tuple.minimumExponentDigits.byteValue());
}
if (tuple.exponentSignAlwaysShown != null) {
fmt.setExponentSignAlwaysShown(tuple.exponentSignAlwaysShown != 0);
}
if (tuple.decimalSeparatorAlwaysShown != null) {
fmt.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
}
if (tuple.padPosition != null) {
fmt.setPadPosition(tuple.padPosition);
}
if (tuple.positivePrefix != null) {
fmt.setPositivePrefix(tuple.positivePrefix);
}
if (tuple.positiveSuffix != null) {
fmt.setPositiveSuffix(tuple.positiveSuffix);
}
if (tuple.negativePrefix != null) {
fmt.setNegativePrefix(tuple.negativePrefix);
}
if (tuple.negativeSuffix != null) {
fmt.setNegativeSuffix(tuple.negativeSuffix);
}
if (tuple.localizedPattern != null) {
fmt.applyLocalizedPattern(tuple.localizedPattern);
}
int lenient = tuple.lenient == null ? 1 : tuple.lenient.intValue();
fmt.setParseStrict(lenient == 0);
if (tuple.parseIntegerOnly != null) {
fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
}
if (tuple.parseCaseSensitive != null) {
// Not supported.
}
if (tuple.decimalPatternMatchRequired != null) {
fmt.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0);
}
if (tuple.parseNoExponent != null) {
// Oops, not supported for now
}
}
};
private DataDrivenNumberFormatTestUtility.CodeUnderTest JDK =
new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
@Override
public Character Id() {
return 'K';
}
@Override
public String format(DataDrivenNumberFormatTestData tuple) {
java.text.DecimalFormat fmt = createDecimalFormat(tuple);
String actual = fmt.format(toNumber(tuple.format));
String expected = tuple.output;
if (!expected.equals(actual)) {
return "Expected " + expected + ", got " + actual;
}
return null;
}
@Override
public String toPattern(DataDrivenNumberFormatTestData tuple) {
java.text.DecimalFormat fmt = createDecimalFormat(tuple);
StringBuilder result = new StringBuilder();
if (tuple.toPattern != null) {
String expected = tuple.toPattern;
String actual = fmt.toPattern();
if (!expected.equals(actual)) {
result.append("Expected toPattern=" + expected + ", got " + actual);
}
}
if (tuple.toLocalizedPattern != null) {
String expected = tuple.toLocalizedPattern;
String actual = fmt.toLocalizedPattern();
if (!expected.equals(actual)) {
result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
}
}
return result.length() == 0 ? null : result.toString();
}
@Override
public String parse(DataDrivenNumberFormatTestData tuple) {
java.text.DecimalFormat fmt = createDecimalFormat(tuple);
ParsePosition ppos = new ParsePosition(0);
Number actual = fmt.parse(tuple.parse, ppos);
if (ppos.getIndex() == 0) {
return "Parse failed; got " + actual + ", but expected " + tuple.output;
}
if (tuple.output.equals("fail")) {
return null;
}
Number expected = toNumber(tuple.output);
// number types cannot be compared, this is the best we can do.
if (expected.doubleValue() != actual.doubleValue()
&& !Double.isNaN(expected.doubleValue())
&& !Double.isNaN(expected.doubleValue())) {
return "Expected: " + expected + ", got: " + actual;
}
return null;
}
/**
* @param tuple
* @return
*/
private java.text.DecimalFormat createDecimalFormat(DataDrivenNumberFormatTestData tuple) {
java.text.DecimalFormat fmt =
new java.text.DecimalFormat(
tuple.pattern == null ? "0" : tuple.pattern,
new java.text.DecimalFormatSymbols(
(tuple.locale == null ? EN : tuple.locale).toLocale()));
adjustDecimalFormat(tuple, fmt);
return fmt;
}
/**
* @param tuple
* @param fmt
*/
private void adjustDecimalFormat(
DataDrivenNumberFormatTestData tuple, java.text.DecimalFormat fmt) {
if (tuple.minIntegerDigits != null) {
fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
}
if (tuple.maxIntegerDigits != null) {
fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
}
if (tuple.minFractionDigits != null) {
fmt.setMinimumFractionDigits(tuple.minFractionDigits);
}
if (tuple.maxFractionDigits != null) {
fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
}
if (tuple.currency != null) {
fmt.setCurrency(java.util.Currency.getInstance(tuple.currency.toString()));
}
if (tuple.minGroupingDigits != null) {
// Oops we don't support this.
}
if (tuple.useSigDigits != null) {
// Oops we don't support this
}
if (tuple.minSigDigits != null) {
// Oops we don't support this
}
if (tuple.maxSigDigits != null) {
// Oops we don't support this
}
if (tuple.useGrouping != null) {
fmt.setGroupingUsed(tuple.useGrouping != 0);
}
if (tuple.multiplier != null) {
fmt.setMultiplier(tuple.multiplier);
}
if (tuple.roundingIncrement != null) {
// Not supported
}
if (tuple.formatWidth != null) {
// Not supported
}
if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
// Not supported
}
if (tuple.useScientific != null) {
// Not supported
}
if (tuple.grouping != null) {
fmt.setGroupingSize(tuple.grouping);
}
if (tuple.grouping2 != null) {
// Not supported
}
if (tuple.roundingMode != null) {
// Not supported
}
if (tuple.currencyUsage != null) {
// Not supported
}
if (tuple.minimumExponentDigits != null) {
// Not supported
}
if (tuple.exponentSignAlwaysShown != null) {
// Not supported
}
if (tuple.decimalSeparatorAlwaysShown != null) {
fmt.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
}
if (tuple.padPosition != null) {
// Not supported
}
if (tuple.positivePrefix != null) {
fmt.setPositivePrefix(tuple.positivePrefix);
}
if (tuple.positiveSuffix != null) {
fmt.setPositiveSuffix(tuple.positiveSuffix);
}
if (tuple.negativePrefix != null) {
fmt.setNegativePrefix(tuple.negativePrefix);
}
if (tuple.negativeSuffix != null) {
fmt.setNegativeSuffix(tuple.negativeSuffix);
}
if (tuple.localizedPattern != null) {
fmt.applyLocalizedPattern(tuple.localizedPattern);
}
// lenient parsing not supported by JDK
if (tuple.parseIntegerOnly != null) {
fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
}
if (tuple.parseCaseSensitive != null) {
// Not supported.
}
if (tuple.decimalPatternMatchRequired != null) {
// Oops, not supported
}
if (tuple.parseNoExponent != null) {
// Oops, not supported for now
}
}
};
@Test
public void TestDataDrivenICU58() {
DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
"numberformattestspecification.txt", ICU58);
}
@Test
public void TestDataDrivenJDK() {
DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
"numberformattestspecification.txt", JDK);
}
@Test
public void TestDataDrivenShane() {
ShanesDataDrivenTestUtility.run();
}
}

View File

@ -176,17 +176,13 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
byte[][] contents = NumberFormatSerialTestData.getContent();
double data = 1234.56;
String[] expected = {
"1,234.56", "$1,234.56", "123,456%", "1.23456E3"};
"1,234.56", "$1,234.56", "1.23456E3", "1,234.56"};
for (int i = 0; i < 4; ++i) {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(contents[i]));
try {
NumberFormat format = (NumberFormat) ois.readObject();
String result = format.format(data);
if (result.equals(expected[i])) {
logln("OK: Deserialized bogus NumberFormat(new version read old version)");
} else {
errln("FAIL: the test data formats are not euqal");
}
assertEquals("Deserialization new version should read old version", expected[i], result);
} catch (Exception e) {
warnln("FAIL: " + e.getMessage());
}
@ -385,4 +381,14 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
errln("FAIL: Parsed result: " + num + " - expected: " + val);
}
}
@Test
public void TestAffixesNoCurrency() {
ULocale locale = new ULocale("en");
DecimalFormat nf = (DecimalFormat) NumberFormat.getInstance(locale, NumberFormat.PLURALCURRENCYSTYLE);
assertEquals(
"Positive suffix should contain the single currency sign when no currency is set",
" \u00A4",
nf.getPositiveSuffix());
}
}

View File

@ -10,13 +10,9 @@
package com.ibm.icu.dev.test.format;
public class NumberFormatSerialTestData {
//get Content
public static byte[][] getContent() {
return content;
}
//NumberFormat.getInstance(Locale.US)
static byte[] generalInstance = new byte[]{
static byte[] generalInstance() { return new byte[] {
-84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1,
3, 98, -40, 114, 48, 58, 2, 0, 22, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83,
@ -85,10 +81,10 @@ public class NumberFormatSerialTestData {
0, 35, 0, 0, 0, 44, 0, 45, 0, 46, 0, 42, 0, 59, 32, 48, 0, 37, 0, 43,
0, 0, 0, 2, 0, 48, 116, 0, 3, -17, -65, -67, 116, 0, 1, 36, 116, 0, 1, 69,
116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
};
};};
//NumberFormat.getCurrencyInstance(Locale.US)
static byte[] currencyInstance = new byte[]{
static byte[] currencyInstance () { return new byte[] {
-84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1,
3, 98, -40, 114, 48, 58, 2, 0, 22, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83,
@ -158,82 +154,10 @@ public class NumberFormatSerialTestData {
0, 59, 32, 48, 0, 37, 0, 43, 0, 0, 0, 2, 0, 48, 116, 0, 3, -17, -65, -67,
116, 0, 1, 36, 116, 0, 1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
};
//NumberFormat.getPercentInstance(Locale.US)
static byte[] percentInstance = new byte[]{
-84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1,
3, 98, -40, 114, 48, 58, 2, 0, 22, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83,
101, 112, 97, 114, 97, 116, 111, 114, 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 90,
0, 23, 101, 120, 112, 111, 110, 101, 110, 116, 83, 105, 103, 110, 65, 108, 119, 97, 121, 115,
83, 104, 111, 119, 110, 73, 0, 11, 102, 111, 114, 109, 97, 116, 87, 105, 100, 116, 104, 66,
0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 83, 105, 122, 101, 66, 0, 13, 103, 114, 111,
117, 112, 105, 110, 103, 83, 105, 122, 101, 50, 66, 0, 17, 109, 105, 110, 69, 120, 112, 111,
110, 101, 110, 116, 68, 105, 103, 105, 116, 115, 73, 0, 10, 109, 117, 108, 116, 105, 112, 108,
105, 101, 114, 67, 0, 3, 112, 97, 100, 73, 0, 11, 112, 97, 100, 80, 111, 115, 105, 116,
105, 111, 110, 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 73, 0,
21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101,
97, 109, 90, 0, 22, 117, 115, 101, 69, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 78,
111, 116, 97, 116, 105, 111, 110, 76, 0, 16, 110, 101, 103, 80, 114, 101, 102, 105, 120, 80,
97, 116, 116, 101, 114, 110, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47,
83, 116, 114, 105, 110, 103, 59, 76, 0, 16, 110, 101, 103, 83, 117, 102, 102, 105, 120, 80,
97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 1, 76, 0, 14, 110, 101, 103, 97, 116, 105,
118, 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 1, 76, 0, 14, 110, 101, 103, 97,
116, 105, 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 1, 76, 0, 16, 112, 111,
115, 80, 114, 101, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 1, 76,
0, 16, 112, 111, 115, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0,
126, 0, 1, 76, 0, 14, 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120,
113, 0, 126, 0, 1, 76, 0, 14, 112, 111, 115, 105, 116, 105, 118, 101, 83, 117, 102, 102,
105, 120, 113, 0, 126, 0, 1, 76, 0, 17, 114, 111, 117, 110, 100, 105, 110, 103, 73, 110,
99, 114, 101, 109, 101, 110, 116, 116, 0, 22, 76, 106, 97, 118, 97, 47, 109, 97, 116, 104,
47, 66, 105, 103, 68, 101, 99, 105, 109, 97, 108, 59, 76, 0, 7, 115, 121, 109, 98, 111,
108, 115, 116, 0, 39, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 116, 101,
120, 116, 47, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98,
111, 108, 115, 59, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, 109, 97, 116, -33, -10, -77,
-65, 19, 125, 7, -24, 3, 0, 11, 90, 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 85,
115, 101, 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103,
105, 116, 115, 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105,
116, 115, 73, 0, 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, 110,
68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, 101,
103, 101, 114, 68, 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116,
105, 111, 110, 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103,
101, 114, 68, 105, 103, 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114,
97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109,
117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97,
114, 115, 101, 73, 110, 116, 101, 103, 101, 114, 79, 110, 108, 121, 73, 0, 21, 115, 101, 114,
105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 120, 114,
0, 16, 106, 97, 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, 116, -5, -40,
-68, 18, -23, 15, 24, 67, 2, 0, 0, 120, 112, 1, 0, 127, 0, 0, 0, 0, 0, 0,
1, 53, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 120, 0, 0,
0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 100, 0, 32, 0, 0, 0, 0, 0, 0, 0,
6, 0, 0, 0, 2, 0, 116, 0, 1, 45, 116, 0, 1, 37, 116, 0, 1, 45, 116, 0,
1, 37, 116, 0, 0, 113, 0, 126, 0, 8, 116, 0, 0, 116, 0, 1, 37, 112, 115, 114,
0, 37, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68,
101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 111, 108, 115, 80,
29, 23, -103, 8, 104, -109, -100, 2, 0, 18, 67, 0, 16, 100, 101, 99, 105, 109, 97, 108,
83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, 105, 103, 105, 116, 67, 0, 11,
101, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, 103, 114, 111, 117, 112, 105,
110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, 105, 110, 117, 115, 83,
105, 103, 110, 67, 0, 17, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97,
116, 111, 114, 67, 0, 9, 112, 97, 100, 69, 115, 99, 97, 112, 101, 67, 0, 16, 112, 97,
116, 116, 101, 114, 110, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 7, 112, 101, 114,
77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, 101, 110, 116, 67, 0, 8, 112, 108, 117,
115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111,
110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 9, 122, 101, 114, 111, 68, 105, 103, 105,
116, 76, 0, 3, 78, 97, 78, 113, 0, 126, 0, 1, 76, 0, 14, 99, 117, 114, 114, 101,
110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 1, 76, 0, 17, 101, 120, 112,
111, 110, 101, 110, 116, 83, 101, 112, 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 1, 76,
0, 8, 105, 110, 102, 105, 110, 105, 116, 121, 113, 0, 126, 0, 1, 76, 0, 18, 105, 110,
116, 108, 67, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0,
1, 120, 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 46, 0, 42, 0, 59, 32,
48, 0, 37, 0, 43, 0, 0, 0, 2, 0, 48, 116, 0, 3, -17, -65, -67, 116, 0, 1,
36, 116, 0, 1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
};
};};
//NumberFormat.getScientificInstance(Locale.US)
static byte[] scientificInstance = new byte[]{
static byte[] scientificInstance() { return new byte[] {
-84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1,
3, 98, -40, 114, 48, 58, 2, 0, 22, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83,
@ -302,7 +226,295 @@ public class NumberFormatSerialTestData {
0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 46, 0, 42, 0, 59, 32, 48, 0, 37,
0, 43, 0, 0, 0, 2, 0, 48, 116, 0, 3, -17, -65, -67, 116, 0, 1, 36, 116, 0,
1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
};
};};
final static byte[][] content = {generalInstance, currencyInstance, percentInstance, scientificInstance};
static byte[] icu58Latest() { return new byte[] {
-84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101,
120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 3, 98, -40,
114, 48, 58, 3, 0, 36, 73, 0, 18, 80, 65, 82, 83, 69, 95, 77, 65, 88, 95, 69, 88, 80, 79, 78,
69, 78, 84, 73, 0, 17, 99, 117, 114, 114, 101, 110, 99, 121, 83, 105, 103, 110, 67, 111, 117,
110, 116, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114,
65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 90, 0, 23, 101, 120, 112, 111, 110, 101,
110, 116, 83, 105, 103, 110, 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 73, 0, 11, 102,
111, 114, 109, 97, 116, 87, 105, 100, 116, 104, 66, 0, 12, 103, 114, 111, 117, 112, 105, 110,
103, 83, 105, 122, 101, 66, 0, 13, 103, 114, 111, 117, 112, 105, 110, 103, 83, 105, 122, 101,
50, 73, 0, 20, 109, 97, 120, 83, 105, 103, 110, 105, 102, 105, 99, 97, 110, 116, 68, 105, 103,
105, 116, 115, 66, 0, 17, 109, 105, 110, 69, 120, 112, 111, 110, 101, 110, 116, 68, 105, 103,
105, 116, 115, 73, 0, 20, 109, 105, 110, 83, 105, 103, 110, 105, 102, 105, 99, 97, 110, 116, 68,
105, 103, 105, 116, 115, 73, 0, 10, 109, 117, 108, 116, 105, 112, 108, 105, 101, 114, 67, 0, 3,
112, 97, 100, 73, 0, 11, 112, 97, 100, 80, 111, 115, 105, 116, 105, 111, 110, 90, 0, 15, 112,
97, 114, 115, 101, 66, 105, 103, 68, 101, 99, 105, 109, 97, 108, 90, 0, 24, 112, 97, 114, 115,
101, 82, 101, 113, 117, 105, 114, 101, 68, 101, 99, 105, 109, 97, 108, 80, 111, 105, 110, 116,
73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 73, 0, 21, 115, 101, 114,
105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 73, 0, 5,
115, 116, 121, 108, 101, 90, 0, 22, 117, 115, 101, 69, 120, 112, 111, 110, 101, 110, 116, 105,
97, 108, 78, 111, 116, 97, 116, 105, 111, 110, 90, 0, 20, 117, 115, 101, 83, 105, 103, 110, 105,
102, 105, 99, 97, 110, 116, 68, 105, 103, 105, 116, 115, 76, 0, 10, 97, 116, 116, 114, 105, 98,
117, 116, 101, 115, 116, 0, 21, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 65, 114, 114,
97, 121, 76, 105, 115, 116, 59, 76, 0, 14, 99, 117, 114, 114, 101, 110, 99, 121, 67, 104, 111,
105, 99, 101, 116, 0, 24, 76, 106, 97, 118, 97, 47, 116, 101, 120, 116, 47, 67, 104, 111, 105,
99, 101, 70, 111, 114, 109, 97, 116, 59, 76, 0, 18, 99, 117, 114, 114, 101, 110, 99, 121, 80,
108, 117, 114, 97, 108, 73, 110, 102, 111, 116, 0, 37, 76, 99, 111, 109, 47, 105, 98, 109, 47,
105, 99, 117, 47, 116, 101, 120, 116, 47, 67, 117, 114, 114, 101, 110, 99, 121, 80, 108, 117,
114, 97, 108, 73, 110, 102, 111, 59, 76, 0, 13, 99, 117, 114, 114, 101, 110, 99, 121, 85, 115,
97, 103, 101, 116, 0, 41, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116,
105, 108, 47, 67, 117, 114, 114, 101, 110, 99, 121, 36, 67, 117, 114, 114, 101, 110, 99, 121,
85, 115, 97, 103, 101, 59, 76, 0, 13, 102, 111, 114, 109, 97, 116, 80, 97, 116, 116, 101, 114,
110, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103,
59, 76, 0, 11, 109, 97, 116, 104, 67, 111, 110, 116, 101, 120, 116, 116, 0, 30, 76, 99, 111,
109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 109, 97, 116, 104, 47, 77, 97, 116, 104, 67, 111,
110, 116, 101, 120, 116, 59, 76, 0, 16, 110, 101, 103, 80, 114, 101, 102, 105, 120, 80, 97, 116,
116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 16, 110, 101, 103, 83, 117, 102, 102, 105, 120,
80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 14, 110, 101, 103, 97, 116, 105, 118,
101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, 14, 110, 101, 103, 97, 116, 105,
118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, 16, 112, 111, 115, 80, 114,
101, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 16, 112, 111,
115, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 14,
112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0,
14, 112, 111, 115, 105, 116, 105, 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 5, 76,
0, 17, 114, 111, 117, 110, 100, 105, 110, 103, 73, 110, 99, 114, 101, 109, 101, 110, 116, 116,
0, 22, 76, 106, 97, 118, 97, 47, 109, 97, 116, 104, 47, 66, 105, 103, 68, 101, 99, 105, 109, 97,
108, 59, 76, 0, 7, 115, 121, 109, 98, 111, 108, 115, 116, 0, 39, 76, 99, 111, 109, 47, 105, 98,
109, 47, 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114,
109, 97, 116, 83, 121, 109, 98, 111, 108, 115, 59, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98,
109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114,
109, 97, 116, -33, -10, -77, -65, 19, 125, 7, -24, 3, 0, 14, 90, 0, 12, 103, 114, 111, 117, 112,
105, 110, 103, 85, 115, 101, 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110,
68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105,
103, 105, 116, 115, 73, 0, 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111,
110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116,
101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116,
105, 111, 110, 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103,
101, 114, 68, 105, 103, 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114,
97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109,
117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97,
114, 115, 101, 73, 110, 116, 101, 103, 101, 114, 79, 110, 108, 121, 90, 0, 11, 112, 97, 114,
115, 101, 83, 116, 114, 105, 99, 116, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115,
105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 76, 0, 21, 99, 97, 112, 105, 116, 97, 108,
105, 122, 97, 116, 105, 111, 110, 83, 101, 116, 116, 105, 110, 103, 116, 0, 33, 76, 99, 111,
109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 105, 115, 112, 108, 97,
121, 67, 111, 110, 116, 101, 120, 116, 59, 76, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 116,
0, 27, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 67,
117, 114, 114, 101, 110, 99, 121, 59, 120, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105,
99, 117, 46, 116, 101, 120, 116, 46, 85, 70, 111, 114, 109, 97, 116, -69, 26, -15, 32, -39, 7,
93, -64, 2, 0, 2, 76, 0, 12, 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, 101, 116, 0, 26,
76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 85, 76, 111,
99, 97, 108, 101, 59, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126,
0, 13, 120, 114, 0, 16, 106, 97, 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97,
116, -5, -40, -68, 18, -23, 15, 24, 67, 2, 0, 0, 120, 112, 112, 112, 1, 3, 127, 0, 0, 0, 3, 0,
0, 1, 53, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 126, 114, 0, 31, 99, 111, 109, 46,
105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 105, 115, 112, 108, 97, 121, 67,
111, 110, 116, 101, 120, 116, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 114, 0, 14, 106, 97, 118,
97, 46, 108, 97, 110, 103, 46, 69, 110, 117, 109, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 112,
116, 0, 19, 67, 65, 80, 73, 84, 65, 76, 73, 90, 65, 84, 73, 79, 78, 95, 78, 79, 78, 69, 115,
114, 0, 45, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, 108, 46, 77,
101, 97, 115, 117, 114, 101, 85, 110, 105, 116, 36, 77, 101, 97, 115, 117, 114, 101, 85, 110,
105, 116, 80, 114, 111, 120, 121, -55, -70, 119, -8, -15, 121, 121, -30, 12, 0, 0, 120, 112,
119, 18, 0, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 0, 3, 85, 83, 68, 0, 0, 120, 120, 0, 0,
3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 32, 0, 0,
0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 115, 114, 0, 19, 106, 97, 118, 97, 46,
117, 116, 105, 108, 46, 65, 114, 114, 97, 121, 76, 105, 115, 116, 120, -127, -46, 29, -103, -57,
97, -99, 3, 0, 1, 73, 0, 4, 115, 105, 122, 101, 120, 112, 0, 0, 0, 0, 119, 4, 0, 0, 0, 0, 120,
112, 112, 126, 114, 0, 39, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105,
108, 46, 67, 117, 114, 114, 101, 110, 99, 121, 36, 67, 117, 114, 114, 101, 110, 99, 121, 85,
115, 97, 103, 101, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 113, 0, 126, 0, 17, 116, 0, 8, 83, 84,
65, 78, 68, 65, 82, 68, 116, 0, 9, 35, 44, 35, 35, 48, 46, 35, 35, 35, 115, 114, 0, 28, 99, 111,
109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 109, 97, 116, 104, 46, 77, 97, 116, 104, 67, 111,
110, 116, 101, 120, 116, 99, 105, 109, 109, 99, 49, 48, 48, 2, 0, 4, 73, 0, 6, 100, 105, 103,
105, 116, 115, 73, 0, 4, 102, 111, 114, 109, 90, 0, 10, 108, 111, 115, 116, 68, 105, 103, 105,
116, 115, 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 120, 112, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 116, 0, 1, 45, 116, 0, 0, 116, 0, 1, 45, 116, 0, 0, 116, 0, 0,
113, 0, 126, 0, 31, 116, 0, 0, 116, 0, 0, 112, 115, 114, 0, 37, 99, 111, 109, 46, 105, 98, 109,
46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109,
97, 116, 83, 121, 109, 98, 111, 108, 115, 80, 29, 23, -103, 8, 104, -109, -100, 2, 0, 38, 67, 0,
16, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100,
105, 103, 105, 116, 67, 0, 11, 101, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17,
103, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109,
105, 110, 117, 115, 83, 105, 103, 110, 67, 0, 25, 109, 111, 110, 101, 116, 97, 114, 121, 71,
114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 17, 109,
111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 112, 97,
100, 69, 115, 99, 97, 112, 101, 67, 0, 16, 112, 97, 116, 116, 101, 114, 110, 83, 101, 112, 97,
114, 97, 116, 111, 114, 67, 0, 7, 112, 101, 114, 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99,
101, 110, 116, 67, 0, 8, 112, 108, 117, 115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105,
97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 8, 115,
105, 103, 68, 105, 103, 105, 116, 67, 0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 116, 76, 0,
3, 78, 97, 78, 113, 0, 126, 0, 5, 76, 0, 12, 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108,
101, 113, 0, 126, 0, 13, 76, 0, 15, 99, 117, 114, 114, 101, 110, 99, 121, 80, 97, 116, 116, 101,
114, 110, 113, 0, 126, 0, 5, 91, 0, 19, 99, 117, 114, 114, 101, 110, 99, 121, 83, 112, 99, 65,
102, 116, 101, 114, 83, 121, 109, 116, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103,
47, 83, 116, 114, 105, 110, 103, 59, 91, 0, 20, 99, 117, 114, 114, 101, 110, 99, 121, 83, 112,
99, 66, 101, 102, 111, 114, 101, 83, 121, 109, 113, 0, 126, 0, 38, 76, 0, 14, 99, 117, 114, 114,
101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 5, 76, 0, 22, 100, 101, 99, 105,
109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0,
126, 0, 5, 91, 0, 12, 100, 105, 103, 105, 116, 83, 116, 114, 105, 110, 103, 115, 113, 0, 126, 0,
38, 91, 0, 6, 100, 105, 103, 105, 116, 115, 116, 0, 2, 91, 67, 76, 0, 26, 101, 120, 112, 111,
110, 101, 110, 116, 77, 117, 108, 116, 105, 112, 108, 105, 99, 97, 116, 105, 111, 110, 83, 105,
103, 110, 113, 0, 126, 0, 5, 76, 0, 17, 101, 120, 112, 111, 110, 101, 110, 116, 83, 101, 112,
97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 5, 76, 0, 23, 103, 114, 111, 117, 112, 105, 110,
103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5,
76, 0, 8, 105, 110, 102, 105, 110, 105, 116, 121, 113, 0, 126, 0, 5, 76, 0, 18, 105, 110, 116,
108, 67, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 5, 76, 0,
11, 109, 105, 110, 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, 76, 0, 31, 109,
111, 110, 101, 116, 97, 114, 121, 71, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114,
97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, 76, 0, 23, 109, 111, 110,
101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103,
113, 0, 126, 0, 5, 76, 0, 13, 112, 101, 114, 77, 105, 108, 108, 83, 116, 114, 105, 110, 103,
113, 0, 126, 0, 5, 76, 0, 13, 112, 101, 114, 99, 101, 110, 116, 83, 116, 114, 105, 110, 103,
113, 0, 126, 0, 5, 76, 0, 10, 112, 108, 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0,
5, 76, 0, 15, 114, 101, 113, 117, 101, 115, 116, 101, 100, 76, 111, 99, 97, 108, 101, 116, 0,
18, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 76, 111, 99, 97, 108, 101, 59, 76, 0, 7,
117, 108, 111, 99, 97, 108, 101, 113, 0, 126, 0, 13, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111,
99, 97, 108, 101, 113, 0, 126, 0, 13, 120, 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 44, 0, 46,
0, 42, 0, 59, 32, 48, 0, 37, 0, 43, 0, 0, 0, 8, 0, 64, 0, 48, 116, 0, 3, 78, 97, 78, 115, 114,
0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, 108, 46, 85, 76,
111, 99, 97, 108, 101, 51, -114, -10, 104, 70, -48, 11, -31, 2, 0, 1, 76, 0, 8, 108, 111, 99,
97, 108, 101, 73, 68, 113, 0, 126, 0, 5, 120, 112, 116, 0, 5, 101, 110, 95, 85, 83, 112, 117,
114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 83, 116, 114, 105, 110, 103,
59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0, 120, 112, 0, 0, 0, 3, 116, 0, 6, 91, 58, 94,
83, 58, 93, 116, 0, 9, 91, 58, 100, 105, 103, 105, 116, 58, 93, 116, 0, 2, -62, -96, 117, 113,
0, 126, 0, 46, 0, 0, 0, 3, 113, 0, 126, 0, 48, 113, 0, 126, 0, 49, 113, 0, 126, 0, 50, 116, 0,
1, 36, 116, 0, 1, 46, 117, 113, 0, 126, 0, 46, 0, 0, 0, 10, 116, 0, 1, 48, 116, 0, 1, 49, 116,
0, 1, 50, 116, 0, 1, 51, 116, 0, 1, 52, 116, 0, 1, 53, 116, 0, 1, 54, 116, 0, 1, 55, 116, 0, 1,
56, 116, 0, 1, 57, 117, 114, 0, 2, 91, 67, -80, 38, 102, -80, -30, 93, -124, -84, 2, 0, 0, 120,
112, 0, 0, 0, 10, 0, 48, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 116, 0,
2, -61, -105, 116, 0, 1, 69, 116, 0, 1, 44, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
116, 0, 1, 45, 113, 0, 126, 0, 69, 113, 0, 126, 0, 53, 116, 0, 3, -30, -128, -80, 116, 0, 1, 37,
116, 0, 1, 43, 115, 114, 0, 16, 106, 97, 118, 97, 46, 117, 116, 105, 108, 46, 76, 111, 99, 97,
108, 101, 126, -8, 17, 96, -100, 48, -7, -20, 3, 0, 6, 73, 0, 8, 104, 97, 115, 104, 99, 111,
100, 101, 76, 0, 7, 99, 111, 117, 110, 116, 114, 121, 113, 0, 126, 0, 5, 76, 0, 10, 101, 120,
116, 101, 110, 115, 105, 111, 110, 115, 113, 0, 126, 0, 5, 76, 0, 8, 108, 97, 110, 103, 117, 97,
103, 101, 113, 0, 126, 0, 5, 76, 0, 6, 115, 99, 114, 105, 112, 116, 113, 0, 126, 0, 5, 76, 0, 7,
118, 97, 114, 105, 97, 110, 116, 113, 0, 126, 0, 5, 120, 112, -1, -1, -1, -1, 116, 0, 2, 85, 83,
116, 0, 0, 116, 0, 2, 101, 110, 113, 0, 126, 0, 79, 113, 0, 126, 0, 79, 120, 115, 113, 0, 126,
0, 43, 113, 0, 126, 0, 45, 113, 0, 126, 0, 44, 120
};};
static byte[] newFromPattern() { return new byte[] {
-84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101,
120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 3, 98, -40,
114, 48, 58, 3, 0, 1, 73, 0, 18, 105, 99, 117, 77, 97, 116, 104, 67, 111, 110, 116, 101, 120,
116, 70, 111, 114, 109, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, 109, 97, 116, -33, -10, -77,
-65, 19, 125, 7, -24, 3, 0, 14, 90, 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 85, 115, 101,
100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115,
66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 73, 0,
21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 105,
116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, 101, 103, 101, 114, 68,
105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105,
103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103,
105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111,
110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109, 117, 109, 73, 110, 116,
101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97, 114, 115, 101, 73, 110,
116, 101, 103, 101, 114, 79, 110, 108, 121, 90, 0, 11, 112, 97, 114, 115, 101, 83, 116, 114,
105, 99, 116, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110,
83, 116, 114, 101, 97, 109, 76, 0, 21, 99, 97, 112, 105, 116, 97, 108, 105, 122, 97, 116, 105,
111, 110, 83, 101, 116, 116, 105, 110, 103, 116, 0, 33, 76, 99, 111, 109, 47, 105, 98, 109, 47,
105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 105, 115, 112, 108, 97, 121, 67, 111, 110, 116,
101, 120, 116, 59, 76, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 116, 0, 27, 76, 99, 111, 109,
47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 67, 117, 114, 114, 101, 110, 99,
121, 59, 120, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120,
116, 46, 85, 70, 111, 114, 109, 97, 116, -69, 26, -15, 32, -39, 7, 93, -64, 2, 0, 2, 76, 0, 12,
97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, 101, 116, 0, 26, 76, 99, 111, 109, 47, 105, 98,
109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 85, 76, 111, 99, 97, 108, 101, 59, 76, 0, 11,
118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 120, 114, 0, 16, 106, 97,
118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, 116, -5, -40, -68, 18, -23, 15, 24,
67, 2, 0, 0, 120, 112, 112, 112, 1, 3, 40, 0, 0, 0, 3, 0, 0, 0, 40, 0, 1, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 2, 126, 114, 0, 31, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116,
101, 120, 116, 46, 68, 105, 115, 112, 108, 97, 121, 67, 111, 110, 116, 101, 120, 116, 0, 0, 0,
0, 0, 0, 0, 0, 18, 0, 0, 120, 114, 0, 14, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 69, 110,
117, 109, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 112, 116, 0, 19, 67, 65, 80, 73, 84, 65, 76,
73, 90, 65, 84, 73, 79, 78, 95, 78, 79, 78, 69, 112, 120, 0, 0, 0, 0, 119, 4, 0, 0, 0, 0, 115,
114, 0, 34, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 105, 109, 112, 108, 46, 110,
117, 109, 98, 101, 114, 46, 80, 114, 111, 112, 101, 114, 116, 105, 101, 115, 56, -42, 52, -54,
-104, -87, -46, 123, 3, 0, 0, 120, 112, 119, 8, 0, 0, 0, 0, 0, 0, 0, 7, 116, 0, 12, 103, 114,
111, 117, 112, 105, 110, 103, 83, 105, 122, 101, 115, 114, 0, 17, 106, 97, 118, 97, 46, 108, 97,
110, 103, 46, 73, 110, 116, 101, 103, 101, 114, 18, -30, -96, -92, -9, -127, -121, 56, 2, 0, 1,
73, 0, 5, 118, 97, 108, 117, 101, 120, 114, 0, 16, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46,
78, 117, 109, 98, 101, 114, -122, -84, -107, 29, 11, -108, -32, -117, 2, 0, 0, 120, 112, 0, 0,
0, 3, 116, 0, 20, 109, 105, 110, 105, 109, 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105,
103, 105, 116, 115, 115, 113, 0, 126, 0, 15, 0, 0, 0, 2, 116, 0, 15, 112, 97, 100, 100, 105,
110, 103, 76, 111, 99, 97, 116, 105, 111, 110, 126, 114, 0, 64, 99, 111, 109, 46, 105, 98, 109,
46, 105, 99, 117, 46, 105, 109, 112, 108, 46, 110, 117, 109, 98, 101, 114, 46, 102, 111, 114,
109, 97, 116, 116, 101, 114, 115, 46, 80, 97, 100, 100, 105, 110, 103, 70, 111, 114, 109, 97,
116, 36, 80, 97, 100, 100, 105, 110, 103, 76, 111, 99, 97, 116, 105, 111, 110, 0, 0, 0, 0, 0, 0,
0, 0, 18, 0, 0, 120, 113, 0, 126, 0, 9, 116, 0, 12, 65, 70, 84, 69, 82, 95, 80, 82, 69, 70, 73,
88, 116, 0, 13, 112, 97, 100, 100, 105, 110, 103, 83, 116, 114, 105, 110, 103, 116, 0, 1, 42,
116, 0, 12, 112, 97, 100, 100, 105, 110, 103, 87, 105, 100, 116, 104, 115, 113, 0, 126, 0, 15,
0, 0, 0, 16, 116, 0, 21, 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120,
80, 97, 116, 116, 101, 114, 110, 116, 0, 2, 65, 45, 116, 0, 21, 112, 111, 115, 105, 116, 105,
118, 101, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 116, 0, 3, 98, -62, -92,
120, 115, 114, 0, 37, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116,
46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 111, 108, 115,
80, 29, 23, -103, 8, 104, -109, -100, 2, 0, 38, 67, 0, 16, 100, 101, 99, 105, 109, 97, 108, 83,
101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, 105, 103, 105, 116, 67, 0, 11, 101, 120,
112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, 103, 114, 111, 117, 112, 105, 110, 103,
83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, 105, 110, 117, 115, 83, 105, 103, 110,
67, 0, 25, 109, 111, 110, 101, 116, 97, 114, 121, 71, 114, 111, 117, 112, 105, 110, 103, 83,
101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 17, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101,
112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 112, 97, 100, 69, 115, 99, 97, 112, 101, 67, 0, 16,
112, 97, 116, 116, 101, 114, 110, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 7, 112, 101,
114, 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, 101, 110, 116, 67, 0, 8, 112, 108, 117,
115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111,
110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 8, 115, 105, 103, 68, 105, 103, 105, 116, 67,
0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 116, 76, 0, 3, 78, 97, 78, 116, 0, 18, 76, 106, 97,
118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 12, 97, 99, 116,
117, 97, 108, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 76, 0, 15, 99, 117, 114, 114, 101,
110, 99, 121, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 33, 91, 0, 19, 99, 117, 114, 114,
101, 110, 99, 121, 83, 112, 99, 65, 102, 116, 101, 114, 83, 121, 109, 116, 0, 19, 91, 76, 106,
97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 91, 0, 20, 99, 117,
114, 114, 101, 110, 99, 121, 83, 112, 99, 66, 101, 102, 111, 114, 101, 83, 121, 109, 113, 0,
126, 0, 34, 76, 0, 14, 99, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0,
126, 0, 33, 76, 0, 22, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111,
114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 91, 0, 12, 100, 105, 103, 105, 116, 83,
116, 114, 105, 110, 103, 115, 113, 0, 126, 0, 34, 91, 0, 6, 100, 105, 103, 105, 116, 115, 116,
0, 2, 91, 67, 76, 0, 26, 101, 120, 112, 111, 110, 101, 110, 116, 77, 117, 108, 116, 105, 112,
108, 105, 99, 97, 116, 105, 111, 110, 83, 105, 103, 110, 113, 0, 126, 0, 33, 76, 0, 17, 101,
120, 112, 111, 110, 101, 110, 116, 83, 101, 112, 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 33,
76, 0, 23, 103, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83,
116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 8, 105, 110, 102, 105, 110, 105, 116, 121,
113, 0, 126, 0, 33, 76, 0, 18, 105, 110, 116, 108, 67, 117, 114, 114, 101, 110, 99, 121, 83,
121, 109, 98, 111, 108, 113, 0, 126, 0, 33, 76, 0, 11, 109, 105, 110, 117, 115, 83, 116, 114,
105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 31, 109, 111, 110, 101, 116, 97, 114, 121, 71, 114,
111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110,
103, 113, 0, 126, 0, 33, 76, 0, 23, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97,
114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 13, 112, 101,
114, 77, 105, 108, 108, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 13, 112, 101,
114, 99, 101, 110, 116, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 10, 112, 108,
117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 15, 114, 101, 113, 117, 101,
115, 116, 101, 100, 76, 111, 99, 97, 108, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 117, 116,
105, 108, 47, 76, 111, 99, 97, 108, 101, 59, 76, 0, 7, 117, 108, 111, 99, 97, 108, 101, 113, 0,
126, 0, 5, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 120,
112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 44, 0, 46, 0, 42, 0, 59, 32, 48, 0, 37, 0, 43, 0, 0,
0, 8, 0, 64, 0, 48, 116, 0, 3, 78, 97, 78, 115, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46,
105, 99, 117, 46, 117, 116, 105, 108, 46, 85, 76, 111, 99, 97, 108, 101, 51, -114, -10, 104, 70,
-48, 11, -31, 2, 0, 1, 76, 0, 8, 108, 111, 99, 97, 108, 101, 73, 68, 113, 0, 126, 0, 33, 120,
112, 116, 0, 5, 101, 110, 95, 85, 83, 112, 117, 114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108,
97, 110, 103, 46, 83, 116, 114, 105, 110, 103, 59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0,
120, 112, 0, 0, 0, 3, 116, 0, 6, 91, 58, 94, 83, 58, 93, 116, 0, 9, 91, 58, 100, 105, 103, 105,
116, 58, 93, 116, 0, 2, -62, -96, 117, 113, 0, 126, 0, 42, 0, 0, 0, 3, 113, 0, 126, 0, 44, 113,
0, 126, 0, 45, 113, 0, 126, 0, 46, 116, 0, 1, 36, 116, 0, 1, 46, 117, 113, 0, 126, 0, 42, 0, 0,
0, 10, 116, 0, 1, 48, 116, 0, 1, 49, 116, 0, 1, 50, 116, 0, 1, 51, 116, 0, 1, 52, 116, 0, 1, 53,
116, 0, 1, 54, 116, 0, 1, 55, 116, 0, 1, 56, 116, 0, 1, 57, 117, 114, 0, 2, 91, 67, -80, 38,
102, -80, -30, 93, -124, -84, 2, 0, 0, 120, 112, 0, 0, 0, 10, 0, 48, 0, 49, 0, 50, 0, 51, 0, 52,
0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 116, 0, 2, -61, -105, 116, 0, 1, 69, 116, 0, 1, 44, 116, 0,
3, -30, -120, -98, 116, 0, 3, 85, 83, 68, 116, 0, 1, 45, 113, 0, 126, 0, 65, 113, 0, 126, 0, 49,
116, 0, 3, -30, -128, -80, 116, 0, 1, 37, 116, 0, 1, 43, 115, 114, 0, 16, 106, 97, 118, 97, 46,
117, 116, 105, 108, 46, 76, 111, 99, 97, 108, 101, 126, -8, 17, 96, -100, 48, -7, -20, 3, 0, 6,
73, 0, 8, 104, 97, 115, 104, 99, 111, 100, 101, 76, 0, 7, 99, 111, 117, 110, 116, 114, 121, 113,
0, 126, 0, 33, 76, 0, 10, 101, 120, 116, 101, 110, 115, 105, 111, 110, 115, 113, 0, 126, 0, 33,
76, 0, 8, 108, 97, 110, 103, 117, 97, 103, 101, 113, 0, 126, 0, 33, 76, 0, 6, 115, 99, 114, 105,
112, 116, 113, 0, 126, 0, 33, 76, 0, 7, 118, 97, 114, 105, 97, 110, 116, 113, 0, 126, 0, 33,
120, 112, -1, -1, -1, -1, 116, 0, 2, 85, 83, 116, 0, 0, 116, 0, 2, 101, 110, 113, 0, 126, 0, 75,
113, 0, 126, 0, 75, 120, 115, 113, 0, 126, 0, 39, 113, 0, 126, 0, 41, 113, 0, 126, 0, 40, 120
};};
static byte[][] getContent() {
return new byte[][] {
generalInstance(),
currencyInstance(),
scientificInstance(),
icu58Latest(),
newFromPattern()
};
}
}

View File

@ -18,12 +18,12 @@ rt: "0.###" 1.0 "1"
# Basics
fp: "0.####" 0.10005 "0.1" 0.1
fp: - 0.10006 "0.1001" 0.1001
pat: - "#0.####"
pat: - "0.####"
fp: "#.####" 0.10005 "0.1" 0.1
pat: - "#0.####"
pat: - "0.####"
rt: "0" 1234 "1234"
pat: - "#0"
pat: - "0"
# Significant digits
fp: "@@@" 1.234567 "1.23" 1.23
@ -68,7 +68,8 @@ pat: "##,@@##" "#,@@##"
pat: "##@@##" "@@##"
pat: "@@.@@" err # decimal sep. disallowed in sig. digits
pat: "@#@" err # only one cluster of sig. digits
# The new pattern parser treats this the same as "@@#"
#pat: "@#@" err # only one cluster of sig. digits
pat: "@@0" err # either @ or 0, not both
# NumberRegression/Test4140009

View File

@ -28,7 +28,6 @@ package com.ibm.icu.dev.test.format;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
@ -424,24 +423,24 @@ public class NumberRegressionTests extends TestFmwk {
try {
dfFoo.applyPattern("0000;-000");
if (!dfFoo.toPattern().equals("#0000"))
if (!dfFoo.toPattern().equals("0000"))
errln("dfFoo.toPattern : " + dfFoo.toPattern());
logln(dfFoo.format(42));
logln(dfFoo.format(-42));
dfFoo.applyPattern("000;-000");
if (!dfFoo.toPattern().equals("#000"))
if (!dfFoo.toPattern().equals("000"))
errln("dfFoo.toPattern : " + dfFoo.toPattern());
logln(dfFoo.format(42));
logln(dfFoo.format(-42));
dfFoo.applyPattern("000;-0000");
if (!dfFoo.toPattern().equals("#000"))
if (!dfFoo.toPattern().equals("000"))
errln("dfFoo.toPattern : " + dfFoo.toPattern());
logln(dfFoo.format(42));
logln(dfFoo.format(-42));
dfFoo.applyPattern("0000;-000");
if (!dfFoo.toPattern().equals("#0000"))
if (!dfFoo.toPattern().equals("0000"))
errln("dfFoo.toPattern : " + dfFoo.toPattern());
logln(dfFoo.format(42));
logln(dfFoo.format(-42));
@ -1043,9 +1042,15 @@ public class NumberRegressionTests extends TestFmwk {
// maximum/minimumFractionDigits and rounding option for the second
// DecimalForamt instance. The fix for ticket#7282 requires this test
// code change to make it work properly.
fmt2.setMaximumFractionDigits(fmt1.getMaximumFractionDigits());
fmt2.setMinimumFractionDigits(fmt1.getMinimumFractionDigits());
if (symbols.getCurrency() != null) {
fmt2.setMaximumFractionDigits(symbols.getCurrency().getDefaultFractionDigits());
fmt2.setMinimumFractionDigits(symbols.getCurrency().getDefaultFractionDigits());
fmt2.setRoundingIncrement(symbols.getCurrency().getRoundingIncrement());
} else {
fmt2.setMaximumFractionDigits(fmt1.getMinimumFractionDigits());
fmt2.setMinimumFractionDigits(fmt1.getMaximumFractionDigits());
fmt2.setRoundingIncrement(fmt1.getRoundingIncrement());
}
String result2 = fmt2.format(1.111);
@ -1246,7 +1251,9 @@ public class NumberRegressionTests extends TestFmwk {
logln("Applying pattern \"" + pattern + "\"");
sdf.applyPattern(pattern);
int minIntDig = sdf.getMinimumIntegerDigits();
if (minIntDig != 0) {
// In ICU 58 and older, this case returned 0.
// Now it returns 1 instead, since the pattern parser enforces at least 1 min digit.
if (minIntDig != 1) {
errln("Test failed");
errln(" Minimum integer digits : " + minIntDig);
errln(" new pattern: " + sdf.toPattern());
@ -1295,9 +1302,7 @@ public class NumberRegressionTests extends TestFmwk {
e.printStackTrace();
}
logln("The string " + s + " parsed as " + n);
if (n.doubleValue() != dbl) {
errln("Round trip failure");
}
assertEquals("Round trip failure", dbl, n.doubleValue());
}
/**
@ -1379,15 +1384,15 @@ public class NumberRegressionTests extends TestFmwk {
@Test
public void Test4176114() {
String[] DATA = {
"00", "#00",
"000", "#000", // No grouping
"#000", "#000", // No grouping
"00", "00",
"000", "000", // No grouping
"#000", "000", // No grouping
"#,##0", "#,##0",
"#,000", "#,000",
"0,000", "#0,000",
"00,000", "#00,000",
"000,000", "#,000,000",
"0,000,000,000,000.0000", "#0,000,000,000,000.0000", // Reported
"0,000", "0,000",
"00,000", "00,000",
"000,000", "000,000",
"0,000,000,000,000.0000", "0,000,000,000,000.0000", // Reported
};
for (int i=0; i<DATA.length; i+=2) {
DecimalFormat df = new DecimalFormat(DATA[i]);
@ -1426,98 +1431,6 @@ public class NumberRegressionTests extends TestFmwk {
}
}
@Test
public void Test4185761() throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
NumberFormat nf = NumberFormat.getInstance(Locale.US);
// Set special values we are going to search for in the output byte stream
// These are all legal values.
nf.setMinimumIntegerDigits(0x111); // Keep under 309
nf.setMaximumIntegerDigits(0x112); // Keep under 309
nf.setMinimumFractionDigits(0x113); // Keep under 340
nf.setMaximumFractionDigits(0x114); // Keep under 340
oos.writeObject(nf);
oos.flush();
baos.close();
byte[] bytes = baos.toByteArray();
// Scan for locations of min/max int/fract values in the byte array.
// At the moment (ICU4J 2.1), there is only one instance of each target pair
// in the byte stream, so assume first match is it. Note this is not entirely
// failsafe, and needs to be checked if we change the package or structure of
// this class.
// Current positions are 890, 880, 886, 876
int[] offsets = new int[4];
for (int i = 0; i < bytes.length - 1; ++i) {
if (bytes[i] == 0x01) { // high byte
for (int j = 0; j < offsets.length; ++j) {
if ((offsets[j] == 0) && (bytes[i+1] == (0x11 + j))) { // low byte
offsets[j] = i;
break;
}
}
}
}
{
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Object o = ois.readObject();
ois.close();
if (!nf.equals(o)) {
errln("Fail: NumberFormat serialization/equality bug");
} else {
logln("NumberFormat serialization/equality is OKAY.");
}
}
// Change the values in the byte stream so that min > max.
// Numberformat should catch this and throw an exception.
for (int i = 0; i < offsets.length; ++i) {
bytes[offsets[i]] = (byte)(4 - i);
}
{
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
try {
NumberFormat format = (NumberFormat) ois.readObject();
logln("format: " + format.format(1234.56)); //fix "The variable is never used"
errln("FAIL: Deserialized bogus NumberFormat with minXDigits > maxXDigits");
} catch (InvalidObjectException e) {
logln("Ok: " + e.getMessage());
}
}
// Set values so they are too high, but min <= max
// Format should pass the min <= max test, and DecimalFormat should reset to current maximum
// (for compatibility with versions streamed out before the maximums were imposed).
for (int i = 0; i < offsets.length; ++i) {
bytes[offsets[i]] = 4;
}
{
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
NumberFormat format = (NumberFormat) ois.readObject();
//For compatibility with previous version
if ((format.getMaximumIntegerDigits() != 309)
|| format.getMaximumFractionDigits() != 340) {
errln("FAIL: Deserialized bogus NumberFormat with values out of range," +
" intMin: " + format.getMinimumIntegerDigits() +
" intMax: " + format.getMaximumIntegerDigits() +
" fracMin: " + format.getMinimumFractionDigits() +
" fracMax: " + format.getMaximumFractionDigits());
} else {
logln("Ok: Digit count out of range");
}
}
}
/**
* Some DecimalFormatSymbols changes are not picked up by DecimalFormat.
* This includes the minus sign, currency symbol, international currency
@ -1627,6 +1540,7 @@ public class NumberRegressionTests extends TestFmwk {
String pat = df.toPattern();
DecimalFormatSymbols symb = new DecimalFormatSymbols(avail[i]);
DecimalFormat f2 = new DecimalFormat(pat, symb);
f2.setCurrency(df.getCurrency()); // Currency does not travel with the pattern string
if (!df.equals(f2)) {
errln("FAIL: " + avail[i] + " #" + j + " -> \"" + pat +
"\" -> \"" + f2.toPattern() + '"');
@ -1644,14 +1558,22 @@ public class NumberRegressionTests extends TestFmwk {
"\" -> \"" + s2 + '"'+ " in locale "+df.getLocale(ULocale.ACTUAL_LOCALE));
}
if (!df.equals(f2)) {
errln("FAIL: " + avail[i] + " #" + j + " -> localized \"" + pat +
"\" -> \"" + f2.toLocalizedPattern() + '"'+ " in locale "+df.getLocale(ULocale.ACTUAL_LOCALE));
errln("s1: "+s1+" s2: "+s2);
}
// Equality of formatter objects is NOT guaranteed across toLocalizedPattern/applyLocalizedPattern.
// However, equality of relevant properties is guaranteed.
assertEquals("Localized FAIL on posPrefix", df.getPositivePrefix(), f2.getPositivePrefix());
assertEquals("Localized FAIL on posSuffix", df.getPositiveSuffix(), f2.getPositiveSuffix());
assertEquals("Localized FAIL on negPrefix", df.getNegativePrefix(), f2.getNegativePrefix());
assertEquals("Localized FAIL on negSuffix", df.getNegativeSuffix(), f2.getNegativeSuffix());
assertEquals("Localized FAIL on groupingSize", df.getGroupingSize(), f2.getGroupingSize());
assertEquals("Localized FAIL on secondaryGroupingSize", df.getSecondaryGroupingSize(), f2.getSecondaryGroupingSize());
assertEquals("Localized FAIL on minFrac", df.getMinimumFractionDigits(), f2.getMinimumFractionDigits());
assertEquals("Localized FAIL on maxFrac", df.getMaximumFractionDigits(), f2.getMaximumFractionDigits());
assertEquals("Localized FAIL on minInt", df.getMinimumIntegerDigits(), f2.getMinimumIntegerDigits());
assertEquals("Localized FAIL on maxInt", df.getMaximumIntegerDigits(), f2.getMaximumIntegerDigits());
}catch(IllegalArgumentException ex){
errln(ex.getMessage()+" for locale "+ df.getLocale(ULocale.ACTUAL_LOCALE));
throw new AssertionError("For locale " + avail[i], ex);
}
@ -1682,7 +1604,7 @@ public class NumberRegressionTests extends TestFmwk {
sym = new DecimalFormatSymbols(Locale.US);
for (int j=0; j<SPECIALS.length; ++j) {
char special = SPECIALS[j];
String pat = "'" + special + "'#0'" + special + "'";
String pat = "'" + special + "'0'" + special + "'";
try {
fmt = new DecimalFormat(pat, sym);
String pat2 = fmt.toPattern();
@ -1713,7 +1635,7 @@ public class NumberRegressionTests extends TestFmwk {
String str = Long.toString(DATA[i]);
for (int m = 1; m <= 100; m++) {
fmt.setMultiplier(m);
long n = ((Number) fmt.parse(str)).longValue();
long n = fmt.parse(str).longValue();
if (n > 0 != DATA[i] > 0) {
errln("\"" + str + "\" parse(x " + fmt.getMultiplier() +
") => " + n);
@ -1916,21 +1838,27 @@ class MyNumberFormat extends NumberFormat {
* For serialization
*/
private static final long serialVersionUID = 1251303884737169952L;
@Override
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
@Override
public StringBuffer format(long number,StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
@Override
public Number parse(String text, ParsePosition parsePosition) {
return new Integer(0);
}
@Override
public StringBuffer format(java.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
@Override
public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}
@Override
public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
return new StringBuffer("");
}

View File

@ -883,8 +883,9 @@ public class TestMessageFormat extends com.ibm.icu.dev.test.TestFmwk {
errln("parsed argument " + parsedArgs[0] + " != " + num);
}
}
catch (Exception e) {
errln("parse of '" + result + " returned exception: " + e.getMessage());
catch (ParseException e) {
errln("parse of '" + result + "' returned exception: "
+ e.getMessage() + " " + e.getErrorOffset());
}
}
}

View File

@ -0,0 +1,126 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Test;
import com.ibm.icu.impl.number.AffixPatternUtils;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.ULocale;
public class AffixPatternUtilsTest {
@Test
public void testEscape() {
Object[][] cases = {
{"", ""},
{"abc", "abc"},
{"-", "'-'"},
{"-!", "'-'!"},
{"", ""},
{"---", "'---'"},
{"-%-", "'-%-'"},
{"'", "''"},
{"-'", "'-'''"},
{"-'-", "'-''-'"},
{"a-'-", "a'-''-'"}
};
StringBuilder sb = new StringBuilder();
for (Object[] cas : cases) {
String input = (String) cas[0];
String expected = (String) cas[1];
sb.setLength(0);
AffixPatternUtils.escape(input, sb);
assertEquals(expected, sb.toString());
}
}
@Test
public void testUnescape() {
Object[][] cases = {
{"", false, 0, ""},
{"abc", false, 3, "abc"},
{"-", false, 1, ""},
{"-!", false, 2, "!"},
{"+", false, 1, "\u061C+"},
{"+!", false, 2, "\u061C+!"},
{"", false, 1, "؉"},
{"‰!", false, 2, "؉!"},
{"-x", false, 2, "x"},
{"'-'x", false, 2, "-x"},
{"'--''-'-x", false, 6, "--'-x"},
{"''", false, 1, "'"},
{"''''", false, 2, "''"},
{"''''''", false, 3, "'''"},
{"''x''", false, 3, "'x'"},
{"¤", true, 1, "$"},
{"¤¤", true, 2, "XXX"},
{"¤¤¤", true, 3, "long name"},
{"¤¤¤¤", true, 4, "\uFFFD"},
{"¤¤¤¤¤", true, 5, "\uFFFD"},
{"¤¤¤a¤¤¤¤", true, 8, "long namea\uFFFD"},
{"a¤¤¤¤b¤¤¤¤¤c", true, 12, "a\uFFFDb\uFFFDc"},
{"¤!", true, 2, "$!"},
{"¤¤!", true, 3, "XXX!"},
{"¤¤¤!", true, 4, "long name!"},
{"-¤¤", true, 3, "XXX"},
{"¤¤-", true, 3, "XXX"},
{"'¤'", false, 1, "¤"},
{"%", false, 1, "٪\u061C"},
{"'%'", false, 1, "%"},
{"¤'-'%", true, 3, "$-٪\u061C"}
};
// ar_SA has an interesting percent sign and various Arabic letter marks
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("ar_SA"));
NumberStringBuilder sb = new NumberStringBuilder();
for (Object[] cas : cases) {
String input = (String) cas[0];
boolean curr = (Boolean) cas[1];
int length = (Integer) cas[2];
String output = (String) cas[3];
assertEquals(
"Currency on <" + input + ">", curr, AffixPatternUtils.hasCurrencySymbols(input));
assertEquals("Length on <" + input + ">", length, AffixPatternUtils.unescapedLength(input));
sb.clear();
AffixPatternUtils.unescape(input, symbols, "$", "XXX", "long name", "", sb);
assertEquals("Output on <" + input + ">", output, sb.toString());
}
}
@Test
public void testInvalid() {
String[] invalidExamples = {"'", "x'", "'x", "'x''", "''x'"};
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("en_US"));
NumberStringBuilder sb = new NumberStringBuilder();
for (String str : invalidExamples) {
try {
AffixPatternUtils.hasCurrencySymbols(str);
fail("No exception was thrown on an invalid string");
} catch (IllegalArgumentException e) {
// OK
}
try {
AffixPatternUtils.unescapedLength(str);
fail("No exception was thrown on an invalid string");
} catch (IllegalArgumentException e) {
// OK
}
try {
AffixPatternUtils.unescape(str, symbols, "$", "XXX", "long name", "", sb);
fail("No exception was thrown on an invalid string");
} catch (IllegalArgumentException e) {
// OK
}
}
}
}

View File

@ -0,0 +1,475 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import org.junit.Test;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.impl.number.Endpoint;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.FormatQuantity1;
import com.ibm.icu.impl.number.FormatQuantity2;
import com.ibm.icu.impl.number.FormatQuantity3;
import com.ibm.icu.impl.number.FormatQuantity4;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
/** TODO: This is a temporary name for this class. Suggestions for a better name? */
public class FormatQuantityTest extends TestFmwk {
@Test
public void testBehavior() throws ParseException {
// Make a list of several formatters to test the behavior of FormatQuantity.
List<Format> formats = new ArrayList<Format>();
Properties properties = new Properties();
Format ndf = Endpoint.fromBTA(properties);
formats.add(ndf);
properties =
new Properties()
.setMinimumSignificantDigits(3)
.setMaximumSignificantDigits(3)
.setCompactStyle(CompactStyle.LONG);
Format cdf = Endpoint.fromBTA(properties);
formats.add(cdf);
properties =
new Properties()
.setMinimumExponentDigits(1)
.setMaximumIntegerDigits(3)
.setMaximumFractionDigits(1);
Format exf = Endpoint.fromBTA(properties);
formats.add(exf);
properties = new Properties().setRoundingIncrement(new BigDecimal("0.5"));
Format rif = Endpoint.fromBTA(properties);
formats.add(rif);
String[] cases = {
"1.0",
"2.01",
"1234.56",
"3000.0",
"0.00026418",
"0.01789261",
"468160.0",
"999000.0",
"999900.0",
"999990.0",
"0.0",
"12345678901.0",
"-5193.48",
};
String[] hardCases = {
"9999999999999900.0",
"789000000000000000000000.0",
"789123123567853156372158.0",
"987654321987654321987654321987654321987654311987654321.0",
};
String[] doubleCases = {
"512.0000000000017",
"4095.9999999999977",
"4095.999999999998",
"4095.9999999999986",
"4095.999999999999",
"4095.9999999999995",
"4096.000000000001",
"4096.000000000002",
"4096.000000000003",
"4096.000000000004",
"4096.000000000005",
"4096.0000000000055",
"4096.000000000006",
"4096.000000000007",
};
int i = 0;
for (String str : cases) {
testFormatQuantity(i++, str, formats, 0);
}
i = 0;
for (String str : hardCases) {
testFormatQuantity(i++, str, formats, 1);
}
i = 0;
for (String str : doubleCases) {
testFormatQuantity(i++, str, formats, 2);
}
}
static void testFormatQuantity(int t, String str, List<Format> formats, int mode) {
if (mode == 2) {
assertEquals("Double is not valid", Double.toString(Double.parseDouble(str)), str);
}
List<FormatQuantity> qs = new ArrayList<FormatQuantity>();
BigDecimal d = new BigDecimal(str);
qs.add(new FormatQuantity1(d));
if (mode == 0) qs.add(new FormatQuantity2(d));
qs.add(new FormatQuantity3(d));
qs.add(new FormatQuantity4(d));
if (new BigDecimal(Double.toString(d.doubleValue())).compareTo(d) == 0) {
double dv = d.doubleValue();
qs.add(new FormatQuantity1(dv));
if (mode == 0) qs.add(new FormatQuantity2(dv));
qs.add(new FormatQuantity3(dv));
qs.add(new FormatQuantity4(dv));
}
if (new BigDecimal(Long.toString(d.longValue())).compareTo(d) == 0) {
double lv = d.longValue();
qs.add(new FormatQuantity1(lv));
if (mode == 0) qs.add(new FormatQuantity2(lv));
qs.add(new FormatQuantity3(lv));
qs.add(new FormatQuantity4(lv));
}
testFormatQuantityExpectedOutput(qs.get(0), str);
if (qs.size() == 1) {
return;
}
for (int i = 1; i < qs.size(); i++) {
FormatQuantity q0 = qs.get(0);
FormatQuantity q1 = qs.get(i);
testFormatQuantityExpectedOutput(q1, str);
testFormatQuantityRounding(q0, q1);
testFormatQuantityRoundingInterval(q0, q1);
testFormatQuantityMath(q0, q1);
testFormatQuantityWithFormats(q0, q1, formats);
}
}
private static void testFormatQuantityExpectedOutput(FormatQuantity rq, String expected) {
StringBuilder sb = new StringBuilder();
FormatQuantity q0 = rq.clone();
// Force an accurate double
q0.roundToInfinity();
q0.setIntegerFractionLength(1, Integer.MAX_VALUE, 1, Integer.MAX_VALUE);
for (int m = q0.getUpperDisplayMagnitude(); m >= q0.getLowerDisplayMagnitude(); m--) {
sb.append(q0.getDigit(m));
if (m == 0) sb.append('.');
}
if (q0.isNegative()) {
sb.insert(0, '-');
}
String actual = sb.toString();
assertEquals("Unexpected output from simple string conversion (" + q0 + ")", expected, actual);
}
private static final MathContext MATH_CONTEXT_HALF_EVEN =
new MathContext(0, RoundingMode.HALF_EVEN);
private static final MathContext MATH_CONTEXT_CEILING = new MathContext(0, RoundingMode.CEILING);
private static final MathContext MATH_CONTEXT_PRECISION =
new MathContext(3, RoundingMode.HALF_UP);
private static void testFormatQuantityRounding(FormatQuantity rq0, FormatQuantity rq1) {
FormatQuantity q0 = rq0.clone();
FormatQuantity q1 = rq1.clone();
q0.roundToMagnitude(-1, MATH_CONTEXT_HALF_EVEN);
q1.roundToMagnitude(-1, MATH_CONTEXT_HALF_EVEN);
testFormatQuantityBehavior(q0, q1);
q0 = rq0.clone();
q1 = rq1.clone();
q0.roundToMagnitude(-1, MATH_CONTEXT_CEILING);
q1.roundToMagnitude(-1, MATH_CONTEXT_CEILING);
testFormatQuantityBehavior(q0, q1);
q0 = rq0.clone();
q1 = rq1.clone();
q0.roundToMagnitude(-1, MATH_CONTEXT_PRECISION);
q1.roundToMagnitude(-1, MATH_CONTEXT_PRECISION);
testFormatQuantityBehavior(q0, q1);
}
private static void testFormatQuantityRoundingInterval(FormatQuantity rq0, FormatQuantity rq1) {
FormatQuantity q0 = rq0.clone();
FormatQuantity q1 = rq1.clone();
q0.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_HALF_EVEN);
q1.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_HALF_EVEN);
testFormatQuantityBehavior(q0, q1);
q0 = rq0.clone();
q1 = rq1.clone();
q0.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_CEILING);
q1.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_CEILING);
testFormatQuantityBehavior(q0, q1);
}
private static void testFormatQuantityMath(FormatQuantity rq0, FormatQuantity rq1) {
FormatQuantity q0 = rq0.clone();
FormatQuantity q1 = rq1.clone();
q0.adjustMagnitude(-3);
q1.adjustMagnitude(-3);
testFormatQuantityBehavior(q0, q1);
q0 = rq0.clone();
q1 = rq1.clone();
q0.multiplyBy(new BigDecimal("3.14159"));
q1.multiplyBy(new BigDecimal("3.14159"));
testFormatQuantityBehavior(q0, q1);
}
private static void testFormatQuantityWithFormats(
FormatQuantity rq0, FormatQuantity rq1, List<Format> formats) {
for (Format format : formats) {
FormatQuantity q0 = rq0.clone();
FormatQuantity q1 = rq1.clone();
String s1 = format.format(q0);
String s2 = format.format(q1);
assertEquals("Different output from formatter (" + q0 + ", " + q1 + ")", s1, s2);
}
}
private static void testFormatQuantityBehavior(FormatQuantity rq0, FormatQuantity rq1) {
FormatQuantity q0 = rq0.clone();
FormatQuantity q1 = rq1.clone();
assertEquals("Different sign (" + q0 + ", " + q1 + ")", q0.isNegative(), q1.isNegative());
assertEquals(
"Different fingerprint (" + q0 + ", " + q1 + ")",
q0.getPositionFingerprint(),
q1.getPositionFingerprint());
assertEquals(
"Different upper range of digits (" + q0 + ", " + q1 + ")",
q0.getUpperDisplayMagnitude(),
q1.getUpperDisplayMagnitude());
assertDoubleEquals(
"Different double values (" + q0 + ", " + q1 + ")", q0.toDouble(), q1.toDouble());
assertBigDecimalEquals(
"Different BigDecimal values (" + q0 + ", " + q1 + ")",
q0.toBigDecimal(),
q1.toBigDecimal());
int equalityDigits = Math.min(q0.maxRepresentableDigits(), q1.maxRepresentableDigits());
for (int m = q0.getUpperDisplayMagnitude(), i = 0;
m >= Math.min(q0.getLowerDisplayMagnitude(), q1.getLowerDisplayMagnitude())
&& i < equalityDigits;
m--, i++) {
assertEquals(
"Different digit at magnitude " + m + " (" + q0 + ", " + q1 + ")",
q0.getDigit(m),
q1.getDigit(m));
}
if (rq0 instanceof FormatQuantity4) {
String message = ((FormatQuantity4) rq0).checkHealth();
if (message != null) errln(message);
}
if (rq1 instanceof FormatQuantity4) {
String message = ((FormatQuantity4) rq1).checkHealth();
if (message != null) errln(message);
}
}
@Test
public void testSwitchStorage() {
FormatQuantity4 fq = new FormatQuantity4();
fq.setToLong(1234123412341234L);
assertFalse("Should not be using byte array", fq.usingBytes());
assertBigDecimalEquals("Failed on initialize", "1234123412341234", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
// Long -> Bytes
fq.appendDigit((byte) 5, 0, true);
assertTrue("Should be using byte array", fq.usingBytes());
assertBigDecimalEquals("Failed on multiply", "12341234123412345", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
// Bytes -> Long
fq.roundToMagnitude(5, MATH_CONTEXT_HALF_EVEN);
assertFalse("Should not be using byte array", fq.usingBytes());
assertBigDecimalEquals("Failed on round", "12341234123400000", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
}
@Test
public void testAppend() {
FormatQuantity4 fq = new FormatQuantity4();
fq.appendDigit((byte) 1, 0, true);
assertBigDecimalEquals("Failed on append", "1.", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
fq.appendDigit((byte) 2, 0, true);
assertBigDecimalEquals("Failed on append", "12.", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
fq.appendDigit((byte) 3, 1, true);
assertBigDecimalEquals("Failed on append", "1203.", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
fq.appendDigit((byte) 0, 1, true);
assertBigDecimalEquals("Failed on append", "120300.", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
fq.appendDigit((byte) 4, 0, true);
assertBigDecimalEquals("Failed on append", "1203004.", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
fq.appendDigit((byte) 0, 0, true);
assertBigDecimalEquals("Failed on append", "12030040.", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
fq.appendDigit((byte) 5, 0, false);
assertBigDecimalEquals("Failed on append", "12030040.5", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
fq.appendDigit((byte) 6, 0, false);
assertBigDecimalEquals("Failed on append", "12030040.56", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
fq.appendDigit((byte) 7, 3, false);
assertBigDecimalEquals("Failed on append", "12030040.560007", fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
StringBuilder expected = new StringBuilder("12030040.560007");
for (int i = 0; i < 10; i++) {
fq.appendDigit((byte) 8, 0, false);
expected.append("8");
assertBigDecimalEquals("Failed on append", expected.toString(), fq.toBigDecimal());
assertNull("Failed health check", fq.checkHealth());
}
}
@Test
public void testConvertToAccurateDouble() {
// based on https://github.com/google/double-conversion/issues/28
double[] hardDoubles = {
1651087494906221570.0,
-5074790912492772E-327,
83602530019752571E-327,
2.207817077636718750000000000000,
1.818351745605468750000000000000,
3.941719055175781250000000000000,
3.738609313964843750000000000000,
3.967735290527343750000000000000,
1.328025817871093750000000000000,
3.920967102050781250000000000000,
1.015235900878906250000000000000,
1.335227966308593750000000000000,
1.344520568847656250000000000000,
2.879127502441406250000000000000,
3.695838928222656250000000000000,
1.845344543457031250000000000000,
3.793952941894531250000000000000,
3.211402893066406250000000000000,
2.565971374511718750000000000000,
0.965156555175781250000000000000,
2.700004577636718750000000000000,
0.767097473144531250000000000000,
1.780448913574218750000000000000,
2.624839782714843750000000000000,
1.305290222167968750000000000000,
3.834922790527343750000000000000,
};
double[] integerDoubles = {
51423,
51423e10,
4.503599627370496E15,
6.789512076111555E15,
9.007199254740991E15,
9.007199254740992E15
};
for (double d : hardDoubles) {
checkDoubleBehavior(d, true, "");
}
for (double d : integerDoubles) {
checkDoubleBehavior(d, false, "");
}
assertEquals("NaN check failed", Double.NaN, new FormatQuantity4(Double.NaN).toDouble());
assertEquals(
"Inf check failed",
Double.POSITIVE_INFINITY,
new FormatQuantity4(Double.POSITIVE_INFINITY).toDouble());
assertEquals(
"-Inf check failed",
Double.NEGATIVE_INFINITY,
new FormatQuantity4(Double.NEGATIVE_INFINITY).toDouble());
// Generate random doubles
String alert = "UNEXPECTED FAILURE: PLEASE REPORT THIS MESSAGE TO THE ICU TEAM: ";
for (int i = 0; i < 1000000; i++) {
double d = Double.longBitsToDouble(ThreadLocalRandom.current().nextLong());
if (Double.isNaN(d) || Double.isInfinite(d)) continue;
checkDoubleBehavior(d, false, alert);
}
}
private static void checkDoubleBehavior(double d, boolean explicitRequired, String alert) {
FormatQuantity4 fq = new FormatQuantity4(d);
if (explicitRequired)
assertTrue(alert + "Should be using approximate double", !fq.explicitExactDouble);
assertEquals(alert + "Initial construction from hard double", d, fq.toDouble());
fq.roundToInfinity();
if (explicitRequired)
assertTrue(alert + "Should not be using approximate double", fq.explicitExactDouble);
assertDoubleEquals(alert + "After conversion to exact BCD (double)", d, fq.toDouble());
assertBigDecimalEquals(
alert + "After conversion to exact BCD (BigDecimal)",
new BigDecimal(Double.toString(d)),
fq.toBigDecimal());
}
@Test
public void testUseApproximateDoubleWhenAble() {
Object[][] cases = {
{1.2345678, 1, MATH_CONTEXT_HALF_EVEN, false},
{1.2345678, 7, MATH_CONTEXT_HALF_EVEN, false},
{1.2345678, 12, MATH_CONTEXT_HALF_EVEN, false},
{1.2345678, 13, MATH_CONTEXT_HALF_EVEN, true},
{1.235, 1, MATH_CONTEXT_HALF_EVEN, false},
{1.235, 2, MATH_CONTEXT_HALF_EVEN, true},
{1.235, 3, MATH_CONTEXT_HALF_EVEN, false},
{1.000000000000001, 0, MATH_CONTEXT_HALF_EVEN, false},
{1.000000000000001, 0, MATH_CONTEXT_CEILING, true},
{1.235, 1, MATH_CONTEXT_CEILING, false},
{1.235, 2, MATH_CONTEXT_CEILING, false},
{1.235, 3, MATH_CONTEXT_CEILING, true}
};
for (Object[] cas : cases) {
double d = (Double) cas[0];
int maxFrac = (Integer) cas[1];
MathContext mc = (MathContext) cas[2];
boolean usesExact = (Boolean) cas[3];
FormatQuantity4 fq = new FormatQuantity4(d);
assertTrue("Should be using approximate double", !fq.explicitExactDouble);
fq.roundToMagnitude(-maxFrac, mc);
assertEquals(
"Using approximate double after rounding: " + d + " maxFrac=" + maxFrac + " " + mc,
usesExact,
fq.explicitExactDouble);
}
}
static void assertDoubleEquals(String message, double d1, double d2) {
boolean equal = (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6);
handleAssert(equal, message, d1, d2, null, false);
}
static void assertBigDecimalEquals(String message, String d1, BigDecimal d2) {
assertBigDecimalEquals(message, new BigDecimal(d1), d2);
}
static void assertBigDecimalEquals(String message, BigDecimal d1, BigDecimal d2) {
boolean equal = d1.compareTo(d2) == 0;
handleAssert(equal, message, d1, d2, null, false);
}
}

View File

@ -0,0 +1,171 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.text.FieldPosition;
import java.text.Format.Field;
import org.junit.Test;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.text.NumberFormat;
/** @author sffc */
public class NumberStringBuilderTest {
private static final String[] EXAMPLE_STRINGS = {
"",
"xyz",
"The quick brown fox jumps over the lazy dog",
"😁",
"mixed 😇 and ASCII",
"with combining characters like 🇦🇧🇨🇩"
};
@Test
public void testInsertAppendCharSequence() {
StringBuilder sb1 = new StringBuilder();
NumberStringBuilder sb2 = new NumberStringBuilder();
for (String str : EXAMPLE_STRINGS) {
NumberStringBuilder sb3 = new NumberStringBuilder();
sb1.append(str);
sb2.append(str, null);
sb3.append(str, null);
assertCharSequenceEquals(sb1, sb2);
assertCharSequenceEquals(sb3, str);
StringBuilder sb4 = new StringBuilder();
NumberStringBuilder sb5 = new NumberStringBuilder();
sb4.append("😇");
sb4.append(str);
sb4.append("xx");
sb5.append("😇xx", null);
sb5.insert(2, str, null);
assertCharSequenceEquals(sb4, sb5);
int start = Math.min(1, str.length());
int end = Math.min(10, str.length());
sb4.insert(3, str, start, end);
sb5.insert(3, str, start, end, null);
assertCharSequenceEquals(sb4, sb5);
sb4.append(str.toCharArray());
sb5.append(str.toCharArray(), null);
assertCharSequenceEquals(sb4, sb5);
sb4.insert(4, str.toCharArray());
sb5.insert(4, str.toCharArray(), null);
assertCharSequenceEquals(sb4, sb5);
}
}
@Test
public void testInsertAppendCodePoint() {
int[] cases = {0, 1, 60, 127, 128, 0x7fff, 0x8000, 0xffff, 0x10000, 0x1f000, 0x10ffff};
StringBuilder sb1 = new StringBuilder();
NumberStringBuilder sb2 = new NumberStringBuilder();
for (int cas : cases) {
NumberStringBuilder sb3 = new NumberStringBuilder();
sb1.appendCodePoint(cas);
sb2.appendCodePoint(cas, null);
sb3.appendCodePoint(cas, null);
assertCharSequenceEquals(sb1, sb2);
assertEquals(Character.codePointAt(sb3, 0), cas);
StringBuilder sb4 = new StringBuilder();
NumberStringBuilder sb5 = new NumberStringBuilder();
sb4.append("😇");
sb4.appendCodePoint(cas); // Java StringBuilder has no insertCodePoint()
sb4.append("xx");
sb5.append("😇xx", null);
sb5.insertCodePoint(2, cas, null);
assertCharSequenceEquals(sb4, sb5);
}
}
@Test
public void testFields() {
for (String str : EXAMPLE_STRINGS) {
NumberStringBuilder sb = new NumberStringBuilder();
sb.append(str, null);
sb.append(str, NumberFormat.Field.CURRENCY);
Field[] fields = sb.toFieldArray();
assertEquals(str.length() * 2, fields.length);
for (int i = 0; i < str.length(); i++) {
assertEquals(null, fields[i]);
assertEquals(NumberFormat.Field.CURRENCY, fields[i + str.length()]);
}
// Very basic FieldPosition test. More robust tests happen in NumberFormatTest.
// Let NumberFormatTest also take care of AttributedCharacterIterator material.
FieldPosition fp = new FieldPosition(NumberFormat.Field.CURRENCY);
sb.populateFieldPosition(fp, 0);
assertEquals(str.length(), fp.getBeginIndex());
assertEquals(str.length() * 2, fp.getEndIndex());
if (str.length() > 0) {
sb.insertCodePoint(2, 100, NumberFormat.Field.INTEGER);
fields = sb.toFieldArray();
assertEquals(str.length() * 2 + 1, fields.length);
assertEquals(fields[2], NumberFormat.Field.INTEGER);
}
sb.append(sb.clone());
sb.append(sb.toCharArray(), sb.toFieldArray());
int numNull = 0;
int numCurr = 0;
int numInt = 0;
Field[] oldFields = fields;
fields = sb.toFieldArray();
for (int i = 0; i < sb.length(); i++) {
assertEquals(oldFields[i % oldFields.length], fields[i]);
if (fields[i] == null) numNull++;
else if (fields[i] == NumberFormat.Field.CURRENCY) numCurr++;
else if (fields[i] == NumberFormat.Field.INTEGER) numInt++;
else throw new AssertionError("Encountered unknown field in " + str);
}
assertEquals(str.length() * 4, numNull);
assertEquals(numNull, numCurr);
assertEquals(str.length() > 0 ? 4 : 0, numInt);
NumberStringBuilder sb2 = new NumberStringBuilder();
sb2.append(sb);
assertTrue(sb.contentEquals(sb2));
assertTrue(sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray()));
sb2.insertCodePoint(0, 50, NumberFormat.Field.FRACTION);
assertTrue(!sb.contentEquals(sb2));
assertTrue(!sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray()));
}
}
@Test
public void testUnlimitedCapacity() {
NumberStringBuilder builder = new NumberStringBuilder();
// The builder should never fail upon repeated appends.
for (int i = 0; i < 1000; i++) {
assertEquals(builder.length(), i);
builder.appendCodePoint('x', null);
assertEquals(builder.length(), i + 1);
}
}
private static void assertCharSequenceEquals(CharSequence a, CharSequence b) {
assertEquals(a.toString(), b.toString());
assertEquals(a.length(), b.length());
for (int i = 0; i < a.length(); i++) {
assertEquals(a.charAt(i), b.charAt(i));
}
int start = Math.min(2, a.length());
int end = Math.min(12, a.length());
if (start != end) {
assertCharSequenceEquals(a.subSequence(start, end), b.subSequence(start, end));
}
}
}

View File

@ -0,0 +1,110 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Test;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.ULocale;
/** @author sffc */
public class PatternStringTest {
@Test
public void testLocalized() {
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
symbols.setDecimalSeparatorString("a");
symbols.setPercentString("b");
symbols.setMinusSignString(".");
symbols.setPlusSignString("'");
String standard = "+-abcb''a''#,##0.0%'a%'";
String localized = ".'ab'c'b''a'''#,##0a0b'a%'";
String toStandard = "+-'ab'c'b''a'''#,##0.0%'a%'";
assertEquals(localized, PatternString.convertLocalized(standard, symbols, true));
assertEquals(toStandard, PatternString.convertLocalized(localized, symbols, false));
}
@Test
public void testToPatternSimple() {
String[][] cases = {
{"#", "0"},
{"0", "0"},
{"#0", "0"},
{"###", "0"},
{"0.##", "0.##"},
{"0.00", "0.00"},
{"0.00#", "0.00#"},
{"#E0", "#E0"},
{"0E0", "0E0"},
{"#00E00", "#00E00"},
{"#,##0", "#,##0"},
{"#,##0E0", "#,##0E0"},
{"#;#", "0;0"},
{"#;-#", "0"}, // ignore a negative prefix pattern of '-' since that is the default
{"**##0", "**##0"},
{"*'x'##0", "*x##0"},
{"a''b0", "a''b0"},
{"*''##0", "*''##0"},
{"*📺##0", "*'📺'##0"},
{"*'நி'##0", "*'நி'##0"},
};
for (String[] cas : cases) {
String input = cas[0];
String output = cas[1];
Properties properties = PatternString.parseToProperties(input);
String actual = PatternString.propertiesToString(properties);
assertEquals(
"Failed on input pattern '" + input + "', properties " + properties, output, actual);
}
}
@Test
public void testToPatternWithProperties() {
Object[][] cases = {
{new Properties().setPositivePrefix("abc"), "abc#"},
{new Properties().setPositiveSuffix("abc"), "#abc"},
{new Properties().setPositivePrefixPattern("abc"), "abc#"},
{new Properties().setPositiveSuffixPattern("abc"), "#abc"},
{new Properties().setNegativePrefix("abc"), "#;abc#"},
{new Properties().setNegativeSuffix("abc"), "#;#abc"},
{new Properties().setNegativePrefixPattern("abc"), "#;abc#"},
{new Properties().setNegativeSuffixPattern("abc"), "#;#abc"},
{new Properties().setPositivePrefix("+"), "'+'#"},
{new Properties().setPositivePrefixPattern("+"), "+#"},
{new Properties().setPositivePrefix("+'"), "'+'''#"},
{new Properties().setPositivePrefix("'+"), "'''+'#"},
{new Properties().setPositivePrefix("'"), "''#"},
{new Properties().setPositivePrefixPattern("+''"), "+''#"},
};
for (Object[] cas : cases) {
Properties input = (Properties) cas[0];
String output = (String) cas[1];
String actual = PatternString.propertiesToString(input);
assertEquals("Failed on input properties " + input, output, actual);
}
}
@Test
public void testExceptionOnInvalid() {
String[] invalidPatterns = {"#.#.#", "0#", "0#.", ".#0", "0#.#0", "@0", "0@"};
for (String pattern : invalidPatterns) {
try {
PatternString.parseToProperties(pattern);
fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern);
} catch (IllegalArgumentException e) {
}
}
}
}

View File

@ -0,0 +1,331 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle;
import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CurrencyPluralInfo;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
public class PropertiesTest {
@Test
public void testBasicEquals() {
Properties p1 = new Properties();
Properties p2 = new Properties();
assertEquals(p1, p2);
p1.setPositivePrefix("abc");
assertNotEquals(p1, p2);
p2.setPositivePrefix("xyz");
assertNotEquals(p1, p2);
p1.setPositivePrefix("xyz");
assertEquals(p1, p2);
}
@Test
public void testFieldCoverage() {
Properties p0 = new Properties();
Properties p1 = new Properties();
Properties p2 = new Properties();
Properties p3 = new Properties();
Properties p4 = new Properties();
Set<Integer> hashCodes = new HashSet<Integer>();
Field[] fields = Properties.class.getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
// Check for getters and setters
String fieldNamePascalCase =
Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
String getterName = "get" + fieldNamePascalCase;
String setterName = "set" + fieldNamePascalCase;
Method getter, setter;
try {
getter = Properties.class.getMethod(getterName);
assertEquals(
"Getter does not return correct type", field.getType(), getter.getReturnType());
} catch (NoSuchMethodException e) {
fail("Could not find method " + getterName + " for field " + field);
continue;
} catch (SecurityException e) {
fail("Could not access method " + getterName + " for field " + field);
continue;
}
try {
setter = Properties.class.getMethod(setterName, field.getType());
assertEquals(
"Method " + setterName + " does not return correct type",
Properties.class,
setter.getReturnType());
} catch (NoSuchMethodException e) {
fail("Could not find method " + setterName + " for field " + field);
continue;
} catch (SecurityException e) {
fail("Could not access method " + setterName + " for field " + field);
continue;
}
// Check for parameter name equality.
// The parameter name is not always available, depending on compiler settings.
Parameter param = setter.getParameters()[0];
if (!param.getName().subSequence(0, 3).equals("arg")) {
assertEquals("Parameter name should equal field name", field.getName(), param.getName());
}
try {
// Check for default value (should be null for objects)
if (field.getType() != Integer.TYPE && field.getType() != Boolean.TYPE) {
Object default0 = getter.invoke(p0);
assertEquals("Field " + field + " has non-null default value:", null, default0);
}
// Check for getter, equals, and hash code behavior
Object val0 = getSampleValueForType(field.getType(), 0);
Object val1 = getSampleValueForType(field.getType(), 1);
Object val2 = getSampleValueForType(field.getType(), 2);
assertNotEquals(val0, val1);
setter.invoke(p1, val0);
setter.invoke(p2, val0);
assertEquals(p1, p2);
assertEquals(p1.hashCode(), p2.hashCode());
assertEquals(getter.invoke(p1), getter.invoke(p2));
assertEquals(getter.invoke(p1), val0);
assertNotEquals(getter.invoke(p1), val1);
hashCodes.add(p1.hashCode());
setter.invoke(p1, val1);
assertNotEquals("Field " + field + " is missing from equals()", p1, p2);
assertNotEquals(getter.invoke(p1), getter.invoke(p2));
assertNotEquals(getter.invoke(p1), val0);
assertEquals(getter.invoke(p1), val1);
setter.invoke(p1, val0);
assertEquals("Field " + field + " setter might have side effects", p1, p2);
assertEquals(p1.hashCode(), p2.hashCode());
assertEquals(getter.invoke(p1), getter.invoke(p2));
setter.invoke(p1, val1);
setter.invoke(p2, val1);
assertEquals(p1, p2);
assertEquals(p1.hashCode(), p2.hashCode());
assertEquals(getter.invoke(p1), getter.invoke(p2));
setter.invoke(p1, val2);
setter.invoke(p1, val1);
assertEquals("Field " + field + " setter might have side effects", p1, p2);
assertEquals(p1.hashCode(), p2.hashCode());
assertEquals(getter.invoke(p1), getter.invoke(p2));
hashCodes.add(p1.hashCode());
// Check for clone behavior
Properties copy = p1.clone();
assertEquals("Field " + field + " did not get copied in clone", p1, copy);
assertEquals(p1.hashCode(), copy.hashCode());
assertEquals(getter.invoke(p1), getter.invoke(copy));
// Check for copyFrom behavior
setter.invoke(p1, val0);
assertNotEquals(p1, p2);
assertNotEquals(getter.invoke(p1), getter.invoke(p2));
p2.copyFrom(p1);
assertEquals("Field " + field + " is missing from copyFrom()", p1, p2);
assertEquals(p1.hashCode(), p2.hashCode());
assertEquals(getter.invoke(p1), getter.invoke(p2));
// Load values into p3 and p4 for clear() behavior test
setter.invoke(p3, getSampleValueForType(field.getType(), 3));
hashCodes.add(p3.hashCode());
setter.invoke(p4, getSampleValueForType(field.getType(), 4));
hashCodes.add(p4.hashCode());
} catch (IllegalAccessException e) {
fail("Could not access method for field " + field);
} catch (IllegalArgumentException e) {
fail("Could call method for field " + field);
} catch (InvocationTargetException e) {
fail("Could invoke method on target for field " + field);
}
}
// Check for clear() behavior
assertNotEquals(p3, p4);
p3.clear();
p4.clear();
assertEquals("A field is missing from the clear() function", p3, p4);
// A good hashCode() implementation should produce very few collisions. We added at most
// 4*fields.length codes to the set. We'll say the implementation is good if we had at least
// fields.length unique values.
// TODO: Should the requirement be stronger than this?
assertTrue(
"Too many hash code collisions: " + hashCodes.size() + " out of " + (fields.length * 4),
hashCodes.size() >= fields.length);
}
/**
* Creates a valid sample instance of the given type. Used to simulate getters and setters.
*
* @param type The type to generate.
* @param seed An integer seed, guaranteed to be positive. The same seed should generate two
* instances that are equal. A different seed should in general generate two instances that
* are not equal; this might not always be possible, such as with booleans or enums where
* there are limited possible values.
* @return An instance of the specified type.
*/
Object getSampleValueForType(Class<?> type, int seed) {
if (type == Integer.TYPE) {
return seed * 1000001;
} else if (type == Boolean.TYPE) {
return (seed % 2) == 0;
} else if (type == BigDecimal.class) {
if (seed == 0) return null;
return new BigDecimal(seed * 1000002);
} else if (type == String.class) {
if (seed == 0) return null;
return BigInteger.valueOf(seed * 1000003).toString(32);
} else if (type == CompactStyle.class) {
if (seed == 0) return null;
CompactStyle[] values = CompactStyle.values();
return values[seed % values.length];
} else if (type == Currency.class) {
if (seed == 0) return null;
Object[] currencies = Currency.getAvailableCurrencies().toArray();
return currencies[seed % currencies.length];
} else if (type == CurrencyPluralInfo.class) {
if (seed == 0) return null;
ULocale[] locales = ULocale.getAvailableLocales();
return CurrencyPluralInfo.getInstance(locales[seed % locales.length]);
} else if (type == CurrencyStyle.class) {
if (seed == 0) return null;
CurrencyStyle[] values = CurrencyStyle.values();
return values[seed % values.length];
} else if (type == CurrencyUsage.class) {
if (seed == 0) return null;
CurrencyUsage[] values = CurrencyUsage.values();
return values[seed % values.length];
} else if (type == FormatWidth.class) {
if (seed == 0) return null;
FormatWidth[] values = FormatWidth.values();
return values[seed % values.length];
} else if (type == MathContext.class) {
if (seed == 0) return null;
RoundingMode[] modes = RoundingMode.values();
return new MathContext(seed, modes[seed % modes.length]);
} else if (type == MeasureUnit.class) {
if (seed == 0) return null;
Object[] units = MeasureUnit.getAvailable().toArray();
return units[seed % units.length];
} else if (type == PadPosition.class) {
if (seed == 0) return null;
PadPosition[] values = PadPosition.values();
return values[seed % values.length];
} else if (type == ParseMode.class) {
if (seed == 0) return null;
ParseMode[] values = ParseMode.values();
return values[seed % values.length];
} else if (type == RoundingMode.class) {
if (seed == 0) return null;
RoundingMode[] values = RoundingMode.values();
return values[seed % values.length];
} else if (type == SignificantDigitsMode.class) {
if (seed == 0) return null;
SignificantDigitsMode[] values = SignificantDigitsMode.values();
return values[seed % values.length];
} else {
fail("Don't know how to handle type " + type + ". Please add it to getSampleValueForType().");
return null;
}
}
@Test
public void TestBasicSerializationRoundTrip() throws IOException, ClassNotFoundException {
Properties props0 = new Properties();
// Write values to some of the fields
PatternString.parseToExistingProperties("A-**####,#00.00#b¤", props0);
// Write to byte stream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(props0);
oos.flush();
baos.close();
byte[] bytes = baos.toByteArray();
// Read from byte stream
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Object obj = ois.readObject();
ois.close();
Properties props1 = (Properties) obj;
// Test equality
assertEquals("Did not round-trip through serialization", props0, props1);
}
/** Handler for serialization compatibility test suite. */
public static class PropertiesHandler implements SerializableTestUtility.Handler {
@Override
public Object[] getTestObjects() {
return new Object[] {
new Properties(),
PatternString.parseToProperties("x#,##0.00%"),
new Properties().setCompactStyle(CompactStyle.LONG).setMinimumExponentDigits(2)
};
}
@Override
public boolean hasSameBehavior(Object a, Object b) {
return a.equals(b);
}
}
}

View File

@ -0,0 +1,134 @@
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.FormatQuantity4;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
public class RounderTest {
@Test
public void testSignificantDigitsRounder() {
Object[][][][] cases = {
{
{{1, -1}, {0, 2}, {2, 4}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
{
{0.0, "0.0", "0.0", "0"},
{0.054321, "0.05432", "0.05", "0.054"},
{0.54321, "0.5432", "0.54", "0.54"},
{1.0, "1.0", "1.0", "1"},
{5.4321, "5.432", "5.43", "5.43"},
{10.0, "10", "10", "10"},
{11.0, "11", "11", "11"},
{100.0, "100", "100", "100"},
{100.23, "100.2", "100.2", "100.2"},
{543210.0, "543200", "543200", "543200"},
}
},
{
{{1, -1}, {0, 0}, {2, -1}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
{
{0.0, "0.0", "0", "0"},
{0.054321, "0.054321", "0", "0.054"},
{0.54321, "0.54321", "1", "0.54"},
{1.0, "1.0", "1", "1"},
{5.4321, "5.4321", "5", "5.4"},
{10.0, "10", "10", "10"},
{11.0, "11", "11", "11"},
{100.0, "100", "100", "100"},
{100.23, "100.23", "100", "100"},
{543210.0, "543210", "543210", "543210"},
}
},
{
{{0, 2}, {1, 2}, {3, 3}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
{
{0.0, ".000", ".00", ".0"},
{0.054321, ".0543", ".05", ".0543"},
{0.54321, ".543", ".54", ".543"},
{1.0, "1.00", "1.00", "1.0"},
{5.4321, "5.43", "5.43", "5.43"},
{10.0, "10.0", "10.0", "10.0"},
{11.0, "11.0", "11.0", "11.0"},
{100.0, "00.0", "00.0", "00.0"},
{100.23, "00.2", "00.2", "00.2"},
{543210.0, "10.0", "10.0", "10.0"}
}
}
};
int caseNumber = 0;
for (Object[][][] cas : cases) {
int minInt = (Integer) cas[0][0][0];
int maxInt = (Integer) cas[0][0][1];
int minFrac = (Integer) cas[0][1][0];
int maxFrac = (Integer) cas[0][1][1];
int minSig = (Integer) cas[0][2][0];
int maxSig = (Integer) cas[0][2][1];
Properties properties = new Properties();
FormatQuantity4 fq = new FormatQuantity4();
properties.setMinimumIntegerDigits(minInt);
properties.setMaximumIntegerDigits(maxInt);
properties.setMinimumFractionDigits(minFrac);
properties.setMaximumFractionDigits(maxFrac);
properties.setMinimumSignificantDigits(minSig);
properties.setMaximumSignificantDigits(maxSig);
int runNumber = 0;
for (Object[] run : cas[1]) {
double input = (Double) run[0];
String expected1 = (String) run[1];
String expected2 = (String) run[2];
String expected3 = (String) run[3];
properties.setSignificantDigitsMode(SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION);
fq.setToDouble(input);
SignificantDigitsRounder.getInstance(properties).apply(fq);
assertEquals(
"Case " + caseNumber + ", run " + runNumber + ", mode 0: " + fq,
expected1,
formatQuantityToString(fq));
properties.setSignificantDigitsMode(SignificantDigitsMode.RESPECT_MAXIMUM_FRACTION);
fq.setToDouble(input);
SignificantDigitsRounder.getInstance(properties).apply(fq);
assertEquals(
"Case " + caseNumber + ", run " + runNumber + ", mode 1: " + fq,
expected2,
formatQuantityToString(fq));
properties.setSignificantDigitsMode(SignificantDigitsMode.ENSURE_MINIMUM_SIGNIFICANT);
fq.setToDouble(input);
SignificantDigitsRounder.getInstance(properties).apply(fq);
assertEquals(
"Case " + caseNumber + ", run " + runNumber + ", mode 2: " + fq,
expected3,
formatQuantityToString(fq));
runNumber++;
}
caseNumber++;
}
}
private String formatQuantityToString(FormatQuantity fq) {
StringBuilder sb = new StringBuilder();
int udm = fq.getUpperDisplayMagnitude();
int ldm = fq.getLowerDisplayMagnitude();
if (udm == -1) sb.append('.');
for (int m = udm; m >= ldm; m--) {
sb.append(fq.getDigit(m));
if (m == 0 && m > ldm) sb.append('.');
}
return sb.toString();
}
}

View File

@ -0,0 +1,345 @@
// © 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 java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.text.ParsePosition;
import com.ibm.icu.dev.test.format.DataDrivenNumberFormatTestData;
import com.ibm.icu.dev.test.format.DataDrivenNumberFormatTestUtility;
import com.ibm.icu.dev.test.format.DataDrivenNumberFormatTestUtility.CodeUnderTest;
import com.ibm.icu.impl.number.Endpoint;
import com.ibm.icu.impl.number.Format;
import com.ibm.icu.impl.number.FormatQuantity;
import com.ibm.icu.impl.number.FormatQuantity1;
import com.ibm.icu.impl.number.FormatQuantity2;
import com.ibm.icu.impl.number.FormatQuantity3;
import com.ibm.icu.impl.number.FormatQuantity4;
import com.ibm.icu.impl.number.Parse;
import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
import com.ibm.icu.text.DecimalFormat;
import com.ibm.icu.text.DecimalFormat.PropertySetter;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ULocale;
public class ShanesDataDrivenTestUtility extends CodeUnderTest {
static final String dataPath =
"../../../icu4j-core-tests/src/com/ibm/icu/dev/data/numberformattestspecification.txt";
public static void run() {
CodeUnderTest tester = new ShanesDataDrivenTestUtility();
DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(dataPath, tester);
}
@Override
public Character Id() {
return 'S';
}
/**
* Runs a single formatting test. On success, returns null. On failure, returns the error. This
* implementation just returns null. Subclasses should override.
*
* @param tuple contains the parameters of the format test.
*/
@Override
public String format(DataDrivenNumberFormatTestData tuple) {
String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
ULocale locale = (tuple.locale == null) ? ULocale.ENGLISH : tuple.locale;
Properties properties = PatternString.parseToProperties(pattern, tuple.currency != null);
propertiesFromTuple(tuple, properties);
Format fmt = Endpoint.fromBTA(properties, locale);
FormatQuantity q1, q2, q3;
if (tuple.format.equals("NaN")) {
q1 = q2 = new FormatQuantity1(Double.NaN);
q3 = new FormatQuantity2(Double.NaN);
} else if (tuple.format.equals("-Inf")) {
q1 = q2 = new FormatQuantity1(Double.NEGATIVE_INFINITY);
q3 = new FormatQuantity1(Double.NEGATIVE_INFINITY);
} else if (tuple.format.equals("Inf")) {
q1 = q2 = new FormatQuantity1(Double.POSITIVE_INFINITY);
q3 = new FormatQuantity1(Double.POSITIVE_INFINITY);
} else {
BigDecimal d = new BigDecimal(tuple.format);
if (d.precision() <= 16) {
q1 = new FormatQuantity1(d);
q2 = new FormatQuantity1(Double.parseDouble(tuple.format));
q3 = new FormatQuantity4(d);
} else {
q1 = new FormatQuantity1(d);
q2 = new FormatQuantity3(d);
q3 = new FormatQuantity4(d); // duplicate values so no null
}
}
String expected = tuple.output;
String actual1 = fmt.format(q1);
if (!expected.equals(actual1)) {
return "Expected \"" + expected + "\", got \"" + actual1 + "\" on FormatQuantity1 BigDecimal";
}
String actual2 = fmt.format(q2);
if (!expected.equals(actual2)) {
return "Expected \"" + expected + "\", got \"" + actual2 + "\" on FormatQuantity1 double";
}
String actual3 = fmt.format(q3);
if (!expected.equals(actual3)) {
return "Expected \"" + expected + "\", got \"" + actual3 + "\" on FormatQuantity4 BigDecimal";
}
return null;
}
/**
* Runs a single toPattern test. On success, returns null. On failure, returns the error. This
* implementation just returns null. Subclasses should override.
*
* @param tuple contains the parameters of the format test.
*/
@Override
public String toPattern(DataDrivenNumberFormatTestData tuple) {
String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
final Properties properties;
DecimalFormat df;
try {
properties = PatternString.parseToProperties(pattern, tuple.currency != null);
propertiesFromTuple(tuple, properties);
// TODO: Use PatternString.propertiesToString() directly. (How to deal with CurrencyUsage?)
df = new DecimalFormat();
df.setProperties(
new PropertySetter() {
@Override
public void set(Properties props) {
props.copyFrom(properties);
}
});
} catch (IllegalArgumentException e) {
e.printStackTrace();
return e.getLocalizedMessage();
}
if (tuple.toPattern != null) {
String expected = tuple.toPattern;
String actual = df.toPattern();
if (!expected.equals(actual)) {
return "Expected toPattern='" + expected + "'; got '" + actual + "'";
}
}
if (tuple.toLocalizedPattern != null) {
String expected = tuple.toLocalizedPattern;
String actual = PatternString.propertiesToString(properties);
if (!expected.equals(actual)) {
return "Expected toLocalizedPattern='" + expected + "'; got '" + actual + "'";
}
}
return null;
}
/**
* Runs a single parse test. On success, returns null. On failure, returns the error. This
* implementation just returns null. Subclasses should override.
*
* @param tuple contains the parameters of the format test.
*/
@Override
public String parse(DataDrivenNumberFormatTestData tuple) {
String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
Properties properties;
ParsePosition ppos = new ParsePosition(0);
Number actual;
try {
properties = PatternString.parseToProperties(pattern, tuple.currency != null);
propertiesFromTuple(tuple, properties);
actual =
Parse.parse(
tuple.parse, ppos, properties, DecimalFormatSymbols.getInstance(tuple.locale));
} catch (IllegalArgumentException e) {
return "parse exception: " + e.getMessage();
}
if (actual == null && ppos.getIndex() != 0) {
throw new AssertionError("Error: value is null but parse position is not zero");
}
if (ppos.getIndex() == 0) {
return "Parse failed; got " + actual + ", but expected " + tuple.output;
}
if (tuple.output.equals("NaN")) {
if (!Double.isNaN(actual.doubleValue())) {
return "Expected NaN, but got: " + actual;
}
return null;
} else if (tuple.output.equals("Inf")) {
if (!Double.isInfinite(actual.doubleValue())
|| Double.compare(actual.doubleValue(), 0.0) < 0) {
return "Expected Inf, but got: " + actual;
}
return null;
} else if (tuple.output.equals("-Inf")) {
if (!Double.isInfinite(actual.doubleValue())
|| Double.compare(actual.doubleValue(), 0.0) > 0) {
return "Expected -Inf, but got: " + actual;
}
return null;
} else if (tuple.output.equals("fail")) {
return null;
} else if (new BigDecimal(tuple.output).compareTo(new BigDecimal(actual.toString())) != 0) {
return "Expected: " + tuple.output + ", got: " + actual;
} else {
return null;
}
}
/**
* Runs a single parse currency test. On success, returns null. On failure, returns the error.
* This implementation just returns null. Subclasses should override.
*
* @param tuple contains the parameters of the format test.
*/
@Override
public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
Properties properties;
ParsePosition ppos = new ParsePosition(0);
CurrencyAmount actual;
try {
properties = PatternString.parseToProperties(pattern, tuple.currency != null);
propertiesFromTuple(tuple, properties);
actual =
Parse.parseCurrency(
tuple.parse, ppos, properties, DecimalFormatSymbols.getInstance(tuple.locale));
} catch (ParseException e) {
e.printStackTrace();
return "parse exception: " + e.getMessage();
}
if (ppos.getIndex() == 0 || actual.getCurrency().getCurrencyCode().equals("XXX")) {
return "Parse failed; got " + actual + ", but expected " + tuple.output;
}
BigDecimal expectedNumber = new BigDecimal(tuple.output);
if (expectedNumber.compareTo(new BigDecimal(actual.getNumber().toString())) != 0) {
return "Wrong number: Expected: " + expectedNumber + ", got: " + actual;
}
String expectedCurrency = tuple.outputCurrency;
if (!expectedCurrency.equals(actual.getCurrency().toString())) {
return "Wrong currency: Expected: " + expectedCurrency + ", got: " + actual;
}
return null;
}
/**
* Runs a single select test. On success, returns null. On failure, returns the error. This
* implementation just returns null. Subclasses should override.
*
* @param tuple contains the parameters of the format test.
*/
@Override
public String select(DataDrivenNumberFormatTestData tuple) {
return null;
}
private static void propertiesFromTuple(
DataDrivenNumberFormatTestData tuple, Properties properties) {
if (tuple.minIntegerDigits != null) {
properties.setMinimumIntegerDigits(tuple.minIntegerDigits);
}
if (tuple.maxIntegerDigits != null) {
properties.setMaximumIntegerDigits(tuple.maxIntegerDigits);
}
if (tuple.minFractionDigits != null) {
properties.setMinimumFractionDigits(tuple.minFractionDigits);
}
if (tuple.maxFractionDigits != null) {
properties.setMaximumFractionDigits(tuple.maxFractionDigits);
}
if (tuple.currency != null) {
properties.setCurrency(tuple.currency);
}
if (tuple.minGroupingDigits != null) {
properties.setMinimumGroupingDigits(tuple.minGroupingDigits);
}
if (tuple.useSigDigits != null) {
// TODO
}
if (tuple.minSigDigits != null) {
properties.setMinimumSignificantDigits(tuple.minSigDigits);
}
if (tuple.maxSigDigits != null) {
properties.setMaximumSignificantDigits(tuple.maxSigDigits);
}
if (tuple.useGrouping != null && tuple.useGrouping == 0) {
properties.setGroupingSize(Integer.MAX_VALUE);
properties.setSecondaryGroupingSize(Integer.MAX_VALUE);
}
if (tuple.multiplier != null) {
properties.setMultiplier(new BigDecimal(tuple.multiplier));
}
if (tuple.roundingIncrement != null) {
properties.setRoundingIncrement(new BigDecimal(tuple.roundingIncrement.toString()));
}
if (tuple.formatWidth != null) {
properties.setFormatWidth(tuple.formatWidth);
}
if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
properties.setPadString(tuple.padCharacter.toString());
}
if (tuple.useScientific != null) {
properties.setMinimumExponentDigits(
tuple.useScientific != 0 ? 1 : Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
}
if (tuple.grouping != null) {
properties.setGroupingSize(tuple.grouping);
}
if (tuple.grouping2 != null) {
properties.setSecondaryGroupingSize(tuple.grouping2);
}
if (tuple.roundingMode != null) {
properties.setRoundingMode(RoundingMode.valueOf(tuple.roundingMode));
}
if (tuple.currencyUsage != null) {
properties.setCurrencyUsage(tuple.currencyUsage);
}
if (tuple.minimumExponentDigits != null) {
properties.setMinimumExponentDigits(tuple.minimumExponentDigits.byteValue());
}
if (tuple.exponentSignAlwaysShown != null) {
properties.setExponentSignAlwaysShown(tuple.exponentSignAlwaysShown != 0);
}
if (tuple.decimalSeparatorAlwaysShown != null) {
properties.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
}
if (tuple.padPosition != null) {
properties.setPadPosition(PadPosition.fromOld(tuple.padPosition));
}
if (tuple.positivePrefix != null) {
properties.setPositivePrefix(tuple.positivePrefix);
}
if (tuple.positiveSuffix != null) {
properties.setPositiveSuffix(tuple.positiveSuffix);
}
if (tuple.negativePrefix != null) {
properties.setNegativePrefix(tuple.negativePrefix);
}
if (tuple.negativeSuffix != null) {
properties.setNegativeSuffix(tuple.negativeSuffix);
}
if (tuple.localizedPattern != null) {
// TODO
}
if (tuple.lenient != null) {
properties.setParseMode(tuple.lenient == 0 ? ParseMode.STRICT : ParseMode.LENIENT);
}
if (tuple.parseIntegerOnly != null) {
properties.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
}
if (tuple.parseCaseSensitive != null) {
properties.setParseCaseSensitive(tuple.parseCaseSensitive != 0);
}
if (tuple.decimalPatternMatchRequired != null) {
properties.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0);
}
if (tuple.parseNoExponent != null) {
properties.setParseNoExponent(tuple.parseNoExponent != 0);
}
}
}

View File

@ -1106,7 +1106,14 @@ public class FormatHandler
NumberFormat format_b = (NumberFormat) b;
double number = 1234.56;
return format_a.format(number).equals(format_b.format(number));
String result_a = format_a.format(number);
String result_b = format_b.format(number);
boolean equal = result_a.equals(result_b);
if (!equal) {
System.out.println(format_a+" "+format_b);
System.out.println(result_a+" "+result_b);
}
return equal;
}
}
@ -1710,7 +1717,17 @@ public class FormatHandler
char chars_a[] = getCharSymbols(dfs_a);
char chars_b[] = getCharSymbols(dfs_b);
return SerializableTestUtility.compareStrings(strings_a, strings_b) && SerializableTestUtility.compareChars(chars_a, chars_b);
// Spot-check char-to-string conversion (ICU 58)
String percent_a1 = Character.toString(dfs_a.getPercent());
String percent_a2 = dfs_a.getPercentString();
String percent_b1 = Character.toString(dfs_b.getPercent());
String percent_b2 = dfs_b.getPercentString();
return SerializableTestUtility.compareStrings(strings_a, strings_b)
&& SerializableTestUtility.compareChars(chars_a, chars_b)
&& percent_a1.equals(percent_b1)
&& percent_a2.equals(percent_b2)
&& percent_a1.equals(percent_a2);
}
}

View File

@ -30,6 +30,7 @@ import java.util.Locale;
import com.ibm.icu.dev.test.format.MeasureUnitTest;
import com.ibm.icu.dev.test.format.PluralRulesTest;
import com.ibm.icu.dev.test.number.PropertiesTest;
import com.ibm.icu.impl.JavaTimeZone;
import com.ibm.icu.impl.OlsonTimeZone;
import com.ibm.icu.impl.TimeZoneAdapter;
@ -827,6 +828,7 @@ public class SerializableTestUtility {
map.put("com.ibm.icu.util.MeasureUnit", new MeasureUnitTest.MeasureUnitHandler());
map.put("com.ibm.icu.util.TimeUnit", new MeasureUnitTest.MeasureUnitHandler());
map.put("com.ibm.icu.text.MeasureFormat", new MeasureUnitTest.MeasureFormatHandler());
map.put("com.ibm.icu.impl.number.Properties", new PropertiesTest.PropertiesHandler());
map.put("com.ibm.icu.util.ICUException", new ICUExceptionHandler());
map.put("com.ibm.icu.util.ICUUncheckedIOException", new ICUUncheckedIOExceptionHandler());
@ -925,6 +927,11 @@ public class SerializableTestUtility {
return;
}
if (className.equals("com.ibm.icu.text.DecimalFormat_ICU58")) {
// Do not test the legacy DecimalFormat class in ICU 59
return;
}
if (c.isEnum() || !serializable.isAssignableFrom(c)) {
//System.out.println("@@@ Skipping: " + className);
return;

View File

@ -8,6 +8,7 @@
*/
package com.ibm.icu.dev.test.util;
import java.util.Arrays;
import java.util.Iterator;
import org.junit.Test;
@ -25,6 +26,11 @@ public class TextTrieMapTest extends TestFmwk {
private static final Integer FRI = new Integer(6);
private static final Integer SAT = new Integer(7);
private static final Integer SUP1 = new Integer(8);
private static final Integer SUP2 = new Integer(9);
private static final Integer SUP3 = new Integer(10);
private static final Integer SUP4 = new Integer(11);
private static final Integer FOO = new Integer(-1);
private static final Integer BAR = new Integer(-2);
@ -49,7 +55,11 @@ public class TextTrieMapTest extends TestFmwk {
{"W", WED},
{"T", THU},
{"F", FRI},
{"S", SAT}
{"S", SAT},
{"L📺", SUP1}, // L, 0xD83D, 0xDCFA
{"L📺1", SUP2}, // L, 0xD83D, 0xDCFA, 1
{"L📻", SUP3}, // L, 0xD83D, 0xDCFB
{"L🃏", SUP4}, // L, 0xD83C, 0xDCCF
};
private static final Object[][] TESTCASES = {
@ -62,7 +72,70 @@ public class TextTrieMapTest extends TestFmwk {
{"TEST", new Object[]{TUE, THU}, new Object[]{TUE, THU}},
{"SUN", new Object[]{SUN, SAT}, SUN},
{"super", null, SUN},
{"NO", null, null}
{"NO", null, null},
{"L📺", SUP1, SUP1},
{"l📺", null, SUP1},
};
private static final Object[][] TESTCASES_PARSE = {
{
"Sunday",
new Object[]{
new Object[]{SAT,SUN}, new Object[]{SAT,SUN}, // matches on "S"
null, null, // matches on "Su"
SUN, SUN, // matches on "Sun"
null, null, // matches on "Sund"
null, null, // matches on "Sunda"
SUN, SUN, // matches on "Sunday"
}
},
{
"sunday",
new Object[]{
null, new Object[]{SAT,SUN}, // matches on "s"
null, null, // matches on "su"
null, SUN, // matches on "sun"
null, null, // matches on "sund"
null, null, // matches on "sunda"
null, SUN, // matches on "sunday"
}
},
{
"MMM",
new Object[]{
MON, MON, // matches on "M"
// no more matches in data
}
},
{
"BBB",
new Object[]{
// no matches in data
}
},
{
"l📺12",
new Object[]{
null, null, // matches on "L"
null, SUP1, // matches on "L📺"
null, SUP2, // matches on "L📺1"
// no more matches in data
}
},
{
"L📻",
new Object[] {
null, null, // matches on "L"
SUP3, SUP3, // matches on "L📻"
}
},
{
"L🃏",
new Object[] {
null, null, // matches on "L"
SUP4, SUP4, // matches on "L🃏"
}
}
};
@Test
@ -76,7 +149,7 @@ public class TextTrieMapTest extends TestFmwk {
logln("Test for get(String)");
for (int i = 0; i < TESTCASES.length; i++) {
itr = map.get((String)TESTCASES[i][0]);
checkResult(itr, TESTCASES[i][1]);
checkResult("get(String) case " + i, itr, TESTCASES[i][1]);
}
logln("Test for get(String, int)");
@ -88,7 +161,14 @@ public class TextTrieMapTest extends TestFmwk {
}
textBuf.append(TESTCASES[i][0]);
itr = map.get(textBuf.toString(), i);
checkResult(itr, TESTCASES[i][1]);
checkResult("get(String, int) case " + i, itr, TESTCASES[i][1]);
}
logln("Test for ParseState");
for (int i = 0; i < TESTCASES_PARSE.length; i++) {
String test = (String) TESTCASES_PARSE[i][0];
Object[] expecteds = (Object[]) TESTCASES_PARSE[i][1];
checkParse(map, test, expecteds, true);
}
// Add duplicated entry
@ -98,7 +178,7 @@ public class TextTrieMapTest extends TestFmwk {
// Make sure the all entries are returned
itr = map.get("Sunday");
checkResult(itr, new Object[]{FOO, SUN});
checkResult("Get Sunday", itr, new Object[]{FOO, SUN});
}
@Test
@ -112,7 +192,7 @@ public class TextTrieMapTest extends TestFmwk {
logln("Test for get(String)");
for (int i = 0; i < TESTCASES.length; i++) {
itr = map.get((String)TESTCASES[i][0]);
checkResult(itr, TESTCASES[i][2]);
checkResult("get(String) case " + i, itr, TESTCASES[i][2]);
}
logln("Test for get(String, int)");
@ -124,7 +204,14 @@ public class TextTrieMapTest extends TestFmwk {
}
textBuf.append(TESTCASES[i][0]);
itr = map.get(textBuf.toString(), i);
checkResult(itr, TESTCASES[i][2]);
checkResult("get(String, int) case " + i, itr, TESTCASES[i][2]);
}
logln("Test for ParseState");
for (int i = 0; i < TESTCASES_PARSE.length; i++) {
String test = (String) TESTCASES_PARSE[i][0];
Object[] expecteds = (Object[]) TESTCASES_PARSE[i][1];
checkParse(map, test, expecteds, false);
}
// Add duplicated entry
@ -134,7 +221,55 @@ public class TextTrieMapTest extends TestFmwk {
// Make sure the all entries are returned
itr = map.get("Sunday");
checkResult(itr, new Object[]{SUN, FOO, BAR});
checkResult("Get Sunday", itr, new Object[]{SUN, FOO, BAR});
}
private void checkParse(TextTrieMap map, String text, Object[] rawExpecteds, boolean caseSensitive) {
// rawExpecteds has even-valued indices for case sensitive and odd-valued indicies for case insensitive
// Get out only the values that we want.
Object[] expecteds = null;
for (int i=rawExpecteds.length/2-1; i>=0; i--) {
int j = i*2+(caseSensitive?0:1);
if (rawExpecteds[j] != null) {
if (expecteds == null) {
expecteds = new Object[i+1];
}
expecteds[i] = rawExpecteds[j];
}
}
if (expecteds == null) {
expecteds = new Object[0];
}
TextTrieMap.ParseState state = null;
for (int charOffset=0, cpOffset=0; charOffset < text.length(); cpOffset++) {
int cp = Character.codePointAt(text, charOffset);
if (state == null) {
state = map.openParseState(cp);
}
if (state == null) {
assertEquals("Expected matches, but no matches are available", 0, expecteds.length);
break;
}
state.accept(cp);
if (cpOffset < expecteds.length - 1) {
assertFalse(
"In middle of parse sequence, but atEnd() is true: '" + text + "' offset " + charOffset,
state.atEnd());
} else if (cpOffset == expecteds.length) {
// Note: it possible for atEnd() to be either true or false at expecteds.length - 1;
// if true, we are at the end of the input string; if false, there is still input string
// left to be consumed, but we don't know if there are remaining matches.
assertTrue(
"At end of parse sequence, but atEnd() is false: '" + text + "' offset " + charOffset,
state.atEnd());
break;
}
Object expected = expecteds[cpOffset];
Iterator actual = state.getCurrentMatches();
checkResult("ParseState '" + text + "' offset " + charOffset, actual, expected);
charOffset += Character.charCount(cp);
}
}
private boolean eql(Object o1, Object o2) {
@ -147,10 +282,13 @@ public class TextTrieMapTest extends TestFmwk {
return o1.equals(o2);
}
private void checkResult(Iterator itr, Object expected) {
private void checkResult(String memo, Iterator itr, Object expected) {
if (itr == null) {
if (expected != null) {
errln("FAIL: Empty results - Expected: " + expected);
String expectedStr = (expected instanceof Object[])
? Arrays.toString((Object[]) expected)
: expected.toString();
errln("FAIL: Empty results: " + memo + ": Expected: " + expectedStr);
}
return;
}

View File

@ -2,17 +2,17 @@
<launchConfiguration type="org.eclipse.ant.AntLaunchConfigurationType">
<booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/icu4j-translit-tests/build.xml"/>
<listEntry value="/icu4j-core-tests"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
<listEntry value="4"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="icu4j-core-tests"/>
<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<mapAttribute key="org.eclipse.ui.externaltools.ATTR_ANT_PROPERTIES">
<mapEntry key="eclipse.running" value="true"/>
<mapEntry key="build.compiler" value="org.eclipse.jdt.core.JDTCompilerAdapter"/>
<mapEntry key="eclipse.running" value="true"/>
</mapAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_ANT_PROPERTY_FILES" value="${workspace_loc:/icu4j-shared/build/locations-eclipse.properties},"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/icu4j-translit-tests/build.xml}"/>