]> granicus.if.org Git - icu/commitdiff
ICU-21908 Add incrementExact to ICU4C Precision
authorShane F. Carr <shane@unicode.org>
Tue, 22 Feb 2022 22:39:11 +0000 (22:39 +0000)
committerShane F. Carr <shane@unicode.org>
Thu, 24 Feb 2022 03:18:16 +0000 (20:18 -0700)
See #1979

14 files changed:
icu4c/source/i18n/number_decimalquantity.cpp
icu4c/source/i18n/number_decimalquantity.h
icu4c/source/i18n/number_mapper.cpp
icu4c/source/i18n/number_patternstring.cpp
icu4c/source/i18n/number_rounding.cpp
icu4c/source/i18n/number_roundingutils.h
icu4c/source/i18n/number_skeletons.cpp
icu4c/source/i18n/number_skeletons.h
icu4c/source/i18n/unicode/numberformatter.h
icu4c/source/test/intltest/numbertest_api.cpp
icu4c/source/test/intltest/numbertest_decimalquantity.cpp
icu4c/source/test/intltest/numfmtst.cpp
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 6a2847b1c18f191f441a6bba58784670fb61f687..ef53d3aa29b011454a2740b68bdf5ff41218f055 100644 (file)
@@ -181,20 +181,22 @@ uint64_t DecimalQuantity::getPositionFingerprint() const {
     return fingerprint;
 }
 
-void DecimalQuantity::roundToIncrement(double roundingIncrement, RoundingMode roundingMode,
-                                       UErrorCode& status) {
+void DecimalQuantity::roundToIncrement(
+        uint64_t increment,
+        digits_t magnitude,
+        RoundingMode roundingMode,
+        UErrorCode& status) {
     // Do not call this method with an increment having only a 1 or a 5 digit!
     // Use a more efficient call to either roundToMagnitude() or roundToNickel().
     // Check a few popular rounding increments; a more thorough check is in Java.
-    U_ASSERT(roundingIncrement != 0.01);
-    U_ASSERT(roundingIncrement != 0.05);
-    U_ASSERT(roundingIncrement != 0.1);
-    U_ASSERT(roundingIncrement != 0.5);
-    U_ASSERT(roundingIncrement != 1);
-    U_ASSERT(roundingIncrement != 5);
+    U_ASSERT(increment != 1);
+    U_ASSERT(increment != 5);
 
+    DecimalQuantity incrementDQ;
+    incrementDQ.setToLong(increment);
+    incrementDQ.adjustMagnitude(magnitude);
     DecNum incrementDN;
-    incrementDN.setTo(roundingIncrement, status);
+    incrementDQ.toDecNum(incrementDN, status);
     if (U_FAILURE(status)) { return; }
 
     // Divide this DecimalQuantity by the increment, round, then multiply back.
@@ -254,6 +256,12 @@ bool DecimalQuantity::adjustMagnitude(int32_t delta) {
     return false;
 }
 
+int32_t DecimalQuantity::adjustToZeroScale() {
+    int32_t retval = scale;
+    scale = 0;
+    return retval;
+}
+
 double DecimalQuantity::getPluralOperand(PluralOperand operand) const {
     // If this assertion fails, you need to call roundToInfinity() or some other rounding method.
     // See the comment at the top of this file explaining the "isApproximate" field.
index 107c09a96a53d25c92ce10bbedd61fc049d2e63c..891478969dbe4e5e1dd492aa30972c17fbd9be25 100644 (file)
@@ -81,11 +81,15 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
      *
      * <p>If rounding to a power of ten, use the more efficient {@link #roundToMagnitude} instead.
      *
-     * @param roundingIncrement The increment to which to round.
+     * @param increment The increment to which to round.
+     * @param magnitude The power of 10 to which to round.
      * @param roundingMode The {@link RoundingMode} to use if rounding is necessary.
      */
-    void roundToIncrement(double roundingIncrement, RoundingMode roundingMode,
-                          UErrorCode& status);
+    void roundToIncrement(
+        uint64_t increment,
+        digits_t magnitude,
+        RoundingMode roundingMode,
+        UErrorCode& status);
 
     /** Removes all fraction digits. */
     void truncate();
@@ -140,6 +144,13 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
      */
     bool adjustMagnitude(int32_t delta);
 
+    /**
+     * Scales the number such that the least significant nonzero digit is at magnitude 0.
+     *
+     * @return The previous magnitude of the least significant digit.
+     */
+    int32_t adjustToZeroScale();
+
     /**
      * @return The power of ten corresponding to the most significant nonzero digit.
      * The number must not be zero.
index 2d4d47a094d999bc5c23405d7b991c86db293b96..350c431dfdd0792ab94ac7f8330adcf7d7dd15c8 100644 (file)
@@ -134,7 +134,8 @@ MacroProps NumberPropertyMapper::oldToNew(const DecimalFormatProperties& propert
         if (PatternStringUtils::ignoreRoundingIncrement(roundingIncrement, maxFrac)) {
             precision = Precision::constructFraction(minFrac, maxFrac);
         } else {
-            precision = Precision::constructIncrement(roundingIncrement, minFrac);
+            // Convert the double increment to an integer increment
+            precision = Precision::increment(roundingIncrement).withMinFraction(minFrac);
         }
     } else if (explicitMinMaxSig) {
         minSig = minSig < 1 ? 1 : minSig > kMaxIntFracSig ? kMaxIntFracSig : minSig;
@@ -293,9 +294,14 @@ MacroProps NumberPropertyMapper::oldToNew(const DecimalFormatProperties& propert
         } else if (rounding_.fType == Precision::PrecisionType::RND_INCREMENT
                 || rounding_.fType == Precision::PrecisionType::RND_INCREMENT_ONE
                 || rounding_.fType == Precision::PrecisionType::RND_INCREMENT_FIVE) {
-            increment_ = rounding_.fUnion.increment.fIncrement;
             minFrac_ = rounding_.fUnion.increment.fMinFrac;
+            // If incrementRounding is used, maxFrac is set equal to minFrac
             maxFrac_ = rounding_.fUnion.increment.fMinFrac;
+            // Convert the integer increment to a double
+            DecimalQuantity dq;
+            dq.setToLong(rounding_.fUnion.increment.fIncrement);
+            dq.adjustMagnitude(rounding_.fUnion.increment.fIncrementMagnitude);
+            increment_ = dq.toDouble();
         } else if (rounding_.fType == Precision::PrecisionType::RND_SIGNIFICANT) {
             minSig_ = rounding_.fUnion.fracSig.fMinSig;
             maxSig_ = rounding_.fUnion.fracSig.fMaxSig;
index e819d39e96769d17c615e09fa1c6b1f61c92df92..2738895d8ad03fcf65d2f5a6d47658012ba25184 100644 (file)
@@ -750,7 +750,7 @@ UnicodeString PatternStringUtils::propertiesToPatternString(const DecimalFormatP
     int32_t groupingLength = grouping1 + grouping2 + 1;
 
     // Figure out the digits we need to put in the pattern.
-    double roundingInterval = properties.roundingIncrement;
+    double increment = properties.roundingIncrement;
     UnicodeString digitsString;
     int32_t digitsStringScale = 0;
     if (maxSig != uprv_min(dosMax, -1)) {
@@ -761,14 +761,14 @@ UnicodeString PatternStringUtils::propertiesToPatternString(const DecimalFormatP
         while (digitsString.length() < maxSig) {
             digitsString.append(u'#');
         }
-    } else if (roundingInterval != 0.0 && !ignoreRoundingIncrement(roundingInterval,maxFrac)) {
-        // Rounding Interval.
-        digitsStringScale = -roundingutils::doubleFractionLength(roundingInterval, nullptr);
-        // TODO: Check for DoS here?
+    } else if (increment != 0.0 && !ignoreRoundingIncrement(increment,maxFrac)) {
+        // Rounding Increment.
         DecimalQuantity incrementQuantity;
-        incrementQuantity.setToDouble(roundingInterval);
+        incrementQuantity.setToDouble(increment);
+        incrementQuantity.roundToInfinity();
+        digitsStringScale = incrementQuantity.getLowerDisplayMagnitude();
         incrementQuantity.adjustMagnitude(-digitsStringScale);
-        incrementQuantity.roundToMagnitude(0, kDefaultMode, status);
+        incrementQuantity.setMinInteger(minInt - digitsStringScale);
         UnicodeString str = incrementQuantity.toPlainString();
         if (str.charAt(0) == u'-') {
             // TODO: Unsupported operation exception or fail silently?
index 473eebbb62226be04b9c12fdd7dd584688605c31..a9b3f16c050d946c9084fe2dd3b5c567726e185f 100644 (file)
@@ -36,27 +36,24 @@ void number::impl::parseIncrementOption(const StringSegment &segment,
     // Utilize DecimalQuantity/decNumber to parse this for us.
     DecimalQuantity dq;
     UErrorCode localStatus = U_ZERO_ERROR;
-    DecNum decnum;
-    decnum.setTo({buffer.data(), buffer.length()}, localStatus);
-    dq.setToDecNum(decnum, localStatus);
-    if (U_FAILURE(localStatus) || decnum.isSpecial()) {
+    dq.setToDecNumber({buffer.data(), buffer.length()}, localStatus);
+    if (U_FAILURE(localStatus) || dq.isNaN() || dq.isInfinite()) {
         // throw new SkeletonSyntaxException("Invalid rounding increment", segment, e);
         status = U_NUMBER_SKELETON_SYNTAX_ERROR;
         return;
     }
-    double increment = dq.toDouble();
-
-    // We also need to figure out how many digits. Do a brute force string operation.
-    int decimalOffset = 0;
-    while (decimalOffset < segment.length() && segment.charAt(decimalOffset) != '.') {
-        decimalOffset++;
-    }
-    if (decimalOffset == segment.length()) {
-        outPrecision = Precision::increment(increment);
-    } else {
-        int32_t fractionLength = segment.length() - decimalOffset - 1;
-        outPrecision = Precision::increment(increment).withMinFraction(fractionLength);
+    // Now we break apart the number into a mantissa and exponent (magnitude).
+    int32_t magnitude = dq.adjustToZeroScale();
+    // setToDecNumber drops trailing zeros, so we search for the '.' manually.
+    for (int32_t i=0; i<buffer.length(); i++) {
+        if (buffer[i] == '.') {
+            int32_t newMagnitude = i - buffer.length() + 1;
+            dq.adjustMagnitude(magnitude - newMagnitude);
+            magnitude = newMagnitude;
+            break;
+        }
     }
+    outPrecision = Precision::incrementExact(dq.toLong(), magnitude);
 }
 
 namespace {
@@ -94,34 +91,6 @@ int32_t getDisplayMagnitudeSignificant(const DecimalQuantity &value, int minSig)
 MultiplierProducer::~MultiplierProducer() = default;
 
 
-digits_t roundingutils::doubleFractionLength(double input, int8_t* singleDigit) {
-    char buffer[DoubleToStringConverter::kBase10MaximalLength + 1];
-    bool sign; // unused; always positive
-    int32_t length;
-    int32_t point;
-    DoubleToStringConverter::DoubleToAscii(
-            input,
-            DoubleToStringConverter::DtoaMode::SHORTEST,
-            0,
-            buffer,
-            sizeof(buffer),
-            &sign,
-            &length,
-            &point
-    );
-
-    if (singleDigit == nullptr) {
-        // no-op
-    } else if (length == 1) {
-        *singleDigit = buffer[0] - '0';
-    } else {
-        *singleDigit = -1;
-    }
-
-    return static_cast<digits_t>(length - point);
-}
-
-
 Precision Precision::unlimited() {
     return Precision(RND_NONE, {});
 }
@@ -204,7 +173,19 @@ Precision Precision::trailingZeroDisplay(UNumberTrailingZeroDisplay trailingZero
 
 IncrementPrecision Precision::increment(double roundingIncrement) {
     if (roundingIncrement > 0.0) {
-        return constructIncrement(roundingIncrement, 0);
+        DecimalQuantity dq;
+        dq.setToDouble(roundingIncrement);
+        dq.roundToInfinity();
+        int32_t magnitude = dq.adjustToZeroScale();
+        return constructIncrement(dq.toLong(), magnitude);
+    } else {
+        return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
+    }
+}
+
+IncrementPrecision Precision::incrementExact(uint64_t mantissa, int16_t magnitude) {
+    if (mantissa > 0.0) {
+        return constructIncrement(mantissa, magnitude);
     } else {
         return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
     }
@@ -269,8 +250,8 @@ Precision Precision::withCurrency(const CurrencyUnit &currency, UErrorCode &stat
     int32_t minMaxFrac = ucurr_getDefaultFractionDigitsForUsage(
             isoCode, fUnion.currencyUsage, &status);
     Precision retval = (increment != 0.0)
-        ? static_cast<Precision>(constructIncrement(increment, minMaxFrac))
-        : static_cast<Precision>(constructFraction(minMaxFrac, minMaxFrac));
+        ? Precision::increment(increment)
+        : static_cast<Precision>(Precision::fixedFraction(minMaxFrac));
     retval.fTrailingZeroDisplay = fTrailingZeroDisplay;
     return retval;
 }
@@ -288,7 +269,9 @@ Precision CurrencyPrecision::withCurrency(const CurrencyUnit &currency) const {
 Precision IncrementPrecision::withMinFraction(int32_t minFrac) const {
     if (fType == RND_ERROR) { return *this; } // no-op in error state
     if (minFrac >= 0 && minFrac <= kMaxIntFracSig) {
-        return constructIncrement(fUnion.increment.fIncrement, minFrac);
+        IncrementPrecision copy = *this;
+        copy.fUnion.increment.fMinFrac = minFrac;
+        return copy;
     } else {
         return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
     }
@@ -333,25 +316,22 @@ Precision::constructFractionSignificant(
     return {RND_FRACTION_SIGNIFICANT, union_};
 }
 
-IncrementPrecision Precision::constructIncrement(double increment, int32_t minFrac) {
+IncrementPrecision Precision::constructIncrement(uint64_t increment, digits_t magnitude) {
     IncrementSettings settings;
     // Note: For number formatting, fIncrement is used for RND_INCREMENT but not
     // RND_INCREMENT_ONE or RND_INCREMENT_FIVE. However, fIncrement is used in all
     // three when constructing a skeleton.
     settings.fIncrement = increment;
-    settings.fMinFrac = static_cast<digits_t>(minFrac);
-    // One of the few pre-computed quantities:
-    // Note: it is possible for minFrac to be more than maxFrac... (misleading)
-    int8_t singleDigit;
-    settings.fMaxFrac = roundingutils::doubleFractionLength(increment, &singleDigit);
+    settings.fIncrementMagnitude = magnitude;
+    settings.fMinFrac = magnitude > 0 ? 0 : -magnitude;
     PrecisionUnion union_;
     union_.increment = settings;
-    if (singleDigit == 1) {
+    if (increment == 1) {
         // NOTE: In C++, we must return the correct value type with the correct union.
         // It would be invalid to return a RND_FRACTION here because the methods on the
         // IncrementPrecision type assume that the union is backed by increment data.
         return {RND_INCREMENT_ONE, union_};
-    } else if (singleDigit == 5) {
+    } else if (increment == 5) {
         return {RND_INCREMENT_FIVE, union_};
     } else {
         return {RND_INCREMENT, union_};
@@ -524,6 +504,7 @@ void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const
         case Precision::RND_INCREMENT:
             value.roundToIncrement(
                     fPrecision.fUnion.increment.fIncrement,
+                    fPrecision.fUnion.increment.fIncrementMagnitude,
                     fRoundingMode,
                     status);
             resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;
@@ -531,7 +512,7 @@ void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const
 
         case Precision::RND_INCREMENT_ONE:
             value.roundToMagnitude(
-                    -fPrecision.fUnion.increment.fMaxFrac,
+                    fPrecision.fUnion.increment.fIncrementMagnitude,
                     fRoundingMode,
                     status);
             resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;
@@ -539,7 +520,7 @@ void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const
 
         case Precision::RND_INCREMENT_FIVE:
             value.roundToNickel(
-                    -fPrecision.fUnion.increment.fMaxFrac,
+                    fPrecision.fUnion.increment.fIncrementMagnitude,
                     fRoundingMode,
                     status);
             resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;
index 06fadd29fd544e032d367720052b56f4fd47d431..665712725458549957ef9502be63e94f33f834fb 100644 (file)
@@ -174,15 +174,6 @@ inline bool roundsAtMidpoint(int roundingMode) {
     }
 }
 
-/**
- * Computes the number of fraction digits in a double. Used for computing maxFrac for an increment.
- * Calls into the DoubleToStringConverter library to do so.
- *
- * @param singleDigit An output parameter; set to a number if that is the
- *        only digit in the double, or -1 if there is more than one digit.
- */
-digits_t doubleFractionLength(double input, int8_t* singleDigit);
-
 } // namespace roundingutils
 
 
index 0cdf6c60a94dd22d802b7905a514ea7629c0ebf5..c51831b682380977e3660f1d6d824cb13d5ad128 100644 (file)
@@ -1397,12 +1397,16 @@ void blueprint_helpers::parseIncrementOption(const StringSegment &segment, Macro
     number::impl::parseIncrementOption(segment, macros.precision, status);
 }
 
-void blueprint_helpers::generateIncrementOption(double increment, int32_t minFrac, UnicodeString& sb,
-                                                UErrorCode&) {
+void blueprint_helpers::generateIncrementOption(
+        uint32_t increment,
+        digits_t incrementMagnitude,
+        int32_t minFrac,
+        UnicodeString& sb,
+        UErrorCode&) {
     // Utilize DecimalQuantity/double_conversion to format this for us.
     DecimalQuantity dq;
-    dq.setToDouble(increment);
-    dq.roundToInfinity();
+    dq.setToLong(increment);
+    dq.adjustMagnitude(incrementMagnitude);
     dq.setMinFraction(minFrac);
     sb.append(dq.toPlainString());
 }
@@ -1638,6 +1642,7 @@ bool GeneratorHelpers::precision(const MacroProps& macros, UnicodeString& sb, UE
         sb.append(u"precision-increment/", -1);
         blueprint_helpers::generateIncrementOption(
                 impl.fIncrement,
+                impl.fIncrementMagnitude,
                 impl.fMinFrac,
                 sb,
                 status);
index be41f1b3237a948da7958edf5e4ac86b629f7b89..27f69cd48c39e9530b6a1624a64e90f2ed672fdd 100644 (file)
@@ -286,7 +286,7 @@ bool parseTrailingZeroOption(const StringSegment& segment, MacroProps& macros, U
 void parseIncrementOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
 
 void
-generateIncrementOption(double increment, int32_t minFrac, UnicodeString& sb, UErrorCode& status);
+generateIncrementOption(uint32_t increment, digits_t incrementMagnitude, int32_t minFrac, UnicodeString& sb, UErrorCode& status);
 
 void parseIntegerWidthOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
 
index f4d82a375ec331f53da3261d7e8a2aa1268a1b7f..abe1579cb55cd688c12c9f2a1270b505dcd97397 100644 (file)
@@ -640,6 +640,33 @@ class U_I18N_API Precision : public UMemory {
      */
     static IncrementPrecision increment(double roundingIncrement);
 
+#ifndef U_HIDE_DRAFT_API
+    /**
+     * Version of `Precision::increment()` that takes an integer at a particular power of 10.
+     *
+     * To round to the nearest 0.5 and display 2 fraction digits, with this function, you should write one of the following:
+     *
+     * <pre>
+     * Precision::incrementExact(5, -1).withMinFraction(2)
+     * Precision::incrementExact(50, -2).withMinFraction(2)
+     * Precision::incrementExact(50, -2)
+     * </pre>
+     *
+     * This is analagous to ICU4J `Precision.increment(new BigDecimal("0.50"))`.
+     *
+     * This behavior is modeled after ECMA-402. For more information, see:
+     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement
+     *
+     * @param mantissa
+     *            The increment to which to round numbers.
+     * @param magnitude
+     *            The power of 10 of the ones digit of the mantissa.
+     * @return A precision for chaining or passing to the NumberFormatter precision() setter.
+     * @draft ICU 71
+     */
+    static IncrementPrecision incrementExact(uint64_t mantissa, int16_t magnitude);
+#endif // U_HIDE_DRAFT_API
+
     /**
      * Show numbers rounded and padded according to the rules for the currency unit. The most common
      * rounding precision settings for currencies include <code>Precision::fixedFraction(2)</code>,
@@ -714,12 +741,14 @@ class U_I18N_API Precision : public UMemory {
         /** @internal (private) */
         struct IncrementSettings {
             // For RND_INCREMENT, RND_INCREMENT_ONE, and RND_INCREMENT_FIVE
+            // Note: This is a union, so we shouldn't own memory, since
+            // the default destructor would leak it.
             /** @internal (private) */
-            double fIncrement;
+            uint64_t fIncrement;
             /** @internal (private) */
-            impl::digits_t fMinFrac;
+            impl::digits_t fIncrementMagnitude;
             /** @internal (private) */
-            impl::digits_t fMaxFrac;
+            impl::digits_t fMinFrac;
         } increment;
         UCurrencyUsage currencyUsage; // For RND_CURRENCY
         UErrorCode errorCode; // For RND_ERROR
@@ -765,7 +794,7 @@ class U_I18N_API Precision : public UMemory {
         UNumberRoundingPriority priority,
         bool retain);
 
-    static IncrementPrecision constructIncrement(double increment, int32_t minFrac);
+    static IncrementPrecision constructIncrement(uint64_t increment, impl::digits_t magnitude);
 
     static CurrencyPrecision constructCurrency(UCurrencyUsage usage);
 
index 2e35db8f373d81e4c1bd2fbadab00bb826ebb559..21b0a0a81e3051b77f110cc88b6b7aa00d08e39b 100644 (file)
@@ -3465,6 +3465,42 @@ void NumberFormatterApiTest::roundingOther() {
             u"0.0",
             u"0.0");
 
+    assertFormatSingle(
+            u"Large integer increment",
+            u"precision-increment/24000000000000000000000",
+            u"precision-increment/24000000000000000000000",
+            NumberFormatter::with().precision(Precision::incrementExact(24, 21)),
+            Locale::getEnglish(),
+            3.1e22,
+            u"24,000,000,000,000,000,000,000");
+
+    assertFormatSingle(
+            u"Quarter rounding",
+            u"precision-increment/250",
+            u"precision-increment/250",
+            NumberFormatter::with().precision(Precision::incrementExact(250, 0)),
+            Locale::getEnglish(),
+            700,
+            u"750");
+
+    assertFormatSingle(
+            u"ECMA-402 limit",
+            u"precision-increment/.00000000000000000020",
+            u"precision-increment/.00000000000000000020",
+            NumberFormatter::with().precision(Precision::incrementExact(20, -20)),
+            Locale::getEnglish(),
+            333e-20,
+            u"0.00000000000000000340");
+
+    assertFormatSingle(
+            u"ECMA-402 limit with increment = 1",
+            u"precision-increment/.00000000000000000001",
+            u"precision-increment/.00000000000000000001",
+            NumberFormatter::with().precision(Precision::incrementExact(1, -20)),
+            Locale::getEnglish(),
+            4321e-21,
+            u"0.00000000000000000432");
+
     assertFormatDescending(
             u"Currency Standard",
             u"currency/CZK precision-currency-standard",
@@ -6011,7 +6047,7 @@ NumberFormatterApiTest::assertFormatSingle(
         // Only compare normalized skeletons: the tests need not provide the normalized forms.
         // Use the normalized form to construct the testing formatter to ensure no loss of info.
         UnicodeString normalized = NumberFormatter::forSkeleton(skeleton, status).toSkeleton(status);
-        assertEquals(message + ": Skeleton:", normalized, f.toSkeleton(status));
+        assertEquals(message + ": Skeleton", normalized, f.toSkeleton(status));
         LocalizedNumberFormatter l3 = NumberFormatter::forSkeleton(normalized, status).locale(locale);
         UnicodeString actual3 = l3.formatDouble(input, status).toString(status);
         assertEquals(message + ": Skeleton Path: '" + normalized + "': " + input, expected, actual3);
index 5e2d28fd4157c4d938d9489ddc55cb60a77ff21e..6a490088ed826179e25138c0322e9d8027413d17 100644 (file)
@@ -101,8 +101,11 @@ void DecimalQuantityTest::testDecimalQuantityBehaviorStandalone() {
     assertToStringAndHealth(fq, u"<DecimalQuantity 2:-3 long 987654321E-6>");
     fq.roundToInfinity();
     assertToStringAndHealth(fq, u"<DecimalQuantity 2:-3 long 987654321E-6>");
-    fq.roundToIncrement(0.005, RoundingMode::UNUM_ROUND_HALFEVEN, status);
+    fq.roundToIncrement(4, -3, RoundingMode::UNUM_ROUND_HALFEVEN, status);
     assertSuccess("Rounding to increment", status);
+    assertToStringAndHealth(fq, u"<DecimalQuantity 2:-3 long 987656E-3>");
+    fq.roundToNickel(-3, RoundingMode::UNUM_ROUND_HALFEVEN, status);
+    assertSuccess("Rounding to nickel", status);
     assertToStringAndHealth(fq, u"<DecimalQuantity 2:-3 long 987655E-3>");
     fq.roundToMagnitude(-2, RoundingMode::UNUM_ROUND_HALFEVEN, status);
     assertSuccess("Rounding to magnitude", status);
index c60fe88d22ff9eafbb91cbcbbf7ce19e01c9084a..7f271cb3f662b067cb0022d253ee26b66a1e2147 100644 (file)
@@ -3357,6 +3357,7 @@ void NumberFormatTest::TestRoundingPattern() {
 
 void NumberFormatTest::checkRounding(DecimalFormat* df, double base, int iterations, double increment) {
     df->setRoundingIncrement(increment);
+    assertEquals("Rounding increment round-trip", increment, df->getRoundingIncrement());
     double lastParsed=INT32_MIN; //Intger.MIN_VALUE
     for (int i=-iterations; i<=iterations;i++) {
         double iValue=base+(increment*(i*0.1));
index 6cdabfbe678b0293bf4916db926551d4e20bf6b4..f98594546d28e9479c590a64d4a17cb8015e5d5b 100644 (file)
@@ -789,7 +789,7 @@ public abstract class Precision {
         @Override
         public void apply(DecimalQuantity value) {
             value.roundToIncrement(increment, mathContext);
-            setResolvedMinFraction(value, increment.scale());
+            setResolvedMinFraction(value, Math.max(0, increment.scale()));
         }
 
         @Override
index ed175af3b169a7bba8e01b222474f3335709314d..0f1fff855a06e45ed3ddc227450c66cb3770954d 100644 (file)
@@ -3461,6 +3461,42 @@ public class NumberFormatterApiTest extends TestFmwk {
                 "0.0",
                 "0.0");
 
+        assertFormatSingle(
+                "Large integer increment",
+                "precision-increment/24000000000000000000000",
+                "precision-increment/24000000000000000000000",
+                NumberFormatter.with().precision(Precision.increment(new BigDecimal("24e21"))),
+                ULocale.ENGLISH,
+                3.1e22,
+                "24,000,000,000,000,000,000,000");
+
+        assertFormatSingle(
+                "Quarter rounding",
+                "precision-increment/250",
+                "precision-increment/250",
+                NumberFormatter.with().precision(Precision.increment(new BigDecimal("250"))),
+                ULocale.ENGLISH,
+                700,
+                "750");
+
+        assertFormatSingle(
+                "ECMA-402 limit",
+                "precision-increment/.00000000000000000020",
+                "precision-increment/.00000000000000000020",
+                NumberFormatter.with().precision(Precision.increment(new BigDecimal("20e-20"))),
+                ULocale.ENGLISH,
+                333e-20,
+                "0.00000000000000000340");
+
+        assertFormatSingle(
+                "ECMA-402 limit with increment = 1",
+                "precision-increment/.00000000000000000001",
+                "precision-increment/.00000000000000000001",
+                NumberFormatter.with().precision(Precision.increment(new BigDecimal("1e-20"))),
+                ULocale.ENGLISH,
+                4321e-21,
+                "0.00000000000000000432");
+
         assertFormatDescending(
                 "Currency Standard",
                 "currency/CZK precision-currency-standard",