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
| 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
| 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.
| 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.
| 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:
| 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:
- `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).
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;
void
blueprint_helpers::generateExponentWidthOption(int32_t minExponentDigits, UnicodeString& sb, UErrorCode&) {
- sb.append(u'+');
+ sb.append(kWildcardChar);
appendMultiple(sb, u'e', minExponentDigits);
}
}
}
if (offset < segment.length()) {
- if (segment.charAt(offset) == u'+') {
+ if (isWildcardChar(segment.charAt(offset))) {
maxFrac = -1;
offset++;
} else {
sb.append(u'.');
appendMultiple(sb, u'0', minFrac);
if (maxFrac == -1) {
- sb.append(u'+');
+ sb.append(kWildcardChar);
} else {
appendMultiple(sb, u'#', maxFrac - minFrac);
}
}
}
if (offset < segment.length()) {
- if (segment.charAt(offset) == u'+') {
+ if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else {
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);
}
// Invalid: @, @@, @@@
// Invalid: @@#, @@##, @@@#
if (offset < segment.length()) {
- if (segment.charAt(offset) == u'+') {
+ if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else if (minSig > 1) {
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 {
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);
}
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.
*
void stemsRequiringOption();
void defaultTokens();
void flexibleSeparators();
+ void wildcardCharacters();
void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0);
assertFormatDescending(
u"Scientific min exponent digits",
- u"scientific/+ee",
+ u"scientific/*ee",
u"E00",
NumberFormatter::with().notation(Notation::scientific().withMinExponentDigits(2)),
Locale::getEnglish(),
assertFormatDescending(
u"Min Fraction",
- u".0+",
+ u".0*",
u".0+",
NumberFormatter::with().precision(Precision::minFraction(1)),
Locale::getEnglish(),
assertFormatSingle(
u"Min Significant",
- u"@@+",
+ u"@@*",
u"@@+",
NumberFormatter::with().precision(Precision::minSignificantDigits(2)),
Locale::getEnglish(),
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)),
assertFormatDescending(
u"FracSig minMaxFrac minSig",
- u".0#/@@@+",
+ u".0#/@@@*",
u".0#/@@@+",
NumberFormatter::with().precision(Precision::minMaxFraction(1, 2).withMinDigits(3)),
Locale::getEnglish(),
assertFormatSingle(
u"FracSig with trailing zeros A",
- u".00/@@@+",
+ u".00/@@@*",
u".00/@@@+",
NumberFormatter::with().precision(Precision::fixedFraction(2).withMinDigits(3)),
Locale::getEnglish(),
assertFormatSingle(
u"FracSig with trailing zeros B",
- u".00/@@@+",
+ u".00/@@@*",
u".00/@@@+",
NumberFormatter::with().precision(Precision::fixedFraction(2).withMinDigits(3)),
Locale::getEnglish(),
assertFormatDescending(
u"Integer Width Zero Fill 0",
- u"integer-width/+",
+ u"integer-width/*",
u"integer-width/+",
NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(0)),
Locale::getEnglish(),
TESTCASE_AUTO(stemsRequiringOption);
TESTCASE_AUTO(defaultTokens);
TESTCASE_AUTO(flexibleSeparators);
+ TESTCASE_AUTO(wildcardCharacters);
TESTCASE_AUTO_END;
}
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",
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",
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++
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/+#",
u"EEE",
u"EEE0",
u"001",
+ u"00*",
u"00+",
};
}
}
+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++) {
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();
/** @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;
}
private static void generateExponentWidthOption(int minExponentDigits, StringBuilder sb) {
- sb.append('+');
+ sb.append(WILDCARD_CHAR);
appendMultiple(sb, 'e', minExponentDigits);
}
}
}
if (offset < segment.length()) {
- if (segment.charAt(offset) == '+') {
+ if (isWildcardChar(segment.charAt(offset))) {
maxFrac = -1;
offset++;
} else {
sb.append('.');
appendMultiple(sb, '0', minFrac);
if (maxFrac == -1) {
- sb.append('+');
+ sb.append(WILDCARD_CHAR);
} else {
appendMultiple(sb, '#', maxFrac - minFrac);
}
}
}
if (offset < segment.length()) {
- if (segment.charAt(offset) == '+') {
+ if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else {
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);
}
// Invalid: @, @@, @@@
// Invalid: @@#, @@##, @@@#
if (offset < segment.length()) {
- if (segment.charAt(offset) == '+') {
+ if (isWildcardChar(segment.charAt(offset))) {
maxSig = -1;
offset++;
} else if (minSig > 1) {
int offset = 0;
int minInt = 0;
int maxInt;
- if (segment.charAt(0) == '+') {
+ if (isWildcardChar(segment.charAt(0))) {
maxInt = -1;
offset++;
} else {
private static void generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb) {
if (maxInt == -1) {
- sb.append('+');
+ sb.append(WILDCARD_CHAR);
} else {
appendMultiple(sb, '#', maxInt - minInt);
}
assertFormatDescending(
"Scientific min exponent digits",
- "scientific/+ee",
+ "scientific/*ee",
"E00",
NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)),
ULocale.ENGLISH,
assertFormatDescending(
"Min Fraction",
- ".0+",
+ ".0*",
".0+",
NumberFormatter.with().precision(Precision.minFraction(1)),
ULocale.ENGLISH,
assertFormatSingle(
"Min Significant",
- "@@+",
+ "@@*",
"@@+",
NumberFormatter.with().precision(Precision.minSignificantDigits(2)),
ULocale.ENGLISH,
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,
assertFormatDescending(
"FracSig minMaxFrac minSig",
- ".0#/@@@+",
+ ".0#/@@@*",
".0#/@@@+",
NumberFormatter.with().precision(Precision.minMaxFraction(1, 2).withMinDigits(3)),
ULocale.ENGLISH,
assertFormatSingle(
"FracSig with trailing zeros A",
- ".00/@@@+",
+ ".00/@@@*",
".00/@@@+",
NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)),
ULocale.ENGLISH,
assertFormatSingle(
"FracSig with trailing zeros B",
- ".00/@@@+",
+ ".00/@@@*",
".00/@@@+",
NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)),
ULocale.ENGLISH,
assertFormatDescending(
"Integer Width Zero Fill 0",
- "integer-width/+",
+ "integer-width/*",
"integer-width/+",
NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)),
ULocale.ENGLISH,
"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",
"group-thousands",
"integer-width/00",
"integer-width/#0",
+ "integer-width/*00",
"integer-width/+00",
"sign-always",
"sign-auto",
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++
"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/+#",
"EEE",
"EEE0",
"001",
+ "00*",
"00+",
};
}
}
+ @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()) {