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.
This commit is contained in:
Shane Carr 2018-10-25 17:50:10 -07:00 committed by Shane F. Carr
parent 5a34bfb151
commit 8018eb84e7
8 changed files with 370 additions and 43 deletions

View 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
}

View 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.
*

View 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);

View 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 */

View 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).
*

View 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

View 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) {

View 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);