From: Shane Carr Date: Fri, 26 Oct 2018 00:50:10 +0000 (-0700) Subject: ICU-13701 Adding custom logic for nickel rounding, C and J. X-Git-Tag: release-64-rc~258 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=8018eb84e76606ec38aff478c0ff1ac3f10fe55b;p=icu ICU-13701 Adding custom logic for nickel rounding, C and J. Avoids expensive arithmetic when performing nickel rounding for currencies such as CAD, CHF, and DKK. --- diff --git a/icu4c/source/i18n/number_decimalquantity.cpp b/icu4c/source/i18n/number_decimalquantity.cpp index 2c4182b1c6e..47b930a564b 100644 --- a/icu4c/source/i18n/number_decimalquantity.cpp +++ b/icu4c/source/i18n/number_decimalquantity.cpp @@ -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 } diff --git a/icu4c/source/i18n/number_decimalquantity.h b/icu4c/source/i18n/number_decimalquantity.h index 8e04dea7eb5..2bef2514bce 100644 --- a/icu4c/source/i18n/number_decimalquantity.h +++ b/icu4c/source/i18n/number_decimalquantity.h @@ -76,7 +76,7 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory { *

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. * diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h index 1c109df5b6a..67f8ce71867 100644 --- a/icu4c/source/test/intltest/numbertest.h +++ b/icu4c/source/test/intltest/numbertest.h @@ -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); diff --git a/icu4c/source/test/intltest/numbertest_decimalquantity.cpp b/icu4c/source/test/intltest/numbertest_decimalquantity.cpp index 48b9f91e27a..ac5e1a4db1f 100644 --- a/icu4c/source/test/intltest/numbertest_decimalquantity.cpp +++ b/icu4c/source/test/intltest/numbertest_decimalquantity.cpp @@ -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 */ diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java index ae2d4690996..f9d2da5e9cb 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java @@ -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). * diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java index 22b13b4f6e3..f5fb4278e1f 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java @@ -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 diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java index 64cb95552f4..8a86b802fc4 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java @@ -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) { diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java index 18a5dc32a2b..e8ec189af9b 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java @@ -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 formats = new ArrayList(); + List 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 qs = new ArrayList(); + List 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);