mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-07 22:44:49 +00:00
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:
parent
5a34bfb151
commit
8018eb84e7
8 changed files with 370 additions and 43 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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).
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue