// Copyright 2018 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef V8_INTL_SUPPORT #error Internationalization is expected to be enabled. #endif // V8_INTL_SUPPORT #include "src/objects/js-number-format.h" #include #include #include "src/execution/isolate.h" #include "src/objects/intl-objects.h" #include "src/objects/js-number-format-inl.h" #include "src/objects/managed-inl.h" #include "src/objects/objects-inl.h" #include "src/objects/option-utils.h" #include "unicode/currunit.h" #include "unicode/locid.h" #include "unicode/numberformatter.h" #include "unicode/numberrangeformatter.h" #include "unicode/numsys.h" #include "unicode/ucurr.h" #include "unicode/uloc.h" #include "unicode/unumberformatter.h" #include "unicode/uvernum.h" // for U_ICU_VERSION_MAJOR_NUM namespace v8 { namespace internal { namespace { // [[Style]] is one of the values "decimal", "percent", "currency", // or "unit" identifying the style of the number format. enum class Style { DECIMAL, PERCENT, CURRENCY, UNIT }; // [[CurrencyDisplay]] is one of the values "code", "symbol", "name", // or "narrowSymbol" identifying the display of the currency number format. enum class CurrencyDisplay { CODE, SYMBOL, NAME, NARROW_SYMBOL, }; // [[CurrencySign]] is one of the String values "standard" or "accounting", // specifying whether to render negative numbers in accounting format, often // signified by parenthesis. It is only used when [[Style]] has the value // "currency" and when [[SignDisplay]] is not "never". enum class CurrencySign { STANDARD, ACCOUNTING, }; // [[UnitDisplay]] is one of the String values "short", "narrow", or "long", // specifying whether to display the unit as a symbol, narrow symbol, or // localized long name if formatting with the "unit" style. It is // only used when [[Style]] has the value "unit". enum class UnitDisplay { SHORT, NARROW, LONG, }; // [[Notation]] is one of the String values "standard", "scientific", // "engineering", or "compact", specifying whether the number should be // displayed without scaling, scaled to the units place with the power of ten // in scientific notation, scaled to the nearest thousand with the power of // ten in scientific notation, or scaled to the nearest locale-dependent // compact decimal notation power of ten with the corresponding compact // decimal notation affix. enum class Notation { STANDARD, SCIENTIFIC, ENGINEERING, COMPACT, }; // [[CompactDisplay]] is one of the String values "short" or "long", // specifying whether to display compact notation affixes in short form ("5K") // or long form ("5 thousand") if formatting with the "compact" notation. It // is only used when [[Notation]] has the value "compact". enum class CompactDisplay { SHORT, LONG, }; // [[SignDisplay]] is one of the String values "auto", "always", "never", or // "exceptZero", specifying whether to show the sign on negative numbers // only, positive and negative numbers including zero, neither positive nor // negative numbers, or positive and negative numbers but not zero. enum class SignDisplay { AUTO, ALWAYS, NEVER, EXCEPT_ZERO, NEGATIVE, }; // [[RoundingMode]] is one of the String values "ceil", "floor", "expand", // "trunc", "halfCeil", "halfFloor", "halfExpand", "halfTrunc", or "halfEven", // specifying the rounding strategy for the number. // Note: To avoid name conflict with RoundingMode defined in other places, // prefix with Intl as IntlRoundingMode enum class IntlRoundingMode { CEIL, FLOOR, EXPAND, TRUNC, HALF_CEIL, HALF_FLOOR, HALF_EXPAND, HALF_TRUNC, HALF_EVEN, }; // [[TrailingZeroDisplay]] is one of the String values "auto" or // "stripIfInteger", specifying the strategy for displaying trailing zeros on // whole number. enum class TrailingZeroDisplay { AUTO, STRIP_IF_INTEGER, }; // [[UseGrouping]] is .... enum class UseGrouping { OFF, MIN2, AUTO, ALWAYS, }; UNumberUnitWidth ToUNumberUnitWidth(CurrencyDisplay currency_display) { switch (currency_display) { case CurrencyDisplay::SYMBOL: return UNumberUnitWidth::UNUM_UNIT_WIDTH_SHORT; case CurrencyDisplay::CODE: return UNumberUnitWidth::UNUM_UNIT_WIDTH_ISO_CODE; case CurrencyDisplay::NAME: return UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME; case CurrencyDisplay::NARROW_SYMBOL: return UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW; } } UNumberUnitWidth ToUNumberUnitWidth(UnitDisplay unit_display) { switch (unit_display) { case UnitDisplay::SHORT: return UNumberUnitWidth::UNUM_UNIT_WIDTH_SHORT; case UnitDisplay::LONG: return UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME; case UnitDisplay::NARROW: return UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW; } } UNumberSignDisplay ToUNumberSignDisplay(SignDisplay sign_display, CurrencySign currency_sign) { switch (sign_display) { case SignDisplay::AUTO: if (currency_sign == CurrencySign::ACCOUNTING) { return UNumberSignDisplay::UNUM_SIGN_ACCOUNTING; } DCHECK(currency_sign == CurrencySign::STANDARD); return UNumberSignDisplay::UNUM_SIGN_AUTO; case SignDisplay::NEVER: return UNumberSignDisplay::UNUM_SIGN_NEVER; case SignDisplay::ALWAYS: if (currency_sign == CurrencySign::ACCOUNTING) { return UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_ALWAYS; } DCHECK(currency_sign == CurrencySign::STANDARD); return UNumberSignDisplay::UNUM_SIGN_ALWAYS; case SignDisplay::EXCEPT_ZERO: if (currency_sign == CurrencySign::ACCOUNTING) { return UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_EXCEPT_ZERO; } DCHECK(currency_sign == CurrencySign::STANDARD); return UNumberSignDisplay::UNUM_SIGN_EXCEPT_ZERO; case SignDisplay::NEGATIVE: if (currency_sign == CurrencySign::ACCOUNTING) { return UNumberSignDisplay::UNUM_SIGN_ACCOUNTING_NEGATIVE; } DCHECK(currency_sign == CurrencySign::STANDARD); return UNumberSignDisplay::UNUM_SIGN_NEGATIVE; } } icu::number::Notation ToICUNotation(Notation notation, CompactDisplay compact_display) { switch (notation) { case Notation::STANDARD: return icu::number::Notation::simple(); case Notation::SCIENTIFIC: return icu::number::Notation::scientific(); case Notation::ENGINEERING: return icu::number::Notation::engineering(); // 29. If notation is "compact", then case Notation::COMPACT: // 29. a. Set numberFormat.[[CompactDisplay]] to compactDisplay. if (compact_display == CompactDisplay::SHORT) { return icu::number::Notation::compactShort(); } DCHECK(compact_display == CompactDisplay::LONG); return icu::number::Notation::compactLong(); } } UNumberFormatRoundingMode ToUNumberFormatRoundingMode( IntlRoundingMode rounding_mode) { switch (rounding_mode) { case IntlRoundingMode::CEIL: return UNumberFormatRoundingMode::UNUM_ROUND_CEILING; case IntlRoundingMode::FLOOR: return UNumberFormatRoundingMode::UNUM_ROUND_FLOOR; case IntlRoundingMode::EXPAND: return UNumberFormatRoundingMode::UNUM_ROUND_UP; case IntlRoundingMode::TRUNC: return UNumberFormatRoundingMode::UNUM_ROUND_DOWN; case IntlRoundingMode::HALF_CEIL: return UNumberFormatRoundingMode::UNUM_ROUND_HALF_CEILING; case IntlRoundingMode::HALF_FLOOR: return UNumberFormatRoundingMode::UNUM_ROUND_HALF_FLOOR; case IntlRoundingMode::HALF_EXPAND: return UNumberFormatRoundingMode::UNUM_ROUND_HALFUP; case IntlRoundingMode::HALF_TRUNC: return UNumberFormatRoundingMode::UNUM_ROUND_HALFDOWN; case IntlRoundingMode::HALF_EVEN: return UNumberFormatRoundingMode::UNUM_ROUND_HALFEVEN; } } UNumberGroupingStrategy ToUNumberGroupingStrategy(UseGrouping use_grouping) { switch (use_grouping) { case UseGrouping::OFF: return UNumberGroupingStrategy::UNUM_GROUPING_OFF; case UseGrouping::MIN2: return UNumberGroupingStrategy::UNUM_GROUPING_MIN2; case UseGrouping::AUTO: return UNumberGroupingStrategy::UNUM_GROUPING_AUTO; case UseGrouping::ALWAYS: return UNumberGroupingStrategy::UNUM_GROUPING_ON_ALIGNED; } } std::map CreateUnitMap() { UErrorCode status = U_ZERO_ERROR; int32_t total = icu::MeasureUnit::getAvailable(nullptr, 0, status); CHECK(U_FAILURE(status)); status = U_ZERO_ERROR; std::vector units(total); total = icu::MeasureUnit::getAvailable(units.data(), total, status); CHECK(U_SUCCESS(status)); std::map map; std::set sanctioned(Intl::SanctionedSimpleUnits()); for (auto it = units.begin(); it != units.end(); ++it) { // Need to skip none/percent if (sanctioned.count(it->getSubtype()) > 0 && strcmp("none", it->getType()) != 0) { map[it->getSubtype()] = *it; } } return map; } class UnitFactory { public: UnitFactory() : map_(CreateUnitMap()) {} virtual ~UnitFactory() = default; // ecma402 #sec-issanctionedsimpleunitidentifier icu::MeasureUnit create(const std::string& unitIdentifier) { // 1. If unitIdentifier is in the following list, return true. auto found = map_.find(unitIdentifier); if (found != map_.end()) { return found->second; } // 2. Return false. return icu::MeasureUnit(); } private: std::map map_; }; // ecma402 #sec-issanctionedsimpleunitidentifier icu::MeasureUnit IsSanctionedUnitIdentifier(const std::string& unit) { static base::LazyInstance::type factory = LAZY_INSTANCE_INITIALIZER; return factory.Pointer()->create(unit); } // ecma402 #sec-iswellformedunitidentifier Maybe> IsWellFormedUnitIdentifier( Isolate* isolate, const std::string& unit) { icu::MeasureUnit result = IsSanctionedUnitIdentifier(unit); icu::MeasureUnit none = icu::MeasureUnit(); // 1. If the result of IsSanctionedUnitIdentifier(unitIdentifier) is true, // then if (result != none) { // a. Return true. std::pair pair(result, none); return Just(pair); } // 2. If the substring "-per-" does not occur exactly once in unitIdentifier, // then size_t first_per = unit.find("-per-"); if (first_per == std::string::npos || unit.find("-per-", first_per + 5) != std::string::npos) { // a. Return false. return Nothing>(); } // 3. Let numerator be the substring of unitIdentifier from the beginning to // just before "-per-". std::string numerator = unit.substr(0, first_per); // 4. If the result of IsSanctionedUnitIdentifier(numerator) is false, then result = IsSanctionedUnitIdentifier(numerator); if (result == none) { // a. Return false. return Nothing>(); } // 5. Let denominator be the substring of unitIdentifier from just after // "-per-" to the end. std::string denominator = unit.substr(first_per + 5); // 6. If the result of IsSanctionedUnitIdentifier(denominator) is false, then icu::MeasureUnit den_result = IsSanctionedUnitIdentifier(denominator); if (den_result == none) { // a. Return false. return Nothing>(); } // 7. Return true. std::pair pair(result, den_result); return Just(pair); } // ecma-402/#sec-currencydigits // The currency is expected to an all upper case string value. int CurrencyDigits(const icu::UnicodeString& currency) { UErrorCode status = U_ZERO_ERROR; uint32_t fraction_digits = ucurr_getDefaultFractionDigits( reinterpret_cast(currency.getBuffer()), &status); // For missing currency codes, default to the most common, 2 return U_SUCCESS(status) ? fraction_digits : 2; } bool IsAToZ(char ch) { return base::IsInRange(AsciiAlphaToLower(ch), 'a', 'z'); } // ecma402/#sec-iswellformedcurrencycode bool IsWellFormedCurrencyCode(const std::string& currency) { // Verifies that the input is a well-formed ISO 4217 currency code. // ecma402/#sec-currency-codes // 2. If the number of elements in normalized is not 3, return false. if (currency.length() != 3) return false; // 1. Let normalized be the result of mapping currency to upper case as // described in 6.1. // // 3. If normalized contains any character that is not in // the range "A" to "Z" (U+0041 to U+005A), return false. // // 4. Return true. // Don't uppercase to test. It could convert invalid code into a valid one. // For example \u00DFP (Eszett+P) becomes SSP. return (IsAToZ(currency[0]) && IsAToZ(currency[1]) && IsAToZ(currency[2])); } // Return the style as a String. Handle StyleAsString(Isolate* isolate, Style style) { switch (style) { case Style::PERCENT: return ReadOnlyRoots(isolate).percent_string_handle(); case Style::CURRENCY: return ReadOnlyRoots(isolate).currency_string_handle(); case Style::UNIT: return ReadOnlyRoots(isolate).unit_string_handle(); case Style::DECIMAL: return ReadOnlyRoots(isolate).decimal_string_handle(); } UNREACHABLE(); } // Parse the 'currencyDisplay' from the skeleton. Handle CurrencyDisplayString(Isolate* isolate, const icu::UnicodeString& skeleton) { // Ex: skeleton as // "currency/TWD .00 rounding-mode-half-up unit-width-iso-code" if (skeleton.indexOf("unit-width-iso-code") >= 0) { return ReadOnlyRoots(isolate).code_string_handle(); } // Ex: skeleton as // "currency/TWD .00 rounding-mode-half-up unit-width-full-name;" if (skeleton.indexOf("unit-width-full-name") >= 0) { return ReadOnlyRoots(isolate).name_string_handle(); } // Ex: skeleton as // "currency/TWD .00 rounding-mode-half-up unit-width-narrow; if (skeleton.indexOf("unit-width-narrow") >= 0) { return ReadOnlyRoots(isolate).narrowSymbol_string_handle(); } // Ex: skeleton as "currency/TWD .00 rounding-mode-half-up" return ReadOnlyRoots(isolate).symbol_string_handle(); } // Return true if there are no "group-off" in the skeleton. bool UseGroupingFromSkeleton(const icu::UnicodeString& skeleton) { return skeleton.indexOf("group-off") == -1; } Handle UseGroupingFromSkeleton(Isolate* isolate, const icu::UnicodeString& skeleton) { Factory* factory = isolate->factory(); static const char* group = "group-"; int32_t start = skeleton.indexOf(group); if (start >= 0) { DCHECK_EQ(6, strlen(group)); icu::UnicodeString check = skeleton.tempSubString(start + 6); // Ex: skeleton as // .### rounding-mode-half-up group-off if (check.startsWith("off")) { return factory->false_value(); } // Ex: skeleton as // .### rounding-mode-half-up group-min2 if (check.startsWith("min2")) { return ReadOnlyRoots(isolate).min2_string_handle(); } // Ex: skeleton as // .### rounding-mode-half-up group-on-aligned if (check.startsWith("on-aligned")) { return ReadOnlyRoots(isolate).always_string_handle(); } } // Ex: skeleton as // .### return ReadOnlyRoots(isolate).auto_string_handle(); } // Parse currency code from skeleton. For example, skeleton as // "currency/TWD .00 rounding-mode-half-up unit-width-full-name;" const icu::UnicodeString CurrencyFromSkeleton( const icu::UnicodeString& skeleton) { const char currency[] = "currency/"; int32_t index = skeleton.indexOf(currency); if (index < 0) return ""; index += static_cast(std::strlen(currency)); return skeleton.tempSubString(index, 3); } const icu::UnicodeString NumberingSystemFromSkeleton( const icu::UnicodeString& skeleton) { const char numbering_system[] = "numbering-system/"; int32_t index = skeleton.indexOf(numbering_system); if (index < 0) return "latn"; index += static_cast(std::strlen(numbering_system)); const icu::UnicodeString res = skeleton.tempSubString(index); index = res.indexOf(" "); if (index < 0) return res; return res.tempSubString(0, index); } // Return CurrencySign as string based on skeleton. Handle CurrencySignString(Isolate* isolate, const icu::UnicodeString& skeleton) { // Ex: skeleton as // "currency/TWD .00 rounding-mode-half-up sign-accounting-always" OR // "currency/TWD .00 rounding-mode-half-up sign-accounting-except-zero" if (skeleton.indexOf("sign-accounting") >= 0) { return ReadOnlyRoots(isolate).accounting_string_handle(); } return ReadOnlyRoots(isolate).standard_string_handle(); } // Return UnitDisplay as string based on skeleton. Handle UnitDisplayString(Isolate* isolate, const icu::UnicodeString& skeleton) { // Ex: skeleton as // "unit/length-meter .### rounding-mode-half-up unit-width-full-name" if (skeleton.indexOf("unit-width-full-name") >= 0) { return ReadOnlyRoots(isolate).long_string_handle(); } // Ex: skeleton as // "unit/length-meter .### rounding-mode-half-up unit-width-narrow". if (skeleton.indexOf("unit-width-narrow") >= 0) { return ReadOnlyRoots(isolate).narrow_string_handle(); } // Ex: skeleton as // "unit/length-foot .### rounding-mode-half-up" return ReadOnlyRoots(isolate).short_string_handle(); } // Parse Notation from skeleton. Notation NotationFromSkeleton(const icu::UnicodeString& skeleton) { // Ex: skeleton as // "scientific .### rounding-mode-half-up" if (skeleton.indexOf("scientific") >= 0) { return Notation::SCIENTIFIC; } // Ex: skeleton as // "engineering .### rounding-mode-half-up" if (skeleton.indexOf("engineering") >= 0) { return Notation::ENGINEERING; } // Ex: skeleton as // "compact-short .### rounding-mode-half-up" or // "compact-long .### rounding-mode-half-up if (skeleton.indexOf("compact-") >= 0) { return Notation::COMPACT; } // Ex: skeleton as // "unit/length-foot .### rounding-mode-half-up" return Notation::STANDARD; } Handle NotationAsString(Isolate* isolate, Notation notation) { switch (notation) { case Notation::SCIENTIFIC: return ReadOnlyRoots(isolate).scientific_string_handle(); case Notation::ENGINEERING: return ReadOnlyRoots(isolate).engineering_string_handle(); case Notation::COMPACT: return ReadOnlyRoots(isolate).compact_string_handle(); case Notation::STANDARD: return ReadOnlyRoots(isolate).standard_string_handle(); } UNREACHABLE(); } // Return CompactString as string based on skeleton. Handle CompactDisplayString(Isolate* isolate, const icu::UnicodeString& skeleton) { // Ex: skeleton as // "compact-long .### rounding-mode-half-up" if (skeleton.indexOf("compact-long") >= 0) { return ReadOnlyRoots(isolate).long_string_handle(); } // Ex: skeleton as // "compact-short .### rounding-mode-half-up" DCHECK_GE(skeleton.indexOf("compact-short"), 0); return ReadOnlyRoots(isolate).short_string_handle(); } // Return SignDisplay as string based on skeleton. Handle SignDisplayString(Isolate* isolate, const icu::UnicodeString& skeleton) { // Ex: skeleton as // "currency/TWD .00 rounding-mode-half-up sign-never" if (skeleton.indexOf("sign-never") >= 0) { return ReadOnlyRoots(isolate).never_string_handle(); } // Ex: skeleton as // ".### rounding-mode-half-up sign-always" or // "currency/TWD .00 rounding-mode-half-up sign-accounting-always" if (skeleton.indexOf("sign-always") >= 0 || skeleton.indexOf("sign-accounting-always") >= 0) { return ReadOnlyRoots(isolate).always_string_handle(); } // Ex: skeleton as // "currency/TWD .00 rounding-mode-half-up sign-accounting-except-zero" or // "currency/TWD .00 rounding-mode-half-up sign-except-zero" if (skeleton.indexOf("sign-accounting-except-zero") >= 0 || skeleton.indexOf("sign-except-zero") >= 0) { return ReadOnlyRoots(isolate).exceptZero_string_handle(); } // Ex: skeleton as // ".### rounding-mode-half-up sign-negative" or // "currency/TWD .00 rounding-mode-half-up sign-accounting-negative" if (skeleton.indexOf("sign-accounting-negative") >= 0 || skeleton.indexOf("sign-negative") >= 0) { return ReadOnlyRoots(isolate).negative_string_handle(); } return ReadOnlyRoots(isolate).auto_string_handle(); } // Return RoundingMode as string based on skeleton. Handle RoundingModeString(Isolate* isolate, const icu::UnicodeString& skeleton) { static const char* rounding_mode = "rounding-mode-"; int32_t start = skeleton.indexOf(rounding_mode); if (start >= 0) { DCHECK_EQ(14, strlen(rounding_mode)); icu::UnicodeString check = skeleton.tempSubString(start + 14); // Ex: skeleton as // .### rounding-mode-ceiling if (check.startsWith("ceiling")) { return ReadOnlyRoots(isolate).ceil_string_handle(); } // Ex: skeleton as // .### rounding-mode-down if (check.startsWith("down")) { return ReadOnlyRoots(isolate).trunc_string_handle(); } // Ex: skeleton as // .### rounding-mode-floor if (check.startsWith("floor")) { return ReadOnlyRoots(isolate).floor_string_handle(); } // Ex: skeleton as // .### rounding-mode-half-ceiling if (check.startsWith("half-ceiling")) { return ReadOnlyRoots(isolate).halfCeil_string_handle(); } // Ex: skeleton as // .### rounding-mode-half-down if (check.startsWith("half-down")) { return ReadOnlyRoots(isolate).halfTrunc_string_handle(); } // Ex: skeleton as // .### rounding-mode-half-floor if (check.startsWith("half-floor")) { return ReadOnlyRoots(isolate).halfFloor_string_handle(); } // Ex: skeleton as // .### rounding-mode-half-up if (check.startsWith("half-up")) { return ReadOnlyRoots(isolate).halfExpand_string_handle(); } // Ex: skeleton as // .### rounding-mode-up if (check.startsWith("up")) { return ReadOnlyRoots(isolate).expand_string_handle(); } } // Ex: skeleton as // .### return ReadOnlyRoots(isolate).halfEven_string_handle(); } Handle RoundingIncrement(Isolate* isolate, const icu::UnicodeString& skeleton) { int32_t cur = skeleton.indexOf(u"precision-increment/"); if (cur < 0) return isolate->factory()->NewNumberFromInt(1); cur += 20; // length of "precision-increment/" int32_t increment = 0; while (cur < skeleton.length()) { char16_t c = skeleton[cur++]; if (c == u'.') continue; if (!IsDecimalDigit(c)) break; increment = increment * 10 + (c - '0'); } return isolate->factory()->NewNumberFromInt(increment); } // Return RoundingPriority as string based on skeleton. Handle RoundingPriorityString(Isolate* isolate, const icu::UnicodeString& skeleton) { int32_t found; // If #r or @r is followed by a SPACE or in the end of line. if ((found = skeleton.indexOf("#r")) >= 0 || (found = skeleton.indexOf("@r")) >= 0) { if (found + 2 == skeleton.length() || skeleton[found + 2] == ' ') { return ReadOnlyRoots(isolate).morePrecision_string_handle(); } } // If #s or @s is followed by a SPACE or in the end of line. if ((found = skeleton.indexOf("#s")) >= 0 || (found = skeleton.indexOf("@s")) >= 0) { if (found + 2 == skeleton.length() || skeleton[found + 2] == ' ') { return ReadOnlyRoots(isolate).lessPrecision_string_handle(); } } return ReadOnlyRoots(isolate).auto_string_handle(); } // Return trailingZeroDisplay as string based on skeleton. Handle TrailingZeroDisplayString(Isolate* isolate, const icu::UnicodeString& skeleton) { int32_t found; if ((found = skeleton.indexOf("/w")) >= 0) { if (found + 2 == skeleton.length() || skeleton[found + 2] == ' ') { return ReadOnlyRoots(isolate).stripIfInteger_string_handle(); } } return ReadOnlyRoots(isolate).auto_string_handle(); } } // anonymous namespace // Return the minimum integer digits by counting the number of '0' after // "integer-width/*" in the skeleton. // Ex: Return 15 for skeleton as // “currency/TWD .00 rounding-mode-half-up integer-width/*000000000000000” // 1 // 123456789012345 // Return default value as 1 if there are no "integer-width/*". int32_t JSNumberFormat::MinimumIntegerDigitsFromSkeleton( const icu::UnicodeString& skeleton) { // count the number of 0 after "integer-width/*" icu::UnicodeString search("integer-width/*"); int32_t index = skeleton.indexOf(search); if (index < 0) return 1; // return 1 if cannot find it. index += search.length(); int32_t matched = 0; while (index < skeleton.length() && skeleton[index] == '0') { matched++; index++; } CHECK_GT(matched, 0); return matched; } // Return true if there are fraction digits, false if not. // The minimum fraction digits is the number of '0' after '.' in the skeleton // The maximum fraction digits is the number of '#' after the above '0's plus // the minimum fraction digits. // For example, as skeleton “.000#### rounding-mode-half-up” // 123 // 4567 // Set The minimum as 3 and maximum as 7. bool JSNumberFormat::FractionDigitsFromSkeleton( const icu::UnicodeString& skeleton, int32_t* minimum, int32_t* maximum) { icu::UnicodeString search("."); int32_t index = skeleton.indexOf(search); if (index < 0) return false; *minimum = 0; index++; // skip the '.' while (index < skeleton.length() && IsDecimalDigit(skeleton[index])) { (*minimum)++; index++; } *maximum = *minimum; while (index < skeleton.length() && skeleton[index] == '#') { (*maximum)++; index++; } return true; } // Return true if there are significant digits, false if not. // The minimum significant digits is the number of '@' in the skeleton // The maximum significant digits is the number of '#' after these '@'s plus // the minimum significant digits. // Ex: Skeleton as "@@@@@####### rounding-mode-half-up" // 12345 // 6789012 // Set The minimum as 5 and maximum as 12. bool JSNumberFormat::SignificantDigitsFromSkeleton( const icu::UnicodeString& skeleton, int32_t* minimum, int32_t* maximum) { icu::UnicodeString search("@"); int32_t index = skeleton.indexOf(search); if (index < 0) return false; *minimum = 1; index++; // skip the first '@' while (index < skeleton.length() && skeleton[index] == '@') { (*minimum)++; index++; } *maximum = *minimum; while (index < skeleton.length() && skeleton[index] == '#') { (*maximum)++; index++; } return true; } namespace { // Ex: percent .### rounding-mode-half-up // Special case for "percent" // Ex: "unit/milliliter-per-acre .### rounding-mode-half-up" // should return "milliliter-per-acre". // Ex: "unit/year .### rounding-mode-half-up" should return // "year". std::string UnitFromSkeleton(const icu::UnicodeString& skeleton) { std::string str; str = skeleton.toUTF8String(str); std::string search("unit/"); size_t begin = str.find(search); if (begin == str.npos) { // Special case for "percent". if (str.find("percent") != str.npos) { return "percent"; } return ""; } // Ex: // "unit/acre .### rounding-mode-half-up" // b // Ex: // "unit/milliliter-per-acre .### rounding-mode-half-up" // b begin += search.size(); if (begin == str.npos) { return ""; } // Find the end of the subtype. size_t end = str.find(" ", begin); // Ex: // "unit/acre .### rounding-mode-half-up" // b e // Ex: // "unit/milliliter-per-acre .### rounding-mode-half-up" // b e if (end == str.npos) { end = str.size(); } return str.substr(begin, end - begin); } Style StyleFromSkeleton(const icu::UnicodeString& skeleton) { if (skeleton.indexOf("currency/") >= 0) { return Style::CURRENCY; } if (skeleton.indexOf("percent") >= 0) { // percent precision-integer rounding-mode-half-up scale/100 if (skeleton.indexOf("scale/100") >= 0) { return Style::PERCENT; } else { return Style::UNIT; } } // Before ICU68: "measure-unit/", since ICU68 "unit/" if (skeleton.indexOf("unit/") >= 0) { return Style::UNIT; } return Style::DECIMAL; } icu::number::UnlocalizedNumberFormatter SetDigitOptionsToFormatterV2( const icu::number::UnlocalizedNumberFormatter& settings, const Intl::NumberFormatDigitOptions& digit_options) { icu::number::UnlocalizedNumberFormatter result = settings; if (digit_options.minimum_integer_digits > 1) { result = result.integerWidth(icu::number::IntegerWidth::zeroFillTo( digit_options.minimum_integer_digits)); } if (digit_options.rounding_type == Intl::RoundingType::kMorePrecision) { return result; } icu::number::Precision precision = (digit_options.minimum_significant_digits > 0) ? icu::number::Precision::minMaxSignificantDigits( digit_options.minimum_significant_digits, digit_options.maximum_significant_digits) : icu::number::Precision::minMaxFraction( digit_options.minimum_fraction_digits, digit_options.maximum_fraction_digits); return result.precision(precision); } icu::number::UnlocalizedNumberFormatter SetDigitOptionsToFormatterV3( const icu::number::UnlocalizedNumberFormatter& settings, const Intl::NumberFormatDigitOptions& digit_options, int rounding_increment, JSNumberFormat::ShowTrailingZeros trailing_zeros) { icu::number::UnlocalizedNumberFormatter result = settings; if (digit_options.minimum_integer_digits > 1) { result = result.integerWidth(icu::number::IntegerWidth::zeroFillTo( digit_options.minimum_integer_digits)); } icu::number::Precision precision = icu::number::Precision::unlimited(); bool relaxed = false; switch (digit_options.rounding_type) { case Intl::RoundingType::kSignificantDigits: precision = icu::number::Precision::minMaxSignificantDigits( digit_options.minimum_significant_digits, digit_options.maximum_significant_digits); break; case Intl::RoundingType::kFractionDigits: precision = icu::number::Precision::minMaxFraction( digit_options.minimum_fraction_digits, digit_options.maximum_fraction_digits); break; case Intl::RoundingType::kMorePrecision: relaxed = true; V8_FALLTHROUGH; case Intl::RoundingType::kLessPrecision: precision = icu::number::Precision::minMaxFraction( digit_options.minimum_fraction_digits, digit_options.maximum_fraction_digits) .withSignificantDigits(digit_options.minimum_significant_digits, digit_options.maximum_significant_digits, relaxed ? UNUM_ROUNDING_PRIORITY_RELAXED : UNUM_ROUNDING_PRIORITY_STRICT); break; } if (rounding_increment != 1) { double icu_increment = rounding_increment * std::pow(10, -digit_options.maximum_fraction_digits); precision = ::icu::number::Precision::increment(icu_increment) .withMinFraction(digit_options.minimum_fraction_digits); } if (trailing_zeros == JSNumberFormat::ShowTrailingZeros::kHide) { precision = precision.trailingZeroDisplay(UNUM_TRAILING_ZERO_HIDE_IF_WHOLE); } return result.precision(precision); } } // anonymous namespace icu::number::UnlocalizedNumberFormatter JSNumberFormat::SetDigitOptionsToFormatter( const icu::number::UnlocalizedNumberFormatter& settings, const Intl::NumberFormatDigitOptions& digit_options, int rounding_increment, JSNumberFormat::ShowTrailingZeros trailing_zeros) { if (FLAG_harmony_intl_number_format_v3) { return SetDigitOptionsToFormatterV3(settings, digit_options, rounding_increment, trailing_zeros); } else { return SetDigitOptionsToFormatterV2(settings, digit_options); } } // static // ecma402 #sec-intl.numberformat.prototype.resolvedoptions Handle JSNumberFormat::ResolvedOptions( Isolate* isolate, Handle number_format) { Factory* factory = isolate->factory(); UErrorCode status = U_ZERO_ERROR; icu::number::LocalizedNumberFormatter* icu_number_formatter = number_format->icu_number_formatter().raw(); icu::UnicodeString skeleton = icu_number_formatter->toSkeleton(status); CHECK(U_SUCCESS(status)); // 4. Let options be ! ObjectCreate(%ObjectPrototype%). Handle options = factory->NewJSObject(isolate->object_function()); Handle locale = Handle(number_format->locale(), isolate); const icu::UnicodeString numberingSystem_ustr = NumberingSystemFromSkeleton(skeleton); // 5. For each row of Table 4, except the header row, in table order, do // Table 4: Resolved Options of NumberFormat Instances // Internal Slot Property // [[Locale]] "locale" // [[NumberingSystem]] "numberingSystem" // [[Style]] "style" // [[Currency]] "currency" // [[CurrencyDisplay]] "currencyDisplay" // [[CurrencySign]] "currencySign" // [[Unit]] "unit" // [[UnitDisplay]] "unitDisplay" // [[MinimumIntegerDigits]] "minimumIntegerDigits" // [[MinimumFractionDigits]] "minimumFractionDigits" // [[MaximumFractionDigits]] "maximumFractionDigits" // [[MinimumSignificantDigits]] "minimumSignificantDigits" // [[MaximumSignificantDigits]] "maximumSignificantDigits" // [[UseGrouping]] "useGrouping" // [[Notation]] "notation" // [[CompactDisplay]] "compactDisplay" // [[SignDisplay]] "signDisplay" // // For v3 // [[RoundingMode]] "roundingMode" // [[RoundingIncrement]] "roundingIncrement" // [[TrailingZeroDisplay]] "trailingZeroDisplay" CHECK(JSReceiver::CreateDataProperty(isolate, options, factory->locale_string(), locale, Just(kDontThrow)) .FromJust()); Handle numberingSystem_string; CHECK(Intl::ToString(isolate, numberingSystem_ustr) .ToHandle(&numberingSystem_string)); CHECK(JSReceiver::CreateDataProperty(isolate, options, factory->numberingSystem_string(), numberingSystem_string, Just(kDontThrow)) .FromJust()); Style style = StyleFromSkeleton(skeleton); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->style_string(), StyleAsString(isolate, style), Just(kDontThrow)) .FromJust()); const icu::UnicodeString currency_ustr = CurrencyFromSkeleton(skeleton); if (!currency_ustr.isEmpty()) { Handle currency_string; CHECK(Intl::ToString(isolate, currency_ustr).ToHandle(¤cy_string)); CHECK(JSReceiver::CreateDataProperty(isolate, options, factory->currency_string(), currency_string, Just(kDontThrow)) .FromJust()); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->currencyDisplay_string(), CurrencyDisplayString(isolate, skeleton), Just(kDontThrow)) .FromJust()); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->currencySign_string(), CurrencySignString(isolate, skeleton), Just(kDontThrow)) .FromJust()); } if (style == Style::UNIT) { std::string unit = UnitFromSkeleton(skeleton); if (!unit.empty()) { CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->unit_string(), isolate->factory()->NewStringFromAsciiChecked(unit.c_str()), Just(kDontThrow)) .FromJust()); } CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->unitDisplay_string(), UnitDisplayString(isolate, skeleton), Just(kDontThrow)) .FromJust()); } CHECK( JSReceiver::CreateDataProperty( isolate, options, factory->minimumIntegerDigits_string(), factory->NewNumberFromInt(MinimumIntegerDigitsFromSkeleton(skeleton)), Just(kDontThrow)) .FromJust()); int32_t minimum = 0, maximum = 0; if (SignificantDigitsFromSkeleton(skeleton, &minimum, &maximum)) { CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->minimumSignificantDigits_string(), factory->NewNumberFromInt(minimum), Just(kDontThrow)) .FromJust()); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->maximumSignificantDigits_string(), factory->NewNumberFromInt(maximum), Just(kDontThrow)) .FromJust()); } else { FractionDigitsFromSkeleton(skeleton, &minimum, &maximum); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->minimumFractionDigits_string(), factory->NewNumberFromInt(minimum), Just(kDontThrow)) .FromJust()); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->maximumFractionDigits_string(), factory->NewNumberFromInt(maximum), Just(kDontThrow)) .FromJust()); } if (FLAG_harmony_intl_number_format_v3) { CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->useGrouping_string(), UseGroupingFromSkeleton(isolate, skeleton), Just(kDontThrow)) .FromJust()); } else { CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->useGrouping_string(), factory->ToBoolean(UseGroupingFromSkeleton(skeleton)), Just(kDontThrow)) .FromJust()); } Notation notation = NotationFromSkeleton(skeleton); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->notation_string(), NotationAsString(isolate, notation), Just(kDontThrow)) .FromJust()); // Only output compactDisplay when notation is compact. if (notation == Notation::COMPACT) { CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->compactDisplay_string(), CompactDisplayString(isolate, skeleton), Just(kDontThrow)) .FromJust()); } CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->signDisplay_string(), SignDisplayString(isolate, skeleton), Just(kDontThrow)) .FromJust()); if (FLAG_harmony_intl_number_format_v3) { CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->roundingMode_string(), RoundingModeString(isolate, skeleton), Just(kDontThrow)) .FromJust()); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->roundingIncrement_string(), RoundingIncrement(isolate, skeleton), Just(kDontThrow)) .FromJust()); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->trailingZeroDisplay_string(), TrailingZeroDisplayString(isolate, skeleton), Just(kDontThrow)) .FromJust()); CHECK(JSReceiver::CreateDataProperty( isolate, options, factory->roundingPriority_string(), RoundingPriorityString(isolate, skeleton), Just(kDontThrow)) .FromJust()); } return options; } // ecma402/#sec-unwrapnumberformat MaybeHandle JSNumberFormat::UnwrapNumberFormat( Isolate* isolate, Handle format_holder) { // old code copy from NumberFormat::Unwrap that has no spec comment and // compiled but fail unit tests. Handle native_context = Handle(isolate->context().native_context(), isolate); Handle constructor = Handle( JSFunction::cast(native_context->intl_number_format_function()), isolate); Handle object; ASSIGN_RETURN_ON_EXCEPTION( isolate, object, Intl::LegacyUnwrapReceiver(isolate, format_holder, constructor, format_holder->IsJSNumberFormat()), JSNumberFormat); // 4. If ... or nf does not have an [[InitializedNumberFormat]] internal slot, // then if (!object->IsJSNumberFormat()) { // a. Throw a TypeError exception. THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kIncompatibleMethodReceiver, isolate->factory()->NewStringFromAsciiChecked( "UnwrapNumberFormat")), JSNumberFormat); } // 5. Return nf. return Handle::cast(object); } // 22. is in « 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, // 5000 » bool IsValidRoundingIncrement(int value) { return value == 1 || value == 2 || value == 5 || value == 10 || value == 20 || value == 25 || value == 50 || value == 100 || value == 200 || value == 250 || value == 500 || value == 1000 || value == 2000 || value == 2500 || value == 5000; } // static MaybeHandle JSNumberFormat::New(Isolate* isolate, Handle map, Handle locales, Handle options_obj, const char* service) { Factory* factory = isolate->factory(); // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales). Maybe> maybe_requested_locales = Intl::CanonicalizeLocaleList(isolate, locales); MAYBE_RETURN(maybe_requested_locales, Handle()); std::vector requested_locales = maybe_requested_locales.FromJust(); // 2. Set options to ? CoerceOptionsToObject(options). Handle options; ASSIGN_RETURN_ON_EXCEPTION( isolate, options, CoerceOptionsToObject(isolate, options_obj, service), JSNumberFormat); // 3. Let opt be a new Record. // 4. Let matcher be ? GetOption(options, "localeMatcher", "string", « // "lookup", "best fit" », "best fit"). // 5. Set opt.[[localeMatcher]] to matcher. Maybe maybe_locale_matcher = Intl::GetLocaleMatcher(isolate, options, service); MAYBE_RETURN(maybe_locale_matcher, MaybeHandle()); Intl::MatcherOption matcher = maybe_locale_matcher.FromJust(); std::unique_ptr numbering_system_str = nullptr; // 6. Let _numberingSystem_ be ? GetOption(_options_, `"numberingSystem"`, // `"string"`, *undefined*, *undefined*). Maybe maybe_numberingSystem = Intl::GetNumberingSystem( isolate, options, service, &numbering_system_str); // 7. If _numberingSystem_ is not *undefined*, then // 8. If _numberingSystem_ does not match the // `(3*8alphanum) *("-" (3*8alphanum))` sequence, throw a *RangeError* // exception. MAYBE_RETURN(maybe_numberingSystem, MaybeHandle()); // 9. Let localeData be %NumberFormat%.[[LocaleData]]. // 10. Let r be ResolveLocale(%NumberFormat%.[[AvailableLocales]], // requestedLocales, opt, %NumberFormat%.[[RelevantExtensionKeys]], // localeData). std::set relevant_extension_keys{"nu"}; Maybe maybe_resolve_locale = Intl::ResolveLocale(isolate, JSNumberFormat::GetAvailableLocales(), requested_locales, matcher, relevant_extension_keys); if (maybe_resolve_locale.IsNothing()) { THROW_NEW_ERROR(isolate, NewRangeError(MessageTemplate::kIcuError), JSNumberFormat); } Intl::ResolvedLocale r = maybe_resolve_locale.FromJust(); icu::Locale icu_locale = r.icu_locale; UErrorCode status = U_ZERO_ERROR; if (numbering_system_str != nullptr) { auto nu_extension_it = r.extensions.find("nu"); if (nu_extension_it != r.extensions.end() && nu_extension_it->second != numbering_system_str.get()) { icu_locale.setUnicodeKeywordValue("nu", nullptr, status); CHECK(U_SUCCESS(status)); } } // 9. Set numberFormat.[[Locale]] to r.[[locale]]. Maybe maybe_locale_str = Intl::ToLanguageTag(icu_locale); MAYBE_RETURN(maybe_locale_str, MaybeHandle()); Handle locale_str = isolate->factory()->NewStringFromAsciiChecked( maybe_locale_str.FromJust().c_str()); if (numbering_system_str != nullptr && Intl::IsValidNumberingSystem(numbering_system_str.get())) { icu_locale.setUnicodeKeywordValue("nu", numbering_system_str.get(), status); CHECK(U_SUCCESS(status)); } std::string numbering_system = Intl::GetNumberingSystem(icu_locale); // 11. Let dataLocale be r.[[dataLocale]]. icu::number::UnlocalizedNumberFormatter settings = icu::number::UnlocalizedNumberFormatter().roundingMode(UNUM_ROUND_HALFUP); // For 'latn' numbering system, skip the adoptSymbols which would cause // 10.1%-13.7% of regression of JSTests/Intl-NewIntlNumberFormat // See crbug/1052751 so we skip calling adoptSymbols and depending on the // default instead. if (!numbering_system.empty() && numbering_system != "latn") { settings = settings.adoptSymbols(icu::NumberingSystem::createInstanceByName( numbering_system.c_str(), status)); CHECK(U_SUCCESS(status)); } // ==== Start SetNumberFormatUnitOptions ==== // 3. Let style be ? GetOption(options, "style", "string", « "decimal", // "percent", "currency", "unit" », "decimal"). Maybe