ICU-20418 Number skeletons: implement star wildcard; user guide fixes

See #1060
This commit is contained in:
Shane F. Carr 2020-03-25 21:22:23 +00:00
parent 0d26f83f83
commit ac4540f8a4
9 changed files with 174 additions and 53 deletions

View File

@ -95,14 +95,15 @@ Start with the stem `scientific` or `engineering`. Those stems take the
following optional options:
- `/sign-xxx` sets the sign display option for the exponent; see [Sign](#sign).
- `/+ee` sets exponent digits to "at least 2"; use `/+eee` for at least 3 digits, etc.
- `/*ee` sets exponent digits to "at least 2"; use `/*eee` for at least 3 digits, etc.
- ***Prior to ICU 67***, use `/+ee` instead of `/*ee`.
For example, all of the following skeletons are valid:
- `scientific`
- `scientific/sign-always`
- `scientific/+ee`
- `scientific/+ee/sign-always`
- `scientific/*ee`
- `scientific/*ee/sign-always`
#### Scientific and Engineering Notation: Concise Form
@ -111,14 +112,14 @@ The following are examples of concise form:
| Concise Skeleton | Equivalent Long-Form Skeleton |
|---|---|
| `E0` | `scientific` |
| `E00` | `scientific/+ee` |
| `EE+0` | `engineering/sign-always` |
| `E00` | `scientific/*ee` |
| `EE+!0` | `engineering/sign-always` |
| `E+?00` | `scientific/sign-except-zero/+ee` |
More precisely:
1. Start with `E` for scientific or `EE` for engineering.
2. Allow either `+` or `+?` as a concise sign display option.
2. Allow either `+!` or `+?` as a concise sign display option.
3. Expect one or more `0`s. If more than one, set minimum integer digits.
### Unit
@ -192,13 +193,13 @@ The following are examples of fraction-precision stems:
| Stem | Explanation | Equivalent C++ Code |
|---|---|---|
| `.00` | Exactly 2 fraction digits | `Precision::fixedFraction(2) ` |
| `.00+` | At least 2 fraction digits | `Precision::minFraction(2)` |
| `.00*` | At least 2 fraction digits | `Precision::minFraction(2)` |
| `.##` | At most 2 fraction digits | `Precision::maxFraction(2) ` |
| `.0#` | Between 1 and 2 fraction digits | `Precision::minMaxFraction(1, 2)` |
More precisely, the fraction precision stem starts with `.`, then contains
zero or more `0` symbols, which implies the minimum fraction digits. Then it
contains either a `+`, for unlimited maximum fraction digits, or zero or more
contains either a `*`, for unlimited maximum fraction digits, or zero or more
`#` symbols, which implies the minimum fraction digits when added to the `0`
symbols.
@ -211,11 +212,11 @@ examples:
| Skeleton | Explanation | Equivalent C++ Code |
|---|---|---|
| `.##/@@@+` | At most 2 fraction digits, but guarantee <br/> at least 3 significant digits | `Precision::maxFraction(2)` <br/> `.withMinDigits(3)` |
| `.##/@@@*` | At most 2 fraction digits, but guarantee <br/> at least 3 significant digits | `Precision::maxFraction(2)` <br/> `.withMinDigits(3)` |
| `.00/@##` | Exactly 2 fraction digits, but do not <br/> display more than 3 significant digits | `Precision::fixedFraction(2)` <br/> `.withMaxDigits(3)` |
Precisely, the option starts with one or more `@` symbols. Then it contains
either a `+`, for `::withMinDigits`, or one or more `#` symbols, for
either a `*`, for `::withMinDigits`, or one or more `#` symbols, for
`::withMaxDigits`. If a `#` symbol is present, there must be only one `@`
symbol.
@ -226,16 +227,22 @@ The following are examples of stems for significant figures:
| Stem | Explanation | Equivalent C++ Code|
|---|---|---|
| `@@@` | Exactly 3 significant digits | `Precision::fixedSignificantDigits(3)` |
| `@@@+` | At least 3 significant digits | `Precision::minSignificantDigits(3)` |
| `@@@*` | At least 3 significant digits | `Precision::minSignificantDigits(3)` |
| `@##` | At most 3 significant digits | `Precision::maxSignificantDigits(3)` |
| `@@#` | Between 2 and 3 significant digits | `...::minMaxSignificantDigits(2, 3)` |
The precise syntax is very similar to fraction precision. The blueprint stem
starts with one or more `@` symbols, which implies the minimum significant
digits. Then it contains either a `+`, for unlimited maximum significant
digits. Then it contains either a `*`, for unlimited maximum significant
digits, or zero or more `#` symbols, which implies the minimum significant
digits when added to the `@` symbols.
#### Wildcard Character
***Prior to ICU 67***, the symbol `+` was used for unlimited precision, instead
of `*` (for example, `.00+`). For backwards compatibility, either `+` or `*` is
accepted. This applies for both fraction digits and significant digits.
### Rounding Mode
The rounding mode can be specified by the following stems:
@ -259,21 +266,23 @@ integer digits):
| Long Form | Concise Form | Explanation | Equivalent C++ Code |
|---|---|---|---|
| `integer-width/+000` | `000` | At least 3 <br/> integer digits | `IntegerWidth::zeroFillTo(3)` |
| `integer-width/*000` | `000` | At least 3 <br/> integer digits | `IntegerWidth::zeroFillTo(3)` |
| `integer-width/##0` | - | Between 1 and 3 <br/> integer digits | `IntegerWidth::zeroFillTo(1)` <br/> `.truncateAt(3)`
| `integer-width/00` | - | Exactly 2 <br/> integer digits | `IntegerWidth::zeroFillTo(2)` <br/> `.truncateAt(2)` |
| `integer-width/+` | - | Zero or more <br/> integer digits | `IntegerWidth::zeroFillTo(0) `
| `integer-width/*` | - | Zero or more <br/> integer digits | `IntegerWidth::zeroFillTo(0) `
The long-form option starts with either a single `+` symbol, signaling no limit
The long-form option starts with either a single `*` symbol, signaling no limit
on the number of integer digits (no *truncateAt*), or zero or more `#` symbols.
It should then be followed by zero or more `0` symbols, indicating the minimum
integer digits (the argument to *zeroFillTo*). If there is no `+` symbol, the
integer digits (the argument to *zeroFillTo*). If there is no `*` symbol, the
maximum integer digits (the argument to *truncateAt*) is the number of `#`
symbols plus the number of `0` symbols.
The concise skeleton is simply one or more `0` characters. This supports
minimum integer digits but not maximum integer digits.
***Prior to ICU 67***, use the symbol `+` instead of `*`.
### Scale
To specify the scale, use the following stem and option:
@ -303,7 +312,7 @@ The grouping strategy can be specified by the following stems:
- `group-min2` or `,?` (concise)
- `group-auto` (or omit since this is the default)
- `group-on-aligned` or `,!` (concise)
- `group-thousands` or `,=` (concise)
- `group-thousands` (no concise equivalent)
For more details, see
[UNumberGroupingStrategy](http://icu-project.org/apiref/icu4c/unumberformatter_8h.html).

View File

@ -897,7 +897,7 @@ void GeneratorHelpers::generateSkeleton(const MacroProps& macros, UnicodeString&
bool blueprint_helpers::parseExponentWidthOption(const StringSegment& segment, MacroProps& macros,
UErrorCode&) {
if (segment.charAt(0) != u'+') {
if (!isWildcardChar(segment.charAt(0))) {
return false;
}
int32_t offset = 1;
@ -919,7 +919,7 @@ bool blueprint_helpers::parseExponentWidthOption(const StringSegment& segment, M
void
blueprint_helpers::generateExponentWidthOption(int32_t minExponentDigits, UnicodeString& sb, UErrorCode&) {
sb.append(u'+');
sb.append(kWildcardChar);
appendMultiple(sb, u'e', minExponentDigits);
}
@ -1071,7 +1071,7 @@ void blueprint_helpers::parseFractionStem(const StringSegment& segment, MacroPro
}
}
if (offset < segment.length()) {
if (segment.charAt(offset) == u'+') {
if (isWildcardChar(segment.charAt(offset))) {
maxFrac = -1;
offset++;
} else {
@ -1113,7 +1113,7 @@ blueprint_helpers::generateFractionStem(int32_t minFrac, int32_t maxFrac, Unicod
sb.append(u'.');
appendMultiple(sb, u'0', minFrac);
if (maxFrac == -1) {
sb.append(u'+');
sb.append(kWildcardChar);
} else {
appendMultiple(sb, u'#', maxFrac - minFrac);
}
@ -1133,7 +1133,7 @@ blueprint_helpers::parseDigitsStem(const StringSegment& segment, MacroProps& mac
}
}
if (offset < segment.length()) {
if (segment.charAt(offset) == u'+') {
if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else {
@ -1166,7 +1166,7 @@ void
blueprint_helpers::generateDigitsStem(int32_t minSig, int32_t maxSig, UnicodeString& sb, UErrorCode&) {
appendMultiple(sb, u'@', minSig);
if (maxSig == -1) {
sb.append(u'+');
sb.append(kWildcardChar);
} else {
appendMultiple(sb, u'#', maxSig - minSig);
}
@ -1262,7 +1262,7 @@ bool blueprint_helpers::parseFracSigOption(const StringSegment& segment, MacroPr
// Invalid: @, @@, @@@
// Invalid: @@#, @@##, @@@#
if (offset < segment.length()) {
if (segment.charAt(offset) == u'+') {
if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else if (minSig > 1) {
@ -1351,7 +1351,7 @@ void blueprint_helpers::parseIntegerWidthOption(const StringSegment& segment, Ma
int32_t offset = 0;
int32_t minInt = 0;
int32_t maxInt;
if (segment.charAt(0) == u'+') {
if (isWildcardChar(segment.charAt(0))) {
maxInt = -1;
offset++;
} else {
@ -1392,7 +1392,7 @@ void blueprint_helpers::parseIntegerWidthOption(const StringSegment& segment, Ma
void blueprint_helpers::generateIntegerWidthOption(int32_t minInt, int32_t maxInt, UnicodeString& sb,
UErrorCode&) {
if (maxInt == -1) {
sb.append(u'+');
sb.append(kWildcardChar);
} else {
appendMultiple(sb, u'#', maxInt - minInt);
}

View File

@ -118,6 +118,17 @@ enum StemEnum {
STEM_SCALE,
};
/** Default wildcard char, accepted on input and printed in output */
constexpr char16_t kWildcardChar = u'*';
/** Alternative wildcard char, accept on input but not printed in output */
constexpr char16_t kAltWildcardChar = u'+';
/** Checks whether the char is a wildcard on input */
inline bool isWildcardChar(char16_t c) {
return c == kWildcardChar || c == kAltWildcardChar;
}
/**
* Creates a NumberFormatter corresponding to the given skeleton string.
*

View File

@ -257,6 +257,7 @@ class NumberSkeletonTest : public IntlTest {
void stemsRequiringOption();
void defaultTokens();
void flexibleSeparators();
void wildcardCharacters();
void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0);

View File

@ -209,7 +209,7 @@ void NumberFormatterApiTest::notationScientific() {
assertFormatDescending(
u"Scientific min exponent digits",
u"scientific/+ee",
u"scientific/*ee",
u"E00",
NumberFormatter::with().notation(Notation::scientific().withMinExponentDigits(2)),
Locale::getEnglish(),
@ -1039,7 +1039,7 @@ void NumberFormatterApiTest::roundingFraction() {
assertFormatDescending(
u"Min Fraction",
u".0+",
u".0*",
u".0+",
NumberFormatter::with().precision(Precision::minFraction(1)),
Locale::getEnglish(),
@ -1116,7 +1116,7 @@ void NumberFormatterApiTest::roundingFigures() {
assertFormatSingle(
u"Min Significant",
u"@@+",
u"@@*",
u"@@+",
NumberFormatter::with().precision(Precision::minSignificantDigits(2)),
Locale::getEnglish(),
@ -1153,7 +1153,7 @@ void NumberFormatterApiTest::roundingFigures() {
assertFormatSingle(
u"Fixed Significant on zero with zero integer width",
u"@ integer-width/+",
u"@ integer-width/*",
u"@ integer-width/+",
NumberFormatter::with().precision(Precision::fixedSignificantDigits(1))
.integerWidth(IntegerWidth::zeroFillTo(0)),
@ -1181,7 +1181,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
assertFormatDescending(
u"FracSig minMaxFrac minSig",
u".0#/@@@+",
u".0#/@@@*",
u".0#/@@@+",
NumberFormatter::with().precision(Precision::minMaxFraction(1, 2).withMinDigits(3)),
Locale::getEnglish(),
@ -1229,7 +1229,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
assertFormatSingle(
u"FracSig with trailing zeros A",
u".00/@@@+",
u".00/@@@*",
u".00/@@@+",
NumberFormatter::with().precision(Precision::fixedFraction(2).withMinDigits(3)),
Locale::getEnglish(),
@ -1238,7 +1238,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
assertFormatSingle(
u"FracSig with trailing zeros B",
u".00/@@@+",
u".00/@@@*",
u".00/@@@+",
NumberFormatter::with().precision(Precision::fixedFraction(2).withMinDigits(3)),
Locale::getEnglish(),
@ -1807,7 +1807,7 @@ void NumberFormatterApiTest::integerWidth() {
assertFormatDescending(
u"Integer Width Zero Fill 0",
u"integer-width/+",
u"integer-width/*",
u"integer-width/+",
NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(0)),
Locale::getEnglish(),

View File

@ -29,6 +29,7 @@ void NumberSkeletonTest::runIndexedTest(int32_t index, UBool exec, const char*&
TESTCASE_AUTO(stemsRequiringOption);
TESTCASE_AUTO(defaultTokens);
TESTCASE_AUTO(flexibleSeparators);
TESTCASE_AUTO(wildcardCharacters);
TESTCASE_AUTO_END;
}
@ -41,26 +42,35 @@ void NumberSkeletonTest::validTokens() {
u"precision-integer",
u"precision-unlimited",
u"@@@##",
u"@@*",
u"@@+",
u".000##",
u".00*",
u".00+",
u".",
u".*",
u".+",
u".######",
u".00/@@*",
u".00/@@+",
u".00/@##",
u"precision-increment/3.14",
u"precision-currency-standard",
u"precision-integer rounding-mode-half-up",
u".00# rounding-mode-ceiling",
u".00/@@* rounding-mode-floor",
u".00/@@+ rounding-mode-floor",
u"scientific",
u"scientific/*ee",
u"scientific/+ee",
u"scientific/sign-always",
u"scientific/*ee/sign-always",
u"scientific/+ee/sign-always",
u"scientific/sign-always/*ee",
u"scientific/sign-always/+ee",
u"scientific/sign-except-zero",
u"engineering",
u"engineering/*eee",
u"engineering/+eee",
u"compact-short",
u"compact-long",
@ -81,6 +91,7 @@ void NumberSkeletonTest::validTokens() {
u"group-thousands",
u"integer-width/00",
u"integer-width/#0",
u"integer-width/*00",
u"integer-width/+00",
u"sign-always",
u"sign-auto",
@ -136,16 +147,22 @@ void NumberSkeletonTest::invalidTokens() {
static const char16_t* cases[] = {
u".00x",
u".00##0",
u".##*",
u".00##*",
u".0#*",
u"@#*",
u".##+",
u".00##+",
u".0#+",
u"@#+",
u"@@x",
u"@@##0",
u"@#+",
u".00/@",
u".00/@@",
u".00/@@x",
u".00/@@#",
u".00/@@#*",
u".00/floor/@@*", // wrong order
u".00/@@#+",
u".00/floor/@@+", // wrong order
u"precision-increment/français", // non-invariant characters for C++
@ -161,6 +178,10 @@ void NumberSkeletonTest::invalidTokens() {
u"currency/ççç", // three characters but not ASCII
u"measure-unit/foo",
u"integer-width/xxx",
u"integer-width/0*",
u"integer-width/*0#",
u"integer-width/*#",
u"integer-width/*#0",
u"integer-width/0+",
u"integer-width/+0#",
u"integer-width/+#",
@ -177,6 +198,7 @@ void NumberSkeletonTest::invalidTokens() {
u"EEE",
u"EEE0",
u"001",
u"00*",
u"00+",
};
@ -304,6 +326,32 @@ void NumberSkeletonTest::flexibleSeparators() {
}
}
void NumberSkeletonTest::wildcardCharacters() {
IcuTestErrorCode status(*this, "wildcardCharacters");
struct TestCase {
const char16_t* star;
const char16_t* plus;
} cases[] = {
{ u".00*", u".00+" },
{ u"@@*", u"@@+" },
{ u".00/@@*", u".00/@@+" },
{ u"scientific/*ee", u"scientific/+ee" },
{ u"integer-width/*00", u"integer-width/+00" },
};
for (const auto& cas : cases) {
UnicodeString star(cas.star);
UnicodeString plus(cas.plus);
status.setScope(star);
UnicodeString normalized = NumberFormatter::forSkeleton(plus, status)
.toSkeleton(status);
assertEquals("Plus should normalize to star", star, normalized);
status.errIfFailureAndReset();
}
}
// In C++, there is no distinguishing between "invalid", "unknown", and "unexpected" tokens.
void NumberSkeletonTest::expectedErrorSkeleton(const char16_t** cases, int32_t casesLen) {
for (int32_t i = 0; i < casesLen; i++) {

View File

@ -122,6 +122,17 @@ class NumberSkeletonImpl {
STEM_SCALE,
};
/** Default wildcard char, accepted on input and printed in output */
static final char WILDCARD_CHAR = '*';
/** Alternative wildcard char, accept on input but not printed in output */
static final char ALT_WILDCARD_CHAR = '+';
/** Checks whether the char is a wildcard on input */
static boolean isWildcardChar(char c) {
return c == WILDCARD_CHAR || c == ALT_WILDCARD_CHAR;
}
/** For mapping from ordinal back to StemEnum in Java. */
static final StemEnum[] STEM_ENUM_VALUES = StemEnum.values();
@ -924,7 +935,7 @@ class NumberSkeletonImpl {
/** @return Whether we successfully found and parsed an exponent width option. */
private static boolean parseExponentWidthOption(StringSegment segment, MacroProps macros) {
if (segment.charAt(0) != '+') {
if (!isWildcardChar(segment.charAt(0))) {
return false;
}
int offset = 1;
@ -945,7 +956,7 @@ class NumberSkeletonImpl {
}
private static void generateExponentWidthOption(int minExponentDigits, StringBuilder sb) {
sb.append('+');
sb.append(WILDCARD_CHAR);
appendMultiple(sb, 'e', minExponentDigits);
}
@ -1044,7 +1055,7 @@ class NumberSkeletonImpl {
}
}
if (offset < segment.length()) {
if (segment.charAt(offset) == '+') {
if (isWildcardChar(segment.charAt(offset))) {
maxFrac = -1;
offset++;
} else {
@ -1083,7 +1094,7 @@ class NumberSkeletonImpl {
sb.append('.');
appendMultiple(sb, '0', minFrac);
if (maxFrac == -1) {
sb.append('+');
sb.append(WILDCARD_CHAR);
} else {
appendMultiple(sb, '#', maxFrac - minFrac);
}
@ -1102,7 +1113,7 @@ class NumberSkeletonImpl {
}
}
if (offset < segment.length()) {
if (segment.charAt(offset) == '+') {
if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else {
@ -1132,7 +1143,7 @@ class NumberSkeletonImpl {
private static void generateDigitsStem(int minSig, int maxSig, StringBuilder sb) {
appendMultiple(sb, '@', minSig);
if (maxSig == -1) {
sb.append('+');
sb.append(WILDCARD_CHAR);
} else {
appendMultiple(sb, '#', maxSig - minSig);
}
@ -1224,7 +1235,7 @@ class NumberSkeletonImpl {
// Invalid: @, @@, @@@
// Invalid: @@#, @@##, @@@#
if (offset < segment.length()) {
if (segment.charAt(offset) == '+') {
if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else if (minSig > 1) {
@ -1278,7 +1289,7 @@ class NumberSkeletonImpl {
int offset = 0;
int minInt = 0;
int maxInt;
if (segment.charAt(0) == '+') {
if (isWildcardChar(segment.charAt(0))) {
maxInt = -1;
offset++;
} else {
@ -1316,7 +1327,7 @@ class NumberSkeletonImpl {
private static void generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb) {
if (maxInt == -1) {
sb.append('+');
sb.append(WILDCARD_CHAR);
} else {
appendMultiple(sb, '#', maxInt - minInt);
}

View File

@ -165,7 +165,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Scientific min exponent digits",
"scientific/+ee",
"scientific/*ee",
"E00",
NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)),
ULocale.ENGLISH,
@ -966,7 +966,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Min Fraction",
".0+",
".0*",
".0+",
NumberFormatter.with().precision(Precision.minFraction(1)),
ULocale.ENGLISH,
@ -1044,7 +1044,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"Min Significant",
"@@+",
"@@*",
"@@+",
NumberFormatter.with().precision(Precision.minSignificantDigits(2)),
ULocale.ENGLISH,
@ -1071,7 +1071,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"Fixed Significant on zero with zero integer width",
"@ integer-width/+",
"@ integer-width/*",
"@ integer-width/+",
NumberFormatter.with().precision(Precision.fixedSignificantDigits(1)).integerWidth(IntegerWidth.zeroFillTo(0)),
ULocale.ENGLISH,
@ -1108,7 +1108,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"FracSig minMaxFrac minSig",
".0#/@@@+",
".0#/@@@*",
".0#/@@@+",
NumberFormatter.with().precision(Precision.minMaxFraction(1, 2).withMinDigits(3)),
ULocale.ENGLISH,
@ -1172,7 +1172,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"FracSig with trailing zeros A",
".00/@@@+",
".00/@@@*",
".00/@@@+",
NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)),
ULocale.ENGLISH,
@ -1181,7 +1181,7 @@ public class NumberFormatterApiTest {
assertFormatSingle(
"FracSig with trailing zeros B",
".00/@@@+",
".00/@@@*",
".00/@@@+",
NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)),
ULocale.ENGLISH,
@ -1726,7 +1726,7 @@ public class NumberFormatterApiTest {
assertFormatDescending(
"Integer Width Zero Fill 0",
"integer-width/+",
"integer-width/*",
"integer-width/+",
NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)),
ULocale.ENGLISH,

View File

@ -28,26 +28,35 @@ public class NumberSkeletonTest {
"precision-integer",
"precision-unlimited",
"@@@##",
"@@*",
"@@+",
".000##",
".00*",
".00+",
".",
".*",
".+",
".######",
".00/@@*",
".00/@@+",
".00/@##",
"precision-increment/3.14",
"precision-currency-standard",
"precision-integer rounding-mode-half-up",
".00# rounding-mode-ceiling",
".00/@@* rounding-mode-floor",
".00/@@+ rounding-mode-floor",
"scientific",
"scientific/*ee",
"scientific/+ee",
"scientific/sign-always",
"scientific/*ee/sign-always",
"scientific/+ee/sign-always",
"scientific/sign-always/*ee",
"scientific/sign-always/+ee",
"scientific/sign-except-zero",
"engineering",
"engineering/*eee",
"engineering/+eee",
"compact-short",
"compact-long",
@ -68,6 +77,7 @@ public class NumberSkeletonTest {
"group-thousands",
"integer-width/00",
"integer-width/#0",
"integer-width/*00",
"integer-width/+00",
"sign-always",
"sign-auto",
@ -122,16 +132,22 @@ public class NumberSkeletonTest {
String[] cases = {
".00x",
".00##0",
".##*",
".00##*",
".0#*",
"@#*",
".##+",
".00##+",
".0#+",
"@#+",
"@@x",
"@@##0",
"@#+",
".00/@",
".00/@@",
".00/@@x",
".00/@@#",
".00/@@#*",
".00/floor/@@*", // wrong order
".00/@@#+",
".00/floor/@@+", // wrong order
"precision-increment/français", // non-invariant characters for C++
@ -147,6 +163,10 @@ public class NumberSkeletonTest {
"currency/ççç", // three characters but not ASCII
"measure-unit/foo",
"integer-width/xxx",
"integer-width/0*",
"integer-width/*0#",
"integer-width/*#",
"integer-width/*#0",
"integer-width/0+",
"integer-width/+0#",
"integer-width/+#",
@ -163,6 +183,7 @@ public class NumberSkeletonTest {
"EEE",
"EEE0",
"001",
"00*",
"00+",
};
@ -296,6 +317,26 @@ public class NumberSkeletonTest {
}
}
@Test
public void wildcardCharacters() {
String[][] cases = {
{ ".00*", ".00+" },
{ "@@*", "@@+" },
{ ".00/@@*", ".00/@@+" },
{ "scientific/*ee", "scientific/+ee" },
{ "integer-width/*00", "integer-width/+00" },
};
for (String[] cas : cases) {
String star = cas[0];
String plus = cas[1];
String normalized = NumberFormatter.forSkeleton(plus)
.toSkeleton();
assertEquals("Plus should normalize to star", star, normalized);
}
}
@Test
public void roundingModeNames() {
for (RoundingMode mode : RoundingMode.values()) {