From 9bb82e60a451065bb63bd106f30dee42860a79de Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" <shane@unicode.org> Date: Sat, 19 Feb 2022 03:29:05 +0000 Subject: [PATCH] ICU-21886 Make rounding priority consistent with ECMA-402 See #1989 --- icu4c/source/i18n/number_rounding.cpp | 58 +++++++++-- icu4c/source/i18n/number_skeletons.cpp | 30 +++--- icu4c/source/i18n/unicode/numberformatter.h | 8 +- icu4c/source/test/intltest/numbertest.h | 1 + icu4c/source/test/intltest/numbertest_api.cpp | 79 ++++++++++++++- .../com/ibm/icu/number/FractionPrecision.java | 6 +- .../ibm/icu/number/NumberSkeletonImpl.java | 28 +++--- .../src/com/ibm/icu/number/Precision.java | 48 +++++++-- .../test/number/NumberFormatterApiTest.java | 98 ++++++++++++++++--- 9 files changed, 296 insertions(+), 60 deletions(-) diff --git a/icu4c/source/i18n/number_rounding.cpp b/icu4c/source/i18n/number_rounding.cpp index 877df63c8f6..473eebbb622 100644 --- a/icu4c/source/i18n/number_rounding.cpp +++ b/icu4c/source/i18n/number_rounding.cpp @@ -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; diff --git a/icu4c/source/i18n/number_skeletons.cpp b/icu4c/source/i18n/number_skeletons.cpp index de70c5cedff..0cdf6c60a94 100644 --- a/icu4c/source/i18n/number_skeletons.cpp +++ b/icu4c/source/i18n/number_skeletons.cpp @@ -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 diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h index ece433b55f0..dbbb7ff28af 100644 --- a/icu4c/source/i18n/unicode/numberformatter.h +++ b/icu4c/source/i18n/unicode/numberformatter.h @@ -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); diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index d6d71543fb6..2e134d5a374 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -75,6 +75,7 @@ class NumberFormatterApiTest : public IntlTestWithFieldPosition { void roundingFractionFigures(); void roundingOther(); void roundingIncrementRegressionTest(); + void roundingPriorityCoverageTest(); void grouping(); void padding(); void integerWidth(); diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp index 8a357436cd1..574ee958ffc 100644 --- a/icu4c/source/test/intltest/numbertest_api.cpp +++ b/icu4c/source/test/intltest/numbertest_api.cpp @@ -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", 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 90c2fd2878f..6a10bdaa774 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 @@ -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 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 c9e0369c5f9..8e60ba19f17 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 @@ -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; 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 15fd8f2d1e6..916fa733a4b 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 @@ -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; } 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 ec2f5f51bd1..5d6dde5aa48 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 @@ -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", -- 2.40.0