ICU-8610 Adds basic support for number skeletons. Includes skeleton support for rounding strategy.

X-SVN-Rev: 41013
This commit is contained in:
Shane Carr 2018-02-28 08:06:42 +00:00
parent f133914b97
commit abb8788d23
7 changed files with 648 additions and 33 deletions

View File

@ -453,6 +453,20 @@ public final class NumberFormatter {
return BASE.locale(locale);
}
/**
* Call this method at the beginning of a NumberFormatter fluent chain to create an instance based
* on a given number skeleton string.
*
* @param skeleton
* The skeleton string off of which to base this NumberFormatter.
* @return An {@link UnlocalizedNumberFormatter}, to be used for chaining.
* @draft ICU 62
* @provisional This API might change or be removed in a future release.
*/
public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) {
return NumberSkeletonImpl.getOrCreate(skeleton);
}
/**
* @internal
* @deprecated ICU 60 This API is ICU internal only.

View File

@ -475,6 +475,18 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
return create(KEY_THRESHOLD, threshold);
}
/**
* Creates a skeleton string representation of this number formatter. A skeleton string is a
* locale-agnostic serialized form of a number formatter.
*
* @return A number skeleton string with behavior corresponding to this number formatter.
* @draft ICU 62
* @provisional This API might change or be removed in a future release.
*/
public String toSkeleton() {
return NumberSkeletonImpl.generate(resolve());
}
/* package-protected */ abstract T create(int key, Object value);
MacroProps resolve() {

View File

@ -0,0 +1,490 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.impl.StringSegment;
import com.ibm.icu.impl.number.MacroProps;
import com.ibm.icu.util.Currency.CurrencyUsage;
/**
* @author sffc
*
*/
class NumberSkeletonImpl {
static enum StemType {
ROUNDER, FRACTION_ROUNDER, MAYBE_INCREMENT_ROUNDER, CURRENCY_ROUNDER
}
static class SkeletonDataStructure {
final Map<String, StemType> stemsToTypes;
final Map<String, Object> stemsToValues;
final Map<Object, String> valuesToStems;
SkeletonDataStructure() {
stemsToTypes = new HashMap<String, StemType>();
stemsToValues = new HashMap<String, Object>();
valuesToStems = new HashMap<Object, String>();
}
public void put(StemType stemType, String content, Object value) {
stemsToTypes.put(content, stemType);
stemsToValues.put(content, value);
valuesToStems.put(value, content);
}
public StemType stemToType(CharSequence content) {
return stemsToTypes.get(content);
}
public Object stemToValue(CharSequence content) {
return stemsToValues.get(content);
}
public String valueToStem(Object value) {
return valuesToStems.get(value);
}
}
static final SkeletonDataStructure skeletonData = new SkeletonDataStructure();
static {
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));
}
private static final Map<String, UnlocalizedNumberFormatter> cache = new ConcurrentHashMap<String, UnlocalizedNumberFormatter>();
/**
* Gets the number formatter for the given number skeleton string from the cache, creating it if it
* does not exist in the cache.
*
* @param skeletonString
* A number skeleton string, possibly not in its shortest form.
* @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;
}
/**
* Creates a NumberFormatter corresponding to the given skeleton string.
*
* @param skeletonString
* A number skeleton string, possibly not in its shortest form.
* @return An UnlocalizedNumberFormatter with behavior defined by the given skeleton string.
*/
public static UnlocalizedNumberFormatter create(String skeletonString) {
MacroProps macros = parseSkeleton(skeletonString);
return NumberFormatter.with().macros(macros);
}
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) {
MacroProps macros = new MacroProps();
StringSegment segment = new StringSegment(skeletonString + " ", false);
StemType stem = null;
int offset = 0;
while (offset < segment.length()) {
int cp = segment.codePointAt(offset);
boolean isWhiteSpace = PatternProps.isWhiteSpace(cp);
if (offset > 0 && (isWhiteSpace || cp == '/')) {
segment.setLength(offset);
if (stem == null) {
stem = parseStem(segment, macros);
} else {
stem = parseOption(stem, segment, macros);
}
segment.resetLength();
segment.adjustOffset(offset + 1);
offset = 0;
} else {
offset += Character.charCount(cp);
}
if (isWhiteSpace && stem != null) {
// Check for stems that require an option
switch (stem) {
case MAYBE_INCREMENT_ROUNDER:
throw new SkeletonSyntaxException("Stem requires an option", segment);
default:
break;
}
stem = null;
}
}
assert stem == null;
return macros;
}
private static StemType parseStem(CharSequence content, MacroProps macros) {
// First try: exact match with a literal stem
StemType stem = skeletonData.stemToType(content);
if (stem != null) {
Object value = skeletonData.stemToValue(content);
switch (stem) {
case ROUNDER:
checkNull(macros.rounder, content);
macros.rounder = (Rounder) value;
break;
default:
assert false;
}
return stem;
}
// Second try: literal stems that require an option
if (content.equals("round-increment")) {
return StemType.MAYBE_INCREMENT_ROUNDER;
}
// Second try: stem "blueprint" syntax
switch (content.charAt(0)) {
case '.':
stem = StemType.FRACTION_ROUNDER;
parseFractionStem(content, macros);
break;
case '@':
stem = StemType.ROUNDER;
parseDigitsStem(content, macros);
break;
}
if (stem != null) {
return stem;
}
// Still no hits: throw an exception
throw new SkeletonSyntaxException("Unknown stem", content);
}
private static StemType parseOption(StemType stem, CharSequence content, MacroProps macros) {
// Frac-sig option
switch (stem) {
case FRACTION_ROUNDER:
if (parseFracSigOption(content, macros)) {
return StemType.ROUNDER;
}
}
// Increment option
switch (stem) {
case MAYBE_INCREMENT_ROUNDER:
// The increment option is required.
parseIncrementOption(content, macros);
return StemType.ROUNDER;
}
// Rounding mode option
switch (stem) {
case ROUNDER:
case FRACTION_ROUNDER:
case CURRENCY_ROUNDER:
if (parseRoundingModeOption(content, macros)) {
break;
}
}
// Unknown option
throw new SkeletonSyntaxException("Unknown option", content);
}
/////
private static void generateSkeleton(MacroProps macros, StringBuilder sb) {
if (macros.rounder != null) {
generateRoundingValue(macros, sb);
sb.append(' ');
}
// Remove the trailing space
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
}
/////
private static void parseFractionStem(CharSequence content, MacroProps macros) {
assert content.charAt(0) == '.';
int offset = 1;
int minFrac = 0;
int maxFrac;
for (; offset < content.length(); offset++) {
if (content.charAt(offset) == '0') {
minFrac++;
} else {
break;
}
}
if (offset < content.length()) {
if (content.charAt(offset) == '+') {
maxFrac = -1;
offset++;
} else {
maxFrac = minFrac;
for (; offset < content.length(); offset++) {
if (content.charAt(offset) == '#') {
maxFrac++;
} else {
break;
}
}
}
} else {
maxFrac = minFrac;
}
if (offset < content.length()) {
throw new SkeletonSyntaxException("Invalid fraction stem", content);
}
// Use the public APIs to enforce bounds checking
if (maxFrac == -1) {
macros.rounder = Rounder.minFraction(minFrac);
} else {
macros.rounder = Rounder.minMaxFraction(minFrac, maxFrac);
}
}
private static void generateFractionStem(int minFrac, int maxFrac, StringBuilder sb) {
if (minFrac == 0 && maxFrac == 0) {
sb.append("round-integer");
return;
}
sb.append('.');
appendMultiple(sb, '0', minFrac);
if (maxFrac == -1) {
sb.append('+');
} else {
appendMultiple(sb, '#', maxFrac - minFrac);
}
}
private static void parseDigitsStem(CharSequence content, MacroProps macros) {
assert content.charAt(0) == '@';
int offset = 0;
int minSig = 0;
int maxSig;
for (; offset < content.length(); offset++) {
if (content.charAt(offset) == '@') {
minSig++;
} else {
break;
}
}
if (offset < content.length()) {
if (content.charAt(offset) == '+') {
maxSig = -1;
offset++;
} else {
maxSig = minSig;
for (; offset < content.length(); offset++) {
if (content.charAt(offset) == '#') {
maxSig++;
} else {
break;
}
}
}
} else {
maxSig = minSig;
}
if (offset < content.length()) {
throw new SkeletonSyntaxException("Invalid significant digits stem", content);
}
// Use the public APIs to enforce bounds checking
if (maxSig == -1) {
macros.rounder = Rounder.minDigits(minSig);
} else {
macros.rounder = Rounder.minMaxDigits(minSig, maxSig);
}
}
private static void generateDigitsStem(int minSig, int maxSig, StringBuilder sb) {
appendMultiple(sb, '@', minSig);
if (maxSig == -1) {
sb.append('+');
} else {
appendMultiple(sb, '#', maxSig - minSig);
}
}
private static boolean parseFracSigOption(CharSequence content, MacroProps macros) {
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);
} else {
macros.rounder = oldRounder.withMaxDigits(intermediate.maxSig);
}
return true;
}
private static void parseIncrementOption(CharSequence content, MacroProps macros) {
// Clunkilly convert the CharSequence to a char array for the BigDecimal constructor.
// We can't use content.toString() because that doesn't create a clean string.
char[] chars = new char[content.length()];
for (int i = 0; i < content.length(); i++) {
chars[i] = content.charAt(i);
}
BigDecimal increment;
try {
increment = new BigDecimal(chars);
} catch (NumberFormatException e) {
throw new SkeletonSyntaxException("Invalid rounding increment", content, e);
}
macros.rounder = Rounder.increment(increment);
}
private static void generateIncrementOption(BigDecimal increment, StringBuilder sb) {
sb.append(increment.toPlainString());
}
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);
return true;
}
}
return false;
}
private static void generateRoundingModeOption(RoundingMode mode, StringBuilder sb) {
sb.append(mode.toString());
}
/////
private static void generateRoundingValue(MacroProps macros, StringBuilder sb) {
// Check for literals
String literal = skeletonData.valueToStem(macros.rounder);
if (literal != null) {
sb.append(literal);
return;
}
// Generate the stem
if (macros.rounder instanceof Rounder.InfiniteRounderImpl) {
sb.append("round-unlimited");
} else if (macros.rounder instanceof Rounder.FractionRounderImpl) {
Rounder.FractionRounderImpl impl = (Rounder.FractionRounderImpl) macros.rounder;
generateFractionStem(impl.minFrac, impl.maxFrac, sb);
} else if (macros.rounder instanceof Rounder.SignificantRounderImpl) {
Rounder.SignificantRounderImpl impl = (Rounder.SignificantRounderImpl) macros.rounder;
generateDigitsStem(impl.minSig, impl.maxSig, sb);
} else if (macros.rounder instanceof Rounder.FracSigRounderImpl) {
Rounder.FracSigRounderImpl impl = (Rounder.FracSigRounderImpl) macros.rounder;
generateFractionStem(impl.minFrac, impl.maxFrac, sb);
sb.append('/');
if (impl.minSig == -1) {
generateDigitsStem(1, impl.maxSig, sb);
} else {
generateDigitsStem(impl.minSig, -1, sb);
}
} else if (macros.rounder instanceof Rounder.IncrementRounderImpl) {
Rounder.IncrementRounderImpl impl = (Rounder.IncrementRounderImpl) macros.rounder;
sb.append("round-increment/");
generateIncrementOption(impl.increment, sb);
} else {
assert macros.rounder instanceof Rounder.CurrencyRounderImpl;
Rounder.CurrencyRounderImpl impl = (Rounder.CurrencyRounderImpl) macros.rounder;
if (impl.usage == CurrencyUsage.STANDARD) {
sb.append("round-currency-standard");
} else {
sb.append("round-currency-cash");
}
}
// Generate the options
if (macros.rounder.mathContext != Rounder.DEFAULT_MATH_CONTEXT) {
sb.append('/');
generateRoundingModeOption(macros.rounder.mathContext.getRoundingMode(), sb);
}
}
/////
private static void checkNull(Object value, CharSequence content) {
if (value != null) {
throw new SkeletonSyntaxException("Duplicated setting", content);
}
}
private static void appendMultiple(StringBuilder sb, int cp, int count) {
for (int i = 0; i < count; i++) {
sb.appendCodePoint(cp);
}
}
}

View File

@ -26,8 +26,11 @@ public abstract class Rounder implements Cloneable {
/* package-private final */ MathContext mathContext;
/* package-private */ static final MathContext DEFAULT_MATH_CONTEXT = RoundingUtils
.mathContextUnlimited(RoundingUtils.DEFAULT_ROUNDING_MODE);
/* package-private */ Rounder() {
mathContext = RoundingUtils.mathContextUnlimited(RoundingUtils.DEFAULT_ROUNDING_MODE);
mathContext = DEFAULT_MATH_CONTEXT;
}
/**
@ -245,7 +248,7 @@ public abstract class Rounder implements Cloneable {
*/
public static Rounder maxDigits(int maxSignificantDigits) {
if (maxSignificantDigits >= 1 && maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
return constructSignificant(0, maxSignificantDigits);
return constructSignificant(1, maxSignificantDigits);
} else {
throw new IllegalArgumentException("Significant digits must be between 1 and "
+ RoundingUtils.MAX_INT_FRAC_SIG

View File

@ -0,0 +1,20 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
/**
* Exception used for illegal number skeleton strings.
*
* @author sffc
*/
public class SkeletonSyntaxException extends IllegalArgumentException {
private static final long serialVersionUID = 7733971331648360554L;
public SkeletonSyntaxException(String message, CharSequence token) {
super("Syntax error in skeleton string: " + message + ": " + token);
}
public SkeletonSyntaxException(String message, CharSequence token, Throwable cause) {
super("Syntax error in skeleton string: " + message + ": " + token, cause);
}
}

View File

@ -770,7 +770,7 @@ public class NumberFormatterApiTest {
public void roundingFraction() {
assertFormatDescending(
"Integer",
"F0",
"round-integer",
NumberFormatter.with().rounding(Rounder.integer()),
ULocale.ENGLISH,
"87,650",
@ -785,7 +785,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Fixed Fraction",
"F3",
".000",
NumberFormatter.with().rounding(Rounder.fixedFraction(3)),
ULocale.ENGLISH,
"87,650.000",
@ -800,7 +800,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Min Fraction",
"F1-",
".0+",
NumberFormatter.with().rounding(Rounder.minFraction(1)),
ULocale.ENGLISH,
"87,650.0",
@ -815,7 +815,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Max Fraction",
"F-1",
".#",
NumberFormatter.with().rounding(Rounder.maxFraction(1)),
ULocale.ENGLISH,
"87,650",
@ -830,7 +830,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Min/Max Fraction",
"F1-3",
".0##",
NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3)),
ULocale.ENGLISH,
"87,650.0",
@ -848,7 +848,7 @@ public class NumberFormatterApiTest {
public void roundingFigures() {
assertFormatSingle(
"Fixed Significant",
"S3",
"@@@",
NumberFormatter.with().rounding(Rounder.fixedDigits(3)),
ULocale.ENGLISH,
-98,
@ -856,7 +856,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"Fixed Significant Rounding",
"S3",
"@@@",
NumberFormatter.with().rounding(Rounder.fixedDigits(3)),
ULocale.ENGLISH,
-98.7654321,
@ -864,7 +864,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"Fixed Significant Zero",
"S3",
"@@@",
NumberFormatter.with().rounding(Rounder.fixedDigits(3)),
ULocale.ENGLISH,
0,
@ -872,7 +872,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"Min Significant",
"S2-",
"@@+",
NumberFormatter.with().rounding(Rounder.minDigits(2)),
ULocale.ENGLISH,
-9,
@ -880,7 +880,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"Max Significant",
"S-4",
"@###",
NumberFormatter.with().rounding(Rounder.maxDigits(4)),
ULocale.ENGLISH,
98.7654321,
@ -888,7 +888,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"Min/Max Significant",
"S3-4",
"@@@#",
NumberFormatter.with().rounding(Rounder.minMaxDigits(3, 4)),
ULocale.ENGLISH,
9.99999,
@ -899,7 +899,7 @@ public class NumberFormatterApiTest {
public void roundingFractionFigures() {
assertFormatDescending(
"Basic Significant", // for comparison
"S-2",
"@#",
NumberFormatter.with().rounding(Rounder.maxDigits(2)),
ULocale.ENGLISH,
"88,000",
@ -914,7 +914,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"FracSig minMaxFrac minSig",
"F1-2>3",
".0#/@@@+",
NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 2).withMinDigits(3)),
ULocale.ENGLISH,
"87,650.0",
@ -929,7 +929,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"FracSig minMaxFrac maxSig A",
"F1-3<2",
".0##/@#",
NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3).withMaxDigits(2)),
ULocale.ENGLISH,
"88,000.0", // maxSig beats maxFrac
@ -944,7 +944,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"FracSig minMaxFrac maxSig B",
"F2<2",
".00/@#",
NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMaxDigits(2)),
ULocale.ENGLISH,
"88,000.00", // maxSig beats maxFrac
@ -959,7 +959,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"FracSig with trailing zeros A",
"",
".00/@@@+",
NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMinDigits(3)),
ULocale.ENGLISH,
0.1,
@ -967,7 +967,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"FracSig with trailing zeros B",
"",
".00/@@@+",
NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMinDigits(3)),
ULocale.ENGLISH,
0.0999999,
@ -978,7 +978,7 @@ public class NumberFormatterApiTest {
public void roundingOther() {
assertFormatDescending(
"Rounding None",
"Y",
"round-unlimited",
NumberFormatter.with().rounding(Rounder.unlimited()),
ULocale.ENGLISH,
"87,650",
@ -993,7 +993,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Increment",
"M0.5",
"round-increment/0.5",
NumberFormatter.with().rounding(Rounder.increment(BigDecimal.valueOf(0.5))),
ULocale.ENGLISH,
"87,650.0",
@ -1008,7 +1008,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Increment with Min Fraction",
"M0.5",
"round-increment/0.50",
NumberFormatter.with().rounding(Rounder.increment(new BigDecimal("0.50"))),
ULocale.ENGLISH,
"87,650.00",
@ -1023,7 +1023,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Currency Standard",
"$CZK GSTANDARD",
"round-currency-standard",
NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.STANDARD)).unit(CZK),
ULocale.ENGLISH,
"CZK 87,650.00",
@ -1038,7 +1038,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Currency Cash",
"$CZK GCASH",
"round-currency-cash",
NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH)).unit(CZK),
ULocale.ENGLISH,
"CZK 87,650",
@ -1053,7 +1053,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Currency Cash with Nickel Rounding",
"$CAD GCASH",
"round-currency-cash",
NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH)).unit(CAD),
ULocale.ENGLISH,
"CA$87,650.00",
@ -1068,7 +1068,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Currency not in top-level fluent chain",
"F0",
"round-currency-cash/CZK",
NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH).withCurrency(CZK)),
ULocale.ENGLISH,
"87,650",
@ -1083,7 +1083,7 @@ public class NumberFormatterApiTest {
// NOTE: Other tests cover the behavior of the other rounding modes.
assertFormatDescending(
"Rounding Mode CEILING",
"round-integer/CEILING",
"",
NumberFormatter.with().rounding(Rounder.integer().withMode(RoundingMode.CEILING)),
ULocale.ENGLISH,
@ -2036,16 +2036,18 @@ public class NumberFormatterApiTest {
double[] inputs,
String... expected) {
assert expected.length == 9;
// TODO: Add a check for skeleton.
// assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(skeleton).locale(locale);
for (int i = 0; i < 9; i++) {
double d = inputs[i];
String actual1 = l1.format(d).toString();
assertEquals(message + ": Unsafe Path: " + d, expected[i], actual1);
String actual2 = l2.format(d).toString();
assertEquals(message + ": Safe Path: " + d, expected[i], actual2);
String actual3 = l3.format(d).toString();
assertEquals(message + ": Skeleton Path: " + d, expected[i], actual3);
}
}
@ -2056,14 +2058,16 @@ public class NumberFormatterApiTest {
ULocale locale,
Number input,
String expected) {
// TODO: Add a check for skeleton.
// assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(skeleton).locale(locale);
String actual1 = l1.format(input).toString();
assertEquals(message + ": Unsafe Path: " + input, expected, actual1);
String actual2 = l2.format(input).toString();
assertEquals(message + ": Safe Path: " + input, expected, actual2);
String actual3 = l3.format(input).toString();
assertEquals(message + ": Skeleton Path: " + input, expected, actual3);
}
private static void assertFormatSingleMeasure(
@ -2073,13 +2077,15 @@ public class NumberFormatterApiTest {
ULocale locale,
Measure input,
String expected) {
// TODO: Add a check for skeleton.
// assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
LocalizedNumberFormatter l3 = NumberFormatter.fromSkeleton(skeleton).locale(locale);
String actual1 = l1.format(input).toString();
assertEquals(message + ": Unsafe Path: " + input, expected, actual1);
String actual2 = l2.format(input).toString();
assertEquals(message + ": Safe Path: " + input, expected, actual2);
String actual3 = l3.format(input).toString();
assertEquals(message + ": Skeleton Path: " + input, expected, actual3);
}
}

View File

@ -0,0 +1,70 @@
// © 2018 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.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Test;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.SkeletonSyntaxException;
/**
* @author sffc
*
*/
public class NumberSkeletonTest {
@Test
public void duplicateValues() {
try {
NumberFormatter.fromSkeleton("round-integer round-integer");
fail();
} catch (SkeletonSyntaxException expected) {
assertTrue(expected.getMessage(), expected.getMessage().contains("Duplicated setting"));
}
}
@Test
public void invalidTokens() {
String[] cases = {
".00x",
".00##0",
".##+",
".0#+",
"@@x",
"@@##0",
"@#+",
"round-increment/xxx",
"round-increment/0.1.2",
};
for (String cas : cases) {
try {
NumberFormatter.fromSkeleton(cas);
fail();
} catch (SkeletonSyntaxException expected) {
assertTrue(expected.getMessage(), expected.getMessage().contains("Invalid"));
}
}
}
@Test
public void stemsRequiringOption() {
String[] cases = {
"round-increment",
"round-increment/",
"round-increment scientific",
};
for (String cas : cases) {
try {
NumberFormatter.fromSkeleton(cas);
fail();
} catch (SkeletonSyntaxException expected) {
assertTrue(expected.getMessage(), expected.getMessage().contains("requires an option"));
}
}
}
}