]> granicus.if.org Git - icu/commitdiff
ICU-13701 Adding custom logic for nickel rounding, C and J.
authorShane Carr <shane@unicode.org>
Fri, 26 Oct 2018 00:50:10 +0000 (17:50 -0700)
committerShane F. Carr <shane@unicode.org>
Mon, 29 Oct 2018 23:28:42 +0000 (16:28 -0700)
Avoids expensive arithmetic when performing nickel rounding for currencies such as CAD, CHF, and DKK.

icu4c/source/i18n/number_decimalquantity.cpp
icu4c/source/i18n/number_decimalquantity.h
icu4c/source/test/intltest/numbertest.h
icu4c/source/test/intltest/numbertest_decimalquantity.cpp
icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java
icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java

index 2c4182b1c6ecdac30056eafd8f5892235e31a2ad..47b930a564bf82f645399115e3c405f42e4dff06 100644 (file)
@@ -172,6 +172,16 @@ uint64_t DecimalQuantity::getPositionFingerprint() const {
 
 void DecimalQuantity::roundToIncrement(double roundingIncrement, RoundingMode roundingMode,
                                        int32_t maxFrac, UErrorCode& status) {
+    // TODO(13701): Move the nickel check into a higher-level API.
+    if (roundingIncrement == 0.05) {
+        roundToMagnitude(-2, roundingMode, true, status);
+        roundToMagnitude(-maxFrac, roundingMode, false, status);
+        return;
+    } else if (roundingIncrement == 0.5) {
+        roundToMagnitude(-1, roundingMode, true, status);
+        roundToMagnitude(-maxFrac, roundingMode, false, status);
+        return;
+    }
     // TODO(13701): This is innefficient.  Improve?
     // TODO(13701): Should we convert to decNumber instead?
     roundToInfinity();
@@ -606,36 +616,62 @@ void DecimalQuantity::truncate() {
     }
 }
 
+void DecimalQuantity::roundToNickel(int32_t magnitude, RoundingMode roundingMode, UErrorCode& status) {
+    roundToMagnitude(magnitude, roundingMode, true, status);
+}
+
 void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingMode, UErrorCode& status) {
+    roundToMagnitude(magnitude, roundingMode, false, status);
+}
+
+void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingMode, bool nickel, UErrorCode& status) {
     // The position in the BCD at which rounding will be performed; digits to the right of position
     // will be rounded away.
-    // TODO: Andy: There was a test failure because of integer overflow here. Should I do
-    // "safe subtraction" everywhere in the code?  What's the nicest way to do it?
     int position = safeSubtract(magnitude, scale);
 
-    if (position <= 0 && !isApproximate) {
+    // "trailing" = least significant digit to the left of rounding
+    int8_t trailingDigit = getDigitPos(position);
+
+    if (position <= 0 && !isApproximate && (!nickel || trailingDigit == 0 || trailingDigit == 5)) {
         // All digits are to the left of the rounding magnitude.
     } else if (precision == 0) {
         // No rounding for zero.
     } else {
         // Perform rounding logic.
         // "leading" = most significant digit to the right of rounding
-        // "trailing" = least significant digit to the left of rounding
         int8_t leadingDigit = getDigitPos(safeSubtract(position, 1));
-        int8_t trailingDigit = getDigitPos(position);
 
         // Compute which section of the number we are in.
         // EDGE means we are at the bottom or top edge, like 1.000 or 1.999 (used by doubles)
         // LOWER means we are between the bottom edge and the midpoint, like 1.391
         // MIDPOINT means we are exactly in the middle, like 1.500
         // UPPER means we are between the midpoint and the top edge, like 1.916
-        roundingutils::Section section = roundingutils::SECTION_MIDPOINT;
+        roundingutils::Section section;
         if (!isApproximate) {
-            if (leadingDigit < 5) {
+            if (nickel && trailingDigit != 2 && trailingDigit != 7) {
+                // Nickel rounding, and not at .02x or .07x
+                if (trailingDigit < 2) {
+                    // .00, .01 => down to .00
+                    section = roundingutils::SECTION_LOWER;
+                } else if (trailingDigit < 5) {
+                    // .03, .04 => up to .05
+                    section = roundingutils::SECTION_UPPER;
+                } else if (trailingDigit < 7) {
+                    // .05, .06 => down to .05
+                    section = roundingutils::SECTION_LOWER;
+                } else {
+                    // .08, .09 => up to .10
+                    section = roundingutils::SECTION_UPPER;
+                }
+            } else if (leadingDigit < 5) {
+                // Includes nickel rounding .020-.024 and .070-.074
                 section = roundingutils::SECTION_LOWER;
             } else if (leadingDigit > 5) {
+                // Includes nickel rounding .026-.029 and .076-.079
                 section = roundingutils::SECTION_UPPER;
             } else {
+                // Includes nickel rounding .025 and .075
+                section = roundingutils::SECTION_MIDPOINT;
                 for (int p = safeSubtract(position, 2); p >= 0; p--) {
                     if (getDigitPos(p) != 0) {
                         section = roundingutils::SECTION_UPPER;
@@ -646,7 +682,7 @@ void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingM
         } else {
             int32_t p = safeSubtract(position, 2);
             int32_t minP = uprv_max(0, precision - 14);
-            if (leadingDigit == 0) {
+            if (leadingDigit == 0 && (!nickel || trailingDigit == 0 || trailingDigit == 5)) {
                 section = roundingutils::SECTION_LOWER_EDGE;
                 for (; p >= minP; p--) {
                     if (getDigitPos(p) != 0) {
@@ -654,21 +690,23 @@ void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingM
                         break;
                     }
                 }
-            } else if (leadingDigit == 4) {
+            } else if (leadingDigit == 4 && (!nickel || trailingDigit == 2 || trailingDigit == 7)) {
+                section = roundingutils::SECTION_MIDPOINT;
                 for (; p >= minP; p--) {
                     if (getDigitPos(p) != 9) {
                         section = roundingutils::SECTION_LOWER;
                         break;
                     }
                 }
-            } else if (leadingDigit == 5) {
+            } else if (leadingDigit == 5 && (!nickel || trailingDigit == 2 || trailingDigit == 7)) {
+                section = roundingutils::SECTION_MIDPOINT;
                 for (; p >= minP; p--) {
                     if (getDigitPos(p) != 0) {
                         section = roundingutils::SECTION_UPPER;
                         break;
                     }
                 }
-            } else if (leadingDigit == 9) {
+            } else if (leadingDigit == 9 && (!nickel || trailingDigit == 4 || trailingDigit == 9)) {
                 section = roundingutils::SECTION_UPPER_EDGE;
                 for (; p >= minP; p--) {
                     if (getDigitPos(p) != 9) {
@@ -676,9 +714,26 @@ void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingM
                         break;
                     }
                 }
+            } else if (nickel && trailingDigit != 2 && trailingDigit != 7) {
+                // Nickel rounding, and not at .02x or .07x
+                if (trailingDigit < 2) {
+                    // .00, .01 => down to .00
+                    section = roundingutils::SECTION_LOWER;
+                } else if (trailingDigit < 5) {
+                    // .03, .04 => up to .05
+                    section = roundingutils::SECTION_UPPER;
+                } else if (trailingDigit < 7) {
+                    // .05, .06 => down to .05
+                    section = roundingutils::SECTION_LOWER;
+                } else {
+                    // .08, .09 => up to .10
+                    section = roundingutils::SECTION_UPPER;
+                }
             } else if (leadingDigit < 5) {
+                // Includes nickel rounding .020-.024 and .070-.074
                 section = roundingutils::SECTION_LOWER;
             } else {
+                // Includes nickel rounding .026-.029 and .076-.079
                 section = roundingutils::SECTION_UPPER;
             }
 
@@ -686,10 +741,10 @@ void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingM
             if (safeSubtract(position, 1) < precision - 14 ||
                 (roundsAtMidpoint && section == roundingutils::SECTION_MIDPOINT) ||
                 (!roundsAtMidpoint && section < 0 /* i.e. at upper or lower edge */)) {
-                // Oops! This means that we have to get the exact representation of the double, because
-                // the zone of uncertainty is along the rounding boundary.
+                // Oops! This means that we have to get the exact representation of the double,
+                // because the zone of uncertainty is along the rounding boundary.
                 convertToAccurateDouble();
-                roundToMagnitude(magnitude, roundingMode, status); // start over
+                roundToMagnitude(magnitude, roundingMode, nickel, status); // start over
                 return;
             }
 
@@ -698,7 +753,7 @@ void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingM
             origDouble = 0.0;
             origDelta = 0;
 
-            if (position <= 0) {
+            if (position <= 0 && (!nickel || trailingDigit == 0 || trailingDigit == 5)) {
                 // All digits are to the left of the rounding magnitude.
                 return;
             }
@@ -708,7 +763,14 @@ void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingM
             if (section == -2) { section = roundingutils::SECTION_UPPER; }
         }
 
-        bool roundDown = roundingutils::getRoundingDirection((trailingDigit % 2) == 0,
+        // Nickel rounding "half even" goes to the nearest whole (away from the 5).
+        bool isEven = nickel
+                ? (trailingDigit < 2 || trailingDigit > 7
+                        || (trailingDigit == 2 && section != roundingutils::SECTION_UPPER)
+                        || (trailingDigit == 7 && section == roundingutils::SECTION_UPPER))
+                : (trailingDigit % 2) == 0;
+
+        bool roundDown = roundingutils::getRoundingDirection(isEven,
                 isNegative(),
                 section,
                 roundingMode,
@@ -725,12 +787,28 @@ void DecimalQuantity::roundToMagnitude(int32_t magnitude, RoundingMode roundingM
             shiftRight(position);
         }
 
+        if (nickel) {
+            if (trailingDigit < 5 && roundDown) {
+                setDigitPos(0, 0);
+                compact();
+                return;
+            } else if (trailingDigit >= 5 && !roundDown) {
+                setDigitPos(0, 9);
+                trailingDigit = 9;
+                // do not return: use the bubbling logic below
+            } else {
+                setDigitPos(0, 5);
+                // compact not necessary: digit at position 0 is nonzero
+                return;
+            }
+        }
+
         // Bubble the result to the higher digits
         if (!roundDown) {
             if (trailingDigit == 9) {
                 int bubblePos = 0;
-                // Note: in the long implementation, the most digits BCD can have at this point is 15,
-                // so bubblePos <= 15 and getDigitPos(bubblePos) is safe.
+                // Note: in the long implementation, the most digits BCD can have at this point is
+                // 15, so bubblePos <= 15 and getDigitPos(bubblePos) is safe.
                 for (; getDigitPos(bubblePos) == 9; bubblePos++) {}
                 shiftRight(bubblePos); // shift off the trailing 9s
             }
index 8e04dea7eb5c43b17ec0b983903d6f6a319937cb..2bef2514bceaa65b79e651a1901b709e50ce71f5 100644 (file)
@@ -76,7 +76,7 @@ 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 mathContext The {@link RoundingMode} to use if rounding is necessary.
+     * @param roundingMode The {@link RoundingMode} to use if rounding is necessary.
      */
     void roundToIncrement(double roundingIncrement, RoundingMode roundingMode,
                           int32_t maxFrac, UErrorCode& status);
@@ -84,12 +84,21 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
     /** Removes all fraction digits. */
     void truncate();
 
+    /**
+     * Rounds the number to the nearest multiple of 5 at the specified magnitude.
+     * For example, when magnitude == -2, this performs rounding to the nearest 0.05.
+     *
+     * @param magnitude The magnitude at which the digit should become either 0 or 5.
+     * @param roundingMode Rounding strategy.
+     */
+    void roundToNickel(int32_t magnitude, RoundingMode roundingMode, UErrorCode& status);
+
     /**
      * Rounds the number to a specified magnitude (power of ten).
      *
      * @param roundingMagnitude The power of ten to which to round. For example, a value of -2 will
      *     round to 2 decimal places.
-     * @param mathContext The {@link RoundingMode} to use if rounding is necessary.
+     * @param roundingMode The {@link RoundingMode} to use if rounding is necessary.
      */
     void roundToMagnitude(int32_t magnitude, RoundingMode roundingMode, UErrorCode& status);
 
@@ -382,6 +391,8 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
      */
     bool explicitExactDouble = false;
 
+    void roundToMagnitude(int32_t magnitude, RoundingMode roundingMode, bool nickel, UErrorCode& status);
+
     /**
      * Returns a single digit from the BCD list. No internal state is changed by calling this method.
      *
index 1c109df5b6a3bad278cbad5798eb58d1f2ed486c..67f8ce71867af24fe61fb7ac1b10b7147fd84820 100644 (file)
@@ -129,6 +129,7 @@ class DecimalQuantityTest : public IntlTest {
     void testHardDoubleConversion();
     void testToDouble();
     void testMaxDigits();
+    void testNickelRounding();
 
     void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0);
 
index 48b9f91e27a34265a4bbe9be0fcc9b6db44136bf..ac5e1a4db1f6ca5e0e8f8fc89761de922498fff1 100644 (file)
@@ -26,6 +26,7 @@ void DecimalQuantityTest::runIndexedTest(int32_t index, UBool exec, const char *
         TESTCASE_AUTO(testHardDoubleConversion);
         TESTCASE_AUTO(testToDouble);
         TESTCASE_AUTO(testMaxDigits);
+        TESTCASE_AUTO(testNickelRounding);
     TESTCASE_AUTO_END;
 }
 
@@ -380,4 +381,75 @@ void DecimalQuantityTest::testMaxDigits() {
     }
 }
 
+void DecimalQuantityTest::testNickelRounding() {
+    IcuTestErrorCode status(*this, "testNickelRounding");
+    struct TestCase {
+        double input;
+        int32_t magnitude;
+        UNumberFormatRoundingMode roundingMode;
+        const char16_t* expected;
+    } cases[] = {
+        {1.000, -2, UNUM_ROUND_HALFEVEN, u"1"},
+        {1.001, -2, UNUM_ROUND_HALFEVEN, u"1"},
+        {1.010, -2, UNUM_ROUND_HALFEVEN, u"1"},
+        {1.020, -2, UNUM_ROUND_HALFEVEN, u"1"},
+        {1.024, -2, UNUM_ROUND_HALFEVEN, u"1"},
+        {1.025, -2, UNUM_ROUND_HALFEVEN, u"1"},
+        {1.025, -2, UNUM_ROUND_HALFDOWN, u"1"},
+        {1.025, -2, UNUM_ROUND_HALFUP,   u"1.05"},
+        {1.026, -2, UNUM_ROUND_HALFEVEN, u"1.05"},
+        {1.030, -2, UNUM_ROUND_HALFEVEN, u"1.05"},
+        {1.040, -2, UNUM_ROUND_HALFEVEN, u"1.05"},
+        {1.050, -2, UNUM_ROUND_HALFEVEN, u"1.05"},
+        {1.060, -2, UNUM_ROUND_HALFEVEN, u"1.05"},
+        {1.070, -2, UNUM_ROUND_HALFEVEN, u"1.05"},
+        {1.074, -2, UNUM_ROUND_HALFEVEN, u"1.05"},
+        {1.075, -2, UNUM_ROUND_HALFDOWN, u"1.05"},
+        {1.075, -2, UNUM_ROUND_HALFUP,   u"1.1"},
+        {1.075, -2, UNUM_ROUND_HALFEVEN, u"1.1"},
+        {1.076, -2, UNUM_ROUND_HALFEVEN, u"1.1"},
+        {1.080, -2, UNUM_ROUND_HALFEVEN, u"1.1"},
+        {1.090, -2, UNUM_ROUND_HALFEVEN, u"1.1"},
+        {1.099, -2, UNUM_ROUND_HALFEVEN, u"1.1"},
+        {1.999, -2, UNUM_ROUND_HALFEVEN, u"2"},
+        {2.25, -1, UNUM_ROUND_HALFEVEN, u"2"},
+        {2.25, -1, UNUM_ROUND_HALFUP,   u"2.5"},
+        {2.75, -1, UNUM_ROUND_HALFDOWN, u"2.5"},
+        {2.75, -1, UNUM_ROUND_HALFEVEN, u"3"},
+        {3.00, -1, UNUM_ROUND_CEILING, u"3"},
+        {3.25, -1, UNUM_ROUND_CEILING, u"3.5"},
+        {3.50, -1, UNUM_ROUND_CEILING, u"3.5"},
+        {3.75, -1, UNUM_ROUND_CEILING, u"4"},
+        {4.00, -1, UNUM_ROUND_FLOOR, u"4"},
+        {4.25, -1, UNUM_ROUND_FLOOR, u"4"},
+        {4.50, -1, UNUM_ROUND_FLOOR, u"4.5"},
+        {4.75, -1, UNUM_ROUND_FLOOR, u"4.5"},
+        {5.00, -1, UNUM_ROUND_UP, u"5"},
+        {5.25, -1, UNUM_ROUND_UP, u"5.5"},
+        {5.50, -1, UNUM_ROUND_UP, u"5.5"},
+        {5.75, -1, UNUM_ROUND_UP, u"6"},
+        {6.00, -1, UNUM_ROUND_DOWN, u"6"},
+        {6.25, -1, UNUM_ROUND_DOWN, u"6"},
+        {6.50, -1, UNUM_ROUND_DOWN, u"6.5"},
+        {6.75, -1, UNUM_ROUND_DOWN, u"6.5"},
+        {7.00, -1, UNUM_ROUND_UNNECESSARY, u"7"},
+        {7.50, -1, UNUM_ROUND_UNNECESSARY, u"7.5"},
+    };
+    for (const auto& cas : cases) {
+        UnicodeString message = DoubleToUnicodeString(cas.input) + u" @ " + Int64ToUnicodeString(cas.magnitude) + u" / " + Int64ToUnicodeString(cas.roundingMode);
+        status.setScope(message);
+        DecimalQuantity dq;
+        dq.setToDouble(cas.input);
+        dq.roundToNickel(cas.magnitude, cas.roundingMode, status);
+        status.errIfFailureAndReset();
+        UnicodeString actual = dq.toPlainString();
+        assertEquals(message, cas.expected, actual);
+    }
+    status.setScope("");
+    DecimalQuantity dq;
+    dq.setToDouble(7.1);
+    dq.roundToNickel(-1, UNUM_ROUND_UNNECESSARY, status);
+    status.expectErrorAndReset(U_FORMAT_INEXACT_ERROR);
+}
+
 #endif /* #if !UCONFIG_NO_FORMATTING */
index ae2d46909966539fe3af9b480e662dca7c8aa92f..f9d2da5e9cbc9ad15fbff70fe6765a7858df494f 100644 (file)
@@ -61,6 +61,17 @@ public interface DecimalQuantity extends PluralRules.IFixedDecimal {
      */
     public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext);
 
+    /**
+     * Rounds the number to the nearest multiple of 5 at the specified magnitude.
+     * For example, when magnitude == -2, this performs rounding to the nearest 0.05.
+     *
+     * @param magnitude
+     *            The magnitude at which the digit should become either 0 or 5.
+     * @param mathContext
+     *            Rounding strategy.
+     */
+    public void roundToNickel(int magnitude, MathContext mathContext);
+
     /**
      * Rounds the number to a specified magnitude (power of ten).
      *
index 22b13b4f6e3c60c7178597c1aa7fe3cc285b59e5..f5fb4278e1f2682a11090dc2a219568870380cbf 100644 (file)
@@ -178,7 +178,12 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
 
     @Override
     public void roundToIncrement(BigDecimal roundingIncrement, MathContext mathContext) {
-        // TODO: Avoid converting back and forth to BigDecimal.
+        // TODO(13701): Avoid this check on every call to roundToIncrement().
+        BigDecimal stripped = roundingIncrement.stripTrailingZeros();
+        if (stripped.unscaledValue().compareTo(BigInteger.valueOf(5)) == 0) {
+            roundToNickel(-stripped.scale(), mathContext);
+            return;
+        }
         BigDecimal temp = toBigDecimal();
         temp = temp.divide(roundingIncrement, 0, mathContext.getRoundingMode())
                 .multiply(roundingIncrement).round(mathContext);
@@ -741,44 +746,70 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
         }
     }
 
+    @Override
+    public void roundToNickel(int magnitude, MathContext mathContext) {
+        roundToMagnitude(magnitude, mathContext, true);
+    }
+
     @Override
     public void roundToMagnitude(int magnitude, MathContext mathContext) {
+        roundToMagnitude(magnitude, mathContext, false);
+    }
+
+    private void roundToMagnitude(int magnitude, MathContext mathContext, boolean nickel) {
         // The position in the BCD at which rounding will be performed; digits to the right of position
         // will be rounded away.
-        // TODO: Andy: There was a test failure because of integer overflow here. Should I do
-        // "safe subtraction" everywhere in the code? What's the nicest way to do it?
         int position = safeSubtract(magnitude, scale);
 
         // Enforce the number of digits required by the MathContext.
         int _mcPrecision = mathContext.getPrecision();
-        if (magnitude == Integer.MAX_VALUE
-                || (_mcPrecision > 0 && precision - position > _mcPrecision)) {
+        if (_mcPrecision > 0 && precision - _mcPrecision > position) {
             position = precision - _mcPrecision;
         }
 
-        if (position <= 0 && !isApproximate) {
+        // "trailing" = least significant digit to the left of rounding
+        byte trailingDigit = getDigitPos(position);
+
+        if (position <= 0 && !isApproximate && (!nickel || trailingDigit == 0 || trailingDigit == 5)) {
             // All digits are to the left of the rounding magnitude.
         } else if (precision == 0) {
             // No rounding for zero.
         } else {
             // Perform rounding logic.
             // "leading" = most significant digit to the right of rounding
-            // "trailing" = least significant digit to the left of rounding
             byte leadingDigit = getDigitPos(safeSubtract(position, 1));
-            byte trailingDigit = getDigitPos(position);
 
             // Compute which section of the number we are in.
             // EDGE means we are at the bottom or top edge, like 1.000 or 1.999 (used by doubles)
             // LOWER means we are between the bottom edge and the midpoint, like 1.391
             // MIDPOINT means we are exactly in the middle, like 1.500
             // UPPER means we are between the midpoint and the top edge, like 1.916
-            int section = RoundingUtils.SECTION_MIDPOINT;
+            int section;
             if (!isApproximate) {
-                if (leadingDigit < 5) {
+                if (nickel && trailingDigit != 2 && trailingDigit != 7) {
+                    // Nickel rounding, and not at .02x or .07x
+                    if (trailingDigit < 2) {
+                        // .00, .01 => down to .00
+                        section = RoundingUtils.SECTION_LOWER;
+                    } else if (trailingDigit < 5) {
+                        // .03, .04 => up to .05
+                        section = RoundingUtils.SECTION_UPPER;
+                    } else if (trailingDigit < 7) {
+                        // .05, .06 => down to .05
+                        section = RoundingUtils.SECTION_LOWER;
+                    } else {
+                        // .08, .09 => up to .10
+                        section = RoundingUtils.SECTION_UPPER;
+                    }
+                } else if (leadingDigit < 5) {
+                    // Includes nickel rounding .020-.024 and .070-.074
                     section = RoundingUtils.SECTION_LOWER;
                 } else if (leadingDigit > 5) {
+                    // Includes nickel rounding .026-.029 and .076-.079
                     section = RoundingUtils.SECTION_UPPER;
                 } else {
+                    // Includes nickel rounding .025 and .075
+                    section = RoundingUtils.SECTION_MIDPOINT;
                     for (int p = safeSubtract(position, 2); p >= 0; p--) {
                         if (getDigitPos(p) != 0) {
                             section = RoundingUtils.SECTION_UPPER;
@@ -789,7 +820,7 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
             } else {
                 int p = safeSubtract(position, 2);
                 int minP = Math.max(0, precision - 14);
-                if (leadingDigit == 0) {
+                if (leadingDigit == 0 && (!nickel || trailingDigit == 0 || trailingDigit == 5)) {
                     section = SECTION_LOWER_EDGE;
                     for (; p >= minP; p--) {
                         if (getDigitPos(p) != 0) {
@@ -797,21 +828,23 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
                             break;
                         }
                     }
-                } else if (leadingDigit == 4) {
+                } else if (leadingDigit == 4 && (!nickel || trailingDigit == 2 || trailingDigit == 7)) {
+                    section = RoundingUtils.SECTION_MIDPOINT;
                     for (; p >= minP; p--) {
                         if (getDigitPos(p) != 9) {
                             section = RoundingUtils.SECTION_LOWER;
                             break;
                         }
                     }
-                } else if (leadingDigit == 5) {
+                } else if (leadingDigit == 5 && (!nickel || trailingDigit == 2 || trailingDigit == 7)) {
+                    section = RoundingUtils.SECTION_MIDPOINT;
                     for (; p >= minP; p--) {
                         if (getDigitPos(p) != 0) {
                             section = RoundingUtils.SECTION_UPPER;
                             break;
                         }
                     }
-                } else if (leadingDigit == 9) {
+                } else if (leadingDigit == 9 && (!nickel || trailingDigit == 4 || trailingDigit == 9)) {
                     section = SECTION_UPPER_EDGE;
                     for (; p >= minP; p--) {
                         if (getDigitPos(p) != 9) {
@@ -819,9 +852,26 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
                             break;
                         }
                     }
+                } else if (nickel && trailingDigit != 2 && trailingDigit != 7) {
+                    // Nickel rounding, and not at .02x or .07x
+                    if (trailingDigit < 2) {
+                        // .00, .01 => down to .00
+                        section = RoundingUtils.SECTION_LOWER;
+                    } else if (trailingDigit < 5) {
+                        // .03, .04 => up to .05
+                        section = RoundingUtils.SECTION_UPPER;
+                    } else if (trailingDigit < 7) {
+                        // .05, .06 => down to .05
+                        section = RoundingUtils.SECTION_LOWER;
+                    } else {
+                        // .08, .09 => up to .10
+                        section = RoundingUtils.SECTION_UPPER;
+                    }
                 } else if (leadingDigit < 5) {
+                    // Includes nickel rounding .020-.024 and .070-.074
                     section = RoundingUtils.SECTION_LOWER;
                 } else {
+                    // Includes nickel rounding .026-.029 and .076-.079
                     section = RoundingUtils.SECTION_UPPER;
                 }
 
@@ -831,10 +881,9 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
                         || (roundsAtMidpoint && section == RoundingUtils.SECTION_MIDPOINT)
                         || (!roundsAtMidpoint && section < 0 /* i.e. at upper or lower edge */)) {
                     // Oops! This means that we have to get the exact representation of the double,
-                    // because
-                    // the zone of uncertainty is along the rounding boundary.
+                    // because the zone of uncertainty is along the rounding boundary.
                     convertToAccurateDouble();
-                    roundToMagnitude(magnitude, mathContext); // start over
+                    roundToMagnitude(magnitude, mathContext, nickel); // start over
                     return;
                 }
 
@@ -843,7 +892,7 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
                 origDouble = 0.0;
                 origDelta = 0;
 
-                if (position <= 0) {
+                if (position <= 0 && (!nickel || trailingDigit == 0 || trailingDigit == 5)) {
                     // All digits are to the left of the rounding magnitude.
                     return;
                 }
@@ -855,7 +904,14 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
                     section = RoundingUtils.SECTION_UPPER;
             }
 
-            boolean roundDown = RoundingUtils.getRoundingDirection((trailingDigit % 2) == 0,
+            // Nickel rounding "half even" goes to the nearest whole (away from the 5).
+            boolean isEven = nickel
+                    ? (trailingDigit < 2 || trailingDigit > 7
+                            || (trailingDigit == 2 && section != RoundingUtils.SECTION_UPPER)
+                            || (trailingDigit == 7 && section == RoundingUtils.SECTION_UPPER))
+                    : (trailingDigit % 2) == 0;
+
+            boolean roundDown = RoundingUtils.getRoundingDirection(isEven,
                     isNegative(),
                     section,
                     mathContext.getRoundingMode().ordinal(),
@@ -869,13 +925,28 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
                 shiftRight(position);
             }
 
+            if (nickel) {
+                if (trailingDigit < 5 && roundDown) {
+                    setDigitPos(0, (byte) 0);
+                    compact();
+                    return;
+                } else if (trailingDigit >= 5 && !roundDown) {
+                    setDigitPos(0, (byte) 9);
+                    trailingDigit = 9;
+                    // do not return: use the bubbling logic below
+                } else {
+                    setDigitPos(0, (byte) 5);
+                    // compact not necessary: digit at position 0 is nonzero
+                    return;
+                }
+            }
+
             // Bubble the result to the higher digits
             if (!roundDown) {
                 if (trailingDigit == 9) {
                     int bubblePos = 0;
                     // Note: in the long implementation, the most digits BCD can have at this point is
-                    // 15,
-                    // so bubblePos <= 15 and getDigitPos(bubblePos) is safe.
+                    // 15, so bubblePos <= 15 and getDigitPos(bubblePos) is safe.
                     for (; getDigitPos(bubblePos) == 9; bubblePos++) {
                     }
                     shiftRight(bubblePos); // shift off the trailing 9s
index 64cb95552f470819a3a2b3b29d2be182e4445da4..8a86b802fc4d60217105e0630c544ccac963a46a 100644 (file)
@@ -363,6 +363,12 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity {
     primary = -1;
   }
 
+  @Override
+  public void roundToNickel(int roundingMagnitude, MathContext mathContext) {
+    BigDecimal nickel = BigDecimal.valueOf(5).scaleByPowerOfTen(roundingMagnitude);
+    roundToIncrement(nickel, mathContext);
+  }
+
   @Override
   public void roundToMagnitude(int roundingMagnitude, MathContext mathContext) {
     if (roundingMagnitude < -1000) {
index 18a5dc32a2b29c327cd69b5fdbab95c61d936ce9..e8ec189af9bfc1d58e195625bd8c61f0e51d46d7 100644 (file)
@@ -22,6 +22,7 @@ import com.ibm.icu.dev.test.TestFmwk;
 import com.ibm.icu.impl.number.DecimalFormatProperties;
 import com.ibm.icu.impl.number.DecimalQuantity;
 import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
+import com.ibm.icu.impl.number.RoundingUtils;
 import com.ibm.icu.number.LocalizedNumberFormatter;
 import com.ibm.icu.number.NumberFormatter;
 import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
@@ -36,7 +37,7 @@ public class DecimalQuantityTest extends TestFmwk {
     public void testBehavior() throws ParseException {
 
         // Make a list of several formatters to test the behavior of DecimalQuantity.
-        List<LocalizedNumberFormatter> formats = new ArrayList<LocalizedNumberFormatter>();
+        List<LocalizedNumberFormatter> formats = new ArrayList<>();
 
         DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
 
@@ -120,7 +121,7 @@ public class DecimalQuantityTest extends TestFmwk {
             assertEquals("Double is not valid", Double.toString(Double.parseDouble(str)), str);
         }
 
-        List<DecimalQuantity> qs = new ArrayList<DecimalQuantity>();
+        List<DecimalQuantity> qs = new ArrayList<>();
         BigDecimal d = new BigDecimal(str);
         qs.add(new DecimalQuantity_SimpleStorage(d));
         if (mode == 0)
@@ -512,6 +513,82 @@ public class DecimalQuantityTest extends TestFmwk {
         }
     }
 
+    @Test
+    public void testNickelRounding() {
+        Object[][] cases = new Object[][] {
+            {1.000, -2, RoundingMode.HALF_EVEN, "1."},
+            {1.001, -2, RoundingMode.HALF_EVEN, "1."},
+            {1.010, -2, RoundingMode.HALF_EVEN, "1."},
+            {1.020, -2, RoundingMode.HALF_EVEN, "1."},
+            {1.024, -2, RoundingMode.HALF_EVEN, "1."},
+            {1.025, -2, RoundingMode.HALF_EVEN, "1."},
+            {1.025, -2, RoundingMode.HALF_DOWN, "1."},
+            {1.025, -2, RoundingMode.HALF_UP,   "1.05"},
+            {1.026, -2, RoundingMode.HALF_EVEN, "1.05"},
+            {1.030, -2, RoundingMode.HALF_EVEN, "1.05"},
+            {1.040, -2, RoundingMode.HALF_EVEN, "1.05"},
+            {1.050, -2, RoundingMode.HALF_EVEN, "1.05"},
+            {1.060, -2, RoundingMode.HALF_EVEN, "1.05"},
+            {1.070, -2, RoundingMode.HALF_EVEN, "1.05"},
+            {1.074, -2, RoundingMode.HALF_EVEN, "1.05"},
+            {1.075, -2, RoundingMode.HALF_DOWN, "1.05"},
+            {1.075, -2, RoundingMode.HALF_UP,   "1.1"},
+            {1.075, -2, RoundingMode.HALF_EVEN, "1.1"},
+            {1.076, -2, RoundingMode.HALF_EVEN, "1.1"},
+            {1.080, -2, RoundingMode.HALF_EVEN, "1.1"},
+            {1.090, -2, RoundingMode.HALF_EVEN, "1.1"},
+            {1.099, -2, RoundingMode.HALF_EVEN, "1.1"},
+            {1.999, -2, RoundingMode.HALF_EVEN, "2."},
+            {2.25, -1, RoundingMode.HALF_EVEN, "2."},
+            {2.25, -1, RoundingMode.HALF_UP,   "2.5"},
+            {2.75, -1, RoundingMode.HALF_DOWN, "2.5"},
+            {2.75, -1, RoundingMode.HALF_EVEN, "3."},
+            {3.00, -1, RoundingMode.CEILING, "3."},
+            {3.25, -1, RoundingMode.CEILING, "3.5"},
+            {3.50, -1, RoundingMode.CEILING, "3.5"},
+            {3.75, -1, RoundingMode.CEILING, "4."},
+            {4.00, -1, RoundingMode.FLOOR, "4."},
+            {4.25, -1, RoundingMode.FLOOR, "4."},
+            {4.50, -1, RoundingMode.FLOOR, "4.5"},
+            {4.75, -1, RoundingMode.FLOOR, "4.5"},
+            {5.00, -1, RoundingMode.UP, "5."},
+            {5.25, -1, RoundingMode.UP, "5.5"},
+            {5.50, -1, RoundingMode.UP, "5.5"},
+            {5.75, -1, RoundingMode.UP, "6."},
+            {6.00, -1, RoundingMode.DOWN, "6."},
+            {6.25, -1, RoundingMode.DOWN, "6."},
+            {6.50, -1, RoundingMode.DOWN, "6.5"},
+            {6.75, -1, RoundingMode.DOWN, "6.5"},
+            {7.00, -1, RoundingMode.UNNECESSARY, "7."},
+            {7.50, -1, RoundingMode.UNNECESSARY, "7.5"},
+        };
+        for (Object[] cas : cases) {
+            double input = (Double) cas[0];
+            int magnitude = (Integer) cas[1];
+            RoundingMode roundingMode = (RoundingMode) cas[2];
+            String expected = (String) cas[3];
+            String message = input + " @ " + magnitude + " / " + roundingMode;
+            for (int i=0; i<2; i++) {
+                DecimalQuantity dq;
+                if (i == 0) {
+                    dq = new DecimalQuantity_DualStorageBCD(input);
+                } else {
+                    dq = new DecimalQuantity_SimpleStorage(input);
+                }
+                dq.roundToNickel(magnitude, RoundingUtils.mathContextUnlimited(roundingMode));
+                String actual = dq.toPlainString();
+                assertEquals(message, expected, actual);
+            }
+        }
+        try {
+            DecimalQuantity_DualStorageBCD dq = new DecimalQuantity_DualStorageBCD(7.1);
+            dq.roundToNickel(-1, RoundingUtils.mathContextUnlimited(RoundingMode.UNNECESSARY));
+            fail("Expected ArithmeticException");
+        } catch (ArithmeticException expected) {
+            // pass
+        }
+    }
+
     static void assertDoubleEquals(String message, double d1, double d2) {
         boolean equal = (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6);
         handleAssert(equal, message, d1, d2, null, false);