NumberFormatterSkeleton.cpp (15827B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 #include "NumberFormatterSkeleton.h" 5 #include "NumberFormat.h" 6 7 #include "MeasureUnitGenerated.h" 8 9 #include "mozilla/RangedPtr.h" 10 11 #include <algorithm> 12 #include <limits> 13 14 #include "unicode/unumberrangeformatter.h" 15 16 namespace mozilla::intl { 17 18 NumberFormatterSkeleton::NumberFormatterSkeleton( 19 const NumberFormatOptions& options) { 20 if (options.mCurrency.isSome()) { 21 if (!currency(options.mCurrency->first) || 22 !currencyDisplay(options.mCurrency->second)) { 23 return; 24 } 25 } else if (options.mUnit.isSome()) { 26 if (!unit(options.mUnit->first) || !unitDisplay(options.mUnit->second)) { 27 return; 28 } 29 } else if (options.mPercent) { 30 if (!percent()) { 31 return; 32 } 33 } 34 35 if (options.mRoundingIncrement != 1) { 36 auto fd = options.mFractionDigits.valueOr(std::pair{0, 0}); 37 if (!roundingIncrement(options.mRoundingIncrement, fd.first, fd.second, 38 options.mStripTrailingZero)) { 39 return; 40 } 41 } else if (options.mRoundingPriority == 42 NumberFormatOptions::RoundingPriority::Auto) { 43 if (options.mFractionDigits.isSome()) { 44 if (!fractionDigits(options.mFractionDigits->first, 45 options.mFractionDigits->second, 46 options.mStripTrailingZero)) { 47 return; 48 } 49 } 50 51 if (options.mSignificantDigits.isSome()) { 52 if (!significantDigits(options.mSignificantDigits->first, 53 options.mSignificantDigits->second, 54 options.mStripTrailingZero)) { 55 return; 56 } 57 } 58 } else { 59 MOZ_ASSERT(options.mFractionDigits); 60 MOZ_ASSERT(options.mSignificantDigits); 61 62 bool relaxed = options.mRoundingPriority == 63 NumberFormatOptions::RoundingPriority::MorePrecision; 64 if (!fractionWithSignificantDigits(options.mFractionDigits->first, 65 options.mFractionDigits->second, 66 options.mSignificantDigits->first, 67 options.mSignificantDigits->second, 68 relaxed, options.mStripTrailingZero)) { 69 return; 70 } 71 } 72 73 if (options.mMinIntegerDigits.isSome()) { 74 if (!minIntegerDigits(*options.mMinIntegerDigits)) { 75 return; 76 } 77 } 78 79 if (!grouping(options.mGrouping)) { 80 return; 81 } 82 83 if (!notation(options.mNotation)) { 84 return; 85 } 86 87 if (!signDisplay(options.mSignDisplay)) { 88 return; 89 } 90 91 if (!roundingMode(options.mRoundingMode)) { 92 return; 93 } 94 95 mValidSkeleton = true; 96 } 97 98 bool NumberFormatterSkeleton::currency(std::string_view currency) { 99 MOZ_ASSERT(currency.size() == 3, 100 "IsWellFormedCurrencyCode permits only length-3 strings"); 101 102 char16_t currencyChars[] = {static_cast<char16_t>(currency[0]), 103 static_cast<char16_t>(currency[1]), 104 static_cast<char16_t>(currency[2]), '\0'}; 105 return append(u"currency/") && append(currencyChars) && append(' '); 106 } 107 108 bool NumberFormatterSkeleton::currencyDisplay( 109 NumberFormatOptions::CurrencyDisplay display) { 110 switch (display) { 111 case NumberFormatOptions::CurrencyDisplay::Code: 112 return appendToken(u"unit-width-iso-code"); 113 case NumberFormatOptions::CurrencyDisplay::Name: 114 return appendToken(u"unit-width-full-name"); 115 case NumberFormatOptions::CurrencyDisplay::Symbol: 116 // Default, no additional tokens needed. 117 return true; 118 case NumberFormatOptions::CurrencyDisplay::NarrowSymbol: 119 return appendToken(u"unit-width-narrow"); 120 } 121 MOZ_ASSERT_UNREACHABLE("unexpected currency display type"); 122 return false; 123 } 124 125 static const SimpleMeasureUnit& FindSimpleMeasureUnit(std::string_view name) { 126 const auto* measureUnit = std::lower_bound( 127 std::begin(simpleMeasureUnits), std::end(simpleMeasureUnits), name, 128 [](const auto& measureUnit, std::string_view name) { 129 return name.compare(measureUnit.name) > 0; 130 }); 131 MOZ_ASSERT(measureUnit != std::end(simpleMeasureUnits), 132 "unexpected unit identifier: unit not found"); 133 MOZ_ASSERT(measureUnit->name == name, 134 "unexpected unit identifier: wrong unit found"); 135 return *measureUnit; 136 } 137 138 static constexpr size_t MaxUnitLength() { 139 size_t length = 0; 140 for (const auto& unit : simpleMeasureUnits) { 141 length = std::max(length, std::char_traits<char>::length(unit.name)); 142 } 143 return length * 2 + std::char_traits<char>::length("-per-"); 144 } 145 146 bool NumberFormatterSkeleton::unit(std::string_view unit) { 147 MOZ_RELEASE_ASSERT(unit.length() <= MaxUnitLength()); 148 149 auto appendUnit = [this](const SimpleMeasureUnit& unit) { 150 return append(unit.type, strlen(unit.type)) && append('-') && 151 append(unit.name, strlen(unit.name)); 152 }; 153 154 // |unit| can be a compound unit identifier, separated by "-per-". 155 static constexpr char separator[] = "-per-"; 156 size_t separator_len = strlen(separator); 157 size_t offset = unit.find(separator); 158 if (offset != std::string_view::npos) { 159 const auto& numerator = FindSimpleMeasureUnit(unit.substr(0, offset)); 160 const auto& denominator = FindSimpleMeasureUnit( 161 std::string_view(unit.data() + offset + separator_len, 162 unit.length() - offset - separator_len)); 163 return append(u"measure-unit/") && appendUnit(numerator) && append(' ') && 164 append(u"per-measure-unit/") && appendUnit(denominator) && 165 append(' '); 166 } 167 168 const auto& simple = FindSimpleMeasureUnit(unit); 169 return append(u"measure-unit/") && appendUnit(simple) && append(' '); 170 } 171 172 bool NumberFormatterSkeleton::unitDisplay( 173 NumberFormatOptions::UnitDisplay display) { 174 switch (display) { 175 case NumberFormatOptions::UnitDisplay::Short: 176 return appendToken(u"unit-width-short"); 177 case NumberFormatOptions::UnitDisplay::Narrow: 178 return appendToken(u"unit-width-narrow"); 179 case NumberFormatOptions::UnitDisplay::Long: 180 return appendToken(u"unit-width-full-name"); 181 } 182 MOZ_ASSERT_UNREACHABLE("unexpected unit display type"); 183 return false; 184 } 185 186 bool NumberFormatterSkeleton::percent() { 187 return appendToken(u"percent scale/100"); 188 } 189 190 bool NumberFormatterSkeleton::fractionDigits(uint32_t min, uint32_t max, 191 bool stripTrailingZero) { 192 // Note: |min| can be zero here. 193 MOZ_ASSERT(min <= max); 194 if (!append('.') || !appendN('0', min) || !appendN('#', max - min)) { 195 return false; 196 } 197 if (stripTrailingZero) { 198 if (!append(u"/w")) { 199 return false; 200 } 201 } 202 return append(' '); 203 } 204 205 bool NumberFormatterSkeleton::fractionWithSignificantDigits( 206 uint32_t mnfd, uint32_t mxfd, uint32_t mnsd, uint32_t mxsd, bool relaxed, 207 bool stripTrailingZero) { 208 // Note: |mnfd| can be zero here. 209 MOZ_ASSERT(mnfd <= mxfd); 210 MOZ_ASSERT(mnsd > 0); 211 MOZ_ASSERT(mnsd <= mxsd); 212 213 if (!append('.') || !appendN('0', mnfd) || !appendN('#', mxfd - mnfd)) { 214 return false; 215 } 216 if (!append('/') || !appendN('@', mnsd) || !appendN('#', mxsd - mnsd)) { 217 return false; 218 } 219 if (!append(relaxed ? 'r' : 's')) { 220 return false; 221 } 222 if (stripTrailingZero) { 223 if (!append(u"/w")) { 224 return false; 225 } 226 } 227 return append(' '); 228 } 229 230 bool NumberFormatterSkeleton::minIntegerDigits(uint32_t min) { 231 MOZ_ASSERT(min > 0); 232 return append(u"integer-width/+") && appendN('0', min) && append(' '); 233 } 234 235 bool NumberFormatterSkeleton::significantDigits(uint32_t min, uint32_t max, 236 bool stripTrailingZero) { 237 MOZ_ASSERT(min > 0); 238 MOZ_ASSERT(min <= max); 239 if (!appendN('@', min) || !appendN('#', max - min)) { 240 return false; 241 } 242 if (stripTrailingZero) { 243 if (!append(u"/w")) { 244 return false; 245 } 246 } 247 return append(' '); 248 } 249 250 bool NumberFormatterSkeleton::grouping(NumberFormatOptions::Grouping grouping) { 251 switch (grouping) { 252 case NumberFormatOptions::Grouping::Auto: 253 // Default, no additional tokens needed. 254 return true; 255 case NumberFormatOptions::Grouping::Always: 256 return appendToken(u"group-on-aligned"); 257 case NumberFormatOptions::Grouping::Min2: 258 return appendToken(u"group-min2"); 259 case NumberFormatOptions::Grouping::Never: 260 return appendToken(u"group-off"); 261 } 262 MOZ_ASSERT_UNREACHABLE("unexpected grouping mode"); 263 return false; 264 } 265 266 bool NumberFormatterSkeleton::notation(NumberFormatOptions::Notation style) { 267 switch (style) { 268 case NumberFormatOptions::Notation::Standard: 269 // Default, no additional tokens needed. 270 return true; 271 case NumberFormatOptions::Notation::Scientific: 272 return appendToken(u"scientific"); 273 case NumberFormatOptions::Notation::Engineering: 274 return appendToken(u"engineering"); 275 case NumberFormatOptions::Notation::CompactShort: 276 return appendToken(u"compact-short"); 277 case NumberFormatOptions::Notation::CompactLong: 278 return appendToken(u"compact-long"); 279 } 280 MOZ_ASSERT_UNREACHABLE("unexpected notation style"); 281 return false; 282 } 283 284 bool NumberFormatterSkeleton::signDisplay( 285 NumberFormatOptions::SignDisplay display) { 286 switch (display) { 287 case NumberFormatOptions::SignDisplay::Auto: 288 // Default, no additional tokens needed. 289 return true; 290 case NumberFormatOptions::SignDisplay::Always: 291 return appendToken(u"sign-always"); 292 case NumberFormatOptions::SignDisplay::Never: 293 return appendToken(u"sign-never"); 294 case NumberFormatOptions::SignDisplay::ExceptZero: 295 return appendToken(u"sign-except-zero"); 296 case NumberFormatOptions::SignDisplay::Negative: 297 return appendToken(u"sign-negative"); 298 case NumberFormatOptions::SignDisplay::Accounting: 299 return appendToken(u"sign-accounting"); 300 case NumberFormatOptions::SignDisplay::AccountingAlways: 301 return appendToken(u"sign-accounting-always"); 302 case NumberFormatOptions::SignDisplay::AccountingExceptZero: 303 return appendToken(u"sign-accounting-except-zero"); 304 case NumberFormatOptions::SignDisplay::AccountingNegative: 305 return appendToken(u"sign-accounting-negative"); 306 } 307 MOZ_ASSERT_UNREACHABLE("unexpected sign display type"); 308 return false; 309 } 310 311 bool NumberFormatterSkeleton::roundingIncrement(uint32_t increment, 312 uint32_t mnfd, uint32_t mxfd, 313 bool stripTrailingZero) { 314 // Note: |mnfd| can be zero here. 315 MOZ_ASSERT(mnfd <= mxfd); 316 MOZ_ASSERT(increment > 1); 317 318 // Limit |mxfd| to 100. 319 constexpr size_t maxFracDigits = 100; 320 MOZ_RELEASE_ASSERT(mxfd <= maxFracDigits); 321 322 static constexpr char digits[] = "0123456789"; 323 324 // We need enough space to print any uint32_t, which is possibly shifted by 325 // |mxfd| decimal places. And additionally we need to reserve space for "0.". 326 static_assert(std::numeric_limits<uint32_t>::digits10 + 1 < maxFracDigits); 327 constexpr size_t maxLength = maxFracDigits + 2; 328 329 char chars[maxLength]; 330 RangedPtr<char> ptr(chars + maxLength, chars, maxLength); 331 const RangedPtr<char> end = ptr; 332 333 // Convert to a signed integer, so we don't have to worry about underflows. 334 int32_t maxFrac = int32_t(mxfd); 335 336 // Write |increment| from back to front. 337 while (increment != 0) { 338 *--ptr = digits[increment % 10]; 339 increment /= 10; 340 maxFrac -= 1; 341 342 if (maxFrac == 0) { 343 *--ptr = '.'; 344 } 345 } 346 347 // Write any remaining zeros from |mxfd| and prepend '0' if we last wrote the 348 // decimal point. 349 while (maxFrac >= 0) { 350 MOZ_ASSERT_IF(maxFrac == 0, *ptr == '.'); 351 352 *--ptr = '0'; 353 maxFrac -= 1; 354 355 if (maxFrac == 0) { 356 *--ptr = '.'; 357 } 358 } 359 360 MOZ_ASSERT(ptr < end, "At least one character is written."); 361 MOZ_ASSERT(*ptr != '.', "First character is a digit."); 362 363 if (!append(u"precision-increment/") || !append(ptr.get(), end - ptr)) { 364 return false; 365 } 366 if (stripTrailingZero) { 367 if (!append(u"/w")) { 368 return false; 369 } 370 } 371 return append(' '); 372 } 373 374 bool NumberFormatterSkeleton::roundingMode( 375 NumberFormatOptions::RoundingMode rounding) { 376 switch (rounding) { 377 case NumberFormatOptions::RoundingMode::Ceil: 378 return appendToken(u"rounding-mode-ceiling"); 379 case NumberFormatOptions::RoundingMode::Floor: 380 return appendToken(u"rounding-mode-floor"); 381 case NumberFormatOptions::RoundingMode::Expand: 382 return appendToken(u"rounding-mode-up"); 383 case NumberFormatOptions::RoundingMode::Trunc: 384 return appendToken(u"rounding-mode-down"); 385 case NumberFormatOptions::RoundingMode::HalfCeil: 386 return appendToken(u"rounding-mode-half-ceiling"); 387 case NumberFormatOptions::RoundingMode::HalfFloor: 388 return appendToken(u"rounding-mode-half-floor"); 389 case NumberFormatOptions::RoundingMode::HalfExpand: 390 return appendToken(u"rounding-mode-half-up"); 391 case NumberFormatOptions::RoundingMode::HalfTrunc: 392 return appendToken(u"rounding-mode-half-down"); 393 case NumberFormatOptions::RoundingMode::HalfEven: 394 return appendToken(u"rounding-mode-half-even"); 395 case NumberFormatOptions::RoundingMode::HalfOdd: 396 return appendToken(u"rounding-mode-half-odd"); 397 } 398 MOZ_ASSERT_UNREACHABLE("unexpected rounding mode"); 399 return false; 400 } 401 402 UNumberFormatter* NumberFormatterSkeleton::toFormatter( 403 std::string_view locale) { 404 if (!mValidSkeleton) { 405 return nullptr; 406 } 407 408 UErrorCode status = U_ZERO_ERROR; 409 UNumberFormatter* nf = unumf_openForSkeletonAndLocale( 410 mVector.begin(), mVector.length(), AssertNullTerminatedString(locale), 411 &status); 412 if (U_FAILURE(status)) { 413 return nullptr; 414 } 415 return nf; 416 } 417 418 static UNumberRangeCollapse ToUNumberRangeCollapse( 419 NumberRangeFormatOptions::RangeCollapse collapse) { 420 using RangeCollapse = NumberRangeFormatOptions::RangeCollapse; 421 switch (collapse) { 422 case RangeCollapse::Auto: 423 return UNUM_RANGE_COLLAPSE_AUTO; 424 case RangeCollapse::None: 425 return UNUM_RANGE_COLLAPSE_NONE; 426 case RangeCollapse::Unit: 427 return UNUM_RANGE_COLLAPSE_UNIT; 428 case RangeCollapse::All: 429 return UNUM_RANGE_COLLAPSE_ALL; 430 } 431 MOZ_ASSERT_UNREACHABLE("unexpected range collapse"); 432 return UNUM_RANGE_COLLAPSE_NONE; 433 } 434 435 static UNumberRangeIdentityFallback ToUNumberRangeIdentityFallback( 436 NumberRangeFormatOptions::RangeIdentityFallback identity) { 437 using RangeIdentityFallback = NumberRangeFormatOptions::RangeIdentityFallback; 438 switch (identity) { 439 case RangeIdentityFallback::SingleValue: 440 return UNUM_IDENTITY_FALLBACK_SINGLE_VALUE; 441 case RangeIdentityFallback::ApproximatelyOrSingleValue: 442 return UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE; 443 case RangeIdentityFallback::Approximately: 444 return UNUM_IDENTITY_FALLBACK_APPROXIMATELY; 445 case RangeIdentityFallback::Range: 446 return UNUM_IDENTITY_FALLBACK_RANGE; 447 } 448 MOZ_ASSERT_UNREACHABLE("unexpected range identity fallback"); 449 return UNUM_IDENTITY_FALLBACK_RANGE; 450 } 451 452 UNumberRangeFormatter* NumberFormatterSkeleton::toRangeFormatter( 453 std::string_view locale, NumberRangeFormatOptions::RangeCollapse collapse, 454 NumberRangeFormatOptions::RangeIdentityFallback identity) { 455 if (!mValidSkeleton) { 456 return nullptr; 457 } 458 459 UParseError* perror = nullptr; 460 UErrorCode status = U_ZERO_ERROR; 461 UNumberRangeFormatter* nrf = 462 unumrf_openForSkeletonWithCollapseAndIdentityFallback( 463 mVector.begin(), mVector.length(), ToUNumberRangeCollapse(collapse), 464 ToUNumberRangeIdentityFallback(identity), 465 AssertNullTerminatedString(locale), perror, &status); 466 if (U_FAILURE(status)) { 467 return nullptr; 468 } 469 return nrf; 470 } 471 472 } // namespace mozilla::intl