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

See #1060
diff --git a/docs/userguide/format_parse/numbers/skeletons.md b/docs/userguide/format_parse/numbers/skeletons.md
index 70a9d39..1c5e9c5 100644
--- a/docs/userguide/format_parse/numbers/skeletons.md
+++ b/docs/userguide/format_parse/numbers/skeletons.md
@@ -95,14 +95,15 @@
 following optional options:
 
 - `/sign-xxx` sets the sign display option for the exponent; see [Sign](#sign).
-- `/+ee` sets exponent digits to "at least 2"; use `/+eee` for at least 3 digits, etc.
+- `/*ee` sets exponent digits to "at least 2"; use `/*eee` for at least 3 digits, etc.
+    - ***Prior to ICU 67***, use `/+ee` instead of `/*ee`.
 
 For example, all of the following skeletons are valid:
 
 - `scientific`
 - `scientific/sign-always`
-- `scientific/+ee`
-- `scientific/+ee/sign-always`
+- `scientific/*ee`
+- `scientific/*ee/sign-always`
 
 #### Scientific and Engineering Notation: Concise Form
 
@@ -111,14 +112,14 @@
 | Concise Skeleton | Equivalent Long-Form Skeleton |
 |---|---|
 | `E0` | `scientific` |
-| `E00` | `scientific/+ee` |
-| `EE+0` | `engineering/sign-always` |
+| `E00` | `scientific/*ee` |
+| `EE+!0` | `engineering/sign-always` |
 | `E+?00` | `scientific/sign-except-zero/+ee` |
 
 More precisely:
 
 1. Start with `E` for scientific or `EE` for engineering.
-2. Allow either `+` or `+?` as a concise sign display option.
+2. Allow either `+!` or `+?` as a concise sign display option.
 3. Expect one or more `0`s.  If more than one, set minimum integer digits.
 
 ### Unit
@@ -192,13 +193,13 @@
 | Stem | Explanation | Equivalent C++ Code |
 |---|---|---|
 | `.00` | Exactly 2 fraction digits | `Precision::fixedFraction(2) ` |
-| `.00+` | At least 2 fraction digits | `Precision::minFraction(2)` |
+| `.00*` | At least 2 fraction digits | `Precision::minFraction(2)` |
 | `.##` | At most 2 fraction digits | `Precision::maxFraction(2) ` |
 | `.0#` | Between 1 and 2 fraction digits | `Precision::minMaxFraction(1, 2)` |
 
 More precisely, the fraction precision stem starts with `.`, then contains
 zero or more `0` symbols, which implies the minimum fraction digits.  Then it
-contains either a `+`, for unlimited maximum fraction digits, or zero or more
+contains either a `*`, for unlimited maximum fraction digits, or zero or more
 `#` symbols, which implies the minimum fraction digits when added to the `0`
 symbols.
 
@@ -211,11 +212,11 @@
 
 | Skeleton | Explanation | Equivalent C++ Code |
 |---|---|---|
-| `.##/@@@+` | At most 2 fraction digits, but guarantee <br/> at least 3 significant digits | `Precision::maxFraction(2)` <br/> `.withMinDigits(3)` |
+| `.##/@@@*` | At most 2 fraction digits, but guarantee <br/> at least 3 significant digits | `Precision::maxFraction(2)` <br/> `.withMinDigits(3)` |
 | `.00/@##` | Exactly 2 fraction digits, but do not <br/> display more than 3 significant digits | `Precision::fixedFraction(2)` <br/> `.withMaxDigits(3)` |
 
 Precisely, the option starts with one or more `@` symbols.  Then it contains
-either a `+`, for `::withMinDigits`, or one or more `#` symbols, for
+either a `*`, for `::withMinDigits`, or one or more `#` symbols, for
 `::withMaxDigits`.  If a `#` symbol is present, there must be only one `@`
 symbol.
 
@@ -226,16 +227,22 @@
 | Stem | Explanation | Equivalent C++ Code|
 |---|---|---|
 | `@@@` | Exactly 3 significant digits | `Precision::fixedSignificantDigits(3)` |
-| `@@@+` | At least 3 significant digits | `Precision::minSignificantDigits(3)` |
+| `@@@*` | At least 3 significant digits | `Precision::minSignificantDigits(3)` |
 | `@##` | At most 3 significant digits | `Precision::maxSignificantDigits(3)` |
 | `@@#` | Between 2 and 3 significant digits | `...::minMaxSignificantDigits(2, 3)` |
 
 The precise syntax is very similar to fraction precision.  The blueprint stem
 starts with one or more `@` symbols, which implies the minimum significant
-digits.  Then it contains either a `+`, for unlimited maximum significant
+digits.  Then it contains either a `*`, for unlimited maximum significant
 digits, or zero or more `#` symbols, which implies the minimum significant
 digits when added to the `@` symbols.
 
+#### Wildcard Character
+
+***Prior to ICU 67***, the symbol `+` was used for unlimited precision, instead
+of `*` (for example, `.00+`). For backwards compatibility, either `+` or `*` is
+accepted. This applies for both fraction digits and significant digits.
+
 ### Rounding Mode
 
 The rounding mode can be specified by the following stems:
@@ -259,21 +266,23 @@
 
 | Long Form | Concise Form | Explanation | Equivalent C++ Code |
 |---|---|---|---|
-| `integer-width/+000` | `000` | At least 3 <br/> integer digits | `IntegerWidth::zeroFillTo(3)` |
+| `integer-width/*000` | `000` | At least 3 <br/> integer digits | `IntegerWidth::zeroFillTo(3)` |
 | `integer-width/##0` | - | Between 1 and 3 <br/> integer digits | `IntegerWidth::zeroFillTo(1)` <br/> `.truncateAt(3)`
 | `integer-width/00` | - | Exactly 2 <br/> integer digits | `IntegerWidth::zeroFillTo(2)` <br/> `.truncateAt(2)` |
-| `integer-width/+` | - | Zero or more <br/> integer digits | `IntegerWidth::zeroFillTo(0) `
+| `integer-width/*` | - | Zero or more <br/> integer digits | `IntegerWidth::zeroFillTo(0) `
 
-The long-form option starts with either a single `+` symbol, signaling no limit
+The long-form option starts with either a single `*` symbol, signaling no limit
 on the number of integer digits (no *truncateAt*), or zero or more `#` symbols.
 It should then be followed by zero or more `0` symbols, indicating the minimum
-integer digits (the argument to *zeroFillTo*).  If there is no `+` symbol, the
+integer digits (the argument to *zeroFillTo*).  If there is no `*` symbol, the
 maximum integer digits (the argument to *truncateAt*) is the number of `#`
 symbols plus the number of `0` symbols.
 
 The concise skeleton is simply one or more `0` characters. This supports
 minimum integer digits but not maximum integer digits.
 
+***Prior to ICU 67***, use the symbol `+` instead of `*`.
+
 ### Scale
 
 To specify the scale, use the following stem and option:
@@ -303,7 +312,7 @@
 - `group-min2` or `,?` (concise)
 - `group-auto` (or omit since this is the default)
 - `group-on-aligned` or `,!` (concise)
-- `group-thousands` or `,=` (concise)
+- `group-thousands` (no concise equivalent)
 
 For more details, see
 [UNumberGroupingStrategy](http://icu-project.org/apiref/icu4c/unumberformatter_8h.html).
diff --git a/icu4c/source/i18n/number_skeletons.cpp b/icu4c/source/i18n/number_skeletons.cpp
index 5b0fe2b..4ba2647 100644
--- a/icu4c/source/i18n/number_skeletons.cpp
+++ b/icu4c/source/i18n/number_skeletons.cpp
@@ -897,7 +897,7 @@
 
 bool blueprint_helpers::parseExponentWidthOption(const StringSegment& segment, MacroProps& macros,
                                                  UErrorCode&) {
-    if (segment.charAt(0) != u'+') {
+    if (!isWildcardChar(segment.charAt(0))) {
         return false;
     }
     int32_t offset = 1;
@@ -919,7 +919,7 @@
 
 void
 blueprint_helpers::generateExponentWidthOption(int32_t minExponentDigits, UnicodeString& sb, UErrorCode&) {
-    sb.append(u'+');
+    sb.append(kWildcardChar);
     appendMultiple(sb, u'e', minExponentDigits);
 }
 
@@ -1071,7 +1071,7 @@
         }
     }
     if (offset < segment.length()) {
-        if (segment.charAt(offset) == u'+') {
+        if (isWildcardChar(segment.charAt(offset))) {
             maxFrac = -1;
             offset++;
         } else {
@@ -1113,7 +1113,7 @@
     sb.append(u'.');
     appendMultiple(sb, u'0', minFrac);
     if (maxFrac == -1) {
-        sb.append(u'+');
+        sb.append(kWildcardChar);
     } else {
         appendMultiple(sb, u'#', maxFrac - minFrac);
     }
@@ -1133,7 +1133,7 @@
         }
     }
     if (offset < segment.length()) {
-        if (segment.charAt(offset) == u'+') {
+        if (isWildcardChar(segment.charAt(offset))) {
             maxSig = -1;
             offset++;
         } else {
@@ -1166,7 +1166,7 @@
 blueprint_helpers::generateDigitsStem(int32_t minSig, int32_t maxSig, UnicodeString& sb, UErrorCode&) {
     appendMultiple(sb, u'@', minSig);
     if (maxSig == -1) {
-        sb.append(u'+');
+        sb.append(kWildcardChar);
     } else {
         appendMultiple(sb, u'#', maxSig - minSig);
     }
@@ -1262,7 +1262,7 @@
     // Invalid: @, @@, @@@
     // Invalid: @@#, @@##, @@@#
     if (offset < segment.length()) {
-        if (segment.charAt(offset) == u'+') {
+        if (isWildcardChar(segment.charAt(offset))) {
             maxSig = -1;
             offset++;
         } else if (minSig > 1) {
@@ -1351,7 +1351,7 @@
     int32_t offset = 0;
     int32_t minInt = 0;
     int32_t maxInt;
-    if (segment.charAt(0) == u'+') {
+    if (isWildcardChar(segment.charAt(0))) {
         maxInt = -1;
         offset++;
     } else {
@@ -1392,7 +1392,7 @@
 void blueprint_helpers::generateIntegerWidthOption(int32_t minInt, int32_t maxInt, UnicodeString& sb,
                                                    UErrorCode&) {
     if (maxInt == -1) {
-        sb.append(u'+');
+        sb.append(kWildcardChar);
     } else {
         appendMultiple(sb, u'#', maxInt - minInt);
     }
diff --git a/icu4c/source/i18n/number_skeletons.h b/icu4c/source/i18n/number_skeletons.h
index 3c03d73..d9b2c0e 100644
--- a/icu4c/source/i18n/number_skeletons.h
+++ b/icu4c/source/i18n/number_skeletons.h
@@ -118,6 +118,17 @@
     STEM_SCALE,
 };
 
+/** Default wildcard char, accepted on input and printed in output */
+constexpr char16_t kWildcardChar = u'*';
+
+/** Alternative wildcard char, accept on input but not printed in output */
+constexpr char16_t kAltWildcardChar = u'+';
+
+/** Checks whether the char is a wildcard on input */
+inline bool isWildcardChar(char16_t c) {
+    return c == kWildcardChar || c == kAltWildcardChar;
+}
+
 /**
  * Creates a NumberFormatter corresponding to the given skeleton string.
  *
diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h
index 30615b9..2597aa8 100644
--- a/icu4c/source/test/intltest/numbertest.h
+++ b/icu4c/source/test/intltest/numbertest.h
@@ -257,6 +257,7 @@
     void stemsRequiringOption();
     void defaultTokens();
     void flexibleSeparators();
+    void wildcardCharacters();
 
     void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0);
 
diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp
index 9652ade..c586603 100644
--- a/icu4c/source/test/intltest/numbertest_api.cpp
+++ b/icu4c/source/test/intltest/numbertest_api.cpp
@@ -209,7 +209,7 @@
 
     assertFormatDescending(
             u"Scientific min exponent digits",
-            u"scientific/+ee",
+            u"scientific/*ee",
             u"E00",
             NumberFormatter::with().notation(Notation::scientific().withMinExponentDigits(2)),
             Locale::getEnglish(),
@@ -1039,7 +1039,7 @@
 
     assertFormatDescending(
             u"Min Fraction",
-            u".0+",
+            u".0*",
             u".0+",
             NumberFormatter::with().precision(Precision::minFraction(1)),
             Locale::getEnglish(),
@@ -1116,7 +1116,7 @@
 
     assertFormatSingle(
             u"Min Significant",
-            u"@@+",
+            u"@@*",
             u"@@+",
             NumberFormatter::with().precision(Precision::minSignificantDigits(2)),
             Locale::getEnglish(),
@@ -1153,7 +1153,7 @@
 
     assertFormatSingle(
             u"Fixed Significant on zero with zero integer width",
-            u"@ integer-width/+",
+            u"@ integer-width/*",
             u"@ integer-width/+",
             NumberFormatter::with().precision(Precision::fixedSignificantDigits(1))
                     .integerWidth(IntegerWidth::zeroFillTo(0)),
@@ -1181,7 +1181,7 @@
 
     assertFormatDescending(
             u"FracSig minMaxFrac minSig",
-            u".0#/@@@+",
+            u".0#/@@@*",
             u".0#/@@@+",
             NumberFormatter::with().precision(Precision::minMaxFraction(1, 2).withMinDigits(3)),
             Locale::getEnglish(),
@@ -1229,7 +1229,7 @@
 
     assertFormatSingle(
             u"FracSig with trailing zeros A",
-            u".00/@@@+",
+            u".00/@@@*",
             u".00/@@@+",
             NumberFormatter::with().precision(Precision::fixedFraction(2).withMinDigits(3)),
             Locale::getEnglish(),
@@ -1238,7 +1238,7 @@
 
     assertFormatSingle(
             u"FracSig with trailing zeros B",
-            u".00/@@@+",
+            u".00/@@@*",
             u".00/@@@+",
             NumberFormatter::with().precision(Precision::fixedFraction(2).withMinDigits(3)),
             Locale::getEnglish(),
@@ -1807,7 +1807,7 @@
 
     assertFormatDescending(
             u"Integer Width Zero Fill 0",
-            u"integer-width/+",
+            u"integer-width/*",
             u"integer-width/+",
             NumberFormatter::with().integerWidth(IntegerWidth::zeroFillTo(0)),
             Locale::getEnglish(),
diff --git a/icu4c/source/test/intltest/numbertest_skeletons.cpp b/icu4c/source/test/intltest/numbertest_skeletons.cpp
index e8ab127..3aad601 100644
--- a/icu4c/source/test/intltest/numbertest_skeletons.cpp
+++ b/icu4c/source/test/intltest/numbertest_skeletons.cpp
@@ -29,6 +29,7 @@
         TESTCASE_AUTO(stemsRequiringOption);
         TESTCASE_AUTO(defaultTokens);
         TESTCASE_AUTO(flexibleSeparators);
+        TESTCASE_AUTO(wildcardCharacters);
     TESTCASE_AUTO_END;
 }
 
@@ -41,26 +42,35 @@
             u"precision-integer",
             u"precision-unlimited",
             u"@@@##",
+            u"@@*",
             u"@@+",
             u".000##",
+            u".00*",
             u".00+",
             u".",
+            u".*",
             u".+",
             u".######",
+            u".00/@@*",
             u".00/@@+",
             u".00/@##",
             u"precision-increment/3.14",
             u"precision-currency-standard",
             u"precision-integer rounding-mode-half-up",
             u".00# rounding-mode-ceiling",
+            u".00/@@* rounding-mode-floor",
             u".00/@@+ rounding-mode-floor",
             u"scientific",
+            u"scientific/*ee",
             u"scientific/+ee",
             u"scientific/sign-always",
+            u"scientific/*ee/sign-always",
             u"scientific/+ee/sign-always",
+            u"scientific/sign-always/*ee",
             u"scientific/sign-always/+ee",
             u"scientific/sign-except-zero",
             u"engineering",
+            u"engineering/*eee",
             u"engineering/+eee",
             u"compact-short",
             u"compact-long",
@@ -81,6 +91,7 @@
             u"group-thousands",
             u"integer-width/00",
             u"integer-width/#0",
+            u"integer-width/*00",
             u"integer-width/+00",
             u"sign-always",
             u"sign-auto",
@@ -136,16 +147,22 @@
     static const char16_t* cases[] = {
             u".00x",
             u".00##0",
+            u".##*",
+            u".00##*",
+            u".0#*",
+            u"@#*",
             u".##+",
             u".00##+",
             u".0#+",
+            u"@#+",
             u"@@x",
             u"@@##0",
-            u"@#+",
             u".00/@",
             u".00/@@",
             u".00/@@x",
             u".00/@@#",
+            u".00/@@#*",
+            u".00/floor/@@*", // wrong order
             u".00/@@#+",
             u".00/floor/@@+", // wrong order
             u"precision-increment/français", // non-invariant characters for C++
@@ -161,6 +178,10 @@
             u"currency/ççç", // three characters but not ASCII
             u"measure-unit/foo",
             u"integer-width/xxx",
+            u"integer-width/0*",
+            u"integer-width/*0#",
+            u"integer-width/*#",
+            u"integer-width/*#0",
             u"integer-width/0+",
             u"integer-width/+0#",
             u"integer-width/+#",
@@ -177,6 +198,7 @@
             u"EEE",
             u"EEE0",
             u"001",
+            u"00*",
             u"00+",
     };
 
@@ -304,6 +326,32 @@
     }
 }
 
+void NumberSkeletonTest::wildcardCharacters() {
+    IcuTestErrorCode status(*this, "wildcardCharacters");
+
+    struct TestCase {
+        const char16_t* star;
+        const char16_t* plus;
+    } cases[] = {
+        { u".00*", u".00+" },
+        { u"@@*", u"@@+" },
+        { u".00/@@*", u".00/@@+" },
+        { u"scientific/*ee", u"scientific/+ee" },
+        { u"integer-width/*00", u"integer-width/+00" },
+    };
+
+    for (const auto& cas : cases) {
+        UnicodeString star(cas.star);
+        UnicodeString plus(cas.plus);
+        status.setScope(star);
+
+        UnicodeString normalized = NumberFormatter::forSkeleton(plus, status)
+            .toSkeleton(status);
+        assertEquals("Plus should normalize to star", star, normalized);
+        status.errIfFailureAndReset();
+    }
+}
+
 // In C++, there is no distinguishing between "invalid", "unknown", and "unexpected" tokens.
 void NumberSkeletonTest::expectedErrorSkeleton(const char16_t** cases, int32_t casesLen) {
     for (int32_t i = 0; i < casesLen; i++) {
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
index 492f6b5..3897ca3 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
@@ -122,6 +122,17 @@
         STEM_SCALE,
     };
 
+    /** Default wildcard char, accepted on input and printed in output */
+    static final char WILDCARD_CHAR = '*';
+
+    /** Alternative wildcard char, accept on input but not printed in output */
+    static final char ALT_WILDCARD_CHAR = '+';
+
+    /** Checks whether the char is a wildcard on input */
+    static boolean isWildcardChar(char c) {
+        return c == WILDCARD_CHAR || c == ALT_WILDCARD_CHAR;
+    }
+
     /** For mapping from ordinal back to StemEnum in Java. */
     static final StemEnum[] STEM_ENUM_VALUES = StemEnum.values();
 
@@ -924,7 +935,7 @@
 
         /** @return Whether we successfully found and parsed an exponent width option. */
         private static boolean parseExponentWidthOption(StringSegment segment, MacroProps macros) {
-            if (segment.charAt(0) != '+') {
+            if (!isWildcardChar(segment.charAt(0))) {
                 return false;
             }
             int offset = 1;
@@ -945,7 +956,7 @@
         }
 
         private static void generateExponentWidthOption(int minExponentDigits, StringBuilder sb) {
-            sb.append('+');
+            sb.append(WILDCARD_CHAR);
             appendMultiple(sb, 'e', minExponentDigits);
         }
 
@@ -1044,7 +1055,7 @@
                 }
             }
             if (offset < segment.length()) {
-                if (segment.charAt(offset) == '+') {
+                if (isWildcardChar(segment.charAt(offset))) {
                     maxFrac = -1;
                     offset++;
                 } else {
@@ -1083,7 +1094,7 @@
             sb.append('.');
             appendMultiple(sb, '0', minFrac);
             if (maxFrac == -1) {
-                sb.append('+');
+                sb.append(WILDCARD_CHAR);
             } else {
                 appendMultiple(sb, '#', maxFrac - minFrac);
             }
@@ -1102,7 +1113,7 @@
                 }
             }
             if (offset < segment.length()) {
-                if (segment.charAt(offset) == '+') {
+                if (isWildcardChar(segment.charAt(offset))) {
                     maxSig = -1;
                     offset++;
                 } else {
@@ -1132,7 +1143,7 @@
         private static void generateDigitsStem(int minSig, int maxSig, StringBuilder sb) {
             appendMultiple(sb, '@', minSig);
             if (maxSig == -1) {
-                sb.append('+');
+                sb.append(WILDCARD_CHAR);
             } else {
                 appendMultiple(sb, '#', maxSig - minSig);
             }
@@ -1224,7 +1235,7 @@
             // Invalid: @, @@, @@@
             // Invalid: @@#, @@##, @@@#
             if (offset < segment.length()) {
-                if (segment.charAt(offset) == '+') {
+                if (isWildcardChar(segment.charAt(offset))) {
                     maxSig = -1;
                     offset++;
                 } else if (minSig > 1) {
@@ -1278,7 +1289,7 @@
             int offset = 0;
             int minInt = 0;
             int maxInt;
-            if (segment.charAt(0) == '+') {
+            if (isWildcardChar(segment.charAt(0))) {
                 maxInt = -1;
                 offset++;
             } else {
@@ -1316,7 +1327,7 @@
 
         private static void generateIntegerWidthOption(int minInt, int maxInt, StringBuilder sb) {
             if (maxInt == -1) {
-                sb.append('+');
+                sb.append(WILDCARD_CHAR);
             } else {
                 appendMultiple(sb, '#', maxInt - minInt);
             }
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
index ae3eab4..2a51b7d 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
@@ -165,7 +165,7 @@
 
         assertFormatDescending(
                 "Scientific min exponent digits",
-                "scientific/+ee",
+                "scientific/*ee",
                 "E00",
                 NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)),
                 ULocale.ENGLISH,
@@ -966,7 +966,7 @@
 
         assertFormatDescending(
                 "Min Fraction",
-                ".0+",
+                ".0*",
                 ".0+",
                 NumberFormatter.with().precision(Precision.minFraction(1)),
                 ULocale.ENGLISH,
@@ -1044,7 +1044,7 @@
 
         assertFormatSingle(
                 "Min Significant",
-                "@@+",
+                "@@*",
                 "@@+",
                 NumberFormatter.with().precision(Precision.minSignificantDigits(2)),
                 ULocale.ENGLISH,
@@ -1071,7 +1071,7 @@
 
         assertFormatSingle(
                 "Fixed Significant on zero with zero integer width",
-                "@ integer-width/+",
+                "@ integer-width/*",
                 "@ integer-width/+",
                 NumberFormatter.with().precision(Precision.fixedSignificantDigits(1)).integerWidth(IntegerWidth.zeroFillTo(0)),
                 ULocale.ENGLISH,
@@ -1108,7 +1108,7 @@
 
         assertFormatDescending(
                 "FracSig minMaxFrac minSig",
-                ".0#/@@@+",
+                ".0#/@@@*",
                 ".0#/@@@+",
                 NumberFormatter.with().precision(Precision.minMaxFraction(1, 2).withMinDigits(3)),
                 ULocale.ENGLISH,
@@ -1172,7 +1172,7 @@
 
         assertFormatSingle(
                 "FracSig with trailing zeros A",
-                ".00/@@@+",
+                ".00/@@@*",
                 ".00/@@@+",
                 NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)),
                 ULocale.ENGLISH,
@@ -1181,7 +1181,7 @@
 
         assertFormatSingle(
                 "FracSig with trailing zeros B",
-                ".00/@@@+",
+                ".00/@@@*",
                 ".00/@@@+",
                 NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)),
                 ULocale.ENGLISH,
@@ -1726,7 +1726,7 @@
 
         assertFormatDescending(
                 "Integer Width Zero Fill 0",
-                "integer-width/+",
+                "integer-width/*",
                 "integer-width/+",
                 NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)),
                 ULocale.ENGLISH,
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java
index a2c9553..03caedc 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java
@@ -28,26 +28,35 @@
                 "precision-integer",
                 "precision-unlimited",
                 "@@@##",
+                "@@*",
                 "@@+",
                 ".000##",
+                ".00*",
                 ".00+",
                 ".",
+                ".*",
                 ".+",
                 ".######",
+                ".00/@@*",
                 ".00/@@+",
                 ".00/@##",
                 "precision-increment/3.14",
                 "precision-currency-standard",
                 "precision-integer rounding-mode-half-up",
                 ".00# rounding-mode-ceiling",
+                ".00/@@* rounding-mode-floor",
                 ".00/@@+ rounding-mode-floor",
                 "scientific",
+                "scientific/*ee",
                 "scientific/+ee",
                 "scientific/sign-always",
+                "scientific/*ee/sign-always",
                 "scientific/+ee/sign-always",
+                "scientific/sign-always/*ee",
                 "scientific/sign-always/+ee",
                 "scientific/sign-except-zero",
                 "engineering",
+                "engineering/*eee",
                 "engineering/+eee",
                 "compact-short",
                 "compact-long",
@@ -68,6 +77,7 @@
                 "group-thousands",
                 "integer-width/00",
                 "integer-width/#0",
+                "integer-width/*00",
                 "integer-width/+00",
                 "sign-always",
                 "sign-auto",
@@ -122,16 +132,22 @@
         String[] cases = {
                 ".00x",
                 ".00##0",
+                ".##*",
+                ".00##*",
+                ".0#*",
+                "@#*",
                 ".##+",
                 ".00##+",
                 ".0#+",
+                "@#+",
                 "@@x",
                 "@@##0",
-                "@#+",
                 ".00/@",
                 ".00/@@",
                 ".00/@@x",
                 ".00/@@#",
+                ".00/@@#*",
+                ".00/floor/@@*", // wrong order
                 ".00/@@#+",
                 ".00/floor/@@+", // wrong order
                 "precision-increment/français", // non-invariant characters for C++
@@ -147,6 +163,10 @@
                 "currency/ççç", // three characters but not ASCII
                 "measure-unit/foo",
                 "integer-width/xxx",
+                "integer-width/0*",
+                "integer-width/*0#",
+                "integer-width/*#",
+                "integer-width/*#0",
                 "integer-width/0+",
                 "integer-width/+0#",
                 "integer-width/+#",
@@ -163,6 +183,7 @@
                 "EEE",
                 "EEE0",
                 "001",
+                "00*",
                 "00+",
         };
 
@@ -297,6 +318,26 @@
     }
 
     @Test
+    public void wildcardCharacters() {
+        String[][] cases = {
+            { ".00*", ".00+" },
+            { "@@*", "@@+" },
+            { ".00/@@*", ".00/@@+" },
+            { "scientific/*ee", "scientific/+ee" },
+            { "integer-width/*00", "integer-width/+00" },
+        };
+    
+        for (String[] cas : cases) {
+            String star = cas[0];
+            String plus = cas[1];
+    
+            String normalized = NumberFormatter.forSkeleton(plus)
+                .toSkeleton();
+            assertEquals("Plus should normalize to star", star, normalized);
+        }
+    }
+
+    @Test
     public void roundingModeNames() {
         for (RoundingMode mode : RoundingMode.values()) {
             if (mode == RoundingMode.HALF_EVEN) {