ICU-8610 Adding more tests; normalized skeleton implementation; minor tweaks.
X-SVN-Rev: 41054
This commit is contained in:
parent
c5e86f87c8
commit
c2fa8cacad
@ -70,6 +70,8 @@ public class IntegerWidth {
|
||||
return this;
|
||||
} else if (maxInt >= 0 && maxInt <= RoundingUtils.MAX_INT_FRAC_SIG && maxInt >= minInt) {
|
||||
return new IntegerWidth(minInt, maxInt);
|
||||
} else if (minInt == 1 && maxInt == -1) {
|
||||
return DEFAULT;
|
||||
} else if (maxInt == -1) {
|
||||
return new IntegerWidth(minInt, -1);
|
||||
} else {
|
||||
|
@ -460,6 +460,7 @@ public final class NumberFormatter {
|
||||
* @param skeleton
|
||||
* The skeleton string off of which to base this NumberFormatter.
|
||||
* @return An {@link UnlocalizedNumberFormatter}, to be used for chaining.
|
||||
* @throws SkeletonSyntaxException If the given string is not a valid number formatting skeleton.
|
||||
* @draft ICU 62
|
||||
* @provisional This API might change or be removed in a future release.
|
||||
*/
|
||||
|
@ -194,7 +194,7 @@ class NumberFormatterImpl {
|
||||
} else if (isCurrency) {
|
||||
micros.rounding = Rounder.MONETARY_STANDARD;
|
||||
} else {
|
||||
micros.rounding = Rounder.MAX_FRAC_6;
|
||||
micros.rounding = Rounder.DEFAULT_MAX_FRAC_6;
|
||||
}
|
||||
micros.rounding = micros.rounding.withLocaleData(currency);
|
||||
|
||||
|
@ -478,8 +478,17 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
|
||||
/**
|
||||
* Creates a skeleton string representation of this number formatter. A skeleton string is a
|
||||
* locale-agnostic serialized form of a number formatter.
|
||||
* <p>
|
||||
* Not all options are capable of being represented in the skeleton string; for example, a
|
||||
* DecimalFormatSymbols object. If any such option is encountered, an
|
||||
* {@link UnsupportedOperationException} is thrown.
|
||||
* <p>
|
||||
* The returned skeleton is in normalized form, such that two number formatters with equivalent
|
||||
* behavior should produce the same skeleton.
|
||||
*
|
||||
* @return A number skeleton string with behavior corresponding to this number formatter.
|
||||
* @throws UnsupportedOperationException
|
||||
* If the number formatter has an option that cannot be represented in a skeleton string.
|
||||
* @draft ICU 62
|
||||
* @provisional This API might change or be removed in a future release.
|
||||
*/
|
||||
|
@ -7,15 +7,17 @@ import java.math.RoundingMode;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.ibm.icu.impl.CacheBase;
|
||||
import com.ibm.icu.impl.PatternProps;
|
||||
import com.ibm.icu.impl.SoftCache;
|
||||
import com.ibm.icu.impl.StringSegment;
|
||||
import com.ibm.icu.impl.number.MacroProps;
|
||||
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
|
||||
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
|
||||
import com.ibm.icu.number.NumberFormatter.SignDisplay;
|
||||
import com.ibm.icu.number.NumberFormatter.UnitWidth;
|
||||
import com.ibm.icu.text.DecimalFormatSymbols;
|
||||
import com.ibm.icu.text.NumberingSystem;
|
||||
import com.ibm.icu.util.Currency;
|
||||
import com.ibm.icu.util.Currency.CurrencyUsage;
|
||||
@ -83,52 +85,64 @@ class NumberSkeletonImpl {
|
||||
static final SkeletonDataStructure skeletonData = new SkeletonDataStructure();
|
||||
|
||||
static {
|
||||
skeletonData.put(StemType.COMPACT_NOTATION, "compact-short", Notation.compactShort());
|
||||
skeletonData.put(StemType.COMPACT_NOTATION, "compact-long", Notation.compactLong());
|
||||
skeletonData.put(StemType.SCIENTIFIC_NOTATION, "scientific", Notation.scientific());
|
||||
skeletonData.put(StemType.SCIENTIFIC_NOTATION, "engineering", Notation.engineering());
|
||||
skeletonData.put(StemType.SIMPLE_NOTATION, "simple-notation", Notation.simple());
|
||||
SkeletonDataStructure d = skeletonData; // abbreviate for shorter lines
|
||||
d.put(StemType.COMPACT_NOTATION, "compact-short", Notation.compactShort());
|
||||
d.put(StemType.COMPACT_NOTATION, "compact-long", Notation.compactLong());
|
||||
d.put(StemType.SCIENTIFIC_NOTATION, "scientific", Notation.scientific());
|
||||
d.put(StemType.SCIENTIFIC_NOTATION, "engineering", Notation.engineering());
|
||||
d.put(StemType.SIMPLE_NOTATION, "notation-simple", Notation.simple());
|
||||
|
||||
skeletonData.put(StemType.NO_UNIT, "base-unit", NoUnit.BASE);
|
||||
skeletonData.put(StemType.NO_UNIT, "percent", NoUnit.PERCENT);
|
||||
skeletonData.put(StemType.NO_UNIT, "permille", NoUnit.PERMILLE);
|
||||
d.put(StemType.NO_UNIT, "base-unit", NoUnit.BASE);
|
||||
d.put(StemType.NO_UNIT, "percent", NoUnit.PERCENT);
|
||||
d.put(StemType.NO_UNIT, "permille", NoUnit.PERMILLE);
|
||||
|
||||
skeletonData.put(StemType.ROUNDER, "round-integer", Rounder.integer());
|
||||
skeletonData.put(StemType.ROUNDER, "round-unlimited", Rounder.unlimited());
|
||||
skeletonData.put(StemType.ROUNDER,
|
||||
"round-currency-standard",
|
||||
Rounder.currency(CurrencyUsage.STANDARD));
|
||||
skeletonData.put(StemType.ROUNDER, "round-currency-cash", Rounder.currency(CurrencyUsage.CASH));
|
||||
d.put(StemType.ROUNDER, "round-integer", Rounder.integer());
|
||||
d.put(StemType.ROUNDER, "round-unlimited", Rounder.unlimited());
|
||||
d.put(StemType.ROUNDER, "round-currency-standard", Rounder.currency(CurrencyUsage.STANDARD));
|
||||
d.put(StemType.ROUNDER, "round-currency-cash", Rounder.currency(CurrencyUsage.CASH));
|
||||
|
||||
skeletonData.put(StemType.GROUPING, "group-off", GroupingStrategy.OFF);
|
||||
skeletonData.put(StemType.GROUPING, "group-min2", GroupingStrategy.MIN2);
|
||||
skeletonData.put(StemType.GROUPING, "group-auto", GroupingStrategy.AUTO);
|
||||
skeletonData.put(StemType.GROUPING, "group-on-aligned", GroupingStrategy.ON_ALIGNED);
|
||||
skeletonData.put(StemType.GROUPING, "group-thousands", GroupingStrategy.THOUSANDS);
|
||||
d.put(StemType.GROUPING, "group-off", GroupingStrategy.OFF);
|
||||
d.put(StemType.GROUPING, "group-min2", GroupingStrategy.MIN2);
|
||||
d.put(StemType.GROUPING, "group-auto", GroupingStrategy.AUTO);
|
||||
d.put(StemType.GROUPING, "group-on-aligned", GroupingStrategy.ON_ALIGNED);
|
||||
d.put(StemType.GROUPING, "group-thousands", GroupingStrategy.THOUSANDS);
|
||||
|
||||
skeletonData.put(StemType.LATIN, "latin", NumberingSystem.LATIN);
|
||||
d.put(StemType.LATIN, "latin", NumberingSystem.LATIN);
|
||||
|
||||
skeletonData.put(StemType.UNIT_WIDTH, "unit-width-narrow", UnitWidth.NARROW);
|
||||
skeletonData.put(StemType.UNIT_WIDTH, "unit-width-short", UnitWidth.SHORT);
|
||||
skeletonData.put(StemType.UNIT_WIDTH, "unit-width-full-name", UnitWidth.FULL_NAME);
|
||||
skeletonData.put(StemType.UNIT_WIDTH, "unit-width-iso-code", UnitWidth.ISO_CODE);
|
||||
skeletonData.put(StemType.UNIT_WIDTH, "unit-width-hidden", UnitWidth.HIDDEN);
|
||||
d.put(StemType.UNIT_WIDTH, "unit-width-narrow", UnitWidth.NARROW);
|
||||
d.put(StemType.UNIT_WIDTH, "unit-width-short", UnitWidth.SHORT);
|
||||
d.put(StemType.UNIT_WIDTH, "unit-width-full-name", UnitWidth.FULL_NAME);
|
||||
d.put(StemType.UNIT_WIDTH, "unit-width-iso-code", UnitWidth.ISO_CODE);
|
||||
d.put(StemType.UNIT_WIDTH, "unit-width-hidden", UnitWidth.HIDDEN);
|
||||
|
||||
skeletonData.put(StemType.SIGN_DISPLAY, "sign-auto", SignDisplay.AUTO);
|
||||
skeletonData.put(StemType.SIGN_DISPLAY, "sign-always", SignDisplay.ALWAYS);
|
||||
skeletonData.put(StemType.SIGN_DISPLAY, "sign-never", SignDisplay.NEVER);
|
||||
skeletonData.put(StemType.SIGN_DISPLAY, "sign-accounting", SignDisplay.ACCOUNTING);
|
||||
skeletonData.put(StemType.SIGN_DISPLAY, "sign-accounting-always", SignDisplay.ACCOUNTING_ALWAYS);
|
||||
skeletonData.put(StemType.SIGN_DISPLAY, "sign-except-zero", SignDisplay.EXCEPT_ZERO);
|
||||
skeletonData.put(StemType.SIGN_DISPLAY,
|
||||
"sign-accounting-except-zero",
|
||||
SignDisplay.ACCOUNTING_EXCEPT_ZERO);
|
||||
d.put(StemType.SIGN_DISPLAY, "sign-auto", SignDisplay.AUTO);
|
||||
d.put(StemType.SIGN_DISPLAY, "sign-always", SignDisplay.ALWAYS);
|
||||
d.put(StemType.SIGN_DISPLAY, "sign-never", SignDisplay.NEVER);
|
||||
d.put(StemType.SIGN_DISPLAY, "sign-accounting", SignDisplay.ACCOUNTING);
|
||||
d.put(StemType.SIGN_DISPLAY, "sign-accounting-always", SignDisplay.ACCOUNTING_ALWAYS);
|
||||
d.put(StemType.SIGN_DISPLAY, "sign-except-zero", SignDisplay.EXCEPT_ZERO);
|
||||
d.put(StemType.SIGN_DISPLAY, "sign-accounting-except-zero", SignDisplay.ACCOUNTING_EXCEPT_ZERO);
|
||||
|
||||
skeletonData.put(StemType.DECIMAL_DISPLAY, "decimal-auto", DecimalSeparatorDisplay.AUTO);
|
||||
skeletonData.put(StemType.DECIMAL_DISPLAY, "decimal-always", DecimalSeparatorDisplay.ALWAYS);
|
||||
d.put(StemType.DECIMAL_DISPLAY, "decimal-auto", DecimalSeparatorDisplay.AUTO);
|
||||
d.put(StemType.DECIMAL_DISPLAY, "decimal-always", DecimalSeparatorDisplay.ALWAYS);
|
||||
}
|
||||
|
||||
private static final Map<String, UnlocalizedNumberFormatter> cache = new ConcurrentHashMap<String, UnlocalizedNumberFormatter>();
|
||||
static final String[] ROUNDING_MODE_STRINGS = {
|
||||
"up",
|
||||
"down",
|
||||
"ceiling",
|
||||
"floor",
|
||||
"half-up",
|
||||
"half-down",
|
||||
"half-even",
|
||||
"unnecessary" };
|
||||
|
||||
private static final CacheBase<String, UnlocalizedNumberFormatter, Void> cache = new SoftCache<String, UnlocalizedNumberFormatter, Void>() {
|
||||
@Override
|
||||
protected UnlocalizedNumberFormatter createInstance(String skeletonString, Void unused) {
|
||||
return create(skeletonString);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number formatter for the given number skeleton string from the cache, creating it if it
|
||||
@ -139,43 +153,9 @@ class NumberSkeletonImpl {
|
||||
* @return An UnlocalizedNumberFormatter with behavior defined by the given skeleton string.
|
||||
*/
|
||||
public static UnlocalizedNumberFormatter getOrCreate(String skeletonString) {
|
||||
String unNormalized = skeletonString; // more appropriate variable name for the implementation
|
||||
|
||||
// First try: look up the un-normalized skeleton.
|
||||
UnlocalizedNumberFormatter formatter = cache.get(unNormalized);
|
||||
if (formatter != null) {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
// Second try: normalize the skeleton, and then access the cache.
|
||||
// Store the un-normalized form for a faster lookup next time.
|
||||
// Synchronize because we need a transaction with multiple queries to the cache.
|
||||
String normalized = normalizeSkeleton(unNormalized);
|
||||
if (cache.containsKey(normalized)) {
|
||||
synchronized (cache) {
|
||||
formatter = cache.get(normalized);
|
||||
if (formatter != null) {
|
||||
cache.putIfAbsent(unNormalized, formatter);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (formatter != null) {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
// Third try: create the formatter, store it in the cache, and return it.
|
||||
formatter = create(normalized);
|
||||
|
||||
// Synchronize because we need a transaction with multiple queries to the cache.
|
||||
synchronized (cache) {
|
||||
if (cache.containsKey(normalized)) {
|
||||
formatter = cache.get(normalized);
|
||||
} else {
|
||||
cache.put(normalized, formatter);
|
||||
}
|
||||
cache.putIfAbsent(unNormalized, formatter);
|
||||
}
|
||||
return formatter;
|
||||
// TODO: This does not currently check the cache for the normalized form of the skeleton.
|
||||
// A new cache implementation would be required for that to work.
|
||||
return cache.getInstance(skeletonString, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,24 +170,19 @@ class NumberSkeletonImpl {
|
||||
return NumberFormatter.with().macros(macros);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a skeleton string corresponding to the given NumberFormatter.
|
||||
*
|
||||
* @param macros
|
||||
* The NumberFormatter options object.
|
||||
* @return A skeleton string in normalized form.
|
||||
*/
|
||||
public static String generate(MacroProps macros) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
generateSkeleton(macros, sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a number skeleton string to the shortest equivalent form.
|
||||
*
|
||||
* @param skeletonString
|
||||
* A number skeleton string, possibly not in its shortest form.
|
||||
* @return An equivalent and possibly simplified skeleton string.
|
||||
*/
|
||||
public static String normalizeSkeleton(String skeletonString) {
|
||||
// FIXME
|
||||
return skeletonString;
|
||||
}
|
||||
|
||||
/////
|
||||
|
||||
private static MacroProps parseSkeleton(String skeletonString) {
|
||||
@ -396,51 +371,60 @@ class NumberSkeletonImpl {
|
||||
}
|
||||
|
||||
// Unknown option
|
||||
throw new SkeletonSyntaxException("Unknown option", content);
|
||||
throw new SkeletonSyntaxException("Invalid option", content);
|
||||
}
|
||||
|
||||
private static void generateSkeleton(MacroProps macros, StringBuilder sb) {
|
||||
if (macros.notation != null) {
|
||||
generateNotationValue(macros, sb);
|
||||
// Supported options
|
||||
if (macros.notation != null && generateNotationValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.unit != null) {
|
||||
generateUnitValue(macros, sb);
|
||||
if (macros.unit != null && generateUnitValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.perUnit != null) {
|
||||
generatePerUnitValue(macros, sb);
|
||||
if (macros.perUnit != null && generatePerUnitValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.rounder != null) {
|
||||
generateRoundingValue(macros, sb);
|
||||
if (macros.rounder != null && generateRoundingValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.grouping != null) {
|
||||
generateGroupingValue(macros, sb);
|
||||
if (macros.grouping != null && generateGroupingValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.integerWidth != null) {
|
||||
generateIntegerWidthValue(macros, sb);
|
||||
if (macros.integerWidth != null && generateIntegerWidthValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.symbols != null) {
|
||||
generateSymbolsValue(macros, sb);
|
||||
if (macros.symbols != null && generateSymbolsValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.unitWidth != null) {
|
||||
generateUnitWidthValue(macros, sb);
|
||||
if (macros.unitWidth != null && generateUnitWidthValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.sign != null) {
|
||||
generateSignValue(macros, sb);
|
||||
if (macros.sign != null && generateSignValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (macros.decimal != null) {
|
||||
generateDecimalValue(macros, sb);
|
||||
if (macros.decimal != null && generateDecimalValue(macros, sb)) {
|
||||
sb.append(' ');
|
||||
}
|
||||
|
||||
// Unsupported options
|
||||
if (macros.padder != null) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Cannot generate number skeleton with custom padder");
|
||||
}
|
||||
if (macros.affixProvider != null) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Cannot generate number skeleton with custom affix provider");
|
||||
}
|
||||
if (macros.multiplier != null) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Cannot generate number skeleton with custom multiplier");
|
||||
}
|
||||
if (macros.rules != null) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Cannot generate number skeleton with custom plural rules");
|
||||
}
|
||||
|
||||
// Remove the trailing space
|
||||
if (sb.length() > 0) {
|
||||
sb.setLength(sb.length() - 1);
|
||||
@ -640,15 +624,51 @@ class NumberSkeletonImpl {
|
||||
if (content.charAt(0) != '@') {
|
||||
return false;
|
||||
}
|
||||
FractionRounder oldRounder = (FractionRounder) macros.rounder;
|
||||
// A little bit of a hack: parse the option as a digits stem, and extract the min/max sig from
|
||||
// the new Rounder saved into the macros.
|
||||
parseDigitsStem(content, macros);
|
||||
Rounder.SignificantRounderImpl intermediate = (Rounder.SignificantRounderImpl) macros.rounder;
|
||||
if (intermediate.maxSig == -1) {
|
||||
macros.rounder = oldRounder.withMinDigits(intermediate.minSig);
|
||||
int offset = 0;
|
||||
int minSig = 0;
|
||||
int maxSig;
|
||||
for (; offset < content.length(); offset++) {
|
||||
if (content.charAt(offset) == '@') {
|
||||
minSig++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// For the frac-sig option, there must be minSig or maxSig but not both.
|
||||
// Valid: @+, @@+, @@@+
|
||||
// Valid: @#, @##, @###
|
||||
// Invalid: @, @@, @@@
|
||||
// Invalid: @@#, @@##, @@@#
|
||||
if (offset < content.length()) {
|
||||
if (content.charAt(offset) == '+') {
|
||||
maxSig = -1;
|
||||
offset++;
|
||||
} else if (minSig > 1) {
|
||||
// @@#, @@##, @@@#
|
||||
throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", content);
|
||||
} else {
|
||||
maxSig = minSig;
|
||||
for (; offset < content.length(); offset++) {
|
||||
if (content.charAt(offset) == '#') {
|
||||
maxSig++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
macros.rounder = oldRounder.withMaxDigits(intermediate.maxSig);
|
||||
// @, @@, @@@
|
||||
throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", content);
|
||||
}
|
||||
if (offset < content.length()) {
|
||||
throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", content);
|
||||
}
|
||||
|
||||
FractionRounder oldRounder = (FractionRounder) macros.rounder;
|
||||
if (maxSig == -1) {
|
||||
macros.rounder = oldRounder.withMinDigits(minSig);
|
||||
} else {
|
||||
macros.rounder = oldRounder.withMaxDigits(maxSig);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -670,11 +690,9 @@ class NumberSkeletonImpl {
|
||||
}
|
||||
|
||||
private static boolean parseRoundingModeOption(CharSequence content, MacroProps macros) {
|
||||
// Iterate over int modes instead of enum modes for performance
|
||||
for (int rm = 0; rm <= BigDecimal.ROUND_UNNECESSARY; rm++) {
|
||||
RoundingMode mode = RoundingMode.valueOf(rm);
|
||||
if (content.equals(mode.toString())) {
|
||||
macros.rounder = macros.rounder.withMode(mode);
|
||||
for (int rm = 0; rm < ROUNDING_MODE_STRINGS.length; rm++) {
|
||||
if (content.equals(ROUNDING_MODE_STRINGS[rm])) {
|
||||
macros.rounder = macros.rounder.withMode(RoundingMode.valueOf(rm));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -682,7 +700,8 @@ class NumberSkeletonImpl {
|
||||
}
|
||||
|
||||
private static void generateRoundingModeOption(RoundingMode mode, StringBuilder sb) {
|
||||
sb.append(mode.toString());
|
||||
String option = ROUNDING_MODE_STRINGS[mode.ordinal()];
|
||||
sb.append(option);
|
||||
}
|
||||
|
||||
private static void parseIntegerWidthOption(CharSequence content, MacroProps macros) {
|
||||
@ -749,18 +768,22 @@ class NumberSkeletonImpl {
|
||||
|
||||
/////
|
||||
|
||||
private static void generateNotationValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generateNotationValue(MacroProps macros, StringBuilder sb) {
|
||||
// Check for literals
|
||||
String literal = skeletonData.valueToStem(macros.notation);
|
||||
if (literal != null) {
|
||||
if ("notation-simple".equals(literal)) {
|
||||
return false; // Default value
|
||||
} else if (literal != null) {
|
||||
sb.append(literal);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generate the stem
|
||||
if (macros.notation instanceof CompactNotation) {
|
||||
// Compact notation generated from custom data (not supported in skeleton)
|
||||
// The other compact notations are literals
|
||||
throw new UnsupportedOperationException(
|
||||
"Cannot generate number skeleton with custom compact data");
|
||||
} else if (macros.notation instanceof ScientificNotation) {
|
||||
ScientificNotation impl = (ScientificNotation) macros.notation;
|
||||
if (impl.engineeringInterval == 3) {
|
||||
@ -776,49 +799,56 @@ class NumberSkeletonImpl {
|
||||
sb.append('/');
|
||||
sb.append(skeletonData.valueToStem(impl.exponentSignDisplay));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
assert macros.notation instanceof SimpleNotation;
|
||||
sb.append("notation-simple");
|
||||
// SimpleNotation should be handled by a literal
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateUnitValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generateUnitValue(MacroProps macros, StringBuilder sb) {
|
||||
// Check for literals
|
||||
String literal = skeletonData.valueToStem(macros.unit);
|
||||
if (literal != null) {
|
||||
if ("base-unit".equals(literal)) {
|
||||
return false; // Default value
|
||||
} else if (literal != null) {
|
||||
sb.append(literal);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generate the stem
|
||||
if (macros.unit instanceof Currency) {
|
||||
sb.append("currency/");
|
||||
generateCurrencyOption((Currency) macros.unit, sb);
|
||||
return true;
|
||||
} else if (macros.unit instanceof NoUnit) {
|
||||
// This should be taken care of by the literals.
|
||||
assert false;
|
||||
throw new AssertionError();
|
||||
} else {
|
||||
sb.append("measure-unit/");
|
||||
generateMeasureUnitOption(macros.unit, sb);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void generatePerUnitValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generatePerUnitValue(MacroProps macros, StringBuilder sb) {
|
||||
// Per-units are currently expected to be only MeasureUnits.
|
||||
if (macros.unit instanceof Currency || macros.unit instanceof NoUnit) {
|
||||
assert false;
|
||||
throw new UnsupportedOperationException(
|
||||
"Cannot generate number skeleton with per-unit that is not a standard measure unit");
|
||||
} else {
|
||||
sb.append("per-measure-unit/");
|
||||
generateMeasureUnitOption(macros.perUnit, sb);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateRoundingValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generateRoundingValue(MacroProps macros, StringBuilder sb) {
|
||||
// Check for literals
|
||||
String literal = skeletonData.valueToStem(macros.rounder);
|
||||
if (literal != null) {
|
||||
sb.append(literal);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generate the stem
|
||||
@ -858,18 +888,34 @@ class NumberSkeletonImpl {
|
||||
sb.append('/');
|
||||
generateRoundingModeOption(macros.rounder.mathContext.getRoundingMode(), sb);
|
||||
}
|
||||
|
||||
// NOTE: Always return true for rounding because the default value depends on other options.
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void generateGroupingValue(MacroProps macros, StringBuilder sb) {
|
||||
appendExpectedLiteral(macros.grouping, sb);
|
||||
private static boolean generateGroupingValue(MacroProps macros, StringBuilder sb) {
|
||||
if (macros.grouping instanceof GroupingStrategy) {
|
||||
if (macros.grouping == GroupingStrategy.AUTO) {
|
||||
return false; // Default value
|
||||
}
|
||||
appendExpectedLiteral(macros.grouping, sb);
|
||||
return true;
|
||||
} else {
|
||||
throw new UnsupportedOperationException(
|
||||
"Cannot generate number skeleton with custom Grouper");
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateIntegerWidthValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generateIntegerWidthValue(MacroProps macros, StringBuilder sb) {
|
||||
if (macros.integerWidth.equals(IntegerWidth.DEFAULT)) {
|
||||
return false; // Default
|
||||
}
|
||||
sb.append("integer-width/");
|
||||
generateIntegerWidthOption(macros.integerWidth.minInt, macros.integerWidth.maxInt, sb);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void generateSymbolsValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generateSymbolsValue(MacroProps macros, StringBuilder sb) {
|
||||
if (macros.symbols instanceof NumberingSystem) {
|
||||
NumberingSystem ns = (NumberingSystem) macros.symbols;
|
||||
if (ns.getName().equals("latn")) {
|
||||
@ -878,21 +924,36 @@ class NumberSkeletonImpl {
|
||||
sb.append("numbering-system/");
|
||||
generateNumberingSystemOption(ns, sb);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// DecimalFormatSymbols (not supported in skeleton)
|
||||
assert macros.symbols instanceof DecimalFormatSymbols;
|
||||
throw new UnsupportedOperationException(
|
||||
"Cannot generate number skeleton with custom DecimalFormatSymbols");
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateUnitWidthValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generateUnitWidthValue(MacroProps macros, StringBuilder sb) {
|
||||
if (macros.unitWidth == UnitWidth.SHORT) {
|
||||
return false; // Default value
|
||||
}
|
||||
appendExpectedLiteral(macros.unitWidth, sb);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void generateSignValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generateSignValue(MacroProps macros, StringBuilder sb) {
|
||||
if (macros.sign == SignDisplay.AUTO) {
|
||||
return false; // Default value
|
||||
}
|
||||
appendExpectedLiteral(macros.sign, sb);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void generateDecimalValue(MacroProps macros, StringBuilder sb) {
|
||||
private static boolean generateDecimalValue(MacroProps macros, StringBuilder sb) {
|
||||
if (macros.decimal == DecimalSeparatorDisplay.AUTO) {
|
||||
return false; // Default value
|
||||
}
|
||||
appendExpectedLiteral(macros.decimal, sb);
|
||||
return true;
|
||||
}
|
||||
|
||||
/////
|
||||
|
@ -402,7 +402,7 @@ public abstract class Rounder implements Cloneable {
|
||||
|
||||
static final FractionRounderImpl FIXED_FRAC_0 = new FractionRounderImpl(0, 0);
|
||||
static final FractionRounderImpl FIXED_FRAC_2 = new FractionRounderImpl(2, 2);
|
||||
static final FractionRounderImpl MAX_FRAC_6 = new FractionRounderImpl(0, 6);
|
||||
static final FractionRounderImpl DEFAULT_MAX_FRAC_6 = new FractionRounderImpl(0, 6);
|
||||
|
||||
static final SignificantRounderImpl FIXED_SIG_2 = new SignificantRounderImpl(2, 2);
|
||||
static final SignificantRounderImpl FIXED_SIG_3 = new SignificantRounderImpl(3, 3);
|
||||
@ -427,7 +427,7 @@ public abstract class Rounder implements Cloneable {
|
||||
} else if (minFrac == 2 && maxFrac == 2) {
|
||||
return FIXED_FRAC_2;
|
||||
} else if (minFrac == 0 && maxFrac == 6) {
|
||||
return MAX_FRAC_6;
|
||||
return DEFAULT_MAX_FRAC_6;
|
||||
} else {
|
||||
return new FractionRounderImpl(minFrac, maxFrac);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ 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.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@ -75,7 +76,7 @@ public class NumberFormatterApiTest {
|
||||
|
||||
assertFormatDescendingBig(
|
||||
"Big Simple",
|
||||
"simple-notation",
|
||||
"notation-simple",
|
||||
NumberFormatter.with().notation(Notation.simple()),
|
||||
ULocale.ENGLISH,
|
||||
"87,650,000",
|
||||
@ -1084,7 +1085,7 @@ public class NumberFormatterApiTest {
|
||||
// NOTE: Other tests cover the behavior of the other rounding modes.
|
||||
assertFormatDescending(
|
||||
"Rounding Mode CEILING",
|
||||
"round-integer/CEILING",
|
||||
"round-integer/ceiling",
|
||||
NumberFormatter.with().rounding(Rounder.integer().withMode(RoundingMode.CEILING)),
|
||||
ULocale.ENGLISH,
|
||||
"87,650",
|
||||
@ -2046,13 +2047,18 @@ public class NumberFormatterApiTest {
|
||||
assertEquals(message + ": Safe Path: " + d, expected[i], actual2);
|
||||
}
|
||||
if (skeleton != null) { // if null, skeleton is declared as undefined.
|
||||
assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
|
||||
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(skeleton).locale(locale);
|
||||
// Only compare normalized skeletons: the tests need not provide the normalized forms.
|
||||
// Use the normalized form to construct the testing formatter to guarantee no loss of info.
|
||||
String normalized = NumberFormatter.fromSkeleton(skeleton).toSkeleton();
|
||||
assertEquals(message + ": Skeleton:", normalized, f.toSkeleton());
|
||||
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(normalized).locale(locale);
|
||||
for (int i = 0; i < 9; i++) {
|
||||
double d = inputs[i];
|
||||
String actual3 = l3.format(d).toString();
|
||||
assertEquals(message + ": Skeleton Path: " + d, expected[i], actual3);
|
||||
}
|
||||
} else {
|
||||
assertUndefinedSkeleton(f);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2070,10 +2076,15 @@ public class NumberFormatterApiTest {
|
||||
String actual2 = l2.format(input).toString();
|
||||
assertEquals(message + ": Safe Path: " + input, expected, actual2);
|
||||
if (skeleton != null) { // if null, skeleton is declared as undefined.
|
||||
assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
|
||||
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(skeleton).locale(locale);
|
||||
// Only compare normalized skeletons: the tests need not provide the normalized forms.
|
||||
// Use the normalized form to construct the testing formatter to ensure no loss of info.
|
||||
String normalized = NumberFormatter.fromSkeleton(skeleton).toSkeleton();
|
||||
assertEquals(message + ": Skeleton:", normalized, f.toSkeleton());
|
||||
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(normalized).locale(locale);
|
||||
String actual3 = l3.format(input).toString();
|
||||
assertEquals(message + ": Skeleton Path: " + input, expected, actual3);
|
||||
} else {
|
||||
assertUndefinedSkeleton(f);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2091,10 +2102,22 @@ public class NumberFormatterApiTest {
|
||||
String actual2 = l2.format(input).toString();
|
||||
assertEquals(message + ": Safe Path: " + input, expected, actual2);
|
||||
if (skeleton != null) { // if null, skeleton is declared as undefined.
|
||||
assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
|
||||
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(skeleton).locale(locale);
|
||||
// Only compare normalized skeletons: the tests need not provide the normalized forms.
|
||||
// Use the normalized form to construct the testing formatter to ensure no loss of info.
|
||||
String normalized = NumberFormatter.fromSkeleton(skeleton).toSkeleton();
|
||||
assertEquals(message + ": Skeleton:", normalized, f.toSkeleton());
|
||||
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(normalized).locale(locale);
|
||||
String actual3 = l3.format(input).toString();
|
||||
assertEquals(message + ": Skeleton Path: " + input, expected, actual3);
|
||||
} else {
|
||||
assertUndefinedSkeleton(f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertUndefinedSkeleton(UnlocalizedNumberFormatter f) {
|
||||
try {
|
||||
String skeleton = f.toSkeleton();
|
||||
fail("Expected toSkeleton to fail, but it passed, producing: " + skeleton);
|
||||
} catch (UnsupportedOperationException expected) {}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,16 @@
|
||||
// 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 static org.junit.Assert.fail;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.ibm.icu.number.NumberFormatter;
|
||||
import com.ibm.icu.number.Rounder;
|
||||
import com.ibm.icu.number.SkeletonSyntaxException;
|
||||
|
||||
/**
|
||||
@ -26,18 +30,101 @@ public class NumberSkeletonTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validTokens() {
|
||||
// This tests only if the tokens are valid, not their behavior.
|
||||
// Most of these are from the design doc.
|
||||
String[] cases = {
|
||||
"round-integer",
|
||||
"round-unlimited",
|
||||
"@@@##",
|
||||
"@@+",
|
||||
".000##",
|
||||
".00+",
|
||||
".",
|
||||
".+",
|
||||
".######",
|
||||
".00/@@+",
|
||||
".00/@##",
|
||||
"round-increment/3.14",
|
||||
"round-currency-standard",
|
||||
"round-integer/half-up",
|
||||
".00#/ceiling",
|
||||
".00/@@+/floor",
|
||||
"scientific",
|
||||
"scientific/+ee",
|
||||
"scientific/sign-always",
|
||||
"scientific/+ee/sign-always",
|
||||
"scientific/sign-always/+ee",
|
||||
"scientific/sign-except-zero",
|
||||
"engineering",
|
||||
"engineering/+eee",
|
||||
"compact-short",
|
||||
"compact-long",
|
||||
"notation-simple",
|
||||
"percent",
|
||||
"permille",
|
||||
"measure-unit/length-meter",
|
||||
"measure-unit/area-square-meter",
|
||||
"measure-unit/energy-joule per-measure-unit/length-meter",
|
||||
"currency/XXX",
|
||||
"group-off",
|
||||
"group-min2",
|
||||
"group-auto",
|
||||
"group-on-aligned",
|
||||
"group-thousands",
|
||||
"integer-width/00",
|
||||
"integer-width/#0",
|
||||
"integer-width/+00",
|
||||
"sign-always",
|
||||
"sign-auto",
|
||||
"sign-never",
|
||||
"sign-accounting",
|
||||
"sign-accounting-always",
|
||||
"sign-except-zero",
|
||||
"sign-accounting-except-zero",
|
||||
"unit-width-narrow",
|
||||
"unit-width-short",
|
||||
"unit-width-iso-code",
|
||||
"unit-width-full-name",
|
||||
"unit-width-hidden",
|
||||
"decimal-auto",
|
||||
"decimal-always",
|
||||
"latin",
|
||||
"numbering-system/arab",
|
||||
"numbering-system/latn" };
|
||||
|
||||
for (String cas : cases) {
|
||||
try {
|
||||
NumberFormatter.fromSkeleton(cas);
|
||||
} catch (SkeletonSyntaxException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidTokens() {
|
||||
String[] cases = {
|
||||
".00x",
|
||||
".00##0",
|
||||
".##+",
|
||||
".00##+",
|
||||
".0#+",
|
||||
"@@x",
|
||||
"@@##0",
|
||||
"@#+",
|
||||
".00/@",
|
||||
".00/@@",
|
||||
".00/@@x",
|
||||
".00/@@#",
|
||||
".00/@@#+",
|
||||
".00/floor/@@+", // wrong order
|
||||
"round-currency-cash/XXX",
|
||||
"scientific/ee",
|
||||
"round-increment/xxx",
|
||||
"round-increment/0.1.2",
|
||||
"group-thousands/foo",
|
||||
"currency/dummy",
|
||||
"measure-unit/foo",
|
||||
"integer-width/xxx",
|
||||
@ -47,7 +134,7 @@ public class NumberSkeletonTest {
|
||||
for (String cas : cases) {
|
||||
try {
|
||||
NumberFormatter.fromSkeleton(cas);
|
||||
fail();
|
||||
fail("Skeleton parses, but it should have failed: " + cas);
|
||||
} catch (SkeletonSyntaxException expected) {
|
||||
assertTrue(expected.getMessage(), expected.getMessage().contains("Invalid"));
|
||||
}
|
||||
@ -85,4 +172,35 @@ public class NumberSkeletonTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultTokens() {
|
||||
String[] cases = {
|
||||
"notation-simple",
|
||||
"base-unit",
|
||||
"group-auto",
|
||||
"integer-width/+0",
|
||||
"sign-auto",
|
||||
"unit-width-short",
|
||||
"decimal-auto" };
|
||||
|
||||
for (String skeleton : cases) {
|
||||
String normalized = NumberFormatter.fromSkeleton(skeleton).toSkeleton();
|
||||
assertEquals("Skeleton should become empty when normalized: " + skeleton, "", normalized);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundingModeNames() {
|
||||
for (RoundingMode mode : RoundingMode.values()) {
|
||||
if (mode == RoundingMode.HALF_EVEN) {
|
||||
// This rounding mode is not printed in the skeleton since it is the default
|
||||
continue;
|
||||
}
|
||||
String skeleton = NumberFormatter.with().rounding(Rounder.integer().withMode(mode))
|
||||
.toSkeleton();
|
||||
String modeString = mode.toString().toLowerCase().replace('_', '-');
|
||||
assertEquals(mode.toString(), modeString, skeleton.substring(14));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user