ICU-20019 Implement withSignificantDigits option

See #1598
diff --git a/docs/userguide/format_parse/numbers/skeletons.md b/docs/userguide/format_parse/numbers/skeletons.md
index 227ea30..fd5e2e9 100644
--- a/docs/userguide/format_parse/numbers/skeletons.md
+++ b/docs/userguide/format_parse/numbers/skeletons.md
@@ -219,20 +219,26 @@
 
 Note that the stem `.` is considered valid and is equivalent to `precision-integer`.
 
-Fraction-precision stems accept a single optional option: the minimum or
-maximum number of significant digits.  This allows you to combine fraction
-precision with certain significant digits capabilities.  The following are
-examples:
+Fraction-precision stems accept a single optional option: a number of significant digits.
+The options here correspond to the API functions on `FractionPrecision`. Some options
+require specifying `r` or `s` for relaxed mode or strict mode. For more information, see
+the API docs for UNumberRoundingPriority.
 
 | Skeleton | Explanation | Equivalent C++ Code |
 |---|---|---|
 | `.##/@@@*` | At most 2 fraction digits, but guarantee <br/> at least 3 significant digits | `Precision::maxFraction(2)` <br/> `.withMinDigits(3)` |
+| `.##/@##r` | Same as above | `Precision::maxFraction(2)` <br/> `.withSignificantDigits(1, 3, RELAXED)` |
+| `.##/@@@r` | Same as above, but pad trailing zeros <br/> to at least 3 significant digits | `Precision::maxFraction(2)` <br/> `.withSignificantDigits(3, 3, RELAXED)` |
 | `.00/@##` | Exactly 2 fraction digits, but do not <br/> display more than 3 significant digits | `Precision::fixedFraction(2)` <br/> `.withMaxDigits(3)` |
+| `.00/@##s` | Same as above | `Precision::fixedFraction(2)` <br/> `.withSignificantDigits(1, 3, STRICT)` |
+| `.00/@##s` | Same as above, but pad trailing zeros <br/> to at least 3 significant digits | `Precision::fixedFraction(2)` <br/> `.withSignificantDigits(3, 3, STRICT)` |
 
-Precisely, the option starts with one or more `@` symbols.  Then it contains
-either a `*`, for `::withMinDigits`, or one or more `#` symbols, for
-`::withMaxDigits`.  If a `#` symbol is present, there must be only one `@`
-symbol.
+Precisely, the option follows the syntax of the significant digits stem (see below),
+but one of the following must be true:
+
+- Option has one or more `@`s followed by the wildcard character (`withMinDigits`)
+- Option has exactly one `@` followed by zero or more `#`s (`withMaxDigits`)
+- Option has one or more `@`s followed by zero or more `#`s and ends in `s` or `r` (`withSignificantDigits`)
 
 #### Significant Digits Precision
 
diff --git a/icu4c/source/i18n/number_rounding.cpp b/icu4c/source/i18n/number_rounding.cpp
index a8fd6bc..0932d2f 100644
--- a/icu4c/source/i18n/number_rounding.cpp
+++ b/icu4c/source/i18n/number_rounding.cpp
@@ -205,10 +205,32 @@
     return constructCurrency(currencyUsage);
 }
 
+Precision FractionPrecision::withSignificantDigits(
+        int32_t minSignificantDigits,
+        int32_t maxSignificantDigits,
+        UNumberRoundingPriority priority) const {
+    if (fType == RND_ERROR) { return *this; } // no-op in error state
+    if (minSignificantDigits >= 1 &&
+            maxSignificantDigits >= minSignificantDigits &&
+            maxSignificantDigits <= kMaxIntFracSig) {
+        return constructFractionSignificant(
+            *this,
+            minSignificantDigits,
+            maxSignificantDigits,
+            priority);
+    } else {
+        return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
+    }
+}
+
 Precision FractionPrecision::withMinDigits(int32_t minSignificantDigits) const {
     if (fType == RND_ERROR) { return *this; } // no-op in error state
     if (minSignificantDigits >= 1 && minSignificantDigits <= kMaxIntFracSig) {
-        return constructFractionSignificant(*this, minSignificantDigits, -1);
+        return constructFractionSignificant(
+            *this,
+            1,
+            minSignificantDigits,
+            UNUM_ROUNDING_PRIORITY_RELAXED);
     } else {
         return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
     }
@@ -217,7 +239,10 @@
 Precision FractionPrecision::withMaxDigits(int32_t maxSignificantDigits) const {
     if (fType == RND_ERROR) { return *this; } // no-op in error state
     if (maxSignificantDigits >= 1 && maxSignificantDigits <= kMaxIntFracSig) {
-        return constructFractionSignificant(*this, -1, maxSignificantDigits);
+        return constructFractionSignificant(*this,
+            1,
+            maxSignificantDigits,
+            UNUM_ROUNDING_PRIORITY_STRICT);
     } else {
         return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
     }
@@ -280,10 +305,15 @@
 }
 
 Precision
-Precision::constructFractionSignificant(const FractionPrecision &base, int32_t minSig, int32_t maxSig) {
+Precision::constructFractionSignificant(
+        const FractionPrecision &base,
+        int32_t minSig,
+        int32_t maxSig,
+        UNumberRoundingPriority priority) {
     FractionSignificantSettings settings = base.fUnion.fracSig;
     settings.fMinSig = static_cast<digits_t>(minSig);
     settings.fMaxSig = static_cast<digits_t>(maxSig);
+    settings.fPriority = priority;
     PrecisionUnion union_;
     union_.fracSig = settings;
     return {RND_FRACTION_SIGNIFICANT, union_};
@@ -417,23 +447,21 @@
             break;
 
         case Precision::RND_FRACTION_SIGNIFICANT: {
-            int32_t displayMag = getDisplayMagnitudeFraction(fPrecision.fUnion.fracSig.fMinFrac);
-            int32_t roundingMag = getRoundingMagnitudeFraction(fPrecision.fUnion.fracSig.fMaxFrac);
-            if (fPrecision.fUnion.fracSig.fMinSig == -1) {
-                // Max Sig override
-                int32_t candidate = getRoundingMagnitudeSignificant(
-                        value,
-                        fPrecision.fUnion.fracSig.fMaxSig);
-                roundingMag = uprv_max(roundingMag, candidate);
+            int32_t roundingMag1 = getRoundingMagnitudeFraction(fPrecision.fUnion.fracSig.fMaxFrac);
+            int32_t roundingMag2 = getRoundingMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMaxSig);
+            int32_t roundingMag;
+            if (fPrecision.fUnion.fracSig.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) {
+                roundingMag = uprv_min(roundingMag1, roundingMag2);
             } else {
-                // Min Sig override
-                int32_t candidate = getDisplayMagnitudeSignificant(
-                        value,
-                        fPrecision.fUnion.fracSig.fMinSig);
-                roundingMag = uprv_min(roundingMag, candidate);
+                roundingMag = uprv_max(roundingMag1, roundingMag2);
             }
             value.roundToMagnitude(roundingMag, fRoundingMode, status);
+
+            int32_t displayMag1 = getDisplayMagnitudeFraction(fPrecision.fUnion.fracSig.fMinFrac);
+            int32_t displayMag2 = getDisplayMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMinSig);
+            int32_t displayMag = uprv_min(displayMag1, displayMag2);
             value.setMinFraction(uprv_max(0, -displayMag));
+
             break;
         }
 
diff --git a/icu4c/source/i18n/number_skeletons.cpp b/icu4c/source/i18n/number_skeletons.cpp
index 5aae555..a07725b 100644
--- a/icu4c/source/i18n/number_skeletons.cpp
+++ b/icu4c/source/i18n/number_skeletons.cpp
@@ -1280,21 +1280,14 @@
             break;
         }
     }
-    // For the frac-sig option, there must be minSig or maxSig but not both.
-    // Valid: @+, @@+, @@@+
-    // Valid: @#, @##, @###
-    // Invalid: @, @@, @@@
-    // Invalid: @@#, @@##, @@@#
     if (offset < segment.length()) {
         if (isWildcardChar(segment.charAt(offset))) {
+            // @+, @@+, @@@+
             maxSig = -1;
             offset++;
-        } else if (minSig > 1) {
-            // @@#, @@##, @@@#
-            // throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
-            status = U_NUMBER_SKELETON_SYNTAX_ERROR;
-            return false;
         } else {
+            // @#, @##, @###
+            // @@#, @@##, @@@#
             maxSig = minSig;
             for (; offset < segment.length(); offset++) {
                 if (segment.charAt(offset) == u'#') {
@@ -1306,22 +1299,45 @@
         }
     } else {
         // @, @@, @@@
-        // throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
-        status = U_NUMBER_SKELETON_SYNTAX_ERROR;
-        return false;
+        maxSig = minSig;
     }
+    UNumberRoundingPriority priority;
     if (offset < segment.length()) {
-        // throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
+        if (maxSig == -1) {
+            // The wildcard character is not allowed with the priority annotation
+            status = U_NUMBER_SKELETON_SYNTAX_ERROR;
+            return false;
+        }
+        if (segment.codePointAt(offset) == u'r') {
+            priority = UNUM_ROUNDING_PRIORITY_RELAXED;
+            offset++;
+        } else if (segment.codePointAt(offset) == u's') {
+            priority = UNUM_ROUNDING_PRIORITY_STRICT;
+            offset++;
+        } else {
+            U_ASSERT(offset < segment.length());
+        }
+        if (offset < segment.length()) {
+            // Invalid digits option for fraction rounder
+            status = U_NUMBER_SKELETON_SYNTAX_ERROR;
+            return false;
+        }
+    } else if (maxSig == -1) {
+        // withMinDigits
+        maxSig = minSig;
+        minSig = 1;
+        priority = UNUM_ROUNDING_PRIORITY_RELAXED;
+    } else if (minSig == 1) {
+        // withMaxDigits
+        priority = UNUM_ROUNDING_PRIORITY_STRICT;
+    } else {
+        // Digits options with both min and max sig require the priority option
         status = U_NUMBER_SKELETON_SYNTAX_ERROR;
         return false;
     }
 
     auto& oldPrecision = static_cast<const FractionPrecision&>(macros.precision);
-    if (maxSig == -1) {
-        macros.precision = oldPrecision.withMinDigits(minSig);
-    } else {
-        macros.precision = oldPrecision.withMaxDigits(maxSig);
-    }
+    macros.precision = oldPrecision.withSignificantDigits(minSig, maxSig, priority);
     return true;
 }
 
@@ -1552,10 +1568,11 @@
         const Precision::FractionSignificantSettings& impl = macros.precision.fUnion.fracSig;
         blueprint_helpers::generateFractionStem(impl.fMinFrac, impl.fMaxFrac, sb, status);
         sb.append(u'/');
-        if (impl.fMinSig == -1) {
-            blueprint_helpers::generateDigitsStem(1, impl.fMaxSig, sb, status);
+        blueprint_helpers::generateDigitsStem(impl.fMinSig, impl.fMaxSig, sb, status);
+        if (impl.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) {
+            sb.append(u'r');
         } else {
-            blueprint_helpers::generateDigitsStem(impl.fMinSig, -1, sb, status);
+            sb.append(u's');
         }
     } else if (macros.precision.fType == Precision::RND_INCREMENT
             || macros.precision.fType == Precision::RND_INCREMENT_ONE
diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h
index 25239c3..10e4830 100644
--- a/icu4c/source/i18n/unicode/numberformatter.h
+++ b/icu4c/source/i18n/unicode/numberformatter.h
@@ -694,6 +694,8 @@
             impl::digits_t fMinSig;
             /** @internal */
             impl::digits_t fMaxSig;
+            /** @internal */
+            UNumberRoundingPriority fPriority;
         } fracSig;
         /** @internal */
         struct IncrementSettings {
@@ -740,8 +742,11 @@
 
     static Precision constructSignificant(int32_t minSig, int32_t maxSig);
 
-    static Precision
-    constructFractionSignificant(const FractionPrecision &base, int32_t minSig, int32_t maxSig);
+    static Precision constructFractionSignificant(
+        const FractionPrecision &base,
+        int32_t minSig,
+        int32_t maxSig,
+        UNumberRoundingPriority priority);
 
     static IncrementPrecision constructIncrement(double increment, int32_t minFrac);
 
@@ -783,16 +788,38 @@
  */
 class U_I18N_API FractionPrecision : public Precision {
   public:
+#ifndef U_HIDE_DRAFT_API
     /**
-     * Ensure that no less than this number of significant digits are retained when rounding according to fraction
-     * rules.
+     * Override maximum fraction digits with maximum significant digits depending on the magnitude
+     * of the number. See UNumberRoundingPriority.
      *
-     * <p>
-     * For example, with integer rounding, the number 3.141 becomes "3". However, with minimum figures set to 2, 3.141
-     * becomes "3.1" instead.
+     * @param minSignificantDigits
+     *            Pad trailing zeros to achieve this minimum number of significant digits.
+     * @param maxSignificantDigits
+     *            Round the number to achieve this maximum number of significant digits.
+     * @param priority
+     *            How to disambiguate between fraction digits and significant digits.
+     * @return A precision for chaining or passing to the NumberFormatter precision() setter.
      *
-     * <p>
-     * This setting does not affect the number of trailing zeros. For example, 3.01 would print as "3", not "3.0".
+     * @draft ICU 69
+     */
+    Precision withSignificantDigits(
+        int32_t minSignificantDigits,
+        int32_t maxSignificantDigits,
+        UNumberRoundingPriority priority) const;
+#endif // U_HIDE_DRAFT_API
+
+    /**
+     * Ensure that no less than this number of significant digits are retained when rounding
+     * according to fraction rules.
+     *
+     * For example, with integer rounding, the number 3.141 becomes "3". However, with minimum
+     * figures set to 2, 3.141 becomes "3.1" instead.
+     *
+     * This setting does not affect the number of trailing zeros. For example, 3.01 would print as
+     * "3", not "3.0".
+     *
+     * This is equivalent to `withSignificantDigits(1, minSignificantDigits, RELAXED)`.
      *
      * @param minSignificantDigits
      *            The number of significant figures to guarantee.
@@ -802,16 +829,16 @@
     Precision withMinDigits(int32_t minSignificantDigits) const;
 
     /**
-     * Ensure that no more than this number of significant digits are retained when rounding according to fraction
-     * rules.
+     * Ensure that no more than this number of significant digits are retained when rounding
+     * according to fraction rules.
      *
-     * <p>
-     * For example, with integer rounding, the number 123.4 becomes "123". However, with maximum figures set to 2, 123.4
-     * becomes "120" instead.
+     * For example, with integer rounding, the number 123.4 becomes "123". However, with maximum
+     * figures set to 2, 123.4 becomes "120" instead.
      *
-     * <p>
-     * This setting does not affect the number of trailing zeros. For example, with fixed fraction of 2, 123.4 would
-     * become "120.00".
+     * This setting does not affect the number of trailing zeros. For example, with fixed fraction
+     * of 2, 123.4 would become "120.00".
+     *
+     * This is equivalent to `withSignificantDigits(1, maxSignificantDigits, STRICT)`.
      *
      * @param maxSignificantDigits
      *            Round the number to no more than this number of significant figures.
diff --git a/icu4c/source/i18n/unicode/unumberformatter.h b/icu4c/source/i18n/unicode/unumberformatter.h
index 71bbc58..1430f65 100644
--- a/icu4c/source/i18n/unicode/unumberformatter.h
+++ b/icu4c/source/i18n/unicode/unumberformatter.h
@@ -78,6 +78,62 @@
  * </pre>
  */
 
+#ifndef U_FORCE_HIDE_DRAFT_API
+/**
+ * An enum declaring how to resolve conflicts between maximum fraction digits and maximum
+ * significant digits.
+ *
+ * There are two modes, RELAXED and STRICT:
+ *
+ * - RELAXED: Relax one of the two constraints (fraction digits or significant digits) in order
+ *   to round the number to a higher level of precision.
+ * - STRICT: Enforce both constraints, resulting in the number being rounded to a lower
+ *   level of precision.
+ *
+ * The default settings for compact notation rounding are Max-Fraction = 0 (round to the nearest
+ * integer), Max-Significant = 2 (round to 2 significant digits), and priority RELAXED (choose
+ * the constraint that results in more digits being displayed).
+ *
+ * Conflicting *minimum* fraction and significant digits are always resolved in the direction that
+ * results in more trailing zeros.
+ *
+ * Example 1: Consider the number 3.141, with various different settings:
+ *
+ * - Max-Fraction = 1: "3.1"
+ * - Max-Significant = 3: "3.14"
+ *
+ * The rounding priority determines how to resolve the conflict when both Max-Fraction and
+ * Max-Significant are set. With RELAXED, the less-strict setting (the one that causes more digits
+ * to be displayed) will be used; Max-Significant wins. With STRICT, the more-strict setting (the
+ * one that causes fewer digits to be displayed) will be used; Max-Fraction wins.
+ *
+ * Example 2: Consider the number 8317, with various different settings:
+ *
+ * - Max-Fraction = 1: "8317"
+ * - Max-Significant = 3: "8320"
+ *
+ * Here, RELAXED favors Max-Fraction and STRICT favors Max-Significant. Note that this larger
+ * number caused the two modes to favor the opposite result.
+ *
+ * @draft ICU 69
+ */
+typedef enum UNumberRoundingPriority {
+    /**
+     * Favor greater precision by relaxing one of the rounding constraints.
+     *
+     * @draft ICU 69
+     */
+    UNUM_ROUNDING_PRIORITY_RELAXED,
+
+    /**
+     * Favor adherence to all rounding constraints by producing lower precision.
+     *
+     * @draft ICU 69
+     */
+    UNUM_ROUNDING_PRIORITY_STRICT,
+} UNumberRoundingPriority;
+#endif // U_FORCE_HIDE_DRAFT_API
+
 /**
  * An enum declaring how to render units, including currencies. Example outputs when formatting 123 USD and 123
  * meters in <em>en-CA</em>:
diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp
index ad2d451..f64c99b 100644
--- a/icu4c/source/test/intltest/numbertest_api.cpp
+++ b/icu4c/source/test/intltest/numbertest_api.cpp
@@ -2708,6 +2708,71 @@
             Locale::getEnglish(),
             0.0999999,
             u"0.10");
+
+    assertFormatDescending(
+            u"FracSig withSignificantDigits RELAXED",
+            u"precision-integer/@#r",
+            u"./@#r",
+            NumberFormatter::with().precision(Precision::maxFraction(0)
+                .withSignificantDigits(1, 2, UNUM_ROUNDING_PRIORITY_RELAXED)),
+            Locale::getEnglish(),
+            u"87,650",
+            u"8,765",
+            u"876",
+            u"88",
+            u"8.8",
+            u"0.88",
+            u"0.088",
+            u"0.0088",
+            u"0");
+
+    assertFormatDescending(
+            u"FracSig withSignificantDigits STRICT",
+            u"precision-integer/@#s",
+            u"./@#",
+            NumberFormatter::with().precision(Precision::maxFraction(0)
+                .withSignificantDigits(1, 2, UNUM_ROUNDING_PRIORITY_STRICT)),
+            Locale::getEnglish(),
+            u"88,000",
+            u"8,800",
+            u"880",
+            u"88",
+            u"9",
+            u"1",
+            u"0",
+            u"0",
+            u"0");
+
+    assertFormatSingle(
+            u"FracSig withSignificantDigits Trailing Zeros RELAXED",
+            u".0/@@@r",
+            u".0/@@@r",
+            NumberFormatter::with().precision(Precision::fixedFraction(1)
+                .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_RELAXED)),
+            Locale::getEnglish(),
+            1,
+            u"1.00");
+
+    // Trailing zeros are always retained:
+    assertFormatSingle(
+            u"FracSig withSignificantDigits Trailing Zeros STRICT",
+            u".0/@@@s",
+            u".0/@@@s",
+            NumberFormatter::with().precision(Precision::fixedFraction(1)
+                .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_STRICT)),
+            Locale::getEnglish(),
+            1,
+            u"1.00");
+
+    assertFormatSingle(
+            u"FracSig withSignificantDigits at rounding boundary",
+            u"precision-integer/@@@s",
+            u"./@@@s",
+            NumberFormatter::with().precision(Precision::fixedFraction(0)
+                    .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_STRICT)),
+            Locale::getEnglish(),
+            9.99,
+            u"10.0");
 }
 
 void NumberFormatterApiTest::roundingOther() {
diff --git a/icu4c/source/test/intltest/numbertest_skeletons.cpp b/icu4c/source/test/intltest/numbertest_skeletons.cpp
index 07a864a..67c755d 100644
--- a/icu4c/source/test/intltest/numbertest_skeletons.cpp
+++ b/icu4c/source/test/intltest/numbertest_skeletons.cpp
@@ -56,6 +56,10 @@
             u".00/@@*",
             u".00/@@+",
             u".00/@##",
+            u".00/@",
+            u".00/@r",
+            u".00/@@s",
+            u".00/@@#r",
             u"precision-increment/3.14",
             u"precision-currency-standard",
             u"precision-integer rounding-mode-half-up",
@@ -159,13 +163,13 @@
             u"@#+",
             u"@@x",
             u"@@##0",
-            u".00/@",
             u".00/@@",
             u".00/@@x",
             u".00/@@#",
             u".00/@@#*",
             u".00/floor/@@*", // wrong order
             u".00/@@#+",
+            u".00/@@@+r",
             u".00/floor/@@+", // wrong order
             u"precision-increment/français", // non-invariant characters for C++
             u"scientific/ee",
@@ -337,7 +341,6 @@
     } 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" },
     };
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java b/icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java
index 3752b1a..90c2fd2 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java
@@ -20,6 +20,36 @@
     }
 
     /**
+     * Override maximum fraction digits with maximum significant digits depending on the magnitude
+     * of the number. See UNumberRoundingPriority.
+     *
+     * @param minSignificantDigits
+     *            Pad trailing zeros to achieve this minimum number of significant digits.
+     * @param maxSignificantDigits
+     *            Round the number to achieve this maximum number of significant digits.
+     * @param priority
+     *            How to disambiguate between fraction digits and significant digits.
+     * @return A precision for chaining or passing to the NumberFormatter precision() setter.
+     *
+     * @draft ICU 69
+     */
+    public Precision withSignificantDigits(
+            int minSignificantDigits,
+            int maxSignificantDigits,
+            NumberFormatter.RoundingPriority priority) {
+        if (maxSignificantDigits >= 1 &&
+                maxSignificantDigits >= minSignificantDigits &&
+                maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
+            return constructFractionSignificant(
+                this, minSignificantDigits, maxSignificantDigits, priority);
+        } else {
+            throw new IllegalArgumentException("Significant digits must be between 1 and "
+                    + RoundingUtils.MAX_INT_FRAC_SIG
+                    + " (inclusive)");
+        }
+    }
+
+    /**
      * Ensure that no less than this number of significant digits are retained when rounding according to
      * fraction rules.
      *
@@ -31,6 +61,9 @@
      * This setting does not affect the number of trailing zeros. For example, 3.01 would print as "3",
      * not "3.0".
      *
+     * <p>
+     * This is equivalent to `withSignificantDigits(1, minSignificantDigits, RELAXED)`.
+     *
      * @param minSignificantDigits
      *            The number of significant figures to guarantee.
      * @return A Precision for chaining or passing to the NumberFormatter rounding() setter.
@@ -40,7 +73,8 @@
      */
     public Precision withMinDigits(int minSignificantDigits) {
         if (minSignificantDigits >= 1 && minSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
-            return constructFractionSignificant(this, minSignificantDigits, -1);
+            return constructFractionSignificant(
+                this, 1, minSignificantDigits, NumberFormatter.RoundingPriority.RELAXED);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
@@ -60,6 +94,9 @@
      * This setting does not affect the number of trailing zeros. For example, with fixed fraction of 2,
      * 123.4 would become "120.00".
      *
+     * <p>
+     * This is equivalent to `withSignificantDigits(1, maxSignificantDigits, STRICT)`.
+     *
      * @param maxSignificantDigits
      *            Round the number to no more than this number of significant figures.
      * @return A Precision for chaining or passing to the NumberFormatter rounding() setter.
@@ -69,7 +106,8 @@
      */
     public Precision withMaxDigits(int maxSignificantDigits) {
         if (maxSignificantDigits >= 1 && maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
-            return constructFractionSignificant(this, -1, maxSignificantDigits);
+            return constructFractionSignificant(
+                this, 1, maxSignificantDigits, NumberFormatter.RoundingPriority.STRICT);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java
index e5e1e4d..3d6316d 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java
@@ -69,6 +69,69 @@
     private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter();
 
     /**
+     * An enum declaring how to resolve conflicts between maximum fraction digits and maximum
+     * significant digits.
+     *
+     * <p>There are two modes, RELAXED and STRICT:
+     *
+     * <ul>
+     * <li> RELAXED: Relax one of the two constraints (fraction digits or significant digits) in order
+     *   to round the number to a higher level of precision.
+     * <li> STRICT: Enforce both constraints, resulting in the number being rounded to a lower
+     *   level of precision.
+     * </ul>
+     *
+     * <p>The default settings for compact notation rounding are Max-Fraction = 0 (round to the nearest
+     * integer), Max-Significant = 2 (round to 2 significant digits), and priority RELAXED (choose
+     * the constraint that results in more digits being displayed).
+     *
+     * <p>Conflicting *minimum* fraction and significant digits are always resolved in the direction that
+     * results in more trailing zeros.
+     *
+     * <p>Example 1: Consider the number 3.141, with various different settings:
+     *
+     * <ul>
+     * <li> Max-Fraction = 1: "3.1"
+     * <li> Max-Significant = 3: "3.14"
+     * </ul>
+     *
+     * <p>The rounding priority determines how to resolve the conflict when both Max-Fraction and
+     * Max-Significant are set. With RELAXED, the less-strict setting (the one that causes more digits
+     * to be displayed) will be used; Max-Significant wins. With STRICT, the more-strict setting (the
+     * one that causes fewer digits to be displayed) will be used; Max-Fraction wins.
+     *
+     * <p>Example 2: Consider the number 8317, with various different settings:
+     *
+     * <ul>
+     * <li> Max-Fraction = 1: "8317"
+     * <li> Max-Significant = 3: "8320"
+     * </ul>
+     *
+     * <p>Here, RELAXED favors Max-Fraction and STRICT favors Max-Significant. Note that this larger
+     * number caused the two modes to favor the opposite result.
+     *
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 69
+     */
+    public static enum RoundingPriority {
+        /**
+         * Favor greater precision by relaxing one of the rounding constraints.
+         *
+         * @provisional This API might change or be removed in a future release.
+         * @draft ICU 69
+         */
+        RELAXED,
+    
+        /**
+         * Favor adherence to all rounding constraints by producing lower precision.
+         *
+         * @provisional This API might change or be removed in a future release.
+         * @draft ICU 69
+         */
+        STRICT,
+    }
+
+    /**
      * An enum declaring how to render units, including currencies. Example outputs when formatting 123
      * USD and 123 meters in <em>en-CA</em>:
      *
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 246abea..3da00bf 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
@@ -14,6 +14,7 @@
 import com.ibm.icu.impl.number.RoundingUtils;
 import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
 import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
+import com.ibm.icu.number.NumberFormatter.RoundingPriority;
 import com.ibm.icu.number.NumberFormatter.SignDisplay;
 import com.ibm.icu.number.NumberFormatter.UnitWidth;
 import com.ibm.icu.text.DecimalFormatSymbols;
@@ -1291,20 +1292,14 @@
                     break;
                 }
             }
-            // For the frac-sig option, there must be minSig or maxSig but not both.
-            // Valid: @+, @@+, @@@+
-            // Valid: @#, @##, @###
-            // Invalid: @, @@, @@@
-            // Invalid: @@#, @@##, @@@#
             if (offset < segment.length()) {
                 if (isWildcardChar(segment.charAt(offset))) {
+                    // @+, @@+, @@@+
                     maxSig = -1;
                     offset++;
-                } else if (minSig > 1) {
-                    // @@#, @@##, @@@#
-                    throw new SkeletonSyntaxException("Invalid digits option for fraction rounder",
-                            segment);
                 } else {
+                    // @#, @##, @###
+                    // @@#, @@##, @@@#
                     maxSig = minSig;
                     for (; offset < segment.length(); offset++) {
                         if (segment.charAt(offset) == '#') {
@@ -1316,18 +1311,43 @@
                 }
             } else {
                 // @, @@, @@@
-                throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
+                maxSig = minSig;
             }
+            RoundingPriority priority;
             if (offset < segment.length()) {
-                throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
+                if (maxSig == -1) {
+                    throw new SkeletonSyntaxException(
+                        "Invalid digits option: Wildcard character not allowed with the priority annotation", segment);
+                }
+                if (segment.codePointAt(offset) == 'r') {
+                    priority = RoundingPriority.RELAXED;
+                    offset++;
+                } else if (segment.codePointAt(offset) == 's') {
+                    priority = RoundingPriority.STRICT;
+                    offset++;
+                } else {
+                    assert offset < segment.length();
+                    priority = RoundingPriority.RELAXED; // make compiler happy (uninitialized variable)
+                }
+                if (offset < segment.length()) {
+                    throw new SkeletonSyntaxException(
+                        "Invalid digits option for fraction rounder", segment);
+                }
+            } else if (maxSig == -1) {
+                // withMinDigits
+                maxSig = minSig;
+                minSig = 1;
+                priority = RoundingPriority.RELAXED;
+            } else if (minSig == 1) {
+                // withMaxDigits
+                priority = RoundingPriority.STRICT;
+            } else {
+                throw new SkeletonSyntaxException(
+                    "Invalid digits option: Priority annotation required", segment);
             }
 
             FractionPrecision oldRounder = (FractionPrecision) macros.precision;
-            if (maxSig == -1) {
-                macros.precision = oldRounder.withMinDigits(minSig);
-            } else {
-                macros.precision = oldRounder.withMaxDigits(maxSig);
-            }
+            macros.precision = oldRounder.withSignificantDigits(minSig, maxSig, priority);
             return true;
         }
 
@@ -1526,10 +1546,11 @@
                 Precision.FracSigRounderImpl impl = (Precision.FracSigRounderImpl) macros.precision;
                 BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
                 sb.append('/');
-                if (impl.minSig == -1) {
-                    BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
+                BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
+                if (impl.priority == RoundingPriority.RELAXED) {
+                    sb.append('r');
                 } else {
-                    BlueprintHelpers.generateDigitsStem(impl.minSig, -1, sb);
+                    sb.append('s');
                 }
             } else if (macros.precision instanceof Precision.IncrementRounderImpl) {
                 Precision.IncrementRounderImpl impl = (Precision.IncrementRounderImpl) macros.precision;
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java b/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
index 0b46cf6..2d0a28a 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
@@ -9,6 +9,7 @@
 import com.ibm.icu.impl.number.DecimalQuantity;
 import com.ibm.icu.impl.number.MultiplierProducer;
 import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.number.NumberFormatter.RoundingPriority;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.Currency.CurrencyUsage;
 
@@ -382,7 +383,7 @@
     static final SignificantRounderImpl FIXED_SIG_3 = new SignificantRounderImpl(3, 3);
     static final SignificantRounderImpl RANGE_SIG_2_3 = new SignificantRounderImpl(2, 3);
 
-    static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 2, -1);
+    static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 1, 2, RoundingPriority.RELAXED);
 
     static final IncrementFiveRounderImpl NICKEL = new IncrementFiveRounderImpl(new BigDecimal("0.05"), 2, 2);
 
@@ -418,14 +419,16 @@
         }
     }
 
-    static Precision constructFractionSignificant(FractionPrecision base_, int minSig, int maxSig) {
+    static Precision constructFractionSignificant(
+            FractionPrecision base_, int minSig, int maxSig, RoundingPriority priority) {
         assert base_ instanceof FractionRounderImpl;
         FractionRounderImpl base = (FractionRounderImpl) base_;
         Precision returnValue;
-        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 2 /* && maxSig == -1 */) {
+        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 1 && maxSig == 2 &&
+                priority == RoundingPriority.RELAXED) {
             returnValue = COMPACT_STRATEGY;
         } else {
-            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig);
+            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig, priority);
         }
         return returnValue.withMode(base.mathContext);
     }
@@ -683,34 +686,37 @@
         final int maxFrac;
         final int minSig;
         final int maxSig;
+        final RoundingPriority priority;
 
-        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig) {
+        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig, RoundingPriority priority) {
             this.minFrac = minFrac;
             this.maxFrac = maxFrac;
             this.minSig = minSig;
             this.maxSig = maxSig;
+            this.priority = priority;
         }
 
         @Override
         public void apply(DecimalQuantity value) {
-            int displayMag = getDisplayMagnitudeFraction(minFrac);
-            int roundingMag = getRoundingMagnitudeFraction(maxFrac);
-            if (minSig == -1) {
-                // Max Sig override
-                int candidate = getRoundingMagnitudeSignificant(value, maxSig);
-                roundingMag = Math.max(roundingMag, candidate);
+            int roundingMag1 = getRoundingMagnitudeFraction(maxFrac);
+            int roundingMag2 = getRoundingMagnitudeSignificant(value, maxSig);
+            int roundingMag;
+            if (priority == RoundingPriority.RELAXED) {
+                roundingMag = Math.min(roundingMag1, roundingMag2);
             } else {
-                // Min Sig override
-                int candidate = getDisplayMagnitudeSignificant(value, minSig);
-                roundingMag = Math.min(roundingMag, candidate);
+                roundingMag = Math.max(roundingMag1, roundingMag2);
             }
             value.roundToMagnitude(roundingMag, mathContext);
+
+            int displayMag1 = getDisplayMagnitudeFraction(minFrac);
+            int displayMag2 = getDisplayMagnitudeSignificant(value, minSig);
+            int displayMag = Math.min(displayMag1, displayMag2);
             value.setMinFraction(Math.max(0, -displayMag));
         }
 
         @Override
         FracSigRounderImpl createCopy() {
-            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig);
+            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig, priority);
             copy.mathContext = mathContext;
             return copy;
         }
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 7f1f75d..d2414df 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
@@ -37,6 +37,7 @@
 import com.ibm.icu.number.NumberFormatter;
 import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
 import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
+import com.ibm.icu.number.NumberFormatter.RoundingPriority;
 import com.ibm.icu.number.NumberFormatter.SignDisplay;
 import com.ibm.icu.number.NumberFormatter.UnitWidth;
 import com.ibm.icu.number.Precision;
@@ -2518,6 +2519,71 @@
                 ULocale.ENGLISH,
                 0.0999999,
                 "0.10");
+
+        assertFormatDescending(
+                "FracSig withSignificantDigits RELAXED",
+                "precision-integer/@#r",
+                "./@#r",
+                NumberFormatter.with().precision(Precision.maxFraction(0)
+                        .withSignificantDigits(1, 2, RoundingPriority.RELAXED)),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876",
+                "88",
+                "8.8",
+                "0.88",
+                "0.088",
+                "0.0088",
+                "0");
+
+        assertFormatDescending(
+                "FracSig withSignificantDigits STRICT",
+                "precision-integer/@#s",
+                "./@#",
+                NumberFormatter.with().precision(Precision.maxFraction(0)
+                        .withSignificantDigits(1, 2, RoundingPriority.STRICT)),
+                ULocale.ENGLISH,
+                "88,000",
+                "8,800",
+                "880",
+                "88",
+                "9",
+                "1",
+                "0",
+                "0",
+                "0");
+        
+        assertFormatSingle(
+                "FracSig withSignificantDigits Trailing Zeros RELAXED",
+                ".0/@@@r",
+                ".0/@@@r",
+                NumberFormatter.with().precision(Precision.fixedFraction(1)
+                        .withSignificantDigits(3, 3, RoundingPriority.RELAXED)),
+                ULocale.ENGLISH,
+                1,
+                "1.00");
+        
+        // Trailing zeros are always retained:
+        assertFormatSingle(
+                "FracSig withSignificantDigits Trailing Zeros STRICT",
+                ".0/@@@s",
+                ".0/@@@s",
+                NumberFormatter.with().precision(Precision.fixedFraction(1)
+                        .withSignificantDigits(3, 3, RoundingPriority.STRICT)),
+                ULocale.ENGLISH,
+                1,
+                "1.00");
+
+        assertFormatSingle(
+                "FracSig withSignificantDigits at rounding boundary",
+                "precision-integer/@@@s",
+                "./@@@s",
+                NumberFormatter.with().precision(Precision.fixedFraction(0)
+                        .withSignificantDigits(3, 3, RoundingPriority.STRICT)),
+                ULocale.ENGLISH,
+                9.99,
+                "10.0");
     }
 
     @Test
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 bda900b..7f9645f 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
@@ -40,6 +40,10 @@
                 ".00/@@*",
                 ".00/@@+",
                 ".00/@##",
+                ".00/@",
+                ".00/@r",
+                ".00/@@s",
+                ".00/@@#r",
                 "precision-increment/3.14",
                 "precision-currency-standard",
                 "precision-integer rounding-mode-half-up",
@@ -142,13 +146,13 @@
                 "@#+",
                 "@@x",
                 "@@##0",
-                ".00/@",
                 ".00/@@",
                 ".00/@@x",
                 ".00/@@#",
                 ".00/@@#*",
                 ".00/floor/@@*", // wrong order
                 ".00/@@#+",
+                ".00/@@@+r",
                 ".00/floor/@@+", // wrong order
                 "precision-increment/français", // non-invariant characters for C++
                 "scientific/ee",
@@ -322,7 +326,6 @@
         String[][] cases = {
             { ".00*", ".00+" },
             { "@@*", "@@+" },
-            { ".00/@@*", ".00/@@+" },
             { "scientific/*ee", "scientific/+ee" },
             { "integer-width/*00", "integer-width/+00" },
         };