Bug 1648137 - Part 5: Add support for roundingIncrement, roundingMode, and trailingZeroDisplay options to intl::NumberFormat. r=platform-i18n-reviewers,dminor

First set of new options added in the "Intl.NumberFormat v3 API" proposal.

- All ICU rounding modes are added. (The Intl proposal doesn't support "HalfOdd".)
- The rounding mode names match the names in the Intl proposal, which are
  different compared to the ICU names.
- Significant digits options are ignored when rounding increment is used. This is
  an ICU limitation. The Intl proposal will throw an error if both options are
  used at the same time.

Differential Revision: https://phabricator.services.mozilla.com/D119745
This commit is contained in:
André Bargull 2021-08-24 09:34:47 +00:00
Родитель adf02e643c
Коммит 83a11eb7f8
6 изменённых файлов: 209 добавлений и 64 удалений

Просмотреть файл

@ -120,6 +120,25 @@ TEST(IntlNumberFormat, Unit)
ASSERT_EQ(std::u16string_view(res), u"12.34 metros por segundo");
}
TEST(IntlNumberFormat, RoundingMode)
{
NumberFormatOptions options;
options.mFractionDigits = Some(std::make_pair(0, 2));
options.mStripTrailingZero = true;
options.mRoundingIncrement = 5;
options.mRoundingMode = NumberFormatOptions::RoundingMode::Ceil;
UniquePtr<NumberFormat> nf = NumberFormat::TryCreate("en", options).unwrap();
const char16_t* res16 = nf->format(1.92).unwrap().data();
ASSERT_TRUE(res16 != nullptr);
ASSERT_EQ(std::u16string_view(res16), u"1.95");
res16 = nf->format(1.96).unwrap().data();
ASSERT_TRUE(res16 != nullptr);
ASSERT_EQ(std::u16string_view(res16), u"2");
}
TEST(IntlNumberFormat, FormatToParts)
{
NumberFormatOptions options;

Просмотреть файл

@ -97,6 +97,14 @@ struct MOZ_STACK_CLASS NumberFormatOptions {
*/
bool mPercent = false;
/**
* Set to true to strip trailing zeros after the decimal point for integer
* values.
*
* https://github.com/unicode-org/icu/blob/master/docs/userguide/format_parse/numbers/skeletons.md#trailing-zero-display
*/
bool mStripTrailingZero = false;
/**
* Enable or disable grouping.
*
@ -127,17 +135,37 @@ struct MOZ_STACK_CLASS NumberFormatOptions {
Never,
Always,
ExceptZero,
Negative,
Accounting,
AccountingAlways,
AccountingExceptZero
AccountingExceptZero,
AccountingNegative,
} mSignDisplay = SignDisplay::Auto;
/**
* Set the rounding mode to 'half-up'.
* Set the rounding increment, which must be a non-zero number.
*
* https://github.com/unicode-org/icu/blob/master/docs/userguide/format_parse/numbers/skeletons.md#precision
*/
uint32_t mRoundingIncrement = 1;
/**
* Set the rounding mode.
*
* https://github.com/unicode-org/icu/blob/master/docs/userguide/format_parse/numbers/skeletons.md#rounding-mode
*/
bool mRoundingModeHalfUp = true;
enum class RoundingMode {
Ceil,
Floor,
Expand,
Trunc,
HalfCeil,
HalfFloor,
HalfExpand,
HalfTrunc,
HalfEven,
HalfOdd,
} mRoundingMode = RoundingMode::HalfExpand;
};
enum class NumberPartType {

Просмотреть файл

@ -6,7 +6,10 @@
#include "MeasureUnitGenerated.h"
#include "mozilla/RangedPtr.h"
#include <algorithm>
#include <limits>
#include "unicode/unumberrangeformatter.h"
@ -30,11 +33,28 @@ NumberFormatterSkeleton::NumberFormatterSkeleton(
}
}
if (options.mFractionDigits.isSome()) {
if (!fractionDigits(options.mFractionDigits->first,
options.mFractionDigits->second)) {
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.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;
}
}
}
if (options.mMinIntegerDigits.isSome()) {
@ -43,13 +63,6 @@ NumberFormatterSkeleton::NumberFormatterSkeleton(
}
}
if (options.mSignificantDigits.isSome()) {
if (!significantDigits(options.mSignificantDigits->first,
options.mSignificantDigits->second)) {
return;
}
}
if (!options.mUseGrouping) {
if (!disableGrouping()) {
return;
@ -64,10 +77,8 @@ NumberFormatterSkeleton::NumberFormatterSkeleton(
return;
}
if (options.mRoundingModeHalfUp) {
if (!roundingModeHalfUp()) {
return;
}
if (!roundingMode(options.mRoundingMode)) {
return;
}
mValidSkeleton = true;
@ -88,22 +99,16 @@ bool NumberFormatterSkeleton::currencyDisplay(
switch (display) {
case NumberFormatOptions::CurrencyDisplay::Code:
return appendToken(u"unit-width-iso-code");
break;
case NumberFormatOptions::CurrencyDisplay::Name:
return appendToken(u"unit-width-full-name");
break;
case NumberFormatOptions::CurrencyDisplay::Symbol:
// Default, no additional tokens needed.
return true;
break;
case NumberFormatOptions::CurrencyDisplay::NarrowSymbol:
return appendToken(u"unit-width-narrow");
break;
default:
MOZ_ASSERT_UNREACHABLE("unexpected currency display type");
return false;
break;
}
MOZ_ASSERT_UNREACHABLE("unexpected currency display type");
return false;
}
static const MeasureUnit& FindSimpleMeasureUnit(std::string_view name) {
@ -158,29 +163,32 @@ bool NumberFormatterSkeleton::unitDisplay(
switch (display) {
case NumberFormatOptions::UnitDisplay::Short:
return appendToken(u"unit-width-short");
break;
case NumberFormatOptions::UnitDisplay::Narrow:
return appendToken(u"unit-width-narrow");
break;
case NumberFormatOptions::UnitDisplay::Long:
return appendToken(u"unit-width-full-name");
break;
default:
MOZ_ASSERT_UNREACHABLE("unexpected unit display type");
return false;
break;
}
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 NumberFormatterSkeleton::fractionDigits(uint32_t min, uint32_t max,
bool stripTrailingZero) {
// Note: |min| can be zero here.
MOZ_ASSERT(min <= max);
return append('.') && appendN('0', min) && appendN('#', max - min) &&
append(' ');
if (!append('.') || !appendN('0', min) || !appendN('#', max - min)) {
return false;
}
if (stripTrailingZero) {
if (!append(u"/w")) {
return false;
}
}
return append(' ');
}
bool NumberFormatterSkeleton::minIntegerDigits(uint32_t min) {
@ -188,10 +196,19 @@ bool NumberFormatterSkeleton::minIntegerDigits(uint32_t min) {
return append(u"integer-width/+") && appendN('0', min) && append(' ');
}
bool NumberFormatterSkeleton::significantDigits(uint32_t min, uint32_t max) {
bool NumberFormatterSkeleton::significantDigits(uint32_t min, uint32_t max,
bool stripTrailingZero) {
MOZ_ASSERT(min > 0);
MOZ_ASSERT(min <= max);
return appendN('@', min) && appendN('#', max - min) && append(' ');
if (!appendN('@', min) || !appendN('#', max - min)) {
return false;
}
if (stripTrailingZero) {
if (!append(u"/w")) {
return false;
}
}
return append(' ');
}
bool NumberFormatterSkeleton::disableGrouping() {
@ -203,24 +220,17 @@ bool NumberFormatterSkeleton::notation(NumberFormatOptions::Notation style) {
case NumberFormatOptions::Notation::Standard:
// Default, no additional tokens needed.
return true;
break;
case NumberFormatOptions::Notation::Scientific:
return appendToken(u"scientific");
break;
case NumberFormatOptions::Notation::Engineering:
return appendToken(u"engineering");
break;
case NumberFormatOptions::Notation::CompactShort:
return appendToken(u"compact-short");
break;
case NumberFormatOptions::Notation::CompactLong:
return appendToken(u"compact-long");
break;
default:
MOZ_ASSERT_UNREACHABLE("unexpected notation style");
return false;
break;
}
MOZ_ASSERT_UNREACHABLE("unexpected notation style");
return false;
}
bool NumberFormatterSkeleton::signDisplay(
@ -229,34 +239,117 @@ bool NumberFormatterSkeleton::signDisplay(
case NumberFormatOptions::SignDisplay::Auto:
// Default, no additional tokens needed.
return true;
break;
case NumberFormatOptions::SignDisplay::Always:
return appendToken(u"sign-always");
break;
case NumberFormatOptions::SignDisplay::Never:
return appendToken(u"sign-never");
break;
case NumberFormatOptions::SignDisplay::ExceptZero:
return appendToken(u"sign-except-zero");
break;
case NumberFormatOptions::SignDisplay::Negative:
return appendToken(u"sign-negative");
case NumberFormatOptions::SignDisplay::Accounting:
return appendToken(u"sign-accounting");
break;
case NumberFormatOptions::SignDisplay::AccountingAlways:
return appendToken(u"sign-accounting-always");
break;
case NumberFormatOptions::SignDisplay::AccountingExceptZero:
return appendToken(u"sign-accounting-except-zero");
break;
default:
MOZ_ASSERT_UNREACHABLE("unexpected sign display type");
return false;
break;
case NumberFormatOptions::SignDisplay::AccountingNegative:
return appendToken(u"sign-accounting-negative");
}
MOZ_ASSERT_UNREACHABLE("unexpected sign display type");
return false;
}
bool NumberFormatterSkeleton::roundingModeHalfUp() {
return appendToken(u"rounding-mode-half-up");
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. (20 is the current limit for ECMA-402, but there are
// plans to change it 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(

Просмотреть файл

@ -80,11 +80,13 @@ class MOZ_STACK_CLASS NumberFormatterSkeleton final {
[[nodiscard]] bool percent();
[[nodiscard]] bool fractionDigits(uint32_t min, uint32_t max);
[[nodiscard]] bool fractionDigits(uint32_t min, uint32_t max,
bool stripTrailingZero);
[[nodiscard]] bool minIntegerDigits(uint32_t min);
[[nodiscard]] bool significantDigits(uint32_t min, uint32_t max);
[[nodiscard]] bool significantDigits(uint32_t min, uint32_t max,
bool stripTrailingZero);
[[nodiscard]] bool disableGrouping();
@ -92,7 +94,10 @@ class MOZ_STACK_CLASS NumberFormatterSkeleton final {
[[nodiscard]] bool signDisplay(NumberFormatOptions::SignDisplay display);
[[nodiscard]] bool roundingModeHalfUp();
[[nodiscard]] bool roundingIncrement(uint32_t increment, uint32_t mnfd,
uint32_t mxfd, bool stripTrailingZero);
[[nodiscard]] bool roundingMode(NumberFormatOptions::RoundingMode rounding);
};
} // namespace intl

Просмотреть файл

@ -131,7 +131,7 @@ struct MOZ_STACK_CLASS PluralRulesOptions {
*/
NumberFormatOptions ToNumberFormatOptions() const {
NumberFormatOptions options;
options.mRoundingModeHalfUp = true;
options.mRoundingMode = NumberFormatOptions::RoundingMode::HalfExpand;
if (mFractionDigits.isSome()) {
options.mFractionDigits.emplace(mFractionDigits.ref());

Просмотреть файл

@ -632,7 +632,7 @@ static mozilla::intl::NumberFormat* NewNumberFormat(
options.mSignDisplay = display;
}
options.mRoundingModeHalfUp = true;
options.mRoundingMode = RoundingMode::HalfExpand;
using NumberFormat = mozilla::intl::NumberFormat;
mozilla::Result<mozilla::UniquePtr<NumberFormat>, NumberFormat::FormatError>