]> granicus.if.org Git - icu/commitdiff
ICU-20418 Adding concise number skeletons in ICU4C
authorShane Carr <shane@unicode.org>
Thu, 12 Dec 2019 04:45:54 +0000 (20:45 -0800)
committerShane F. Carr <shane@unicode.org>
Tue, 14 Jan 2020 10:52:27 +0000 (11:52 +0100)
docs/userguide/format_parse/numbers/skeletons.md
icu4c/source/i18n/number_skeletons.cpp
icu4c/source/i18n/number_skeletons.h
icu4c/source/test/intltest/numbertest.h
icu4c/source/test/intltest/numbertest_api.cpp
icu4c/source/test/intltest/numbertest_skeletons.cpp

index da58b196b0e8f42326aa5eb251a3c197a2c56daf..70a9d393ad56a8eb0a11734f2d8c9d6098ac8e71 100644 (file)
@@ -9,16 +9,24 @@ Number Skeletons
 Number skeletons are a locale-agnostic way to configure a NumberFormatter in
 ICU.  Number skeletons work in MessageFormat.
 
-Number skeletons consist of *space-separated tokens* that correspond to
-settings in ICU NumberFormatter.  For example, to format a currency in compact
-notation, you could use this skeleton:
+Number skeletons consist of case-sensitive tokens that correspond to settings
+in ICU NumberFormatter.  For example, to format a currency in compact notation
+with the sign always shown, you could use this skeleton:
 
-    compact-short currency/GBP
+    sign-always compact-short currency/GBP
+
+***Since ICU 67***, you can also use more concise syntax:
+
+    +! K currency/GBP
 
 To use a skeleton in MessageFormat, use the "number" type and prefix the
 skeleton with `::`
 
-    {0, number, ::compact-short currency/GBP}
+    {0, number, :: +! K currency/GBP}
+
+The ICU `toSkeleton()` API outputs the long-form skeletons, but all parts of
+ICU that read user-specified number skeletons accept both long-form and
+concise skeletons.
 
 ## Syntax
 
@@ -27,6 +35,9 @@ occurs before the first "/" character in a token, and the options are each of
 the subsequent "/"-delimited strings.  For example, "compact-short" and
 "currency" are stems, and "GBP" is an option.
 
+Tokens are space-separated, with exceptions for concise skeletons listed at
+the end of this document.
+
 Stems might also be dynamic strings (not a fixed list); these are called
 *blueprint stems*.  For example, to format a number with 2-3 significant
 digits, you could use the following stem:
@@ -39,28 +50,28 @@ Options](#skeleton-stems-and-options).
 
 ## Examples
 
-| Skeleton | Input | en-US Output | Comments |
-|---|---|---|---|
-| `percent` | 25 | 25% |
-| `.00` | 25 | 25.00 | Equivalent to Precision::fixedFraction(2) |
-| `percent .00` | 25 | 25.00% |
-| `scale/100` | 0.3 | 30 | Multiply by 100 before formatting |
-| `percent scale/100` | 0.3 | 30% |
-| `measure-unit/length-meter` | 5 | 5 m | UnitWidth defaults to Short |
-| `measure-unit/length-meter` <br/> `unit-width-full-name` | 5 | 5 meters |
-| `currency/CAD` | 10 | CA$10.00 |
-| `currency/CAD` <br/> `unit-width-narrow` | 10 | $10.00 | Use the narrow symbol variant |
-| `compact-short` | 5000 | 5K |
-| `compact-long` | 5000 | 5 thousand |
-| `compact-short` <br/> `currency/CAD` | 5000 | CA$5K |
-| - | 5000 | 5,000 |
-| `group-min2` | 5000 | 5000 | Require 2 digits in group for separator |
-| `group-min2` | 15000 | 15,000 |
-| `sign-always` | 60 | +60 | Show sign on all numbers |
-| `sign-always` | 0 | +0 |
-| `sign-except-zero` | 60 | +60 | Show sign on all numbers except 0 |
-| `sign-except-zero` | 0 | 0 |
-| `sign-accounting` <br/> `currency/CAD` | -40 | (CA$40.00) |
+| Long Skeleton | Concise Skeleton | Input | en-US Output | Comments |
+|---|---|---|---|---|
+| `percent` | `%` | 25 | 25% |
+| `.00` | `.00` | 25 | 25.00 | Equivalent to Precision::fixedFraction(2) |
+| `percent .00` | `% .00` | 25 | 25.00% |
+| `scale/100` | `scale/100` | 0.3 | 30 | Multiply by 100 before formatting |
+| `percent scale/100` | `%x100` | 0.3 | 30% |
+| `measure-unit/length-meter` | `unit/meter` | 5 | 5 m | UnitWidth defaults to Short |
+| `measure-unit/length-meter` <br/> `unit-width-full-name` | `unit/meter` <br/> `unit-width-full-name` | 5 | 5 meters |
+| `currency/CAD` | `currency/CAD` | 10 | CA$10.00 |
+| `currency/CAD` <br/> `unit-width-narrow` | `currency/CAD` <br/> `unit-width-narrow` | 10 | $10.00 | Use the narrow symbol variant |
+| `compact-short` | `K` | 5000 | 5K |
+| `compact-long` | `KK` | 5000 | 5 thousand |
+| `compact-short` <br/> `currency/CAD` | `K currency/CAD` | 5000 | CA$5K |
+| - | - | 5000 | 5,000 |
+| `group-min2` | `,?` | 5000 | 5000 | Require 2 digits in group for separator |
+| `group-min2` | `,?` | 15000 | 15,000 |
+| `sign-always` | `+!` | 60 | +60 | Show sign on all numbers |
+| `sign-always` | `+!` | 0 | +0 |
+| `sign-except-zero` | `+?` | 60 | +60 | Show sign on all numbers except 0 |
+| `sign-except-zero` | `+?` | 0 | 0 |
+| `sign-accounting` <br/> `currency/CAD` | `() currency/CAD` | -40 | (CA$40.00) |
 
 ## Skeleton Stems and Options
 
@@ -69,16 +80,19 @@ below.
 
 ### Notation
 
-Use one of the following stems to select your notation style:
+Use one of the following stems to select compact or simple notation:
 
-- `compact-short`
-- `compact-long`
-- `scientific`
-- `engineering`
-- `notation-simple`
+- `compact-short` or `K` (concise)
+- `compact-long` or `KK` (concise)
+- `notation-simple` (or omit since this is default)
+
+There are two ways to select scientific or engineering notation: using long-form
+syntax or concise syntax.
 
-The skeletons `scientific` and `engineering` take the following optional
-options:
+#### Scientific and Engineering Notation: Long Form
+
+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.
@@ -90,16 +104,34 @@ For example, all of the following skeletons are valid:
 - `scientific/+ee`
 - `scientific/+ee/sign-always`
 
+#### Scientific and Engineering Notation: Concise Form
+
+The following are examples of concise form:
+
+| Concise Skeleton | Equivalent Long-Form Skeleton |
+|---|---|
+| `E0` | `scientific` |
+| `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.
+3. Expect one or more `0`s.  If more than one, set minimum integer digits.
+
 ### Unit
 
 The supported types of units are percent, currency, and measurement units.
 The following skeleton tokens are accepted:
 
-- `percent`
+- `percent` or `%` (concise)
+- Special: `%x100` to scale the number by 100 and then format with percent
 - `permille`
 - `base-unit`
 - `currency/XXX`
-- `measure-unit/aaaa-bbbb`
+- `measure-unit/aaaa-bbbb` or `unit/bbb` (concise)
 
 The `percent`, `permille`, and `base-unit` stems do not take any options.
 
@@ -110,13 +142,19 @@ The `measure-unit` stem takes one required option: the unit identifier of the
 unit to be formatted.  The full unit identifier is required: both the type and
 the subtype (for example, `length-meter`).
 
+The `unit` stem is an alternative to `measure-unit` that accepts a core unit
+identifier with the subtype but not the type (for example, `meter` instead of
+`length-meter`).  It also supports variations allowed by UTS 35, including the per unit with the `-per-` infix (for example, `unit/furlong-per-second`).
+
 ### Per Unit
 
-To specify a unit to put in the denominator, use the following skeleton token:
+To specify a unit to put in the denominator, use the following skeleton token.
+As with the `measure-unit` stem, pass the unit identifier as the option:
 
 - `per-measure-unit/aaaa-bbbb`
 
-As with the `measure-unit` stem, pass the unit identifier as the option.
+Note that if the `unit` stem is used, the demonimator can be placed in the same
+token as the numerator.
 
 ### Unit Width
 
@@ -219,20 +257,23 @@ Modes](http://userguide.icu-project.org/formatparse/numbers/rounding-modes).
 The following examples show how to specify integer width (minimum or maximum
 integer digits):
 
-| Token | Explanation | Equivalent C++ Code |
-|---|---|---|
-| `integer-width/+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) `
-
-The option start with either a single `+` symbols, 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
+| Long Form | Concise Form | Explanation | Equivalent C++ Code |
+|---|---|---|---|
+| `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) `
+
+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
 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.
+
 ### Scale
 
 To specify the scale, use the following stem and option:
@@ -258,11 +299,11 @@ is able to be parsed by both engines.
 
 The grouping strategy can be specified by the following stems:
 
-- `group-off`
-- `group-min2`
-- `group-auto`
-- `group-on-aligned`
-- `group-thousands`
+- `group-off` or `,_` (concise)
+- `group-min2` or `,?` (concise)
+- `group-auto` (or omit since this is the default)
+- `group-on-aligned` or `,!` (concise)
+- `group-thousands` or `,=` (concise)
 
 For more details, see
 [UNumberGroupingStrategy](http://icu-project.org/apiref/icu4c/unumberformatter_8h.html).
@@ -280,13 +321,13 @@ A custom NDecimalFormatSymbols instance is not supported at this time.
 
 The following stems specify sign display:
 
-- `sign-auto`
-- `sign-always`
-- `sign-never`
-- `sign-accounting`
-- `sign-accounting-always`
-- `sign-except-zero`
-- `sign-accounting-except-zero`
+- `sign-auto` (or omit since this is the default)
+- `sign-always` or `+!` (concise)
+- `sign-never` or `+_` (concise)
+- `sign-accounting` or `()` (concise)
+- `sign-accounting-always` or `()!` (concise)
+- `sign-except-zero` or `+?` (concise)
+- `sign-accounting-except-zero` or `()?` (concise)
 
 For more details, see
 [UNumberSignDisplay](http://icu-project.org/apiref/icu4c/unumberformatter_8h.html).
index 4025539239b786d02c5c081181d01fbe406607dd..88a5bc1a26dde5d5cf2e91ee14c507380d05333c 100644 (file)
@@ -21,6 +21,7 @@
 #include "uinvchar.h"
 #include "charstr.h"
 #include "string_segment.h"
+#include "unicode/errorcode.h"
 
 using namespace icu;
 using namespace icu::number;
@@ -93,12 +94,29 @@ void U_CALLCONV initNumberSkeletons(UErrorCode& status) {
     b.add(u"precision-increment", STEM_PRECISION_INCREMENT, status);
     b.add(u"measure-unit", STEM_MEASURE_UNIT, status);
     b.add(u"per-measure-unit", STEM_PER_MEASURE_UNIT, status);
+    b.add(u"unit", STEM_UNIT, status);
     b.add(u"currency", STEM_CURRENCY, status);
     b.add(u"integer-width", STEM_INTEGER_WIDTH, status);
     b.add(u"numbering-system", STEM_NUMBERING_SYSTEM, status);
     b.add(u"scale", STEM_SCALE, status);
     if (U_FAILURE(status)) { return; }
 
+    // Section 3 (concise tokens):
+    b.add(u"K", STEM_COMPACT_SHORT, status);
+    b.add(u"KK", STEM_COMPACT_LONG, status);
+    b.add(u"%", STEM_PERCENT, status);
+    b.add(u"%x100", STEM_PERCENT_100, status);
+    b.add(u",_", STEM_GROUP_OFF, status);
+    b.add(u",?", STEM_GROUP_MIN2, status);
+    b.add(u",!", STEM_GROUP_ON_ALIGNED, status);
+    b.add(u"+!", STEM_SIGN_ALWAYS, status);
+    b.add(u"+_", STEM_SIGN_NEVER, status);
+    b.add(u"()", STEM_SIGN_ACCOUNTING, status);
+    b.add(u"()!", STEM_SIGN_ACCOUNTING_ALWAYS, status);
+    b.add(u"+?", STEM_SIGN_EXCEPT_ZERO, status);
+    b.add(u"()?", STEM_SIGN_ACCOUNTING_EXCEPT_ZERO, status);
+    if (U_FAILURE(status)) { return; }
+
     // Build the CharsTrie
     // TODO: Use SLOW or FAST here?
     UnicodeString result;
@@ -529,6 +547,7 @@ MacroProps skeleton::parseSkeleton(
                 case STATE_INCREMENT_PRECISION:
                 case STATE_MEASURE_UNIT:
                 case STATE_PER_MEASURE_UNIT:
+                case STATE_IDENTIFIER_UNIT:
                 case STATE_CURRENCY_UNIT:
                 case STATE_INTEGER_WIDTH:
                 case STATE_NUMBERING_SYSTEM:
@@ -564,6 +583,14 @@ skeleton::parseStem(const StringSegment& segment, const UCharsTrie& stemTrie, Se
         CHECK_NULL(seen, precision, status);
             blueprint_helpers::parseDigitsStem(segment, macros, status);
             return STATE_NULL;
+        case u'E':
+        CHECK_NULL(seen, notation, status);
+            blueprint_helpers::parseScientificStem(segment, macros, status);
+            return STATE_NULL;
+        case u'0':
+        CHECK_NULL(seen, notation, status);
+            blueprint_helpers::parseIntegerStem(segment, macros, status);
+            return STATE_NULL;
         default:
             break;
     }
@@ -604,6 +631,13 @@ skeleton::parseStem(const StringSegment& segment, const UCharsTrie& stemTrie, Se
             macros.unit = stem_to_object::unit(stem);
             return STATE_NULL;
 
+        case STEM_PERCENT_100:
+        CHECK_NULL(seen, scale, status);
+        CHECK_NULL(seen, unit, status);
+            macros.scale = Scale::powerOfTen(2);
+            macros.unit = NoUnit::percent();
+            return STATE_NULL;
+
         case STEM_PRECISION_INTEGER:
         case STEM_PRECISION_UNLIMITED:
         case STEM_PRECISION_CURRENCY_STANDARD:
@@ -683,6 +717,11 @@ skeleton::parseStem(const StringSegment& segment, const UCharsTrie& stemTrie, Se
         CHECK_NULL(seen, perUnit, status);
             return STATE_PER_MEASURE_UNIT;
 
+        case STEM_UNIT:
+        CHECK_NULL(seen, unit, status);
+        CHECK_NULL(seen, perUnit, status);
+            return STATE_IDENTIFIER_UNIT;
+
         case STEM_CURRENCY:
         CHECK_NULL(seen, unit, status);
             return STATE_CURRENCY_UNIT;
@@ -719,6 +758,9 @@ ParseState skeleton::parseOption(ParseState stem, const StringSegment& segment,
         case STATE_PER_MEASURE_UNIT:
             blueprint_helpers::parseMeasurePerUnitOption(segment, macros, status);
             return STATE_NULL;
+        case STATE_IDENTIFIER_UNIT:
+            blueprint_helpers::parseIdentifierUnitOption(segment, macros, status);
+            return STATE_NULL;
         case STATE_INCREMENT_PRECISION:
             blueprint_helpers::parseIncrementOption(segment, macros, status);
             return STATE_NULL;
@@ -981,7 +1023,7 @@ void blueprint_helpers::generateMeasureUnitOption(const MeasureUnit& measureUnit
 
 void blueprint_helpers::parseMeasurePerUnitOption(const StringSegment& segment, MacroProps& macros,
                                                   UErrorCode& status) {
-    // A little bit of a hack: safe the current unit (numerator), call the main measure unit
+    // A little bit of a hack: save the current unit (numerator), call the main measure unit
     // parsing code, put back the numerator unit, and put the new unit into per-unit.
     MeasureUnit numerator = macros.unit;
     parseMeasureUnitOption(segment, macros, status);
@@ -990,6 +1032,22 @@ void blueprint_helpers::parseMeasurePerUnitOption(const StringSegment& segment,
     macros.unit = numerator;
 }
 
+void blueprint_helpers::parseIdentifierUnitOption(const StringSegment& segment, MacroProps& macros,
+                                                  UErrorCode& status) {
+    // Need to do char <-> UChar conversion...
+    U_ASSERT(U_SUCCESS(status));
+    CharString buffer;
+    SKELETON_UCHAR_TO_CHAR(buffer, segment.toTempUnicodeString(), 0, segment.length(), status);
+
+    ErrorCode internalStatus;
+    MeasureUnit::parseCoreUnitIdentifier(buffer.toStringPiece(), &macros.unit, &macros.perUnit, internalStatus);
+    if (internalStatus.isFailure()) {
+        // throw new SkeletonSyntaxException("Invalid core unit identifier", segment, e);
+        status = U_NUMBER_SKELETON_SYNTAX_ERROR;
+        return;
+    }
+}
+
 void blueprint_helpers::parseFractionStem(const StringSegment& segment, MacroProps& macros,
                                           UErrorCode& status) {
     U_ASSERT(segment.charAt(0) == u'.');
@@ -1027,7 +1085,11 @@ void blueprint_helpers::parseFractionStem(const StringSegment& segment, MacroPro
     }
     // Use the public APIs to enforce bounds checking
     if (maxFrac == -1) {
-        macros.precision = Precision::minFraction(minFrac);
+        if (minFrac == 0) {
+            macros.precision = Precision::unlimited();
+        } else {
+            macros.precision = Precision::minFraction(minFrac);
+        }
     } else {
         macros.precision = Precision::minMaxFraction(minFrac, maxFrac);
     }
@@ -1051,9 +1113,9 @@ blueprint_helpers::generateFractionStem(int32_t minFrac, int32_t maxFrac, Unicod
 void
 blueprint_helpers::parseDigitsStem(const StringSegment& segment, MacroProps& macros, UErrorCode& status) {
     U_ASSERT(segment.charAt(0) == u'@');
-    int offset = 0;
-    int minSig = 0;
-    int maxSig;
+    int32_t offset = 0;
+    int32_t minSig = 0;
+    int32_t maxSig;
     for (; offset < segment.length(); offset++) {
         if (segment.charAt(offset) == u'@') {
             minSig++;
@@ -1101,6 +1163,75 @@ blueprint_helpers::generateDigitsStem(int32_t minSig, int32_t maxSig, UnicodeStr
     }
 }
 
+void blueprint_helpers::parseScientificStem(const StringSegment& segment, MacroProps& macros, UErrorCode& status) {
+    U_ASSERT(segment.charAt(0) == u'E');
+    {
+        int32_t offset = 1;
+        if (segment.length() == offset) {
+            goto fail;
+        }
+        bool isEngineering = false;
+        if (segment.charAt(offset) == u'E') {
+            isEngineering = true;
+            offset++;
+            if (segment.length() == offset) {
+                goto fail;
+            }
+        }
+        UNumberSignDisplay signDisplay = UNUM_SIGN_AUTO;
+        if (segment.charAt(offset) == u'+') {
+            offset++;
+            if (segment.length() == offset) {
+                goto fail;
+            }
+            if (segment.charAt(offset) == u'!') {
+                signDisplay = UNUM_SIGN_ALWAYS;
+            } else if (segment.charAt(offset) == u'?') {
+                signDisplay = UNUM_SIGN_EXCEPT_ZERO;
+            } else {
+                goto fail;
+            }
+            offset++;
+            if (segment.length() == offset) {
+                goto fail;
+            }
+        }
+        int32_t minDigits = 0;
+        for (; offset < segment.length(); offset++) {
+            if (segment.charAt(offset) != u'0') {
+                goto fail;
+            }
+            minDigits++;
+        }
+        macros.notation = (isEngineering ? Notation::engineering() : Notation::scientific())
+            .withExponentSignDisplay(signDisplay)
+            .withMinExponentDigits(minDigits);
+        return;
+    }
+    fail: void();
+    // throw new SkeletonSyntaxException("Invalid scientific stem", segment);
+    status = U_NUMBER_SKELETON_SYNTAX_ERROR;
+    return;
+}
+
+void blueprint_helpers::parseIntegerStem(const StringSegment& segment, MacroProps& macros, UErrorCode& status) {
+    U_ASSERT(segment.charAt(0) == u'0');
+    int32_t offset = 1;
+    for (; offset < segment.length(); offset++) {
+        if (segment.charAt(offset) != u'0') {
+            offset--;
+            break;
+        }
+    }
+    if (offset < segment.length()) {
+        // throw new SkeletonSyntaxException("Invalid integer stem", segment);
+        status = U_NUMBER_SKELETON_SYNTAX_ERROR;
+        return;
+    }
+    macros.integerWidth = IntegerWidth::zeroFillTo(offset);
+    return;
+}
+
 bool blueprint_helpers::parseFracSigOption(const StringSegment& segment, MacroProps& macros,
                                            UErrorCode& status) {
     if (segment.charAt(0) != u'@') {
index 59af771928fbd0faa696ffe2119b50b56dc22d0b..3c03d733e9f1d085b1e572e4ddbf65c8f5c39e51 100644 (file)
@@ -46,6 +46,7 @@ enum ParseState {
     STATE_INCREMENT_PRECISION,
     STATE_MEASURE_UNIT,
     STATE_PER_MEASURE_UNIT,
+    STATE_IDENTIFIER_UNIT,
     STATE_CURRENCY_UNIT,
     STATE_INTEGER_WIDTH,
     STATE_NUMBERING_SYSTEM,
@@ -71,6 +72,7 @@ enum StemEnum {
     STEM_BASE_UNIT,
     STEM_PERCENT,
     STEM_PERMILLE,
+    STEM_PERCENT_100, // concise-only
     STEM_PRECISION_INTEGER,
     STEM_PRECISION_UNLIMITED,
     STEM_PRECISION_CURRENCY_STANDARD,
@@ -109,6 +111,7 @@ enum StemEnum {
     STEM_PRECISION_INCREMENT,
     STEM_MEASURE_UNIT,
     STEM_PER_MEASURE_UNIT,
+    STEM_UNIT,
     STEM_CURRENCY,
     STEM_INTEGER_WIDTH,
     STEM_NUMBERING_SYSTEM,
@@ -226,6 +229,8 @@ void generateMeasureUnitOption(const MeasureUnit& measureUnit, UnicodeString& sb
 
 void parseMeasurePerUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
 
+void parseIdentifierUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
+
 void parseFractionStem(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
 
 void generateFractionStem(int32_t minFrac, int32_t maxFrac, UnicodeString& sb, UErrorCode& status);
@@ -234,6 +239,14 @@ void parseDigitsStem(const StringSegment& segment, MacroProps& macros, UErrorCod
 
 void generateDigitsStem(int32_t minSig, int32_t maxSig, UnicodeString& sb, UErrorCode& status);
 
+void parseScientificStem(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
+
+// Note: no generateScientificStem since this syntax was added later in ICU 67
+
+void parseIntegerStem(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
+
+// Note: no generateIntegerStem since this syntax was added later in ICU 67
+
 /** @return Whether we successfully found and parsed a frac-sig option. */
 bool parseFracSigOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
 
index adccc9e775560be7c6a94f494fa0e953a7da7fa7..f20cb4829056b6b395d2ba9f5b7922951dc125bb 100644 (file)
@@ -114,16 +114,38 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition {
     DecimalFormatSymbols SWISS_SYMBOLS;
     DecimalFormatSymbols MYANMAR_SYMBOLS;
 
-    void assertFormatDescending(const char16_t* message, const char16_t* skeleton,
-                                const UnlocalizedNumberFormatter& f, Locale locale, ...);
+    /**
+     * skeleton is the full length skeleton, which must round-trip.
+     *
+     * conciseSkeleton should be the shortest available skeleton.
+     * The concise skeleton can be read but not printed.
+     */
+    void assertFormatDescending(
+      const char16_t* message,
+      const char16_t* skeleton,
+      const char16_t* conciseSkeleton,
+      const UnlocalizedNumberFormatter& f,
+      Locale locale,
+      ...);
 
-    void assertFormatDescendingBig(const char16_t* message, const char16_t* skeleton,
-                                   const UnlocalizedNumberFormatter& f, Locale locale, ...);
+    /** See notes above regarding skeleton vs conciseSkeleton */
+    void assertFormatDescendingBig(
+      const char16_t* message,
+      const char16_t* skeleton,
+      const char16_t* conciseSkeleton,
+      const UnlocalizedNumberFormatter& f,
+      Locale locale,
+      ...);
 
-    FormattedNumber
-    assertFormatSingle(const char16_t* message, const char16_t* skeleton,
-                       const UnlocalizedNumberFormatter& f, Locale locale, double input,
-                       const UnicodeString& expected);
+    /** See notes above regarding skeleton vs conciseSkeleton */
+    FormattedNumber assertFormatSingle(
+      const char16_t* message,
+      const char16_t* skeleton,
+      const char16_t* conciseSkeleton,
+      const UnlocalizedNumberFormatter& f,
+      Locale locale,
+      double input,
+      const UnicodeString& expected);
 
     void assertUndefinedSkeleton(const UnlocalizedNumberFormatter& f);
 
index 6b4024a85bcfe84469bf3114b97ce65e0e99382b..ba3b927ad9ec2d12734c1cb6dfb051864fb2e5b5 100644 (file)
@@ -113,6 +113,7 @@ void NumberFormatterApiTest::notationSimple() {
     assertFormatDescending(
             u"Basic",
             u"",
+            u"",
             NumberFormatter::with(),
             Locale::getEnglish(),
             u"87,650",
@@ -128,6 +129,7 @@ void NumberFormatterApiTest::notationSimple() {
     assertFormatDescendingBig(
             u"Big Simple",
             u"notation-simple",
+            u"",
             NumberFormatter::with().notation(Notation::simple()),
             Locale::getEnglish(),
             u"87,650,000",
@@ -143,6 +145,7 @@ void NumberFormatterApiTest::notationSimple() {
     assertFormatSingle(
             u"Basic with Negative Sign",
             u"",
+            u"",
             NumberFormatter::with(),
             Locale::getEnglish(),
             -9876543.21,
@@ -154,6 +157,7 @@ void NumberFormatterApiTest::notationScientific() {
     assertFormatDescending(
             u"Scientific",
             u"scientific",
+            u"E0",
             NumberFormatter::with().notation(Notation::scientific()),
             Locale::getEnglish(),
             u"8.765E4",
@@ -169,6 +173,7 @@ void NumberFormatterApiTest::notationScientific() {
     assertFormatDescending(
             u"Engineering",
             u"engineering",
+            u"EE0",
             NumberFormatter::with().notation(Notation::engineering()),
             Locale::getEnglish(),
             u"87.65E3",
@@ -184,6 +189,7 @@ void NumberFormatterApiTest::notationScientific() {
     assertFormatDescending(
             u"Scientific sign always shown",
             u"scientific/sign-always",
+            u"E+!0",
             NumberFormatter::with().notation(
                     Notation::scientific().withExponentSignDisplay(UNumberSignDisplay::UNUM_SIGN_ALWAYS)),
             Locale::getEnglish(),
@@ -200,6 +206,7 @@ void NumberFormatterApiTest::notationScientific() {
     assertFormatDescending(
             u"Scientific min exponent digits",
             u"scientific/+ee",
+            u"E00",
             NumberFormatter::with().notation(Notation::scientific().withMinExponentDigits(2)),
             Locale::getEnglish(),
             u"8.765E04",
@@ -215,6 +222,7 @@ void NumberFormatterApiTest::notationScientific() {
     assertFormatSingle(
             u"Scientific Negative",
             u"scientific",
+            u"E0",
             NumberFormatter::with().notation(Notation::scientific()),
             Locale::getEnglish(),
             -1000000,
@@ -223,6 +231,7 @@ void NumberFormatterApiTest::notationScientific() {
     assertFormatSingle(
             u"Scientific Infinity",
             u"scientific",
+            u"E0",
             NumberFormatter::with().notation(Notation::scientific()),
             Locale::getEnglish(),
             -uprv_getInfinity(),
@@ -231,6 +240,7 @@ void NumberFormatterApiTest::notationScientific() {
     assertFormatSingle(
             u"Scientific NaN",
             u"scientific",
+            u"E0",
             NumberFormatter::with().notation(Notation::scientific()),
             Locale::getEnglish(),
             uprv_getNaN(),
@@ -241,6 +251,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatDescending(
             u"Compact Short",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             u"88K",
@@ -256,6 +267,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatDescending(
             u"Compact Long",
             u"compact-long",
+            u"KK",
             NumberFormatter::with().notation(Notation::compactLong()),
             Locale::getEnglish(),
             u"88 thousand",
@@ -271,6 +283,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatDescending(
             u"Compact Short Currency",
             u"compact-short currency/USD",
+            u"K currency/USD",
             NumberFormatter::with().notation(Notation::compactShort()).unit(USD),
             Locale::getEnglish(),
             u"$88K",
@@ -286,6 +299,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatDescending(
             u"Compact Short with ISO Currency",
             u"compact-short currency/USD unit-width-iso-code",
+            u"K currency/USD unit-width-iso-code",
             NumberFormatter::with().notation(Notation::compactShort())
                     .unit(USD)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_ISO_CODE),
@@ -303,6 +317,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatDescending(
             u"Compact Short with Long Name Currency",
             u"compact-short currency/USD unit-width-full-name",
+            u"K currency/USD unit-width-full-name",
             NumberFormatter::with().notation(Notation::compactShort())
                     .unit(USD)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
@@ -322,6 +337,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatDescending(
             u"Compact Long Currency",
             u"compact-long currency/USD",
+            u"KK currency/USD",
             NumberFormatter::with().notation(Notation::compactLong()).unit(USD),
             Locale::getEnglish(),
             u"$88K", // should be something like "$88 thousand"
@@ -339,6 +355,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatDescending(
             u"Compact Long with ISO Currency",
             u"compact-long currency/USD unit-width-iso-code",
+            u"KK currency/USD unit-width-iso-code",
             NumberFormatter::with().notation(Notation::compactLong())
                     .unit(USD)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_ISO_CODE),
@@ -357,6 +374,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatDescending(
             u"Compact Long with Long Name Currency",
             u"compact-long currency/USD unit-width-full-name",
+            u"KK currency/USD unit-width-full-name",
             NumberFormatter::with().notation(Notation::compactLong())
                     .unit(USD)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
@@ -374,6 +392,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact Plural One",
             u"compact-long",
+            u"KK",
             NumberFormatter::with().notation(Notation::compactLong()),
             Locale::createFromName("es"),
             1000000,
@@ -382,6 +401,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact Plural Other",
             u"compact-long",
+            u"KK",
             NumberFormatter::with().notation(Notation::compactLong()),
             Locale::createFromName("es"),
             2000000,
@@ -390,6 +410,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact with Negative Sign",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             -9876543.21,
@@ -398,6 +419,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact Rounding",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             990000,
@@ -406,6 +428,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact Rounding",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             999000,
@@ -414,6 +437,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact Rounding",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             999900,
@@ -422,6 +446,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact Rounding",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             9900000,
@@ -430,6 +455,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact Rounding",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             9990000,
@@ -438,6 +464,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact in zh-Hant-HK",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale("zh-Hant-HK"),
             1e7,
@@ -446,6 +473,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact in zh-Hant",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale("zh-Hant"),
             1e7,
@@ -454,6 +482,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact Infinity",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             -uprv_getInfinity(),
@@ -462,6 +491,7 @@ void NumberFormatterApiTest::notationCompact() {
     assertFormatSingle(
             u"Compact NaN",
             u"compact-short",
+            u"K",
             NumberFormatter::with().notation(Notation::compactShort()),
             Locale::getEnglish(),
             uprv_getNaN(),
@@ -475,6 +505,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatDescending(
             u"Meters Short and unit() method",
             u"measure-unit/length-meter",
+            u"unit/meter",
             NumberFormatter::with().unit(MeasureUnit::getMeter()),
             Locale::getEnglish(),
             u"87,650 m",
@@ -490,6 +521,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatDescending(
             u"Meters Long and adoptUnit() method",
             u"measure-unit/length-meter unit-width-full-name",
+            u"unit/meter unit-width-full-name",
             NumberFormatter::with().adoptUnit(new MeasureUnit(METER))
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
             Locale::getEnglish(),
@@ -506,6 +538,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatDescending(
             u"Compact Meters Long",
             u"compact-long measure-unit/length-meter unit-width-full-name",
+            u"KK unit/meter unit-width-full-name",
             NumberFormatter::with().notation(Notation::compactLong())
                     .unit(METER)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
@@ -539,6 +572,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"Meters with Negative Sign",
             u"measure-unit/length-meter",
+            u"unit/meter",
             NumberFormatter::with().unit(METER),
             Locale::getEnglish(),
             -9876543.21,
@@ -548,6 +582,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"Interesting Data Fallback 1",
             u"measure-unit/duration-day unit-width-full-name",
+            u"unit/day unit-width-full-name",
             NumberFormatter::with().unit(DAY).unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
             Locale::createFromName("brx"),
             5.43,
@@ -557,6 +592,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"Interesting Data Fallback 2",
             u"measure-unit/duration-day unit-width-narrow",
+            u"unit/day unit-width-narrow",
             NumberFormatter::with().unit(DAY).unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW),
             Locale::createFromName("brx"),
             5.43,
@@ -567,6 +603,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"Interesting Data Fallback 3",
             u"measure-unit/area-square-meter unit-width-narrow",
+            u"unit/square-meter unit-width-narrow",
             NumberFormatter::with().unit(SQUARE_METER).unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW),
             Locale::createFromName("en-GB"),
             5.43,
@@ -576,6 +613,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"Interesting Data Fallback 4",
             u"measure-unit/area-square-meter unit-width-narrow",
+            u"unit/square-meter unit-width-narrow",
             NumberFormatter::with().unit(SQUARE_METER).unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW),
             Locale::createFromName("root"),
             5.43,
@@ -586,6 +624,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"Difference between Narrow and Short (Narrow Version)",
             u"measure-unit/temperature-fahrenheit unit-width-narrow",
+            u"unit/fahrenheit unit-width-narrow",
             NumberFormatter::with().unit(FAHRENHEIT).unitWidth(UNUM_UNIT_WIDTH_NARROW),
             Locale("es-US"),
             5.43,
@@ -594,6 +633,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"Difference between Narrow and Short (Short Version)",
             u"measure-unit/temperature-fahrenheit unit-width-short",
+            u"unit/fahrenheit unit-width-short",
             NumberFormatter::with().unit(FAHRENHEIT).unitWidth(UNUM_UNIT_WIDTH_SHORT),
             Locale("es-US"),
             5.43,
@@ -602,6 +642,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"MeasureUnit form without {0} in CLDR pattern",
             u"measure-unit/temperature-kelvin unit-width-full-name",
+            u"unit/kelvin unit-width-full-name",
             NumberFormatter::with().unit(KELVIN).unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
             Locale("es-MX"),
             1,
@@ -610,6 +651,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"MeasureUnit form without {0} in CLDR pattern and wide base form",
             u"measure-unit/temperature-kelvin .00000000000000000000 unit-width-full-name",
+            u"unit/kelvin .00000000000000000000 unit-width-full-name",
             NumberFormatter::with().precision(Precision::fixedFraction(20))
                     .unit(KELVIN)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
@@ -620,6 +662,7 @@ void NumberFormatterApiTest::unitMeasure() {
     assertFormatSingle(
             u"Person unit not in short form",
             u"measure-unit/duration-year-person unit-width-full-name",
+            u"unit/year-person unit-width-full-name",
             NumberFormatter::with().unit(MeasureUnit::getYearPerson())
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
             Locale("es-MX"),
@@ -631,6 +674,7 @@ void NumberFormatterApiTest::unitCompoundMeasure() {
     assertFormatDescending(
             u"Meters Per Second Short (unit that simplifies) and perUnit method",
             u"measure-unit/length-meter per-measure-unit/duration-second",
+            u"~unit/meter-per-second", // does not round-trip to the full skeleton above
             NumberFormatter::with().unit(METER).perUnit(SECOND),
             Locale::getEnglish(),
             u"87,650 m/s",
@@ -646,6 +690,7 @@ void NumberFormatterApiTest::unitCompoundMeasure() {
     assertFormatDescending(
             u"Pounds Per Square Mile Short (secondary unit has per-format) and adoptPerUnit method",
             u"measure-unit/mass-pound per-measure-unit/area-square-mile",
+            u"unit/pound-per-square-mile",
             NumberFormatter::with().unit(POUND).adoptPerUnit(new MeasureUnit(SQUARE_MILE)),
             Locale::getEnglish(),
             u"87,650 lb/mi²",
@@ -661,6 +706,7 @@ void NumberFormatterApiTest::unitCompoundMeasure() {
     assertFormatDescending(
             u"Joules Per Furlong Short (unit with no simplifications or special patterns)",
             u"measure-unit/energy-joule per-measure-unit/length-furlong",
+            u"unit/joule-per-furlong",
             NumberFormatter::with().unit(JOULE).perUnit(FURLONG),
             Locale::getEnglish(),
             u"87,650 J/fur",
@@ -678,6 +724,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatDescending(
             u"Currency",
             u"currency/GBP",
+            u"currency/GBP",
             NumberFormatter::with().unit(GBP),
             Locale::getEnglish(),
             u"£87,650.00",
@@ -693,6 +740,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatDescending(
             u"Currency ISO",
             u"currency/GBP unit-width-iso-code",
+            u"currency/GBP unit-width-iso-code",
             NumberFormatter::with().unit(GBP).unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_ISO_CODE),
             Locale::getEnglish(),
             u"GBP 87,650.00",
@@ -708,6 +756,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatDescending(
             u"Currency Long Name",
             u"currency/GBP unit-width-full-name",
+            u"currency/GBP unit-width-full-name",
             NumberFormatter::with().unit(GBP).unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
             Locale::getEnglish(),
             u"87,650.00 British pounds",
@@ -723,6 +772,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatDescending(
             u"Currency Hidden",
             u"currency/GBP unit-width-hidden",
+            u"currency/GBP unit-width-hidden",
             NumberFormatter::with().unit(GBP).unitWidth(UNUM_UNIT_WIDTH_HIDDEN),
             Locale::getEnglish(),
             u"87,650.00",
@@ -753,6 +803,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency with Negative Sign",
             u"currency/GBP",
+            u"currency/GBP",
             NumberFormatter::with().unit(GBP),
             Locale::getEnglish(),
             -9876543.21,
@@ -763,6 +814,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency Difference between Narrow and Short (Narrow Version)",
             u"currency/USD unit-width-narrow",
+            u"currency/USD unit-width-narrow",
             NumberFormatter::with().unit(USD).unitWidth(UNUM_UNIT_WIDTH_NARROW),
             Locale("en-CA"),
             5.43,
@@ -771,6 +823,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency Difference between Narrow and Short (Short Version)",
             u"currency/USD unit-width-short",
+            u"currency/USD unit-width-short",
             NumberFormatter::with().unit(USD).unitWidth(UNUM_UNIT_WIDTH_SHORT),
             Locale("en-CA"),
             5.43,
@@ -779,6 +832,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency-dependent format (Control)",
             u"currency/USD unit-width-short",
+            u"currency/USD unit-width-short",
             NumberFormatter::with().unit(USD).unitWidth(UNUM_UNIT_WIDTH_SHORT),
             Locale("ca"),
             444444.55,
@@ -787,6 +841,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency-dependent format (Test)",
             u"currency/ESP unit-width-short",
+            u"currency/ESP unit-width-short",
             NumberFormatter::with().unit(ESP).unitWidth(UNUM_UNIT_WIDTH_SHORT),
             Locale("ca"),
             444444.55,
@@ -795,6 +850,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency-dependent symbols (Control)",
             u"currency/USD unit-width-short",
+            u"currency/USD unit-width-short",
             NumberFormatter::with().unit(USD).unitWidth(UNUM_UNIT_WIDTH_SHORT),
             Locale("pt-PT"),
             444444.55,
@@ -805,6 +861,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency-dependent symbols (Test Short)",
             u"currency/PTE unit-width-short",
+            u"currency/PTE unit-width-short",
             NumberFormatter::with().unit(PTE).unitWidth(UNUM_UNIT_WIDTH_SHORT),
             Locale("pt-PT"),
             444444.55,
@@ -813,6 +870,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency-dependent symbols (Test Narrow)",
             u"currency/PTE unit-width-narrow",
+            u"currency/PTE unit-width-narrow",
             NumberFormatter::with().unit(PTE).unitWidth(UNUM_UNIT_WIDTH_NARROW),
             Locale("pt-PT"),
             444444.55,
@@ -821,6 +879,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Currency-dependent symbols (Test ISO Code)",
             u"currency/PTE unit-width-iso-code",
+            u"currency/PTE unit-width-iso-code",
             NumberFormatter::with().unit(PTE).unitWidth(UNUM_UNIT_WIDTH_ISO_CODE),
             Locale("pt-PT"),
             444444.55,
@@ -829,6 +888,7 @@ void NumberFormatterApiTest::unitCurrency() {
     assertFormatSingle(
             u"Plural form depending on visible digits (ICU-20499)",
             u"currency/RON unit-width-full-name",
+            u"currency/RON unit-width-full-name",
             NumberFormatter::with().unit(RON).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
             Locale("ro-RO"),
             24,
@@ -839,6 +899,7 @@ void NumberFormatterApiTest::unitPercent() {
     assertFormatDescending(
             u"Percent",
             u"percent",
+            u"%",
             NumberFormatter::with().unit(NoUnit::percent()),
             Locale::getEnglish(),
             u"87,650%",
@@ -854,6 +915,7 @@ void NumberFormatterApiTest::unitPercent() {
     assertFormatDescending(
             u"Permille",
             u"permille",
+            u"permille",
             NumberFormatter::with().unit(NoUnit::permille()),
             Locale::getEnglish(),
             u"87,650‰",
@@ -869,6 +931,7 @@ void NumberFormatterApiTest::unitPercent() {
     assertFormatSingle(
             u"NoUnit Base",
             u"base-unit",
+            u"",
             NumberFormatter::with().unit(NoUnit::base()),
             Locale::getEnglish(),
             51423,
@@ -877,6 +940,7 @@ void NumberFormatterApiTest::unitPercent() {
     assertFormatSingle(
             u"Percent with Negative Sign",
             u"percent",
+            u"%",
             NumberFormatter::with().unit(NoUnit::percent()),
             Locale::getEnglish(),
             -98.7654321,
@@ -914,6 +978,7 @@ void NumberFormatterApiTest::roundingFraction() {
     assertFormatDescending(
             u"Integer",
             u"precision-integer",
+            u".",
             NumberFormatter::with().precision(Precision::integer()),
             Locale::getEnglish(),
             u"87,650",
@@ -929,6 +994,7 @@ void NumberFormatterApiTest::roundingFraction() {
     assertFormatDescending(
             u"Fixed Fraction",
             u".000",
+            u".000",
             NumberFormatter::with().precision(Precision::fixedFraction(3)),
             Locale::getEnglish(),
             u"87,650.000",
@@ -944,6 +1010,7 @@ void NumberFormatterApiTest::roundingFraction() {
     assertFormatDescending(
             u"Min Fraction",
             u".0+",
+            u".0+",
             NumberFormatter::with().precision(Precision::minFraction(1)),
             Locale::getEnglish(),
             u"87,650.0",
@@ -959,6 +1026,7 @@ void NumberFormatterApiTest::roundingFraction() {
     assertFormatDescending(
             u"Max Fraction",
             u".#",
+            u".#",
             NumberFormatter::with().precision(Precision::maxFraction(1)),
             Locale::getEnglish(),
             u"87,650",
@@ -974,6 +1042,7 @@ void NumberFormatterApiTest::roundingFraction() {
     assertFormatDescending(
             u"Min/Max Fraction",
             u".0##",
+            u".0##",
             NumberFormatter::with().precision(Precision::minMaxFraction(1, 3)),
             Locale::getEnglish(),
             u"87,650.0",
@@ -991,6 +1060,7 @@ void NumberFormatterApiTest::roundingFigures() {
     assertFormatSingle(
             u"Fixed Significant",
             u"@@@",
+            u"@@@",
             NumberFormatter::with().precision(Precision::fixedSignificantDigits(3)),
             Locale::getEnglish(),
             -98,
@@ -999,6 +1069,7 @@ void NumberFormatterApiTest::roundingFigures() {
     assertFormatSingle(
             u"Fixed Significant Rounding",
             u"@@@",
+            u"@@@",
             NumberFormatter::with().precision(Precision::fixedSignificantDigits(3)),
             Locale::getEnglish(),
             -98.7654321,
@@ -1007,6 +1078,7 @@ void NumberFormatterApiTest::roundingFigures() {
     assertFormatSingle(
             u"Fixed Significant Zero",
             u"@@@",
+            u"@@@",
             NumberFormatter::with().precision(Precision::fixedSignificantDigits(3)),
             Locale::getEnglish(),
             0,
@@ -1015,6 +1087,7 @@ void NumberFormatterApiTest::roundingFigures() {
     assertFormatSingle(
             u"Min Significant",
             u"@@+",
+            u"@@+",
             NumberFormatter::with().precision(Precision::minSignificantDigits(2)),
             Locale::getEnglish(),
             -9,
@@ -1023,6 +1096,7 @@ void NumberFormatterApiTest::roundingFigures() {
     assertFormatSingle(
             u"Max Significant",
             u"@###",
+            u"@###",
             NumberFormatter::with().precision(Precision::maxSignificantDigits(4)),
             Locale::getEnglish(),
             98.7654321,
@@ -1031,6 +1105,7 @@ void NumberFormatterApiTest::roundingFigures() {
     assertFormatSingle(
             u"Min/Max Significant",
             u"@@@#",
+            u"@@@#",
             NumberFormatter::with().precision(Precision::minMaxSignificantDigits(3, 4)),
             Locale::getEnglish(),
             9.99999,
@@ -1039,6 +1114,7 @@ void NumberFormatterApiTest::roundingFigures() {
     assertFormatSingle(
             u"Fixed Significant on zero with lots of integer width",
             u"@ integer-width/+000",
+            u"@ 000",
             NumberFormatter::with().precision(Precision::fixedSignificantDigits(1))
                     .integerWidth(IntegerWidth::zeroFillTo(3)),
             Locale::getEnglish(),
@@ -1048,6 +1124,7 @@ void NumberFormatterApiTest::roundingFigures() {
     assertFormatSingle(
             u"Fixed Significant on zero with zero integer width",
             u"@ integer-width/+",
+            u"@ integer-width/+",
             NumberFormatter::with().precision(Precision::fixedSignificantDigits(1))
                     .integerWidth(IntegerWidth::zeroFillTo(0)),
             Locale::getEnglish(),
@@ -1059,6 +1136,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
     assertFormatDescending(
             u"Basic Significant", // for comparison
             u"@#",
+            u"@#",
             NumberFormatter::with().precision(Precision::maxSignificantDigits(2)),
             Locale::getEnglish(),
             u"88,000",
@@ -1074,6 +1152,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
     assertFormatDescending(
             u"FracSig minMaxFrac minSig",
             u".0#/@@@+",
+            u".0#/@@@+",
             NumberFormatter::with().precision(Precision::minMaxFraction(1, 2).withMinDigits(3)),
             Locale::getEnglish(),
             u"87,650.0",
@@ -1089,6 +1168,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
     assertFormatDescending(
             u"FracSig minMaxFrac maxSig A",
             u".0##/@#",
+            u".0##/@#",
             NumberFormatter::with().precision(Precision::minMaxFraction(1, 3).withMaxDigits(2)),
             Locale::getEnglish(),
             u"88,000.0", // maxSig beats maxFrac
@@ -1104,6 +1184,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
     assertFormatDescending(
             u"FracSig minMaxFrac maxSig B",
             u".00/@#",
+            u".00/@#",
             NumberFormatter::with().precision(Precision::fixedFraction(2).withMaxDigits(2)),
             Locale::getEnglish(),
             u"88,000.00", // maxSig beats maxFrac
@@ -1119,6 +1200,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
     assertFormatSingle(
             u"FracSig with trailing zeros A",
             u".00/@@@+",
+            u".00/@@@+",
             NumberFormatter::with().precision(Precision::fixedFraction(2).withMinDigits(3)),
             Locale::getEnglish(),
             0.1,
@@ -1127,6 +1209,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
     assertFormatSingle(
             u"FracSig with trailing zeros B",
             u".00/@@@+",
+            u".00/@@@+",
             NumberFormatter::with().precision(Precision::fixedFraction(2).withMinDigits(3)),
             Locale::getEnglish(),
             0.0999999,
@@ -1137,6 +1220,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Rounding None",
             u"precision-unlimited",
+            u".+",
             NumberFormatter::with().precision(Precision::unlimited()),
             Locale::getEnglish(),
             u"87,650",
@@ -1152,6 +1236,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Increment",
             u"precision-increment/0.5",
+            u"precision-increment/0.5",
             NumberFormatter::with().precision(Precision::increment(0.5).withMinFraction(1)),
             Locale::getEnglish(),
             u"87,650.0",
@@ -1167,6 +1252,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Increment with Min Fraction",
             u"precision-increment/0.50",
+            u"precision-increment/0.50",
             NumberFormatter::with().precision(Precision::increment(0.5).withMinFraction(2)),
             Locale::getEnglish(),
             u"87,650.00",
@@ -1182,6 +1268,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Strange Increment",
             u"precision-increment/3.140",
+            u"precision-increment/3.140",
             NumberFormatter::with().precision(Precision::increment(3.14).withMinFraction(3)),
             Locale::getEnglish(),
             u"87,649.960",
@@ -1197,6 +1284,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Increment Resolving to Power of 10",
             u"precision-increment/0.010",
+            u"precision-increment/0.010",
             NumberFormatter::with().precision(Precision::increment(0.01).withMinFraction(3)),
             Locale::getEnglish(),
             u"87,650.000",
@@ -1212,6 +1300,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Currency Standard",
             u"currency/CZK precision-currency-standard",
+            u"currency/CZK precision-currency-standard",
             NumberFormatter::with().precision(Precision::currency(UCurrencyUsage::UCURR_USAGE_STANDARD))
                     .unit(CZK),
             Locale::getEnglish(),
@@ -1228,6 +1317,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Currency Cash",
             u"currency/CZK precision-currency-cash",
+            u"currency/CZK precision-currency-cash",
             NumberFormatter::with().precision(Precision::currency(UCurrencyUsage::UCURR_USAGE_CASH))
                     .unit(CZK),
             Locale::getEnglish(),
@@ -1244,6 +1334,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Currency Cash with Nickel Rounding",
             u"currency/CAD precision-currency-cash",
+            u"currency/CAD precision-currency-cash",
             NumberFormatter::with().precision(Precision::currency(UCurrencyUsage::UCURR_USAGE_CASH))
                     .unit(CAD),
             Locale::getEnglish(),
@@ -1260,6 +1351,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Currency not in top-level fluent chain",
             u"precision-integer", // calling .withCurrency() applies currency rounding rules immediately
+            u".",
             NumberFormatter::with().precision(
                     Precision::currency(UCurrencyUsage::UCURR_USAGE_CASH).withCurrency(CZK)),
             Locale::getEnglish(),
@@ -1277,6 +1369,7 @@ void NumberFormatterApiTest::roundingOther() {
     assertFormatDescending(
             u"Rounding Mode CEILING",
             u"precision-integer rounding-mode-ceiling",
+            u". rounding-mode-ceiling",
             NumberFormatter::with().precision(Precision::integer()).roundingMode(UNUM_ROUND_CEILING),
             Locale::getEnglish(),
             u"87,650",
@@ -1294,6 +1387,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Western Grouping",
             u"group-auto",
+            u"",
             NumberFormatter::with().grouping(UNUM_GROUPING_AUTO),
             Locale::getEnglish(),
             u"87,650,000",
@@ -1309,6 +1403,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Indic Grouping",
             u"group-auto",
+            u"",
             NumberFormatter::with().grouping(UNUM_GROUPING_AUTO),
             Locale("en-IN"),
             u"8,76,50,000",
@@ -1324,6 +1419,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Western Grouping, Min 2",
             u"group-min2",
+            u",?",
             NumberFormatter::with().grouping(UNUM_GROUPING_MIN2),
             Locale::getEnglish(),
             u"87,650,000",
@@ -1339,6 +1435,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Indic Grouping, Min 2",
             u"group-min2",
+            u",?",
             NumberFormatter::with().grouping(UNUM_GROUPING_MIN2),
             Locale("en-IN"),
             u"8,76,50,000",
@@ -1354,6 +1451,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"No Grouping",
             u"group-off",
+            u",_",
             NumberFormatter::with().grouping(UNUM_GROUPING_OFF),
             Locale("en-IN"),
             u"87650000",
@@ -1369,6 +1467,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Indic locale with THOUSANDS grouping",
             u"group-thousands",
+            u"group-thousands",
             NumberFormatter::with().grouping(UNUM_GROUPING_THOUSANDS),
             Locale("en-IN"),
             u"87,650,000",
@@ -1387,6 +1486,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Polish Grouping",
             u"group-auto",
+            u"",
             NumberFormatter::with().grouping(UNUM_GROUPING_AUTO),
             Locale("pl"),
             u"87 650 000",
@@ -1402,6 +1502,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Polish Grouping, Min 2",
             u"group-min2",
+            u",?",
             NumberFormatter::with().grouping(UNUM_GROUPING_MIN2),
             Locale("pl"),
             u"87 650 000",
@@ -1417,6 +1518,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Polish Grouping, Always",
             u"group-on-aligned",
+            u",!",
             NumberFormatter::with().grouping(UNUM_GROUPING_ON_ALIGNED),
             Locale("pl"),
             u"87 650 000",
@@ -1434,6 +1536,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Bulgarian Currency Grouping",
             u"currency/USD group-auto",
+            u"currency/USD",
             NumberFormatter::with().grouping(UNUM_GROUPING_AUTO).unit(USD),
             Locale("bg"),
             u"87650000,00 щ.д.",
@@ -1449,6 +1552,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Bulgarian Currency Grouping, Always",
             u"currency/USD group-on-aligned",
+            u"currency/USD ,!",
             NumberFormatter::with().grouping(UNUM_GROUPING_ON_ALIGNED).unit(USD),
             Locale("bg"),
             u"87 650 000,00 щ.д.",
@@ -1466,6 +1570,7 @@ void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Custom Grouping via Internal API",
             nullptr,
+            nullptr,
             NumberFormatter::with().macros(macros),
             Locale::getEnglish(),
             u"8,7,6,5,0000",
@@ -1483,6 +1588,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatDescending(
             u"Padding",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(Padder::none()),
             Locale::getEnglish(),
             u"87,650",
@@ -1498,6 +1604,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatDescending(
             u"Padding",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                     Padder::codePoints(
                             '*', 8, PadPosition::UNUM_PAD_AFTER_PREFIX)),
@@ -1515,6 +1622,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatDescending(
             u"Padding with code points",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                     Padder::codePoints(
                             0x101E4, 8, PadPosition::UNUM_PAD_AFTER_PREFIX)),
@@ -1532,6 +1640,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatDescending(
             u"Padding with wide digits",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                             Padder::codePoints(
                                     '*', 8, PadPosition::UNUM_PAD_AFTER_PREFIX))
@@ -1550,6 +1659,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatDescending(
             u"Padding with currency spacing",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                             Padder::codePoints(
                                     '*', 10, PadPosition::UNUM_PAD_AFTER_PREFIX))
@@ -1569,6 +1679,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatSingle(
             u"Pad Before Prefix",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                     Padder::codePoints(
                             '*', 8, PadPosition::UNUM_PAD_BEFORE_PREFIX)),
@@ -1579,6 +1690,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatSingle(
             u"Pad After Prefix",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                     Padder::codePoints(
                             '*', 8, PadPosition::UNUM_PAD_AFTER_PREFIX)),
@@ -1589,6 +1701,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatSingle(
             u"Pad Before Suffix",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                     Padder::codePoints(
                             '*', 8, PadPosition::UNUM_PAD_BEFORE_SUFFIX)).unit(NoUnit::percent()),
@@ -1599,6 +1712,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatSingle(
             u"Pad After Suffix",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                     Padder::codePoints(
                             '*', 8, PadPosition::UNUM_PAD_AFTER_SUFFIX)).unit(NoUnit::percent()),
@@ -1609,6 +1723,7 @@ void NumberFormatterApiTest::padding() {
     assertFormatSingle(
             u"Currency Spacing with Zero Digit Padding Broken",
             nullptr,
+            nullptr,
             NumberFormatter::with().padding(
                             Padder::codePoints(
                                     '0', 12, PadPosition::UNUM_PAD_AFTER_PREFIX))
@@ -1623,6 +1738,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatDescending(
             u"Integer Width Default",
             u"integer-width/+0",
+            u"0",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(1)),
             Locale::getEnglish(),
             u"87,650",
@@ -1638,6 +1754,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatDescending(
             u"Integer Width Zero Fill 0",
             u"integer-width/+",
+            u"integer-width/+",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(0)),
             Locale::getEnglish(),
             u"87,650",
@@ -1653,6 +1770,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatDescending(
             u"Integer Width Zero Fill 3",
             u"integer-width/+000",
+            u"000",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(3)),
             Locale::getEnglish(),
             u"87,650",
@@ -1668,6 +1786,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatDescending(
             u"Integer Width Max 3",
             u"integer-width/##0",
+            u"integer-width/##0",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(1).truncateAt(3)),
             Locale::getEnglish(),
             u"650",
@@ -1683,6 +1802,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatDescending(
             u"Integer Width Fixed 2",
             u"integer-width/00",
+            u"integer-width/00",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(2).truncateAt(2)),
             Locale::getEnglish(),
             u"50",
@@ -1698,6 +1818,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatDescending(
             u"Integer Width Compact",
             u"compact-short integer-width/000",
+            u"compact-short integer-width/000",
             NumberFormatter::with()
                 .notation(Notation::compactShort())
                 .integerWidth(IntegerWidth::zeroFillTo(3).truncateAt(3)),
@@ -1715,6 +1836,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatDescending(
             u"Integer Width Scientific",
             u"scientific integer-width/000",
+            u"scientific integer-width/000",
             NumberFormatter::with()
                 .notation(Notation::scientific())
                 .integerWidth(IntegerWidth::zeroFillTo(3).truncateAt(3)),
@@ -1732,6 +1854,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatDescending(
             u"Integer Width Engineering",
             u"engineering integer-width/000",
+            u"engineering integer-width/000",
             NumberFormatter::with()
                 .notation(Notation::engineering())
                 .integerWidth(IntegerWidth::zeroFillTo(3).truncateAt(3)),
@@ -1749,6 +1872,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatSingle(
             u"Integer Width Remove All A",
             u"integer-width/00",
+            u"integer-width/00",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(2).truncateAt(2)),
             "en",
             2500,
@@ -1757,6 +1881,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatSingle(
             u"Integer Width Remove All B",
             u"integer-width/00",
+            u"integer-width/00",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(2).truncateAt(2)),
             "en",
             25000,
@@ -1765,6 +1890,7 @@ void NumberFormatterApiTest::integerWidth() {
     assertFormatSingle(
             u"Integer Width Remove All B, Bytes Mode",
             u"integer-width/00",
+            u"integer-width/00",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(2).truncateAt(2)),
             "en",
             // Note: this double produces all 17 significant digits
@@ -1776,6 +1902,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatDescending(
             u"French Symbols with Japanese Data 1",
             nullptr,
+            nullptr,
             NumberFormatter::with().symbols(FRENCH_SYMBOLS),
             Locale::getJapan(),
             u"87\u202F650",
@@ -1791,6 +1918,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"French Symbols with Japanese Data 2",
             nullptr,
+            nullptr,
             NumberFormatter::with().notation(Notation::compactShort()).symbols(FRENCH_SYMBOLS),
             Locale::getJapan(),
             12345,
@@ -1799,6 +1927,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatDescending(
             u"Latin Numbering System with Arabic Data",
             u"currency/USD latin",
+            u"currency/USD latin",
             NumberFormatter::with().adoptSymbols(new NumberingSystem(LATN)).unit(USD),
             Locale("ar"),
             u"US$ 87,650.00",
@@ -1814,6 +1943,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatDescending(
             u"Math Numbering System with French Data",
             u"numbering-system/mathsanb",
+            u"numbering-system/mathsanb",
             NumberFormatter::with().adoptSymbols(new NumberingSystem(MATHSANB)),
             Locale::getFrench(),
             u"𝟴𝟳\u202F𝟲𝟱𝟬",
@@ -1829,6 +1959,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"Swiss Symbols (used in documentation)",
             nullptr,
+            nullptr,
             NumberFormatter::with().symbols(SWISS_SYMBOLS),
             Locale::getEnglish(),
             12345.67,
@@ -1837,6 +1968,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"Myanmar Symbols (used in documentation)",
             nullptr,
+            nullptr,
             NumberFormatter::with().symbols(MYANMAR_SYMBOLS),
             Locale::getEnglish(),
             12345.67,
@@ -1847,6 +1979,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"Currency symbol should precede number in ar with NS latn",
             u"currency/USD latin",
+            u"currency/USD latin",
             NumberFormatter::with().adoptSymbols(new NumberingSystem(LATN)).unit(USD),
             Locale("ar"),
             12345.67,
@@ -1855,6 +1988,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"Currency symbol should precede number in ar@numbers=latn",
             u"currency/USD",
+            u"currency/USD",
             NumberFormatter::with().unit(USD),
             Locale("ar@numbers=latn"),
             12345.67,
@@ -1863,6 +1997,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"Currency symbol should follow number in ar-EG with NS arab",
             u"currency/USD",
+            u"currency/USD",
             NumberFormatter::with().unit(USD),
             Locale("ar-EG"),
             12345.67,
@@ -1871,6 +2006,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"Currency symbol should follow number in ar@numbers=arab",
             u"currency/USD",
+            u"currency/USD",
             NumberFormatter::with().unit(USD),
             Locale("ar@numbers=arab"),
             12345.67,
@@ -1879,6 +2015,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"NumberingSystem in API should win over @numbers keyword",
             u"currency/USD latin",
+            u"currency/USD latin",
             NumberFormatter::with().adoptSymbols(new NumberingSystem(LATN)).unit(USD),
             Locale("ar@numbers=arab"),
             12345.67,
@@ -1897,11 +2034,18 @@ void NumberFormatterApiTest::symbols() {
     UnlocalizedNumberFormatter f = NumberFormatter::with().symbols(symbols);
     symbols.setSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kGroupingSeparatorSymbol, u"!", status);
     assertFormatSingle(
-            u"Symbols object should be copied", nullptr, f, Locale::getEnglish(), 12345.67, u"12’345.67");
+            u"Symbols object should be copied",
+            nullptr,
+            nullptr,
+            f,
+            Locale::getEnglish(),
+            12345.67,
+            u"12’345.67");
 
     assertFormatSingle(
             u"The last symbols setter wins",
             u"latin",
+            u"latin",
             NumberFormatter::with().symbols(symbols).adoptSymbols(new NumberingSystem(LATN)),
             Locale::getEnglish(),
             12345.67,
@@ -1910,6 +2054,7 @@ void NumberFormatterApiTest::symbols() {
     assertFormatSingle(
             u"The last symbols setter wins",
             nullptr,
+            nullptr,
             NumberFormatter::with().adoptSymbols(new NumberingSystem(LATN)).symbols(symbols),
             Locale::getEnglish(),
             12345.67,
@@ -1933,6 +2078,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Auto Positive",
             u"sign-auto",
+            u"",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_AUTO),
             Locale::getEnglish(),
             444444,
@@ -1941,6 +2087,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Auto Negative",
             u"sign-auto",
+            u"",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_AUTO),
             Locale::getEnglish(),
             -444444,
@@ -1949,6 +2096,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Auto Zero",
             u"sign-auto",
+            u"",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_AUTO),
             Locale::getEnglish(),
             0,
@@ -1957,6 +2105,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Always Positive",
             u"sign-always",
+            u"+!",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ALWAYS),
             Locale::getEnglish(),
             444444,
@@ -1965,6 +2114,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Always Negative",
             u"sign-always",
+            u"+!",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ALWAYS),
             Locale::getEnglish(),
             -444444,
@@ -1973,6 +2123,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Always Zero",
             u"sign-always",
+            u"+!",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ALWAYS),
             Locale::getEnglish(),
             0,
@@ -1981,6 +2132,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Never Positive",
             u"sign-never",
+            u"+_",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_NEVER),
             Locale::getEnglish(),
             444444,
@@ -1989,6 +2141,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Never Negative",
             u"sign-never",
+            u"+_",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_NEVER),
             Locale::getEnglish(),
             -444444,
@@ -1997,6 +2150,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Never Zero",
             u"sign-never",
+            u"+_",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_NEVER),
             Locale::getEnglish(),
             0,
@@ -2005,6 +2159,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting Positive",
             u"currency/USD sign-accounting",
+            u"currency/USD ()",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING).unit(USD),
             Locale::getEnglish(),
             444444,
@@ -2013,6 +2168,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting Negative",
             u"currency/USD sign-accounting",
+            u"currency/USD ()",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING).unit(USD),
             Locale::getEnglish(),
             -444444,
@@ -2021,6 +2177,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting Zero",
             u"currency/USD sign-accounting",
+            u"currency/USD ()",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING).unit(USD),
             Locale::getEnglish(),
             0,
@@ -2029,6 +2186,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting-Always Positive",
             u"currency/USD sign-accounting-always",
+            u"currency/USD ()!",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_ALWAYS).unit(USD),
             Locale::getEnglish(),
             444444,
@@ -2037,6 +2195,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting-Always Negative",
             u"currency/USD sign-accounting-always",
+            u"currency/USD ()!",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_ALWAYS).unit(USD),
             Locale::getEnglish(),
             -444444,
@@ -2045,6 +2204,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting-Always Zero",
             u"currency/USD sign-accounting-always",
+            u"currency/USD ()!",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_ALWAYS).unit(USD),
             Locale::getEnglish(),
             0,
@@ -2053,6 +2213,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Except-Zero Positive",
             u"sign-except-zero",
+            u"+?",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_EXCEPT_ZERO),
             Locale::getEnglish(),
             444444,
@@ -2061,6 +2222,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Except-Zero Negative",
             u"sign-except-zero",
+            u"+?",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_EXCEPT_ZERO),
             Locale::getEnglish(),
             -444444,
@@ -2069,6 +2231,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Except-Zero Zero",
             u"sign-except-zero",
+            u"+?",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_EXCEPT_ZERO),
             Locale::getEnglish(),
             0,
@@ -2077,6 +2240,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting-Except-Zero Positive",
             u"currency/USD sign-accounting-except-zero",
+            u"currency/USD ()?",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_EXCEPT_ZERO).unit(USD),
             Locale::getEnglish(),
             444444,
@@ -2085,6 +2249,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting-Except-Zero Negative",
             u"currency/USD sign-accounting-except-zero",
+            u"currency/USD ()?",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_EXCEPT_ZERO).unit(USD),
             Locale::getEnglish(),
             -444444,
@@ -2093,6 +2258,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting-Except-Zero Zero",
             u"currency/USD sign-accounting-except-zero",
+            u"currency/USD ()?",
             NumberFormatter::with().sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_EXCEPT_ZERO).unit(USD),
             Locale::getEnglish(),
             0,
@@ -2101,6 +2267,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting Negative Hidden",
             u"currency/USD unit-width-hidden sign-accounting",
+            u"currency/USD unit-width-hidden ()",
             NumberFormatter::with()
                     .sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING)
                     .unit(USD)
@@ -2112,6 +2279,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting Negative Narrow",
             u"currency/USD unit-width-narrow sign-accounting",
+            u"currency/USD unit-width-narrow ()",
             NumberFormatter::with()
                 .sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING)
                 .unit(USD)
@@ -2123,6 +2291,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting Negative Short",
             u"currency/USD sign-accounting",
+            u"currency/USD ()",
             NumberFormatter::with()
                 .sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING)
                 .unit(USD)
@@ -2134,6 +2303,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting Negative Iso Code",
             u"currency/USD unit-width-iso-code sign-accounting",
+            u"currency/USD unit-width-iso-code ()",
             NumberFormatter::with()
                 .sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING)
                 .unit(USD)
@@ -2147,6 +2317,7 @@ void NumberFormatterApiTest::sign() {
     assertFormatSingle(
             u"Sign Accounting Negative Full Name",
             u"currency/USD unit-width-full-name sign-accounting",
+            u"currency/USD unit-width-full-name ()",
             NumberFormatter::with()
                 .sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING)
                 .unit(USD)
@@ -2236,6 +2407,7 @@ void NumberFormatterApiTest::decimal() {
     assertFormatDescending(
             u"Decimal Default",
             u"decimal-auto",
+            u"",
             NumberFormatter::with().decimal(UNumberDecimalSeparatorDisplay::UNUM_DECIMAL_SEPARATOR_AUTO),
             Locale::getEnglish(),
             u"87,650",
@@ -2251,6 +2423,7 @@ void NumberFormatterApiTest::decimal() {
     assertFormatDescending(
             u"Decimal Always Shown",
             u"decimal-always",
+            u"decimal-always",
             NumberFormatter::with().decimal(UNumberDecimalSeparatorDisplay::UNUM_DECIMAL_SEPARATOR_ALWAYS),
             Locale::getEnglish(),
             u"87,650.",
@@ -2268,6 +2441,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatDescending(
             u"Multiplier None",
             u"scale/1",
+            u"",
             NumberFormatter::with().scale(Scale::none()),
             Locale::getEnglish(),
             u"87,650",
@@ -2283,6 +2457,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatDescending(
             u"Multiplier Power of Ten",
             u"scale/1000000",
+            u"scale/1E6",
             NumberFormatter::with().scale(Scale::powerOfTen(6)),
             Locale::getEnglish(),
             u"87,650,000,000",
@@ -2298,6 +2473,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatDescending(
             u"Multiplier Arbitrary Double",
             u"scale/5.2",
+            u"scale/5.2",
             NumberFormatter::with().scale(Scale::byDouble(5.2)),
             Locale::getEnglish(),
             u"455,780",
@@ -2313,6 +2489,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatDescending(
             u"Multiplier Arbitrary BigDecimal",
             u"scale/5.2",
+            u"scale/5.2",
             NumberFormatter::with().scale(Scale::byDecimal({"5.2", -1})),
             Locale::getEnglish(),
             u"455,780",
@@ -2328,6 +2505,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatDescending(
             u"Multiplier Arbitrary Double And Power Of Ten",
             u"scale/5200",
+            u"scale/5200",
             NumberFormatter::with().scale(Scale::byDoubleAndPowerOfTen(5.2, 3)),
             Locale::getEnglish(),
             u"455,780,000",
@@ -2343,6 +2521,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatDescending(
             u"Multiplier Zero",
             u"scale/0",
+            u"scale/0",
             NumberFormatter::with().scale(Scale::byDouble(0)),
             Locale::getEnglish(),
             u"0",
@@ -2358,6 +2537,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatSingle(
             u"Multiplier Skeleton Scientific Notation and Percent",
             u"percent scale/1E2",
+            u"%x100",
             NumberFormatter::with().unit(NoUnit::percent()).scale(Scale::powerOfTen(2)),
             Locale::getEnglish(),
             0.5,
@@ -2366,6 +2546,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatSingle(
             u"Negative Multiplier",
             u"scale/-5.2",
+            u"scale/-5.2",
             NumberFormatter::with().scale(Scale::byDouble(-5.2)),
             Locale::getEnglish(),
             2,
@@ -2374,6 +2555,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatSingle(
             u"Negative One Multiplier",
             u"scale/-1",
+            u"scale/-1",
             NumberFormatter::with().scale(Scale::byDouble(-1)),
             Locale::getEnglish(),
             444444,
@@ -2382,6 +2564,7 @@ void NumberFormatterApiTest::scale() {
     assertFormatSingle(
             u"Two-Type Multiplier with Overlap",
             u"scale/10000",
+            u"scale/1E4",
             NumberFormatter::with().scale(Scale::byDoubleAndPowerOfTen(100, 2)),
             Locale::getEnglish(),
             2,
@@ -2402,29 +2585,30 @@ void NumberFormatterApiTest::skeletonUserGuideExamples() {
     // Test the skeleton examples in userguide/format_parse/numbers/skeletons.md
     struct TestCase {
         const char16_t* skeleton;
+        const char16_t* conciseSkeleton;
         double input;
         const char16_t* expected;
     } cases[] = {
-        {u"percent", 25, u"25%"},
-        {u".00", 25, u"25.00"},
-        {u"percent .00", 25, u"25.00%"},
-        {u"scale/100", 0.3, u"30"},
-        {u"percent scale/100", 0.3, u"30%"},
-        {u"measure-unit/length-meter", 5, u"5 m"},
-        {u"measure-unit/length-meter unit-width-full-name", 5, u"5 meters"},
-        {u"currency/CAD", 10, u"CA$10.00"},
-        {u"currency/CAD unit-width-narrow", 10, u"$10.00"},
-        {u"compact-short", 5000, u"5K"},
-        {u"compact-long", 5000, u"5 thousand"},
-        {u"compact-short currency/CAD", 5000, u"CA$5K"},
-        {u"", 5000, u"5,000"},
-        {u"group-min2", 5000, u"5000"},
-        {u"group-min2", 15000, u"15,000"},
-        {u"sign-always", 60, u"+60"},
-        {u"sign-always", 0, u"+0"},
-        {u"sign-except-zero", 60, u"+60"},
-        {u"sign-except-zero", 0, u"0"},
-        {u"sign-accounting currency/CAD", -40, u"(CA$40.00)"}
+        {u"percent", u"%", 25, u"25%"},
+        {u".00", u".00", 25, u"25.00"},
+        {u"percent .00", u"% .00", 25, u"25.00%"},
+        {u"scale/100", u"scale/100", 0.3, u"30"},
+        {u"percent scale/100", u"%x100", 0.3, u"30%"},
+        {u"measure-unit/length-meter", u"unit/meter", 5, u"5 m"},
+        {u"measure-unit/length-meter unit-width-full-name", u"unit/meter unit-width-full-name", 5, u"5 meters"},
+        {u"currency/CAD", u"currency/CAD", 10, u"CA$10.00"},
+        {u"currency/CAD unit-width-narrow", u"currency/CAD unit-width-narrow", 10, u"$10.00"},
+        {u"compact-short", u"K", 5000, u"5K"},
+        {u"compact-long", u"KK", 5000, u"5 thousand"},
+        {u"compact-short currency/CAD", u"K currency/CAD", 5000, u"CA$5K"},
+        {u"", u"", 5000, u"5,000"},
+        {u"group-min2", u",?", 5000, u"5000"},
+        {u"group-min2", u",?", 15000, u"15,000"},
+        {u"sign-always", u"+!", 60, u"+60"},
+        {u"sign-always", u"+!", 0, u"+0"},
+        {u"sign-except-zero", u"+?", 60, u"+60"},
+        {u"sign-except-zero", u"+?", 0, u"0"},
+        {u"sign-accounting currency/CAD", u"() currency/CAD", -40, u"(CA$40.00)"}
     };
 
     for (const auto& cas : cases) {
@@ -2434,6 +2618,11 @@ void NumberFormatterApiTest::skeletonUserGuideExamples() {
             .formatDouble(cas.input, status);
         assertEquals(cas.skeleton, cas.expected, actual.toTempString(status));
         status.errIfFailureAndReset();
+        FormattedNumber actualConcise = NumberFormatter::forSkeleton(cas.conciseSkeleton, status)
+            .locale("en-US")
+            .formatDouble(cas.input, status);
+        assertEquals(cas.conciseSkeleton, cas.expected, actualConcise.toTempString(status));
+        status.errIfFailureAndReset();
     }
 }
 
@@ -2467,6 +2656,7 @@ void NumberFormatterApiTest::fieldPositionLogic() {
     FormattedNumber fmtd = assertFormatSingle(
             message,
             u"",
+            u"",
             NumberFormatter::with(),
             Locale::getEnglish(),
             -9876543210.12,
@@ -2522,6 +2712,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"measure-unit/temperature-fahrenheit",
+                u"unit/fahrenheit",
                 NumberFormatter::with().unit(FAHRENHEIT),
                 Locale::getEnglish(),
                 68,
@@ -2542,6 +2733,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"measure-unit/temperature-fahrenheit per-measure-unit/duration-day",
+                u"unit/fahrenheit-per-day",
                 NumberFormatter::with().unit(FAHRENHEIT).perUnit(DAY),
                 Locale::getEnglish(),
                 68,
@@ -2563,6 +2755,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"measure-unit/length-meter unit-width-full-name",
+                u"unit/meter unit-width-full-name",
                 NumberFormatter::with().unit(METER).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
                 Locale::getEnglish(),
                 68,
@@ -2584,6 +2777,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
+                u"~unit/meter-per-second unit-width-full-name", // does not round-trip to the full skeleton above
                 NumberFormatter::with().unit(METER).perUnit(SECOND).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
                 "ky", // locale with the interesting data
                 68,
@@ -2605,6 +2799,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"measure-unit/temperature-fahrenheit unit-width-full-name",
+                u"unit/fahrenheit unit-width-full-name",
                 NumberFormatter::with().unit(FAHRENHEIT).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
                 "vi", // locale with the interesting data
                 68,
@@ -2629,6 +2824,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"measure-unit/temperature-kelvin",
+                u"unit/kelvin",
                 NumberFormatter::with().unit(KELVIN),
                 "fa", // locale with the interesting data
                 68,
@@ -2649,6 +2845,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"compact-short",
+                u"K",
                 NumberFormatter::with().notation(Notation::compactShort()),
                 Locale::getUS(),
                 65000,
@@ -2669,6 +2866,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"compact-long",
+                u"KK",
                 NumberFormatter::with().notation(Notation::compactLong()),
                 Locale::getUS(),
                 65000,
@@ -2689,6 +2887,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"compact-long",
+                u"KK",
                 NumberFormatter::with().notation(Notation::compactLong()),
                 "fil",  // locale with interesting data
                 6000,
@@ -2709,6 +2908,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"compact-long",
+                u"KK",
                 NumberFormatter::with().notation(Notation::compactLong()),
                 "he",  // locale with interesting data
                 6000,
@@ -2729,6 +2929,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"compact-short currency/USD",
+                u"K currency/USD",
                 NumberFormatter::with().notation(Notation::compactShort()).unit(USD),
                 "sr_Latn",  // locale with interesting data
                 65000,
@@ -2750,6 +2951,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"currency/USD unit-width-full-name",
+                u"currency/USD unit-width-full-name",
                 NumberFormatter::with().unit(USD)
                     .unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME),
                 "en",
@@ -2774,6 +2976,7 @@ void NumberFormatterApiTest::fieldPositionCoverage() {
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"compact-long measure-unit/length-meter unit-width-full-name",
+                u"KK unit/meter unit-width-full-name",
                 NumberFormatter::with().notation(Notation::compactLong())
                     .unit(METER)
                     .unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
@@ -3143,9 +3346,13 @@ void NumberFormatterApiTest::toDecimalNumber() {
 }
 
 
-void NumberFormatterApiTest::assertFormatDescending(const char16_t* umessage, const char16_t* uskeleton,
-                                                    const UnlocalizedNumberFormatter& f, Locale locale,
-                                                    ...) {
+void NumberFormatterApiTest::assertFormatDescending(
+        const char16_t* umessage,
+        const char16_t* uskeleton,
+        const char16_t* conciseSkeleton,
+        const UnlocalizedNumberFormatter& f,
+        Locale locale,
+        ...) {
     va_list args;
     va_start(args, locale);
     UnicodeString message(TRUE, umessage, -1);
@@ -3179,14 +3386,34 @@ void NumberFormatterApiTest::assertFormatDescending(const char16_t* umessage, co
             UnicodeString actual3 = l3.formatDouble(d, status).toString(status);
             assertEquals(message + ": Skeleton Path: '" + normalized + "': " + d, expecteds[i], actual3);
         }
+        // Concise skeletons should have same output, and usually round-trip to the normalized skeleton.
+        // If the concise skeleton starts with '~', disable the round-trip check.
+        bool shouldRoundTrip = true;
+        if (conciseSkeleton[0] == u'~') {
+            conciseSkeleton++;
+            shouldRoundTrip = false;
+        }
+        LocalizedNumberFormatter l4 = NumberFormatter::forSkeleton(conciseSkeleton, status).locale(locale);
+        if (shouldRoundTrip) {
+            assertEquals(message + ": Concise Skeleton:", normalized, l4.toSkeleton(status));
+        }
+        for (int32_t i = 0; i < 9; i++) {
+            double d = inputs[i];
+            UnicodeString actual4 = l4.formatDouble(d, status).toString(status);
+            assertEquals(message + ": Concise Skeleton Path: '" + normalized + "': " + d, expecteds[i], actual4);
+        }
     } else {
         assertUndefinedSkeleton(f);
     }
 }
 
-void NumberFormatterApiTest::assertFormatDescendingBig(const char16_t* umessage, const char16_t* uskeleton,
-                                                       const UnlocalizedNumberFormatter& f, Locale locale,
-                                                       ...) {
+void NumberFormatterApiTest::assertFormatDescendingBig(
+        const char16_t* umessage,
+        const char16_t* uskeleton,
+        const char16_t* conciseSkeleton,
+        const UnlocalizedNumberFormatter& f,
+        Locale locale,
+        ...) {
     va_list args;
     va_start(args, locale);
     UnicodeString message(TRUE, umessage, -1);
@@ -3220,15 +3447,36 @@ void NumberFormatterApiTest::assertFormatDescendingBig(const char16_t* umessage,
             UnicodeString actual3 = l3.formatDouble(d, status).toString(status);
             assertEquals(message + ": Skeleton Path: '" + normalized + "': " + d, expecteds[i], actual3);
         }
+        // Concise skeletons should have same output, and usually round-trip to the normalized skeleton.
+        // If the concise skeleton starts with '~', disable the round-trip check.
+        bool shouldRoundTrip = true;
+        if (conciseSkeleton[0] == u'~') {
+            conciseSkeleton++;
+            shouldRoundTrip = false;
+        }
+        LocalizedNumberFormatter l4 = NumberFormatter::forSkeleton(conciseSkeleton, status).locale(locale);
+        if (shouldRoundTrip) {
+            assertEquals(message + ": Concise Skeleton:", normalized, l4.toSkeleton(status));
+        }
+        for (int32_t i = 0; i < 9; i++) {
+            double d = inputs[i];
+            UnicodeString actual4 = l4.formatDouble(d, status).toString(status);
+            assertEquals(message + ": Concise Skeleton Path: '" + normalized + "': " + d, expecteds[i], actual4);
+        }
     } else {
         assertUndefinedSkeleton(f);
     }
 }
 
 FormattedNumber
-NumberFormatterApiTest::assertFormatSingle(const char16_t* umessage, const char16_t* uskeleton,
-                                           const UnlocalizedNumberFormatter& f, Locale locale,
-                                           double input, const UnicodeString& expected) {
+NumberFormatterApiTest::assertFormatSingle(
+        const char16_t* umessage,
+        const char16_t* uskeleton,
+        const char16_t* conciseSkeleton,
+        const UnlocalizedNumberFormatter& f,
+        Locale locale,
+        double input,
+        const UnicodeString& expected) {
     UnicodeString message(TRUE, umessage, -1);
     const LocalizedNumberFormatter l1 = f.threshold(0).locale(locale); // no self-regulation
     const LocalizedNumberFormatter l2 = f.threshold(1).locale(locale); // all self-regulation
@@ -3250,6 +3498,19 @@ NumberFormatterApiTest::assertFormatSingle(const char16_t* umessage, const char1
         LocalizedNumberFormatter l3 = NumberFormatter::forSkeleton(normalized, status).locale(locale);
         UnicodeString actual3 = l3.formatDouble(input, status).toString(status);
         assertEquals(message + ": Skeleton Path: '" + normalized + "': " + input, expected, actual3);
+        // Concise skeletons should have same output, and usually round-trip to the normalized skeleton.
+        // If the concise skeleton starts with '~', disable the round-trip check.
+        bool shouldRoundTrip = true;
+        if (conciseSkeleton[0] == u'~') {
+            conciseSkeleton++;
+            shouldRoundTrip = false;
+        }
+        LocalizedNumberFormatter l4 = NumberFormatter::forSkeleton(conciseSkeleton, status).locale(locale);
+        if (shouldRoundTrip) {
+            assertEquals(message + ": Concise Skeleton:", normalized, l4.toSkeleton(status));
+        }
+        UnicodeString actual4 = l4.formatDouble(input, status).toString(status);
+        assertEquals(message + ": Concise Skeleton Path: '" + normalized + "': " + input, expected, actual4);
     } else {
         assertUndefinedSkeleton(f);
     }
@@ -3266,8 +3527,10 @@ void NumberFormatterApiTest::assertUndefinedSkeleton(const UnlocalizedNumberForm
 }
 
 void NumberFormatterApiTest::assertNumberFieldPositions(
-        const char16_t* message, const FormattedNumber& formattedNumber,
-        const UFieldPosition* expectedFieldPositions, int32_t length) {
+        const char16_t* message,
+        const FormattedNumber& formattedNumber,
+        const UFieldPosition* expectedFieldPositions,
+        int32_t length) {
     IcuTestErrorCode status(*this, "assertNumberFieldPositions");
 
     // Check FormattedValue functions
index 635ea87990600a799316223eea524171bd5f3cbf..e8ab127fcfc678182525f9414fb442d49872e2ea 100644 (file)
@@ -70,6 +70,7 @@ void NumberSkeletonTest::validTokens() {
             u"measure-unit/length-meter",
             u"measure-unit/area-square-meter",
             u"measure-unit/energy-joule per-measure-unit/length-meter",
+            u"unit/square-meter-per-square-meter",
             u"currency/XXX",
             u"currency/ZZZ",
             u"currency/usd",
@@ -105,7 +106,20 @@ void NumberSkeletonTest::validTokens() {
             u"numbering-system/latn",
             u"precision-integer/@##",
             u"precision-integer rounding-mode-ceiling",
-            u"precision-currency-cash rounding-mode-ceiling"};
+            u"precision-currency-cash rounding-mode-ceiling",
+            u"0",
+            u"00",
+            u"000",
+            u"E0",
+            u"E00",
+            u"E000",
+            u"EE0",
+            u"EE00",
+            u"EE+?0",
+            u"EE+?00",
+            u"EE+!0",
+            u"EE+!00",
+    };
 
     for (auto& cas : cases) {
         UnicodeString skeletonString(cas);
@@ -151,7 +165,20 @@ void NumberSkeletonTest::invalidTokens() {
             u"integer-width/+0#",
             u"integer-width/+#",
             u"integer-width/+#0",
-            u"scientific/foo"};
+            u"scientific/foo",
+            u"E",
+            u"E1",
+            u"E+",
+            u"E+?",
+            u"E+!",
+            u"E+0",
+            u"EE",
+            u"EE+",
+            u"EEE",
+            u"EEE0",
+            u"001",
+            u"00+",
+    };
 
     expectedErrorSkeleton(cases, UPRV_LENGTHOF(cases));
 }