ICU-21908 Add incrementExact to ICU4C Precision

See #1979
This commit is contained in:
Shane F. Carr 2022-02-22 22:39:11 +00:00
parent fcca13200f
commit 4059be5964
14 changed files with 207 additions and 100 deletions

View file

@ -181,20 +181,22 @@ uint64_t DecimalQuantity::getPositionFingerprint() const {
return fingerprint;
}
void DecimalQuantity::roundToIncrement(double roundingIncrement, RoundingMode roundingMode,
UErrorCode& status) {
void DecimalQuantity::roundToIncrement(
uint64_t increment,
digits_t magnitude,
RoundingMode roundingMode,
UErrorCode& status) {
// Do not call this method with an increment having only a 1 or a 5 digit!
// Use a more efficient call to either roundToMagnitude() or roundToNickel().
// Check a few popular rounding increments; a more thorough check is in Java.
U_ASSERT(roundingIncrement != 0.01);
U_ASSERT(roundingIncrement != 0.05);
U_ASSERT(roundingIncrement != 0.1);
U_ASSERT(roundingIncrement != 0.5);
U_ASSERT(roundingIncrement != 1);
U_ASSERT(roundingIncrement != 5);
U_ASSERT(increment != 1);
U_ASSERT(increment != 5);
DecimalQuantity incrementDQ;
incrementDQ.setToLong(increment);
incrementDQ.adjustMagnitude(magnitude);
DecNum incrementDN;
incrementDN.setTo(roundingIncrement, status);
incrementDQ.toDecNum(incrementDN, status);
if (U_FAILURE(status)) { return; }
// Divide this DecimalQuantity by the increment, round, then multiply back.
@ -254,6 +256,12 @@ bool DecimalQuantity::adjustMagnitude(int32_t delta) {
return false;
}
int32_t DecimalQuantity::adjustToZeroScale() {
int32_t retval = scale;
scale = 0;
return retval;
}
double DecimalQuantity::getPluralOperand(PluralOperand operand) const {
// If this assertion fails, you need to call roundToInfinity() or some other rounding method.
// See the comment at the top of this file explaining the "isApproximate" field.

View file

@ -81,11 +81,15 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
*
* <p>If rounding to a power of ten, use the more efficient {@link #roundToMagnitude} instead.
*
* @param roundingIncrement The increment to which to round.
* @param increment The increment to which to round.
* @param magnitude The power of 10 to which to round.
* @param roundingMode The {@link RoundingMode} to use if rounding is necessary.
*/
void roundToIncrement(double roundingIncrement, RoundingMode roundingMode,
UErrorCode& status);
void roundToIncrement(
uint64_t increment,
digits_t magnitude,
RoundingMode roundingMode,
UErrorCode& status);
/** Removes all fraction digits. */
void truncate();
@ -140,6 +144,13 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
*/
bool adjustMagnitude(int32_t delta);
/**
* Scales the number such that the least significant nonzero digit is at magnitude 0.
*
* @return The previous magnitude of the least significant digit.
*/
int32_t adjustToZeroScale();
/**
* @return The power of ten corresponding to the most significant nonzero digit.
* The number must not be zero.

View file

@ -134,7 +134,8 @@ MacroProps NumberPropertyMapper::oldToNew(const DecimalFormatProperties& propert
if (PatternStringUtils::ignoreRoundingIncrement(roundingIncrement, maxFrac)) {
precision = Precision::constructFraction(minFrac, maxFrac);
} else {
precision = Precision::constructIncrement(roundingIncrement, minFrac);
// Convert the double increment to an integer increment
precision = Precision::increment(roundingIncrement).withMinFraction(minFrac);
}
} else if (explicitMinMaxSig) {
minSig = minSig < 1 ? 1 : minSig > kMaxIntFracSig ? kMaxIntFracSig : minSig;
@ -293,9 +294,14 @@ MacroProps NumberPropertyMapper::oldToNew(const DecimalFormatProperties& propert
} else if (rounding_.fType == Precision::PrecisionType::RND_INCREMENT
|| rounding_.fType == Precision::PrecisionType::RND_INCREMENT_ONE
|| rounding_.fType == Precision::PrecisionType::RND_INCREMENT_FIVE) {
increment_ = rounding_.fUnion.increment.fIncrement;
minFrac_ = rounding_.fUnion.increment.fMinFrac;
// If incrementRounding is used, maxFrac is set equal to minFrac
maxFrac_ = rounding_.fUnion.increment.fMinFrac;
// Convert the integer increment to a double
DecimalQuantity dq;
dq.setToLong(rounding_.fUnion.increment.fIncrement);
dq.adjustMagnitude(rounding_.fUnion.increment.fIncrementMagnitude);
increment_ = dq.toDouble();
} else if (rounding_.fType == Precision::PrecisionType::RND_SIGNIFICANT) {
minSig_ = rounding_.fUnion.fracSig.fMinSig;
maxSig_ = rounding_.fUnion.fracSig.fMaxSig;

View file

@ -750,7 +750,7 @@ UnicodeString PatternStringUtils::propertiesToPatternString(const DecimalFormatP
int32_t groupingLength = grouping1 + grouping2 + 1;
// Figure out the digits we need to put in the pattern.
double roundingInterval = properties.roundingIncrement;
double increment = properties.roundingIncrement;
UnicodeString digitsString;
int32_t digitsStringScale = 0;
if (maxSig != uprv_min(dosMax, -1)) {
@ -761,14 +761,14 @@ UnicodeString PatternStringUtils::propertiesToPatternString(const DecimalFormatP
while (digitsString.length() < maxSig) {
digitsString.append(u'#');
}
} else if (roundingInterval != 0.0 && !ignoreRoundingIncrement(roundingInterval,maxFrac)) {
// Rounding Interval.
digitsStringScale = -roundingutils::doubleFractionLength(roundingInterval, nullptr);
// TODO: Check for DoS here?
} else if (increment != 0.0 && !ignoreRoundingIncrement(increment,maxFrac)) {
// Rounding Increment.
DecimalQuantity incrementQuantity;
incrementQuantity.setToDouble(roundingInterval);
incrementQuantity.setToDouble(increment);
incrementQuantity.roundToInfinity();
digitsStringScale = incrementQuantity.getLowerDisplayMagnitude();
incrementQuantity.adjustMagnitude(-digitsStringScale);
incrementQuantity.roundToMagnitude(0, kDefaultMode, status);
incrementQuantity.setMinInteger(minInt - digitsStringScale);
UnicodeString str = incrementQuantity.toPlainString();
if (str.charAt(0) == u'-') {
// TODO: Unsupported operation exception or fail silently?

View file

@ -36,27 +36,24 @@ void number::impl::parseIncrementOption(const StringSegment &segment,
// Utilize DecimalQuantity/decNumber to parse this for us.
DecimalQuantity dq;
UErrorCode localStatus = U_ZERO_ERROR;
DecNum decnum;
decnum.setTo({buffer.data(), buffer.length()}, localStatus);
dq.setToDecNum(decnum, localStatus);
if (U_FAILURE(localStatus) || decnum.isSpecial()) {
dq.setToDecNumber({buffer.data(), buffer.length()}, localStatus);
if (U_FAILURE(localStatus) || dq.isNaN() || dq.isInfinite()) {
// throw new SkeletonSyntaxException("Invalid rounding increment", segment, e);
status = U_NUMBER_SKELETON_SYNTAX_ERROR;
return;
}
double increment = dq.toDouble();
// We also need to figure out how many digits. Do a brute force string operation.
int decimalOffset = 0;
while (decimalOffset < segment.length() && segment.charAt(decimalOffset) != '.') {
decimalOffset++;
}
if (decimalOffset == segment.length()) {
outPrecision = Precision::increment(increment);
} else {
int32_t fractionLength = segment.length() - decimalOffset - 1;
outPrecision = Precision::increment(increment).withMinFraction(fractionLength);
// Now we break apart the number into a mantissa and exponent (magnitude).
int32_t magnitude = dq.adjustToZeroScale();
// setToDecNumber drops trailing zeros, so we search for the '.' manually.
for (int32_t i=0; i<buffer.length(); i++) {
if (buffer[i] == '.') {
int32_t newMagnitude = i - buffer.length() + 1;
dq.adjustMagnitude(magnitude - newMagnitude);
magnitude = newMagnitude;
break;
}
}
outPrecision = Precision::incrementExact(dq.toLong(), magnitude);
}
namespace {
@ -94,34 +91,6 @@ int32_t getDisplayMagnitudeSignificant(const DecimalQuantity &value, int minSig)
MultiplierProducer::~MultiplierProducer() = default;
digits_t roundingutils::doubleFractionLength(double input, int8_t* singleDigit) {
char buffer[DoubleToStringConverter::kBase10MaximalLength + 1];
bool sign; // unused; always positive
int32_t length;
int32_t point;
DoubleToStringConverter::DoubleToAscii(
input,
DoubleToStringConverter::DtoaMode::SHORTEST,
0,
buffer,
sizeof(buffer),
&sign,
&length,
&point
);
if (singleDigit == nullptr) {
// no-op
} else if (length == 1) {
*singleDigit = buffer[0] - '0';
} else {
*singleDigit = -1;
}
return static_cast<digits_t>(length - point);
}
Precision Precision::unlimited() {
return Precision(RND_NONE, {});
}
@ -204,7 +173,19 @@ Precision Precision::trailingZeroDisplay(UNumberTrailingZeroDisplay trailingZero
IncrementPrecision Precision::increment(double roundingIncrement) {
if (roundingIncrement > 0.0) {
return constructIncrement(roundingIncrement, 0);
DecimalQuantity dq;
dq.setToDouble(roundingIncrement);
dq.roundToInfinity();
int32_t magnitude = dq.adjustToZeroScale();
return constructIncrement(dq.toLong(), magnitude);
} else {
return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
}
}
IncrementPrecision Precision::incrementExact(uint64_t mantissa, int16_t magnitude) {
if (mantissa > 0.0) {
return constructIncrement(mantissa, magnitude);
} else {
return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
}
@ -269,8 +250,8 @@ Precision Precision::withCurrency(const CurrencyUnit &currency, UErrorCode &stat
int32_t minMaxFrac = ucurr_getDefaultFractionDigitsForUsage(
isoCode, fUnion.currencyUsage, &status);
Precision retval = (increment != 0.0)
? static_cast<Precision>(constructIncrement(increment, minMaxFrac))
: static_cast<Precision>(constructFraction(minMaxFrac, minMaxFrac));
? Precision::increment(increment)
: static_cast<Precision>(Precision::fixedFraction(minMaxFrac));
retval.fTrailingZeroDisplay = fTrailingZeroDisplay;
return retval;
}
@ -288,7 +269,9 @@ Precision CurrencyPrecision::withCurrency(const CurrencyUnit &currency) const {
Precision IncrementPrecision::withMinFraction(int32_t minFrac) const {
if (fType == RND_ERROR) { return *this; } // no-op in error state
if (minFrac >= 0 && minFrac <= kMaxIntFracSig) {
return constructIncrement(fUnion.increment.fIncrement, minFrac);
IncrementPrecision copy = *this;
copy.fUnion.increment.fMinFrac = minFrac;
return copy;
} else {
return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
}
@ -333,25 +316,22 @@ Precision::constructFractionSignificant(
return {RND_FRACTION_SIGNIFICANT, union_};
}
IncrementPrecision Precision::constructIncrement(double increment, int32_t minFrac) {
IncrementPrecision Precision::constructIncrement(uint64_t increment, digits_t magnitude) {
IncrementSettings settings;
// Note: For number formatting, fIncrement is used for RND_INCREMENT but not
// RND_INCREMENT_ONE or RND_INCREMENT_FIVE. However, fIncrement is used in all
// three when constructing a skeleton.
settings.fIncrement = increment;
settings.fMinFrac = static_cast<digits_t>(minFrac);
// One of the few pre-computed quantities:
// Note: it is possible for minFrac to be more than maxFrac... (misleading)
int8_t singleDigit;
settings.fMaxFrac = roundingutils::doubleFractionLength(increment, &singleDigit);
settings.fIncrementMagnitude = magnitude;
settings.fMinFrac = magnitude > 0 ? 0 : -magnitude;
PrecisionUnion union_;
union_.increment = settings;
if (singleDigit == 1) {
if (increment == 1) {
// NOTE: In C++, we must return the correct value type with the correct union.
// It would be invalid to return a RND_FRACTION here because the methods on the
// IncrementPrecision type assume that the union is backed by increment data.
return {RND_INCREMENT_ONE, union_};
} else if (singleDigit == 5) {
} else if (increment == 5) {
return {RND_INCREMENT_FIVE, union_};
} else {
return {RND_INCREMENT, union_};
@ -524,6 +504,7 @@ void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const
case Precision::RND_INCREMENT:
value.roundToIncrement(
fPrecision.fUnion.increment.fIncrement,
fPrecision.fUnion.increment.fIncrementMagnitude,
fRoundingMode,
status);
resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;
@ -531,7 +512,7 @@ void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const
case Precision::RND_INCREMENT_ONE:
value.roundToMagnitude(
-fPrecision.fUnion.increment.fMaxFrac,
fPrecision.fUnion.increment.fIncrementMagnitude,
fRoundingMode,
status);
resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;
@ -539,7 +520,7 @@ void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const
case Precision::RND_INCREMENT_FIVE:
value.roundToNickel(
-fPrecision.fUnion.increment.fMaxFrac,
fPrecision.fUnion.increment.fIncrementMagnitude,
fRoundingMode,
status);
resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;

View file

@ -174,15 +174,6 @@ inline bool roundsAtMidpoint(int roundingMode) {
}
}
/**
* Computes the number of fraction digits in a double. Used for computing maxFrac for an increment.
* Calls into the DoubleToStringConverter library to do so.
*
* @param singleDigit An output parameter; set to a number if that is the
* only digit in the double, or -1 if there is more than one digit.
*/
digits_t doubleFractionLength(double input, int8_t* singleDigit);
} // namespace roundingutils

View file

@ -1397,12 +1397,16 @@ void blueprint_helpers::parseIncrementOption(const StringSegment &segment, Macro
number::impl::parseIncrementOption(segment, macros.precision, status);
}
void blueprint_helpers::generateIncrementOption(double increment, int32_t minFrac, UnicodeString& sb,
UErrorCode&) {
void blueprint_helpers::generateIncrementOption(
uint32_t increment,
digits_t incrementMagnitude,
int32_t minFrac,
UnicodeString& sb,
UErrorCode&) {
// Utilize DecimalQuantity/double_conversion to format this for us.
DecimalQuantity dq;
dq.setToDouble(increment);
dq.roundToInfinity();
dq.setToLong(increment);
dq.adjustMagnitude(incrementMagnitude);
dq.setMinFraction(minFrac);
sb.append(dq.toPlainString());
}
@ -1638,6 +1642,7 @@ bool GeneratorHelpers::precision(const MacroProps& macros, UnicodeString& sb, UE
sb.append(u"precision-increment/", -1);
blueprint_helpers::generateIncrementOption(
impl.fIncrement,
impl.fIncrementMagnitude,
impl.fMinFrac,
sb,
status);

View file

@ -286,7 +286,7 @@ bool parseTrailingZeroOption(const StringSegment& segment, MacroProps& macros, U
void parseIncrementOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
void
generateIncrementOption(double increment, int32_t minFrac, UnicodeString& sb, UErrorCode& status);
generateIncrementOption(uint32_t increment, digits_t incrementMagnitude, int32_t minFrac, UnicodeString& sb, UErrorCode& status);
void parseIntegerWidthOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);

View file

@ -640,6 +640,33 @@ class U_I18N_API Precision : public UMemory {
*/
static IncrementPrecision increment(double roundingIncrement);
#ifndef U_HIDE_DRAFT_API
/**
* Version of `Precision::increment()` that takes an integer at a particular power of 10.
*
* To round to the nearest 0.5 and display 2 fraction digits, with this function, you should write one of the following:
*
* <pre>
* Precision::incrementExact(5, -1).withMinFraction(2)
* Precision::incrementExact(50, -2).withMinFraction(2)
* Precision::incrementExact(50, -2)
* </pre>
*
* This is analagous to ICU4J `Precision.increment(new BigDecimal("0.50"))`.
*
* This behavior is modeled after ECMA-402. For more information, see:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement
*
* @param mantissa
* The increment to which to round numbers.
* @param magnitude
* The power of 10 of the ones digit of the mantissa.
* @return A precision for chaining or passing to the NumberFormatter precision() setter.
* @draft ICU 71
*/
static IncrementPrecision incrementExact(uint64_t mantissa, int16_t magnitude);
#endif // U_HIDE_DRAFT_API
/**
* Show numbers rounded and padded according to the rules for the currency unit. The most common
* rounding precision settings for currencies include <code>Precision::fixedFraction(2)</code>,
@ -714,12 +741,14 @@ class U_I18N_API Precision : public UMemory {
/** @internal (private) */
struct IncrementSettings {
// For RND_INCREMENT, RND_INCREMENT_ONE, and RND_INCREMENT_FIVE
// Note: This is a union, so we shouldn't own memory, since
// the default destructor would leak it.
/** @internal (private) */
double fIncrement;
uint64_t fIncrement;
/** @internal (private) */
impl::digits_t fIncrementMagnitude;
/** @internal (private) */
impl::digits_t fMinFrac;
/** @internal (private) */
impl::digits_t fMaxFrac;
} increment;
UCurrencyUsage currencyUsage; // For RND_CURRENCY
UErrorCode errorCode; // For RND_ERROR
@ -765,7 +794,7 @@ class U_I18N_API Precision : public UMemory {
UNumberRoundingPriority priority,
bool retain);
static IncrementPrecision constructIncrement(double increment, int32_t minFrac);
static IncrementPrecision constructIncrement(uint64_t increment, impl::digits_t magnitude);
static CurrencyPrecision constructCurrency(UCurrencyUsage usage);

View file

@ -3465,6 +3465,42 @@ void NumberFormatterApiTest::roundingOther() {
u"0.0",
u"0.0");
assertFormatSingle(
u"Large integer increment",
u"precision-increment/24000000000000000000000",
u"precision-increment/24000000000000000000000",
NumberFormatter::with().precision(Precision::incrementExact(24, 21)),
Locale::getEnglish(),
3.1e22,
u"24,000,000,000,000,000,000,000");
assertFormatSingle(
u"Quarter rounding",
u"precision-increment/250",
u"precision-increment/250",
NumberFormatter::with().precision(Precision::incrementExact(250, 0)),
Locale::getEnglish(),
700,
u"750");
assertFormatSingle(
u"ECMA-402 limit",
u"precision-increment/.00000000000000000020",
u"precision-increment/.00000000000000000020",
NumberFormatter::with().precision(Precision::incrementExact(20, -20)),
Locale::getEnglish(),
333e-20,
u"0.00000000000000000340");
assertFormatSingle(
u"ECMA-402 limit with increment = 1",
u"precision-increment/.00000000000000000001",
u"precision-increment/.00000000000000000001",
NumberFormatter::with().precision(Precision::incrementExact(1, -20)),
Locale::getEnglish(),
4321e-21,
u"0.00000000000000000432");
assertFormatDescending(
u"Currency Standard",
u"currency/CZK precision-currency-standard",
@ -6011,7 +6047,7 @@ NumberFormatterApiTest::assertFormatSingle(
// Only compare normalized skeletons: the tests need not provide the normalized forms.
// Use the normalized form to construct the testing formatter to ensure no loss of info.
UnicodeString normalized = NumberFormatter::forSkeleton(skeleton, status).toSkeleton(status);
assertEquals(message + ": Skeleton:", normalized, f.toSkeleton(status));
assertEquals(message + ": Skeleton", normalized, f.toSkeleton(status));
LocalizedNumberFormatter l3 = NumberFormatter::forSkeleton(normalized, status).locale(locale);
UnicodeString actual3 = l3.formatDouble(input, status).toString(status);
assertEquals(message + ": Skeleton Path: '" + normalized + "': " + input, expected, actual3);

View file

@ -101,8 +101,11 @@ void DecimalQuantityTest::testDecimalQuantityBehaviorStandalone() {
assertToStringAndHealth(fq, u"<DecimalQuantity 2:-3 long 987654321E-6>");
fq.roundToInfinity();
assertToStringAndHealth(fq, u"<DecimalQuantity 2:-3 long 987654321E-6>");
fq.roundToIncrement(0.005, RoundingMode::UNUM_ROUND_HALFEVEN, status);
fq.roundToIncrement(4, -3, RoundingMode::UNUM_ROUND_HALFEVEN, status);
assertSuccess("Rounding to increment", status);
assertToStringAndHealth(fq, u"<DecimalQuantity 2:-3 long 987656E-3>");
fq.roundToNickel(-3, RoundingMode::UNUM_ROUND_HALFEVEN, status);
assertSuccess("Rounding to nickel", status);
assertToStringAndHealth(fq, u"<DecimalQuantity 2:-3 long 987655E-3>");
fq.roundToMagnitude(-2, RoundingMode::UNUM_ROUND_HALFEVEN, status);
assertSuccess("Rounding to magnitude", status);

View file

@ -3357,6 +3357,7 @@ void NumberFormatTest::TestRoundingPattern() {
void NumberFormatTest::checkRounding(DecimalFormat* df, double base, int iterations, double increment) {
df->setRoundingIncrement(increment);
assertEquals("Rounding increment round-trip", increment, df->getRoundingIncrement());
double lastParsed=INT32_MIN; //Intger.MIN_VALUE
for (int i=-iterations; i<=iterations;i++) {
double iValue=base+(increment*(i*0.1));

View file

@ -789,7 +789,7 @@ public abstract class Precision {
@Override
public void apply(DecimalQuantity value) {
value.roundToIncrement(increment, mathContext);
setResolvedMinFraction(value, increment.scale());
setResolvedMinFraction(value, Math.max(0, increment.scale()));
}
@Override

View file

@ -3461,6 +3461,42 @@ public class NumberFormatterApiTest extends TestFmwk {
"0.0",
"0.0");
assertFormatSingle(
"Large integer increment",
"precision-increment/24000000000000000000000",
"precision-increment/24000000000000000000000",
NumberFormatter.with().precision(Precision.increment(new BigDecimal("24e21"))),
ULocale.ENGLISH,
3.1e22,
"24,000,000,000,000,000,000,000");
assertFormatSingle(
"Quarter rounding",
"precision-increment/250",
"precision-increment/250",
NumberFormatter.with().precision(Precision.increment(new BigDecimal("250"))),
ULocale.ENGLISH,
700,
"750");
assertFormatSingle(
"ECMA-402 limit",
"precision-increment/.00000000000000000020",
"precision-increment/.00000000000000000020",
NumberFormatter.with().precision(Precision.increment(new BigDecimal("20e-20"))),
ULocale.ENGLISH,
333e-20,
"0.00000000000000000340");
assertFormatSingle(
"ECMA-402 limit with increment = 1",
"precision-increment/.00000000000000000001",
"precision-increment/.00000000000000000001",
NumberFormatter.with().precision(Precision.increment(new BigDecimal("1e-20"))),
ULocale.ENGLISH,
4321e-21,
"0.00000000000000000432");
assertFormatDescending(
"Currency Standard",
"currency/CZK precision-currency-standard",