]> granicus.if.org Git - icu/commitdiff
ICU-21886 Make rounding priority consistent with ECMA-402
authorShane F. Carr <shane@unicode.org>
Sat, 19 Feb 2022 03:29:05 +0000 (03:29 +0000)
committerShane F. Carr <shane@unicode.org>
Tue, 22 Feb 2022 22:40:54 +0000 (15:40 -0700)
See #1989

icu4c/source/i18n/number_rounding.cpp
icu4c/source/i18n/number_skeletons.cpp
icu4c/source/i18n/unicode/numberformatter.h
icu4c/source/test/intltest/numbertest.h
icu4c/source/test/intltest/numbertest_api.cpp
icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java
icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java

index 877df63c8f68efebdde6982fa19d32b159e13cca..473eebbb62226be04b9c12fdd7dd584688605c31 100644 (file)
@@ -226,7 +226,8 @@ Precision FractionPrecision::withSignificantDigits(
             *this,
             minSignificantDigits,
             maxSignificantDigits,
-            priority);
+            priority,
+            false);
     } else {
         return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
     }
@@ -239,7 +240,8 @@ Precision FractionPrecision::withMinDigits(int32_t minSignificantDigits) const {
             *this,
             1,
             minSignificantDigits,
-            UNUM_ROUNDING_PRIORITY_RELAXED);
+            UNUM_ROUNDING_PRIORITY_RELAXED,
+            true);
     } else {
         return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
     }
@@ -251,7 +253,8 @@ Precision FractionPrecision::withMaxDigits(int32_t maxSignificantDigits) const {
         return constructFractionSignificant(*this,
             1,
             maxSignificantDigits,
-            UNUM_ROUNDING_PRIORITY_STRICT);
+            UNUM_ROUNDING_PRIORITY_STRICT,
+            true);
     } else {
         return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
     }
@@ -318,11 +321,13 @@ Precision::constructFractionSignificant(
         const FractionPrecision &base,
         int32_t minSig,
         int32_t maxSig,
-        UNumberRoundingPriority priority) {
+        UNumberRoundingPriority priority,
+        bool retain) {
     FractionSignificantSettings settings = base.fUnion.fracSig;
     settings.fMinSig = static_cast<digits_t>(minSig);
     settings.fMaxSig = static_cast<digits_t>(maxSig);
     settings.fPriority = priority;
+    settings.fRetain = retain;
     PrecisionUnion union_;
     union_.fracSig = settings;
     return {RND_FRACTION_SIGNIFICANT, union_};
@@ -457,6 +462,23 @@ void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const
             break;
 
         case Precision::RND_FRACTION_SIGNIFICANT: {
+            // From ECMA-402:
+            /*
+            Let sResult be ToRawPrecision(...).
+            Let fResult be ToRawFixed(...).
+            If intlObj.[[RoundingType]] is morePrecision, then
+                If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then
+                    Let result be sResult.
+                Else,
+                    Let result be fResult.
+            Else,
+                Assert: intlObj.[[RoundingType]] is lessPrecision.
+                If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then
+                    Let result be fResult.
+                Else,
+                    Let result be sResult.
+            */
+
             int32_t roundingMag1 = getRoundingMagnitudeFraction(fPrecision.fUnion.fracSig.fMaxFrac);
             int32_t roundingMag2 = getRoundingMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMaxSig);
             int32_t roundingMag;
@@ -465,11 +487,35 @@ void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const
             } else {
                 roundingMag = uprv_max(roundingMag1, roundingMag2);
             }
-            value.roundToMagnitude(roundingMag, fRoundingMode, status);
+            if (!value.isZeroish()) {
+                int32_t upperMag = value.getMagnitude();
+                value.roundToMagnitude(roundingMag, fRoundingMode, status);
+                if (!value.isZeroish() && value.getMagnitude() != upperMag && roundingMag1 == roundingMag2) {
+                    // roundingMag2 needs to be the magnitude after rounding
+                    roundingMag2 += 1;
+                }
+            }
 
             int32_t displayMag1 = getDisplayMagnitudeFraction(fPrecision.fUnion.fracSig.fMinFrac);
             int32_t displayMag2 = getDisplayMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMinSig);
-            int32_t displayMag = uprv_min(displayMag1, displayMag2);
+            int32_t displayMag;
+            if (fPrecision.fUnion.fracSig.fRetain) {
+                // withMinDigits + withMaxDigits
+                displayMag = uprv_min(displayMag1, displayMag2);
+            } else if (fPrecision.fUnion.fracSig.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) {
+                if (roundingMag2 <= roundingMag1) {
+                    displayMag = displayMag2;
+                } else {
+                    displayMag = displayMag1;
+                }
+            } else {
+                U_ASSERT(fPrecision.fUnion.fracSig.fPriority == UNUM_ROUNDING_PRIORITY_STRICT);
+                if (roundingMag2 <= roundingMag1) {
+                    displayMag = displayMag1;
+                } else {
+                    displayMag = displayMag2;
+                }
+            }
             resolvedMinFraction = uprv_max(0, -displayMag);
 
             break;
index de70c5cedff3ca185579e49d3c3a02830e937d74..0cdf6c60a94dd22d802b7905a514ea7629c0ebf5 100644 (file)
@@ -1344,8 +1344,9 @@ bool blueprint_helpers::parseFracSigOption(const StringSegment& segment, MacroPr
         // @, @@, @@@
         maxSig = minSig;
     }
-    UNumberRoundingPriority priority;
+    auto& oldPrecision = static_cast<const FractionPrecision&>(macros.precision);
     if (offset < segment.length()) {
+        UNumberRoundingPriority priority;
         if (maxSig == -1) {
             // The wildcard character is not allowed with the priority annotation
             status = U_NUMBER_SKELETON_SYNTAX_ERROR;
@@ -1367,22 +1368,19 @@ bool blueprint_helpers::parseFracSigOption(const StringSegment& segment, MacroPr
             status = U_NUMBER_SKELETON_SYNTAX_ERROR;
             return false;
         }
+        macros.precision = oldPrecision.withSignificantDigits(minSig, maxSig, priority);
     } else if (maxSig == -1) {
         // withMinDigits
-        maxSig = minSig;
-        minSig = 1;
-        priority = UNUM_ROUNDING_PRIORITY_RELAXED;
+        macros.precision = oldPrecision.withMinDigits(minSig);
     } else if (minSig == 1) {
         // withMaxDigits
-        priority = UNUM_ROUNDING_PRIORITY_STRICT;
+        macros.precision = oldPrecision.withMaxDigits(maxSig);
     } 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);
-    macros.precision = oldPrecision.withSignificantDigits(minSig, maxSig, priority);
     return true;
 }
 
@@ -1617,11 +1615,21 @@ bool GeneratorHelpers::precision(const MacroProps& macros, UnicodeString& sb, UE
         const Precision::FractionSignificantSettings& impl = macros.precision.fUnion.fracSig;
         blueprint_helpers::generateFractionStem(impl.fMinFrac, impl.fMaxFrac, sb, status);
         sb.append(u'/');
-        blueprint_helpers::generateDigitsStem(impl.fMinSig, impl.fMaxSig, sb, status);
-        if (impl.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) {
-            sb.append(u'r');
+        if (impl.fRetain) {
+            if (impl.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) {
+                // withMinDigits
+                blueprint_helpers::generateDigitsStem(impl.fMaxSig, -1, sb, status);
+            } else {
+                // withMaxDigits
+                blueprint_helpers::generateDigitsStem(1, impl.fMaxSig, sb, status);
+            }
         } else {
-            sb.append(u's');
+            blueprint_helpers::generateDigitsStem(impl.fMinSig, impl.fMaxSig, sb, status);
+            if (impl.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) {
+                sb.append(u'r');
+            } else {
+                sb.append(u's');
+            }
         }
     } else if (macros.precision.fType == Precision::RND_INCREMENT
             || macros.precision.fType == Precision::RND_INCREMENT_ONE
index ece433b55f09ea98af0dce1e3fb6c664073fef25..dbbb7ff28af12725839a404ad5f72fcb8d16ec77 100644 (file)
@@ -707,6 +707,11 @@ class U_I18N_API Precision : public UMemory {
             impl::digits_t fMaxSig;
             /** @internal (private) */
             UNumberRoundingPriority fPriority;
+            /**
+             * Whether to retain trailing zeros based on the looser strategy.
+             * @internal (private)
+             */
+            bool fRetain;
         } fracSig;
         /** @internal (private) */
         struct IncrementSettings {
@@ -759,7 +764,8 @@ class U_I18N_API Precision : public UMemory {
         const FractionPrecision &base,
         int32_t minSig,
         int32_t maxSig,
-        UNumberRoundingPriority priority);
+        UNumberRoundingPriority priority,
+        bool retain);
 
     static IncrementPrecision constructIncrement(double increment, int32_t minFrac);
 
index d6d71543fb605f7741f133d838074ec2de0a4c57..2e134d5a3745f722be18807df67c1f3f676b3851 100644 (file)
@@ -75,6 +75,7 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition {
     void roundingFractionFigures();
     void roundingOther();
     void roundingIncrementRegressionTest();
+    void roundingPriorityCoverageTest();
     void grouping();
     void padding();
     void integerWidth();
index 8a357436cd151a96fe7ec4743770342386a38c85..574ee958ffc0f6aaa04190d5aaad437255b9d304 100644 (file)
@@ -99,6 +99,7 @@ void NumberFormatterApiTest::runIndexedTest(int32_t index, UBool exec, const cha
         TESTCASE_AUTO(roundingFractionFigures);
         TESTCASE_AUTO(roundingOther);
         TESTCASE_AUTO(roundingIncrementRegressionTest);
+        TESTCASE_AUTO(roundingPriorityCoverageTest);
         TESTCASE_AUTO(grouping);
         TESTCASE_AUTO(padding);
         TESTCASE_AUTO(integerWidth);
@@ -3032,6 +3033,15 @@ void NumberFormatterApiTest::roundingFigures() {
             -98.7654321,
             u"-98.8");
 
+    assertFormatSingle(
+            u"Fixed Significant at rounding boundary",
+            u"@@@",
+            u"@@@",
+            NumberFormatter::with().precision(Precision::fixedSignificantDigits(3)),
+            Locale::getEnglish(),
+            9.999,
+            u"10.0");
+
     assertFormatSingle(
             u"Fixed Significant Zero",
             u"@@@",
@@ -3192,7 +3202,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
     assertFormatDescending(
             u"FracSig withSignificantDigits STRICT",
             u"precision-integer/@#s",
-            u"./@#",
+            u"./@#s",
             NumberFormatter::with().precision(Precision::maxFraction(0)
                 .withSignificantDigits(1, 2, UNUM_ROUNDING_PRIORITY_STRICT)),
             Locale::getEnglish(),
@@ -3216,7 +3226,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
             1,
             u"1.00");
 
-    // Trailing zeros are always retained:
+    // Trailing zeros follow the strategy that was chosen:
     assertFormatSingle(
             u"FracSig withSignificantDigits Trailing Zeros STRICT",
             u".0/@@@s",
@@ -3225,7 +3235,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
                 .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_STRICT)),
             Locale::getEnglish(),
             1,
-            u"1.00");
+            u"1.0");
 
     assertFormatSingle(
             u"FracSig withSignificantDigits at rounding boundary",
@@ -3235,7 +3245,7 @@ void NumberFormatterApiTest::roundingFractionFigures() {
                     .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_STRICT)),
             Locale::getEnglish(),
             9.99,
-            u"10.0");
+            u"10");
 
     assertFormatSingle(
             u"FracSig with Trailing Zero Display",
@@ -3619,6 +3629,67 @@ void NumberFormatterApiTest::roundingIncrementRegressionTest() {
     assertEquals("ICU-21668", u"5,000", increment);
 }
 
+void NumberFormatterApiTest::roundingPriorityCoverageTest() {
+    IcuTestErrorCode status(*this, "roundingPriorityCoverageTest");
+    struct TestCase {
+        double input;
+        const char16_t* expectedRelaxed0113;
+        const char16_t* expectedStrict0113;
+        const char16_t* expectedRelaxed1133;
+        const char16_t* expectedStrict1133;
+    } cases[] = {
+        { 0.9999, u"1",      u"1",     u"1.00",    u"1.0" },
+        { 9.9999, u"10",     u"10",    u"10.0",    u"10.0" },
+        { 99.999, u"100",    u"100",   u"100.0",   u"100" },
+        { 999.99, u"1000",   u"1000",  u"1000.0",  u"1000" },
+
+        { 0, u"0", u"0", u"0.00", u"0.0" },
+
+        { 9.876,  u"9.88",   u"9.9",   u"9.88",   u"9.9" },
+        { 9.001,  u"9",      u"9",     u"9.00",   u"9.0" },
+    };
+    for (const auto& cas : cases) {
+        auto precisionRelaxed0113 = Precision::minMaxFraction(0, 1)
+            .withSignificantDigits(1, 3, UNUM_ROUNDING_PRIORITY_RELAXED);
+        auto precisionStrict0113 = Precision::minMaxFraction(0, 1)
+            .withSignificantDigits(1, 3, UNUM_ROUNDING_PRIORITY_STRICT);
+        auto precisionRelaxed1133 = Precision::minMaxFraction(1, 1)
+            .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_RELAXED);
+        auto precisionStrict1133 = Precision::minMaxFraction(1, 1)
+            .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_STRICT);
+
+        auto messageBase = DoubleToUnicodeString(cas.input);
+
+        auto check = [&](
+            const char16_t* name,
+            const UnicodeString& expected,
+            const Precision& precision
+        ) {
+            assertEquals(
+                messageBase + name,
+                expected,
+                NumberFormatter::withLocale(Locale::getEnglish())
+                    .precision(precision)
+                    .grouping(UNUM_GROUPING_OFF)
+                    .formatDouble(cas.input, status)
+                    .toString(status)
+            );
+        };
+
+        check(u" Relaxed 0113", cas.expectedRelaxed0113, precisionRelaxed0113);
+        if (status.errIfFailureAndReset()) continue;
+
+        check(u" Strict 0113", cas.expectedStrict0113, precisionStrict0113);
+        if (status.errIfFailureAndReset()) continue;
+
+        check(u" Relaxed 1133", cas.expectedRelaxed1133, precisionRelaxed1133);
+        if (status.errIfFailureAndReset()) continue;
+
+        check(u" Strict 1133", cas.expectedStrict1133, precisionStrict1133);
+        if (status.errIfFailureAndReset()) continue;
+    }
+}
+
 void NumberFormatterApiTest::grouping() {
     assertFormatDescendingBig(
             u"Western Grouping",
index 90c2fd2878fed42e4931f11c93859dc5f3d917b8..6a10bdaa7744d089cd70fea7c4ebf15d2cba9ae1 100644 (file)
@@ -41,7 +41,7 @@ public abstract class FractionPrecision extends Precision {
                 maxSignificantDigits >= minSignificantDigits &&
                 maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
             return constructFractionSignificant(
-                this, minSignificantDigits, maxSignificantDigits, priority);
+                this, minSignificantDigits, maxSignificantDigits, priority, false);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
@@ -74,7 +74,7 @@ public abstract class FractionPrecision extends Precision {
     public Precision withMinDigits(int minSignificantDigits) {
         if (minSignificantDigits >= 1 && minSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
             return constructFractionSignificant(
-                this, 1, minSignificantDigits, NumberFormatter.RoundingPriority.RELAXED);
+                this, 1, minSignificantDigits, NumberFormatter.RoundingPriority.RELAXED, true);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
@@ -107,7 +107,7 @@ public abstract class FractionPrecision extends Precision {
     public Precision withMaxDigits(int maxSignificantDigits) {
         if (maxSignificantDigits >= 1 && maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
             return constructFractionSignificant(
-                this, 1, maxSignificantDigits, NumberFormatter.RoundingPriority.STRICT);
+                this, 1, maxSignificantDigits, NumberFormatter.RoundingPriority.STRICT, true);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
index c9e0369c5f9502b2a597aaba6eaf141835018eb4..8e60ba19f1762d31ffe6175fc347ae612036f8b5 100644 (file)
@@ -1335,8 +1335,9 @@ class NumberSkeletonImpl {
                 // @, @@, @@@
                 maxSig = minSig;
             }
-            RoundingPriority priority;
+            FractionPrecision oldRounder = (FractionPrecision) macros.precision;
             if (offset < segment.length()) {
+                RoundingPriority priority;
                 if (maxSig == -1) {
                     throw new SkeletonSyntaxException(
                         "Invalid digits option: Wildcard character not allowed with the priority annotation", segment);
@@ -1355,21 +1356,18 @@ class NumberSkeletonImpl {
                     throw new SkeletonSyntaxException(
                         "Invalid digits option for fraction rounder", segment);
                 }
+                macros.precision = oldRounder.withSignificantDigits(minSig, maxSig, priority);
             } else if (maxSig == -1) {
                 // withMinDigits
-                maxSig = minSig;
-                minSig = 1;
-                priority = RoundingPriority.RELAXED;
+                macros.precision = oldRounder.withMinDigits(minSig);
             } else if (minSig == 1) {
                 // withMaxDigits
-                priority = RoundingPriority.STRICT;
+                macros.precision = oldRounder.withMaxDigits(maxSig);
             } else {
                 throw new SkeletonSyntaxException(
                     "Invalid digits option: Priority annotation required", segment);
             }
 
-            FractionPrecision oldRounder = (FractionPrecision) macros.precision;
-            macros.precision = oldRounder.withSignificantDigits(minSig, maxSig, priority);
             return true;
         }
 
@@ -1577,11 +1575,19 @@ class NumberSkeletonImpl {
                 Precision.FracSigRounderImpl impl = (Precision.FracSigRounderImpl) macros.precision;
                 BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
                 sb.append('/');
-                BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
-                if (impl.priority == RoundingPriority.RELAXED) {
-                    sb.append('r');
+                if (impl.retain) {
+                    if (impl.priority == RoundingPriority.RELAXED) {
+                        BlueprintHelpers.generateDigitsStem(impl.maxSig, -1, sb);
+                    } else {
+                        BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
+                    }
                 } else {
-                    sb.append('s');
+                    BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
+                    if (impl.priority == RoundingPriority.RELAXED) {
+                        sb.append('r');
+                    } else {
+                        sb.append('s');
+                    }
                 }
             } else if (macros.precision instanceof Precision.IncrementRounderImpl) {
                 Precision.IncrementRounderImpl impl = (Precision.IncrementRounderImpl) macros.precision;
index 15fd8f2d1e6ad8dfb1ca6a96593859b59cbb079b..916fa733a4bf97643467d1fc1cd4ebb01349951e 100644 (file)
@@ -399,7 +399,8 @@ public abstract class Precision {
     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, 1, 2, RoundingPriority.RELAXED);
+    static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 1, 2, RoundingPriority.RELAXED,
+            false);
 
     static final IncrementFiveRounderImpl NICKEL = new IncrementFiveRounderImpl(new BigDecimal("0.05"), 2, 2);
 
@@ -435,16 +436,16 @@ public abstract class Precision {
         }
     }
 
-    static Precision constructFractionSignificant(
-            FractionPrecision base_, int minSig, int maxSig, RoundingPriority priority) {
+    static Precision constructFractionSignificant(FractionPrecision base_, int minSig, int maxSig,
+            RoundingPriority priority, boolean retain) {
         assert base_ instanceof FractionRounderImpl;
         FractionRounderImpl base = (FractionRounderImpl) base_;
         Precision returnValue;
-        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 1 && maxSig == 2 &&
-                priority == RoundingPriority.RELAXED) {
+        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 1 && maxSig == 2 && priority == RoundingPriority.RELAXED
+                && !retain) {
             returnValue = COMPACT_STRATEGY;
         } else {
-            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig, priority);
+            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig, priority, retain);
         }
         return returnValue.withMode(base.mathContext);
     }
@@ -703,13 +704,16 @@ public abstract class Precision {
         final int minSig;
         final int maxSig;
         final RoundingPriority priority;
+        final boolean retain;
 
-        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig, RoundingPriority priority) {
+        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig, RoundingPriority priority,
+                boolean retain) {
             this.minFrac = minFrac;
             this.maxFrac = maxFrac;
             this.minSig = minSig;
             this.maxSig = maxSig;
             this.priority = priority;
+            this.retain = retain;
         }
 
         @Override
@@ -722,17 +726,41 @@ public abstract class Precision {
             } else {
                 roundingMag = Math.max(roundingMag1, roundingMag2);
             }
-            value.roundToMagnitude(roundingMag, mathContext);
+            if (!value.isZeroish()) {
+                int upperMag = value.getMagnitude();
+                value.roundToMagnitude(roundingMag, mathContext);
+                if (!value.isZeroish() && value.getMagnitude() != upperMag && roundingMag1 == roundingMag2) {
+                    // roundingMag2 needs to be the magnitude after rounding
+                    roundingMag2 += 1;
+                }
+            }
 
             int displayMag1 = getDisplayMagnitudeFraction(minFrac);
             int displayMag2 = getDisplayMagnitudeSignificant(value, minSig);
-            int displayMag = Math.min(displayMag1, displayMag2);
+            int displayMag;
+            if (retain) {
+                // withMinDigits + withMaxDigits
+                displayMag = Math.min(displayMag1, displayMag2);
+            } else if (priority == RoundingPriority.RELAXED) {
+                if (roundingMag2 <= roundingMag1) {
+                    displayMag = displayMag2;
+                } else {
+                    displayMag = displayMag1;
+                }
+            } else {
+                assert(priority == RoundingPriority.STRICT);
+                if (roundingMag2 <= roundingMag1) {
+                    displayMag = displayMag1;
+                } else {
+                    displayMag = displayMag2;
+                }
+            }
             setResolvedMinFraction(value, Math.max(0, -displayMag));
         }
 
         @Override
         FracSigRounderImpl createCopy() {
-            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig, priority);
+            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig, priority, retain);
             copy.mathContext = mathContext;
             return copy;
         }
index ec2f5f51bd1dd386f0b235d8e72c8160da601a5c..5d6dde5aa489a6e0effc77bb8f1bc14a6a3af90d 100644 (file)
@@ -2981,7 +2981,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 ULocale.ENGLISH,
                 1.2,
                 "1.20");
-        
+
         assertFormatSingle(
                 "Hide If Whole B",
                 ".00/w",
@@ -3013,6 +3013,15 @@ public class NumberFormatterApiTest extends TestFmwk {
                 -98.7654321,
                 "-98.8");
 
+        assertFormatSingle(
+                "Fixed Significant at rounding boundary",
+                "@@@",
+                "@@@",
+                NumberFormatter.with().precision(Precision.fixedSignificantDigits(3)),
+                ULocale.ENGLISH,
+                9.999,
+                "10.0");
+
         assertFormatSingle(
                 "Fixed Significant Zero",
                 "@@@",
@@ -3188,7 +3197,7 @@ public class NumberFormatterApiTest extends TestFmwk {
         assertFormatDescending(
                 "FracSig withSignificantDigits STRICT",
                 "precision-integer/@#s",
-                "./@#",
+                "./@#s",
                 NumberFormatter.with().precision(Precision.maxFraction(0)
                         .withSignificantDigits(1, 2, RoundingPriority.STRICT)),
                 ULocale.ENGLISH,
@@ -3201,7 +3210,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 "0",
                 "0",
                 "0");
-        
+
         assertFormatSingle(
                 "FracSig withSignificantDigits Trailing Zeros RELAXED",
                 ".0/@@@r",
@@ -3211,8 +3220,8 @@ public class NumberFormatterApiTest extends TestFmwk {
                 ULocale.ENGLISH,
                 1,
                 "1.00");
-        
-        // Trailing zeros are always retained:
+
+        // Trailing zeros follow the strategy that was chosen:
         assertFormatSingle(
                 "FracSig withSignificantDigits Trailing Zeros STRICT",
                 ".0/@@@s",
@@ -3221,7 +3230,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                         .withSignificantDigits(3, 3, RoundingPriority.STRICT)),
                 ULocale.ENGLISH,
                 1,
-                "1.00");
+                "1.0");
 
         assertFormatSingle(
                 "FracSig withSignificantDigits at rounding boundary",
@@ -3231,7 +3240,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                         .withSignificantDigits(3, 3, RoundingPriority.STRICT)),
                 ULocale.ENGLISH,
                 9.99,
-                "10.0");
+                "10");
 
         assertFormatSingle(
                 "FracSig with Trailing Zero Display",
@@ -3327,7 +3336,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 "50",
                 "50",
                 "0");
-        
+
         assertFormatDescending(
                 "Large nickel increment with rounding mode up (ICU-21668)",
                 "precision-increment/5000 rounding-mode-up",
@@ -3345,7 +3354,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 "5,000",
                 "5,000",
                 "0");
-        
+
         assertFormatDescending(
                 "Large dime increment with rounding mode up (ICU-21668)",
                 "precision-increment/10000 rounding-mode-up",
@@ -3363,7 +3372,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 "10,000",
                 "10,000",
                 "0");
-        
+
         assertFormatDescending(
                 "Large non-nickel increment with rounding mode up (ICU-21668)",
                 "precision-increment/15000 rounding-mode-up",
@@ -3607,6 +3616,67 @@ public class NumberFormatterApiTest extends TestFmwk {
         assertEquals("ICU-21668", "5,000", increment);
     }
 
+    static interface RoundingPriorityCheckFn {
+        void check(String name, String expected, Precision precision);
+    }
+
+    @Test
+    public void roundingPriorityCoverageTest() {
+        String[][] cases = new String[][] {
+            // Input, relaxed 0113, strict 0113, relaxed 1133, strict 1133
+            { "0.9999", "1",      "1",     "1.00",    "1.0" },
+            { "9.9999", "10",     "10",    "10.0",    "10.0" },
+            { "99.999", "100",    "100",   "100.0",   "100" },
+            { "999.99", "1000",   "1000",  "1000.0",  "1000" },
+
+            { "0", "0", "0", "0.00", "0.0" },
+
+            { "9.876",  "9.88",   "9.9",   "9.88",   "9.9" },
+            { "9.001",  "9",      "9",     "9.00",   "9.0" },
+        };
+        for (String[] cas : cases) {
+            final double input = Double.parseDouble(cas[0]);
+            String expectedRelaxed0113 = cas[1];
+            String expectedStrict0113 = cas[2];
+            String expectedRelaxed1133 = cas[3];
+            String expectedStrict1133 = cas[4];
+
+            Precision precisionRelaxed0113 = Precision.minMaxFraction(0, 1)
+                .withSignificantDigits(1, 3, RoundingPriority.RELAXED);
+            Precision precisionStrict0113 = Precision.minMaxFraction(0, 1)
+                .withSignificantDigits(1, 3, RoundingPriority.STRICT);
+            Precision precisionRelaxed1133 = Precision.minMaxFraction(1, 1)
+                .withSignificantDigits(3, 3, RoundingPriority.RELAXED);
+            Precision precisionStrict1133 = Precision.minMaxFraction(1, 1)
+                .withSignificantDigits(3, 3, RoundingPriority.STRICT);
+
+            final String messageBase = cas[0];
+
+            RoundingPriorityCheckFn checker = new RoundingPriorityCheckFn() {
+                @Override
+                public void check(String name, String expected, Precision precision) {
+                    assertEquals(
+                        messageBase + name,
+                        expected,
+                        NumberFormatter.withLocale(ULocale.ENGLISH)
+                            .precision(precision)
+                            .grouping(GroupingStrategy.OFF)
+                            .format(input)
+                            .toString()
+                    );
+                }
+            };
+
+            checker.check(" Relaxed 0113", expectedRelaxed0113, precisionRelaxed0113);
+
+            checker.check(" Strict 0113", expectedStrict0113, precisionStrict0113);
+
+            checker.check(" Relaxed 1133", expectedRelaxed1133, precisionRelaxed1133);
+
+            checker.check(" Strict 1133", expectedStrict1133, precisionStrict1133);
+        }
+    }
+
     @Test
     public void grouping() {
         assertFormatDescendingBig(
@@ -4521,7 +4591,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 ULocale.ENGLISH,
                 444444,
                 "444,444");
-        
+
         assertFormatSingle(
                 "Sign Negative Negative",
                 "sign-negative",
@@ -4530,7 +4600,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 ULocale.ENGLISH,
                 -444444,
                 "-444,444");
-        
+
         assertFormatSingle(
                 "Sign Negative Negative Zero",
                 "sign-negative",
@@ -4539,7 +4609,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 ULocale.ENGLISH,
                 -0.0000001,
                 "0");
-        
+
         assertFormatSingle(
                 "Sign Accounting-Negative Positive",
                 "currency/USD sign-accounting-negative",
@@ -4548,7 +4618,7 @@ public class NumberFormatterApiTest extends TestFmwk {
                 ULocale.ENGLISH,
                 444444,
                 "$444,444.00");
-        
+
         assertFormatSingle(
                 "Sign Accounting-Negative Negative",
                 "currency/USD sign-accounting-negative",