Implements the changes from <https://github.com/tc39/ecma402/pull/786>. Drive-by change: Enable another test which no longer fails. Differential Revision: https://phabricator.services.mozilla.com/D189541
473 lines
16 KiB
C++
473 lines
16 KiB
C++
/* 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 <algorithm>
|
|
#include <limits>
|
|
|
|
#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<char16_t>(currency[0]),
|
|
static_cast<char16_t>(currency[1]),
|
|
static_cast<char16_t>(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<char>::length(unit.name));
|
|
}
|
|
return length * 2 + std::char_traits<char>::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<uint32_t>::digits10 + 1 < maxFracDigits);
|
|
constexpr size_t maxLength = maxFracDigits + 2;
|
|
|
|
char chars[maxLength];
|
|
RangedPtr<char> ptr(chars + maxLength, chars, maxLength);
|
|
const RangedPtr<char> 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
|