diff options
Diffstat (limited to 'intl/icu/source/i18n/number_rounding.cpp')
-rw-r--r-- | intl/icu/source/i18n/number_rounding.cpp | 552 |
1 files changed, 552 insertions, 0 deletions
diff --git a/intl/icu/source/i18n/number_rounding.cpp b/intl/icu/source/i18n/number_rounding.cpp new file mode 100644 index 0000000000..e6bb509ffd --- /dev/null +++ b/intl/icu/source/i18n/number_rounding.cpp @@ -0,0 +1,552 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING + +#include "charstr.h" +#include "uassert.h" +#include "unicode/numberformatter.h" +#include "number_types.h" +#include "number_decimalquantity.h" +#include "double-conversion.h" +#include "number_roundingutils.h" +#include "number_skeletons.h" +#include "number_decnum.h" +#include "putilimp.h" +#include "string_segment.h" + +using namespace icu; +using namespace icu::number; +using namespace icu::number::impl; + + +using double_conversion::DoubleToStringConverter; +using icu::StringSegment; + +void number::impl::parseIncrementOption(const StringSegment &segment, + Precision &outPrecision, + UErrorCode &status) { + // Need to do char <-> char16_t conversion... + U_ASSERT(U_SUCCESS(status)); + CharString buffer; + SKELETON_UCHAR_TO_CHAR(buffer, segment.toTempUnicodeString(), 0, segment.length(), status); + + // Utilize DecimalQuantity/decNumber to parse this for us. + DecimalQuantity dq; + UErrorCode localStatus = U_ZERO_ERROR; + 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; + } + // 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 { + +int32_t getRoundingMagnitudeFraction(int maxFrac) { + if (maxFrac == -1) { + return INT32_MIN; + } + return -maxFrac; +} + +int32_t getRoundingMagnitudeSignificant(const DecimalQuantity &value, int maxSig) { + if (maxSig == -1) { + return INT32_MIN; + } + int magnitude = value.isZeroish() ? 0 : value.getMagnitude(); + return magnitude - maxSig + 1; +} + +int32_t getDisplayMagnitudeFraction(int minFrac) { + if (minFrac == 0) { + return INT32_MAX; + } + return -minFrac; +} + +int32_t getDisplayMagnitudeSignificant(const DecimalQuantity &value, int minSig) { + int magnitude = value.isZeroish() ? 0 : value.getMagnitude(); + return magnitude - minSig + 1; +} + +} + + +MultiplierProducer::~MultiplierProducer() = default; + + +Precision Precision::unlimited() { + return Precision(RND_NONE, {}); +} + +FractionPrecision Precision::integer() { + return constructFraction(0, 0); +} + +FractionPrecision Precision::fixedFraction(int32_t minMaxFractionPlaces) { + if (minMaxFractionPlaces >= 0 && minMaxFractionPlaces <= kMaxIntFracSig) { + return constructFraction(minMaxFractionPlaces, minMaxFractionPlaces); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +FractionPrecision Precision::minFraction(int32_t minFractionPlaces) { + if (minFractionPlaces >= 0 && minFractionPlaces <= kMaxIntFracSig) { + return constructFraction(minFractionPlaces, -1); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +FractionPrecision Precision::maxFraction(int32_t maxFractionPlaces) { + if (maxFractionPlaces >= 0 && maxFractionPlaces <= kMaxIntFracSig) { + return constructFraction(0, maxFractionPlaces); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +FractionPrecision Precision::minMaxFraction(int32_t minFractionPlaces, int32_t maxFractionPlaces) { + if (minFractionPlaces >= 0 && maxFractionPlaces <= kMaxIntFracSig && + minFractionPlaces <= maxFractionPlaces) { + return constructFraction(minFractionPlaces, maxFractionPlaces); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +Precision Precision::fixedSignificantDigits(int32_t minMaxSignificantDigits) { + if (minMaxSignificantDigits >= 1 && minMaxSignificantDigits <= kMaxIntFracSig) { + return constructSignificant(minMaxSignificantDigits, minMaxSignificantDigits); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +Precision Precision::minSignificantDigits(int32_t minSignificantDigits) { + if (minSignificantDigits >= 1 && minSignificantDigits <= kMaxIntFracSig) { + return constructSignificant(minSignificantDigits, -1); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +Precision Precision::maxSignificantDigits(int32_t maxSignificantDigits) { + if (maxSignificantDigits >= 1 && maxSignificantDigits <= kMaxIntFracSig) { + return constructSignificant(1, maxSignificantDigits); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +Precision Precision::minMaxSignificantDigits(int32_t minSignificantDigits, int32_t maxSignificantDigits) { + if (minSignificantDigits >= 1 && maxSignificantDigits <= kMaxIntFracSig && + minSignificantDigits <= maxSignificantDigits) { + return constructSignificant(minSignificantDigits, maxSignificantDigits); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +Precision Precision::trailingZeroDisplay(UNumberTrailingZeroDisplay trailingZeroDisplay) const { + Precision result(*this); // copy constructor + result.fTrailingZeroDisplay = trailingZeroDisplay; + return result; +} + +IncrementPrecision Precision::increment(double roundingIncrement) { + if (roundingIncrement > 0.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}; + } +} + +CurrencyPrecision Precision::currency(UCurrencyUsage currencyUsage) { + return constructCurrency(currencyUsage); +} + +Precision FractionPrecision::withSignificantDigits( + int32_t minSignificantDigits, + int32_t maxSignificantDigits, + UNumberRoundingPriority priority) const { + if (fType == RND_ERROR) { return *this; } // no-op in error state + if (minSignificantDigits >= 1 && + maxSignificantDigits >= minSignificantDigits && + maxSignificantDigits <= kMaxIntFracSig) { + return constructFractionSignificant( + *this, + minSignificantDigits, + maxSignificantDigits, + priority, + false); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +Precision FractionPrecision::withMinDigits(int32_t minSignificantDigits) const { + if (fType == RND_ERROR) { return *this; } // no-op in error state + if (minSignificantDigits >= 1 && minSignificantDigits <= kMaxIntFracSig) { + return constructFractionSignificant( + *this, + 1, + minSignificantDigits, + UNUM_ROUNDING_PRIORITY_RELAXED, + true); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +Precision FractionPrecision::withMaxDigits(int32_t maxSignificantDigits) const { + if (fType == RND_ERROR) { return *this; } // no-op in error state + if (maxSignificantDigits >= 1 && maxSignificantDigits <= kMaxIntFracSig) { + return constructFractionSignificant(*this, + 1, + maxSignificantDigits, + UNUM_ROUNDING_PRIORITY_STRICT, + true); + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +// Private method on base class +Precision Precision::withCurrency(const CurrencyUnit ¤cy, UErrorCode &status) const { + if (fType == RND_ERROR) { return *this; } // no-op in error state + U_ASSERT(fType == RND_CURRENCY); + const char16_t *isoCode = currency.getISOCurrency(); + double increment = ucurr_getRoundingIncrementForUsage(isoCode, fUnion.currencyUsage, &status); + int32_t minMaxFrac = ucurr_getDefaultFractionDigitsForUsage( + isoCode, fUnion.currencyUsage, &status); + Precision retval = (increment != 0.0) + ? Precision::increment(increment) + : static_cast<Precision>(Precision::fixedFraction(minMaxFrac)); + retval.fTrailingZeroDisplay = fTrailingZeroDisplay; + return retval; +} + +// Public method on CurrencyPrecision subclass +Precision CurrencyPrecision::withCurrency(const CurrencyUnit ¤cy) const { + UErrorCode localStatus = U_ZERO_ERROR; + Precision result = Precision::withCurrency(currency, localStatus); + if (U_FAILURE(localStatus)) { + return {localStatus}; + } + return result; +} + +Precision IncrementPrecision::withMinFraction(int32_t minFrac) const { + if (fType == RND_ERROR) { return *this; } // no-op in error state + if (minFrac >= 0 && minFrac <= kMaxIntFracSig) { + IncrementPrecision copy = *this; + copy.fUnion.increment.fMinFrac = minFrac; + return copy; + } else { + return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR}; + } +} + +FractionPrecision Precision::constructFraction(int32_t minFrac, int32_t maxFrac) { + FractionSignificantSettings settings; + settings.fMinFrac = static_cast<digits_t>(minFrac); + settings.fMaxFrac = static_cast<digits_t>(maxFrac); + settings.fMinSig = -1; + settings.fMaxSig = -1; + PrecisionUnion union_; + union_.fracSig = settings; + return {RND_FRACTION, union_}; +} + +Precision Precision::constructSignificant(int32_t minSig, int32_t maxSig) { + FractionSignificantSettings settings; + settings.fMinFrac = -1; + settings.fMaxFrac = -1; + settings.fMinSig = static_cast<digits_t>(minSig); + settings.fMaxSig = static_cast<digits_t>(maxSig); + PrecisionUnion union_; + union_.fracSig = settings; + return {RND_SIGNIFICANT, union_}; +} + +Precision +Precision::constructFractionSignificant( + const FractionPrecision &base, + int32_t minSig, + int32_t maxSig, + UNumberRoundingPriority priority, + bool retain) { + FractionSignificantSettings settings = base.fUnion.fracSig; + settings.fMinSig = static_cast<digits_t>(minSig); + settings.fMaxSig = static_cast<digits_t>(maxSig); + settings.fPriority = priority; + settings.fRetain = retain; + PrecisionUnion union_; + union_.fracSig = settings; + return {RND_FRACTION_SIGNIFICANT, union_}; +} + +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.fIncrementMagnitude = magnitude; + settings.fMinFrac = magnitude > 0 ? 0 : -magnitude; + PrecisionUnion union_; + union_.increment = settings; + 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 (increment == 5) { + return {RND_INCREMENT_FIVE, union_}; + } else { + return {RND_INCREMENT, union_}; + } +} + +CurrencyPrecision Precision::constructCurrency(UCurrencyUsage usage) { + PrecisionUnion union_; + union_.currencyUsage = usage; + return {RND_CURRENCY, union_}; +} + + +RoundingImpl::RoundingImpl(const Precision& precision, UNumberFormatRoundingMode roundingMode, + const CurrencyUnit& currency, UErrorCode& status) + : fPrecision(precision), fRoundingMode(roundingMode), fPassThrough(false) { + if (precision.fType == Precision::RND_CURRENCY) { + fPrecision = precision.withCurrency(currency, status); + } +} + +RoundingImpl RoundingImpl::passThrough() { + return {}; +} + +bool RoundingImpl::isSignificantDigits() const { + return fPrecision.fType == Precision::RND_SIGNIFICANT; +} + +int32_t +RoundingImpl::chooseMultiplierAndApply(impl::DecimalQuantity &input, const impl::MultiplierProducer &producer, + UErrorCode &status) { + // Do not call this method with zero, NaN, or infinity. + U_ASSERT(!input.isZeroish()); + + // Perform the first attempt at rounding. + int magnitude = input.getMagnitude(); + int multiplier = producer.getMultiplier(magnitude); + input.adjustMagnitude(multiplier); + apply(input, status); + + // If the number rounded to zero, exit. + if (input.isZeroish() || U_FAILURE(status)) { + return multiplier; + } + + // If the new magnitude after rounding is the same as it was before rounding, then we are done. + // This case applies to most numbers. + if (input.getMagnitude() == magnitude + multiplier) { + return multiplier; + } + + // If the above case DIDN'T apply, then we have a case like 99.9 -> 100 or 999.9 -> 1000: + // The number rounded up to the next magnitude. Check if the multiplier changes; if it doesn't, + // we do not need to make any more adjustments. + int _multiplier = producer.getMultiplier(magnitude + 1); + if (multiplier == _multiplier) { + return multiplier; + } + + // We have a case like 999.9 -> 1000, where the correct output is "1K", not "1000". + // Fix the magnitude and re-apply the rounding strategy. + input.adjustMagnitude(_multiplier - multiplier); + apply(input, status); + return _multiplier; +} + +/** This is the method that contains the actual rounding logic. */ +void RoundingImpl::apply(impl::DecimalQuantity &value, UErrorCode& status) const { + if (U_FAILURE(status)) { + return; + } + if (fPassThrough) { + return; + } + int32_t resolvedMinFraction = 0; + switch (fPrecision.fType) { + case Precision::RND_BOGUS: + case Precision::RND_ERROR: + // Errors should be caught before the apply() method is called + status = U_INTERNAL_PROGRAM_ERROR; + break; + + case Precision::RND_NONE: + value.roundToInfinity(); + break; + + case Precision::RND_FRACTION: + value.roundToMagnitude( + getRoundingMagnitudeFraction(fPrecision.fUnion.fracSig.fMaxFrac), + fRoundingMode, + status); + resolvedMinFraction = + uprv_max(0, -getDisplayMagnitudeFraction(fPrecision.fUnion.fracSig.fMinFrac)); + break; + + case Precision::RND_SIGNIFICANT: + value.roundToMagnitude( + getRoundingMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMaxSig), + fRoundingMode, + status); + resolvedMinFraction = + uprv_max(0, -getDisplayMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMinSig)); + // Make sure that digits are displayed on zero. + if (value.isZeroish() && fPrecision.fUnion.fracSig.fMinSig > 0) { + value.setMinInteger(1); + } + break; + + case Precision::RND_FRACTION_SIGNIFICANT: { + // From ECMA-402: + /* + Let sResult be ToRawPrecision(...). + Let fResult be ToRawFixed(...). + If intlObj.[[RoundingType]] is morePrecision, then + If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then + Let result be sResult. + Else, + Let result be fResult. + Else, + Assert: intlObj.[[RoundingType]] is lessPrecision. + If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then + Let result be fResult. + Else, + Let result be sResult. + */ + + int32_t roundingMag1 = getRoundingMagnitudeFraction(fPrecision.fUnion.fracSig.fMaxFrac); + int32_t roundingMag2 = getRoundingMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMaxSig); + int32_t roundingMag; + if (fPrecision.fUnion.fracSig.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) { + roundingMag = uprv_min(roundingMag1, roundingMag2); + } else { + roundingMag = uprv_max(roundingMag1, roundingMag2); + } + if (!value.isZeroish()) { + int32_t upperMag = value.getMagnitude(); + value.roundToMagnitude(roundingMag, fRoundingMode, status); + if (!value.isZeroish() && value.getMagnitude() != upperMag && roundingMag1 == roundingMag2) { + // roundingMag2 needs to be the magnitude after rounding + roundingMag2 += 1; + } + } + + int32_t displayMag1 = getDisplayMagnitudeFraction(fPrecision.fUnion.fracSig.fMinFrac); + int32_t displayMag2 = getDisplayMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMinSig); + int32_t displayMag; + if (fPrecision.fUnion.fracSig.fRetain) { + // withMinDigits + withMaxDigits + displayMag = uprv_min(displayMag1, displayMag2); + } else if (fPrecision.fUnion.fracSig.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) { + if (roundingMag2 <= roundingMag1) { + displayMag = displayMag2; + } else { + displayMag = displayMag1; + } + } else { + U_ASSERT(fPrecision.fUnion.fracSig.fPriority == UNUM_ROUNDING_PRIORITY_STRICT); + if (roundingMag2 <= roundingMag1) { + displayMag = displayMag1; + } else { + displayMag = displayMag2; + } + } + resolvedMinFraction = uprv_max(0, -displayMag); + + break; + } + + case Precision::RND_INCREMENT: + value.roundToIncrement( + fPrecision.fUnion.increment.fIncrement, + fPrecision.fUnion.increment.fIncrementMagnitude, + fRoundingMode, + status); + resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac; + break; + + case Precision::RND_INCREMENT_ONE: + value.roundToMagnitude( + fPrecision.fUnion.increment.fIncrementMagnitude, + fRoundingMode, + status); + resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac; + break; + + case Precision::RND_INCREMENT_FIVE: + value.roundToNickel( + fPrecision.fUnion.increment.fIncrementMagnitude, + fRoundingMode, + status); + resolvedMinFraction = fPrecision.fUnion.increment.fMinFrac; + break; + + case Precision::RND_CURRENCY: + // Call .withCurrency() before .apply()! + UPRV_UNREACHABLE_EXIT; + + default: + UPRV_UNREACHABLE_EXIT; + } + + if (fPrecision.fTrailingZeroDisplay == UNUM_TRAILING_ZERO_AUTO || + // PLURAL_OPERAND_T returns fraction digits as an integer + value.getPluralOperand(PLURAL_OPERAND_T) != 0) { + value.setMinFraction(resolvedMinFraction); + } +} + +void RoundingImpl::apply(impl::DecimalQuantity &value, int32_t minInt, UErrorCode /*status*/) { + // This method is intended for the one specific purpose of helping print "00.000E0". + // Question: Is it useful to look at trailingZeroDisplay here? + U_ASSERT(isSignificantDigits()); + U_ASSERT(value.isZeroish()); + value.setMinFraction(fPrecision.fUnion.fracSig.fMinSig - minInt); +} + +#endif /* #if !UCONFIG_NO_FORMATTING */ |