/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "NumberFormatterSkeleton.h" #include "NumberFormat.h" #include "MeasureUnitGenerated.h" #include "mozilla/RangedPtr.h" #include #include #include "unicode/unumberrangeformatter.h" namespace mozilla::intl { NumberFormatterSkeleton::NumberFormatterSkeleton( const NumberFormatOptions& options) { if (options.mCurrency.isSome()) { if (!currency(options.mCurrency->first) || !currencyDisplay(options.mCurrency->second)) { return; } } else if (options.mUnit.isSome()) { if (!unit(options.mUnit->first) || !unitDisplay(options.mUnit->second)) { return; } } else if (options.mPercent) { if (!percent()) { return; } } if (options.mRoundingIncrement != 1) { auto fd = options.mFractionDigits.valueOr(std::pair{0, 0}); if (!roundingIncrement(options.mRoundingIncrement, fd.first, fd.second, options.mStripTrailingZero)) { return; } } else if (options.mRoundingPriority == NumberFormatOptions::RoundingPriority::Auto) { if (options.mFractionDigits.isSome()) { if (!fractionDigits(options.mFractionDigits->first, options.mFractionDigits->second, options.mStripTrailingZero)) { return; } } if (options.mSignificantDigits.isSome()) { if (!significantDigits(options.mSignificantDigits->first, options.mSignificantDigits->second, options.mStripTrailingZero)) { return; } } } else { MOZ_ASSERT(options.mFractionDigits); MOZ_ASSERT(options.mSignificantDigits); bool relaxed = options.mRoundingPriority == NumberFormatOptions::RoundingPriority::MorePrecision; if (!fractionWithSignificantDigits(options.mFractionDigits->first, options.mFractionDigits->second, options.mSignificantDigits->first, options.mSignificantDigits->second, relaxed, options.mStripTrailingZero)) { return; } } if (options.mMinIntegerDigits.isSome()) { if (!minIntegerDigits(*options.mMinIntegerDigits)) { return; } } if (!grouping(options.mGrouping)) { return; } if (!notation(options.mNotation)) { return; } if (!signDisplay(options.mSignDisplay)) { return; } if (!roundingMode(options.mRoundingMode)) { return; } mValidSkeleton = true; } bool NumberFormatterSkeleton::currency(std::string_view currency) { MOZ_ASSERT(currency.size() == 3, "IsWellFormedCurrencyCode permits only length-3 strings"); char16_t currencyChars[] = {static_cast(currency[0]), static_cast(currency[1]), static_cast(currency[2]), '\0'}; return append(u"currency/") && append(currencyChars) && append(' '); } bool NumberFormatterSkeleton::currencyDisplay( NumberFormatOptions::CurrencyDisplay display) { switch (display) { case NumberFormatOptions::CurrencyDisplay::Code: return appendToken(u"unit-width-iso-code"); case NumberFormatOptions::CurrencyDisplay::Name: return appendToken(u"unit-width-full-name"); case NumberFormatOptions::CurrencyDisplay::Symbol: // Default, no additional tokens needed. return true; case NumberFormatOptions::CurrencyDisplay::NarrowSymbol: return appendToken(u"unit-width-narrow"); } MOZ_ASSERT_UNREACHABLE("unexpected currency display type"); return false; } static const SimpleMeasureUnit& FindSimpleMeasureUnit(std::string_view name) { const auto* measureUnit = std::lower_bound( std::begin(simpleMeasureUnits), std::end(simpleMeasureUnits), name, [](const auto& measureUnit, std::string_view name) { return name.compare(measureUnit.name) > 0; }); MOZ_ASSERT(measureUnit != std::end(simpleMeasureUnits), "unexpected unit identifier: unit not found"); MOZ_ASSERT(measureUnit->name == name, "unexpected unit identifier: wrong unit found"); return *measureUnit; } static constexpr size_t MaxUnitLength() { size_t length = 0; for (const auto& unit : simpleMeasureUnits) { length = std::max(length, std::char_traits::length(unit.name)); } return length * 2 + std::char_traits::length("-per-"); } bool NumberFormatterSkeleton::unit(std::string_view unit) { MOZ_RELEASE_ASSERT(unit.length() <= MaxUnitLength()); auto appendUnit = [this](const SimpleMeasureUnit& unit) { return append(unit.type, strlen(unit.type)) && append('-') && append(unit.name, strlen(unit.name)); }; // |unit| can be a compound unit identifier, separated by "-per-". static constexpr char separator[] = "-per-"; size_t separator_len = strlen(separator); size_t offset = unit.find(separator); if (offset != std::string_view::npos) { const auto& numerator = FindSimpleMeasureUnit(unit.substr(0, offset)); const auto& denominator = FindSimpleMeasureUnit( std::string_view(unit.data() + offset + separator_len, unit.length() - offset - separator_len)); return append(u"measure-unit/") && appendUnit(numerator) && append(' ') && append(u"per-measure-unit/") && appendUnit(denominator) && append(' '); } const auto& simple = FindSimpleMeasureUnit(unit); return append(u"measure-unit/") && appendUnit(simple) && append(' '); } bool NumberFormatterSkeleton::unitDisplay( NumberFormatOptions::UnitDisplay display) { switch (display) { case NumberFormatOptions::UnitDisplay::Short: return appendToken(u"unit-width-short"); case NumberFormatOptions::UnitDisplay::Narrow: return appendToken(u"unit-width-narrow"); case NumberFormatOptions::UnitDisplay::Long: return appendToken(u"unit-width-full-name"); } MOZ_ASSERT_UNREACHABLE("unexpected unit display type"); return false; } bool NumberFormatterSkeleton::percent() { return appendToken(u"percent scale/100"); } bool NumberFormatterSkeleton::fractionDigits(uint32_t min, uint32_t max, bool stripTrailingZero) { // Note: |min| can be zero here. MOZ_ASSERT(min <= max); if (!append('.') || !appendN('0', min) || !appendN('#', max - min)) { return false; } if (stripTrailingZero) { if (!append(u"/w")) { return false; } } return append(' '); } bool NumberFormatterSkeleton::fractionWithSignificantDigits( uint32_t mnfd, uint32_t mxfd, uint32_t mnsd, uint32_t mxsd, bool relaxed, bool stripTrailingZero) { // Note: |mnfd| can be zero here. MOZ_ASSERT(mnfd <= mxfd); MOZ_ASSERT(mnsd > 0); MOZ_ASSERT(mnsd <= mxsd); if (!append('.') || !appendN('0', mnfd) || !appendN('#', mxfd - mnfd)) { return false; } if (!append('/') || !appendN('@', mnsd) || !appendN('#', mxsd - mnsd)) { return false; } if (!append(relaxed ? 'r' : 's')) { return false; } if (stripTrailingZero) { if (!append(u"/w")) { return false; } } return append(' '); } bool NumberFormatterSkeleton::minIntegerDigits(uint32_t min) { MOZ_ASSERT(min > 0); return append(u"integer-width/+") && appendN('0', min) && append(' '); } bool NumberFormatterSkeleton::significantDigits(uint32_t min, uint32_t max, bool stripTrailingZero) { MOZ_ASSERT(min > 0); MOZ_ASSERT(min <= max); if (!appendN('@', min) || !appendN('#', max - min)) { return false; } if (stripTrailingZero) { if (!append(u"/w")) { return false; } } return append(' '); } bool NumberFormatterSkeleton::grouping(NumberFormatOptions::Grouping grouping) { switch (grouping) { case NumberFormatOptions::Grouping::Auto: // Default, no additional tokens needed. return true; case NumberFormatOptions::Grouping::Always: return appendToken(u"group-on-aligned"); case NumberFormatOptions::Grouping::Min2: return appendToken(u"group-min2"); case NumberFormatOptions::Grouping::Never: return appendToken(u"group-off"); } MOZ_ASSERT_UNREACHABLE("unexpected grouping mode"); return false; } bool NumberFormatterSkeleton::notation(NumberFormatOptions::Notation style) { switch (style) { case NumberFormatOptions::Notation::Standard: // Default, no additional tokens needed. return true; case NumberFormatOptions::Notation::Scientific: return appendToken(u"scientific"); case NumberFormatOptions::Notation::Engineering: return appendToken(u"engineering"); case NumberFormatOptions::Notation::CompactShort: return appendToken(u"compact-short"); case NumberFormatOptions::Notation::CompactLong: return appendToken(u"compact-long"); } MOZ_ASSERT_UNREACHABLE("unexpected notation style"); return false; } bool NumberFormatterSkeleton::signDisplay( NumberFormatOptions::SignDisplay display) { switch (display) { case NumberFormatOptions::SignDisplay::Auto: // Default, no additional tokens needed. return true; case NumberFormatOptions::SignDisplay::Always: return appendToken(u"sign-always"); case NumberFormatOptions::SignDisplay::Never: return appendToken(u"sign-never"); case NumberFormatOptions::SignDisplay::ExceptZero: return appendToken(u"sign-except-zero"); case NumberFormatOptions::SignDisplay::Negative: return appendToken(u"sign-negative"); case NumberFormatOptions::SignDisplay::Accounting: return appendToken(u"sign-accounting"); case NumberFormatOptions::SignDisplay::AccountingAlways: return appendToken(u"sign-accounting-always"); case NumberFormatOptions::SignDisplay::AccountingExceptZero: return appendToken(u"sign-accounting-except-zero"); case NumberFormatOptions::SignDisplay::AccountingNegative: return appendToken(u"sign-accounting-negative"); } MOZ_ASSERT_UNREACHABLE("unexpected sign display type"); return false; } bool NumberFormatterSkeleton::roundingIncrement(uint32_t increment, uint32_t mnfd, uint32_t mxfd, bool stripTrailingZero) { // Note: |mnfd| can be zero here. MOZ_ASSERT(mnfd <= mxfd); MOZ_ASSERT(increment > 1); // Limit |mxfd| to 100. constexpr size_t maxFracDigits = 100; MOZ_RELEASE_ASSERT(mxfd <= maxFracDigits); static constexpr char digits[] = "0123456789"; // We need enough space to print any uint32_t, which is possibly shifted by // |mxfd| decimal places. And additionally we need to reserve space for "0.". static_assert(std::numeric_limits::digits10 + 1 < maxFracDigits); constexpr size_t maxLength = maxFracDigits + 2; char chars[maxLength]; RangedPtr ptr(chars + maxLength, chars, maxLength); const RangedPtr end = ptr; // Convert to a signed integer, so we don't have to worry about underflows. int32_t maxFrac = int32_t(mxfd); // Write |increment| from back to front. while (increment != 0) { *--ptr = digits[increment % 10]; increment /= 10; maxFrac -= 1; if (maxFrac == 0) { *--ptr = '.'; } } // Write any remaining zeros from |mxfd| and prepend '0' if we last wrote the // decimal point. while (maxFrac >= 0) { MOZ_ASSERT_IF(maxFrac == 0, *ptr == '.'); *--ptr = '0'; maxFrac -= 1; if (maxFrac == 0) { *--ptr = '.'; } } MOZ_ASSERT(ptr < end, "At least one character is written."); MOZ_ASSERT(*ptr != '.', "First character is a digit."); if (!append(u"precision-increment/") || !append(ptr.get(), end - ptr)) { return false; } if (stripTrailingZero) { if (!append(u"/w")) { return false; } } return append(' '); } bool NumberFormatterSkeleton::roundingMode( NumberFormatOptions::RoundingMode rounding) { switch (rounding) { case NumberFormatOptions::RoundingMode::Ceil: return appendToken(u"rounding-mode-ceiling"); case NumberFormatOptions::RoundingMode::Floor: return appendToken(u"rounding-mode-floor"); case NumberFormatOptions::RoundingMode::Expand: return appendToken(u"rounding-mode-up"); case NumberFormatOptions::RoundingMode::Trunc: return appendToken(u"rounding-mode-down"); case NumberFormatOptions::RoundingMode::HalfCeil: return appendToken(u"rounding-mode-half-ceiling"); case NumberFormatOptions::RoundingMode::HalfFloor: return appendToken(u"rounding-mode-half-floor"); case NumberFormatOptions::RoundingMode::HalfExpand: return appendToken(u"rounding-mode-half-up"); case NumberFormatOptions::RoundingMode::HalfTrunc: return appendToken(u"rounding-mode-half-down"); case NumberFormatOptions::RoundingMode::HalfEven: return appendToken(u"rounding-mode-half-even"); case NumberFormatOptions::RoundingMode::HalfOdd: return appendToken(u"rounding-mode-half-odd"); } MOZ_ASSERT_UNREACHABLE("unexpected rounding mode"); return false; } UNumberFormatter* NumberFormatterSkeleton::toFormatter( std::string_view locale) { if (!mValidSkeleton) { return nullptr; } UErrorCode status = U_ZERO_ERROR; UNumberFormatter* nf = unumf_openForSkeletonAndLocale( mVector.begin(), mVector.length(), AssertNullTerminatedString(locale), &status); if (U_FAILURE(status)) { return nullptr; } return nf; } static UNumberRangeCollapse ToUNumberRangeCollapse( NumberRangeFormatOptions::RangeCollapse collapse) { using RangeCollapse = NumberRangeFormatOptions::RangeCollapse; switch (collapse) { case RangeCollapse::Auto: return UNUM_RANGE_COLLAPSE_AUTO; case RangeCollapse::None: return UNUM_RANGE_COLLAPSE_NONE; case RangeCollapse::Unit: return UNUM_RANGE_COLLAPSE_UNIT; case RangeCollapse::All: return UNUM_RANGE_COLLAPSE_ALL; } MOZ_ASSERT_UNREACHABLE("unexpected range collapse"); return UNUM_RANGE_COLLAPSE_NONE; } static UNumberRangeIdentityFallback ToUNumberRangeIdentityFallback( NumberRangeFormatOptions::RangeIdentityFallback identity) { using RangeIdentityFallback = NumberRangeFormatOptions::RangeIdentityFallback; switch (identity) { case RangeIdentityFallback::SingleValue: return UNUM_IDENTITY_FALLBACK_SINGLE_VALUE; case RangeIdentityFallback::ApproximatelyOrSingleValue: return UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE; case RangeIdentityFallback::Approximately: return UNUM_IDENTITY_FALLBACK_APPROXIMATELY; case RangeIdentityFallback::Range: return UNUM_IDENTITY_FALLBACK_RANGE; } MOZ_ASSERT_UNREACHABLE("unexpected range identity fallback"); return UNUM_IDENTITY_FALLBACK_RANGE; } UNumberRangeFormatter* NumberFormatterSkeleton::toRangeFormatter( std::string_view locale, NumberRangeFormatOptions::RangeCollapse collapse, NumberRangeFormatOptions::RangeIdentityFallback identity) { if (!mValidSkeleton) { return nullptr; } UParseError* perror = nullptr; UErrorCode status = U_ZERO_ERROR; UNumberRangeFormatter* nrf = unumrf_openForSkeletonWithCollapseAndIdentityFallback( mVector.begin(), mVector.length(), ToUNumberRangeCollapse(collapse), ToUNumberRangeIdentityFallback(identity), AssertNullTerminatedString(locale), perror, &status); if (U_FAILURE(status)) { return nullptr; } return nrf; } } // namespace mozilla::intl