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.
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.
*
* <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();
*/
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.
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;
} 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;
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)) {
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?
// 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 {
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, {});
}
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};
}
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;
}
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};
}
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_};
case Precision::RND_INCREMENT:
value.roundToIncrement(
fPrecision.fUnion.increment.fIncrement,
+ fPrecision.fUnion.increment.fIncrementMagnitude,
fRoundingMode,
status);
resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;
case Precision::RND_INCREMENT_ONE:
value.roundToMagnitude(
- -fPrecision.fUnion.increment.fMaxFrac,
+ fPrecision.fUnion.increment.fIncrementMagnitude,
fRoundingMode,
status);
resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;
case Precision::RND_INCREMENT_FIVE:
value.roundToNickel(
- -fPrecision.fUnion.increment.fMaxFrac,
+ fPrecision.fUnion.increment.fIncrementMagnitude,
fRoundingMode,
status);
resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac;
}
}
-/**
- * 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
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());
}
sb.append(u"precision-increment/", -1);
blueprint_helpers::generateIncrementOption(
impl.fIncrement,
+ impl.fIncrementMagnitude,
impl.fMinFrac,
sb,
status);
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);
*/
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>,
/** @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 fMinFrac;
+ impl::digits_t fIncrementMagnitude;
/** @internal (private) */
- impl::digits_t fMaxFrac;
+ impl::digits_t fMinFrac;
} increment;
UCurrencyUsage currencyUsage; // For RND_CURRENCY
UErrorCode errorCode; // For RND_ERROR
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);
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",
// 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);
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);
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));
@Override
public void apply(DecimalQuantity value) {
value.roundToIncrement(increment, mathContext);
- setResolvedMinFraction(value, increment.scale());
+ setResolvedMinFraction(value, Math.max(0, increment.scale()));
}
@Override
"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",