1 | /* |
2 | * Copyright (C) 2015 Andy VanWagoner ([email protected]) |
3 | * Copyright (C) 2016 Sukolsak Sakshuwong ([email protected]) |
4 | * Copyright (C) 2016-2019 Apple Inc. All rights reserved. |
5 | * |
6 | * Redistribution and use in source and binary forms, with or without |
7 | * modification, are permitted provided that the following conditions |
8 | * are met: |
9 | * 1. Redistributions of source code must retain the above copyright |
10 | * notice, this list of conditions and the following disclaimer. |
11 | * 2. Redistributions in binary form must reproduce the above copyright |
12 | * notice, this list of conditions and the following disclaimer in the |
13 | * documentation and/or other materials provided with the distribution. |
14 | * |
15 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
17 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
18 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
19 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
20 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
21 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
22 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
23 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
24 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
25 | * THE POSSIBILITY OF SUCH DAMAGE. |
26 | */ |
27 | |
28 | #include "config.h" |
29 | #include "IntlNumberFormat.h" |
30 | |
31 | #if ENABLE(INTL) |
32 | |
33 | #include "CatchScope.h" |
34 | #include "Error.h" |
35 | #include "IntlNumberFormatConstructor.h" |
36 | #include "IntlObject.h" |
37 | #include "JSBoundFunction.h" |
38 | #include "JSCInlines.h" |
39 | #include "ObjectConstructor.h" |
40 | |
41 | #if HAVE(ICU_FORMAT_DOUBLE_FOR_FIELDS) |
42 | #include <unicode/ufieldpositer.h> |
43 | #endif |
44 | |
45 | namespace JSC { |
46 | |
47 | const ClassInfo IntlNumberFormat::s_info = { "Object" , &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(IntlNumberFormat) }; |
48 | |
49 | static const char* const relevantNumberExtensionKeys[1] = { "nu" }; |
50 | |
51 | void IntlNumberFormat::UNumberFormatDeleter::operator()(UNumberFormat* numberFormat) const |
52 | { |
53 | if (numberFormat) |
54 | unum_close(numberFormat); |
55 | } |
56 | |
57 | IntlNumberFormat* IntlNumberFormat::create(VM& vm, Structure* structure) |
58 | { |
59 | IntlNumberFormat* format = new (NotNull, allocateCell<IntlNumberFormat>(vm.heap)) IntlNumberFormat(vm, structure); |
60 | format->finishCreation(vm); |
61 | return format; |
62 | } |
63 | |
64 | Structure* IntlNumberFormat::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) |
65 | { |
66 | return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); |
67 | } |
68 | |
69 | IntlNumberFormat::IntlNumberFormat(VM& vm, Structure* structure) |
70 | : JSDestructibleObject(vm, structure) |
71 | { |
72 | } |
73 | |
74 | void IntlNumberFormat::finishCreation(VM& vm) |
75 | { |
76 | Base::finishCreation(vm); |
77 | ASSERT(inherits(vm, info())); |
78 | } |
79 | |
80 | void IntlNumberFormat::destroy(JSCell* cell) |
81 | { |
82 | static_cast<IntlNumberFormat*>(cell)->IntlNumberFormat::~IntlNumberFormat(); |
83 | } |
84 | |
85 | void IntlNumberFormat::visitChildren(JSCell* cell, SlotVisitor& visitor) |
86 | { |
87 | IntlNumberFormat* thisObject = jsCast<IntlNumberFormat*>(cell); |
88 | ASSERT_GC_OBJECT_INHERITS(thisObject, info()); |
89 | |
90 | Base::visitChildren(thisObject, visitor); |
91 | |
92 | visitor.append(thisObject->m_boundFormat); |
93 | } |
94 | |
95 | namespace IntlNFInternal { |
96 | static Vector<String> localeData(const String& locale, size_t keyIndex) |
97 | { |
98 | // 9.1 Internal slots of Service Constructors & 11.2.3 Internal slots (ECMA-402 2.0) |
99 | ASSERT_UNUSED(keyIndex, !keyIndex); // The index of the extension key "nu" in relevantExtensionKeys is 0. |
100 | return numberingSystemsForLocale(locale); |
101 | } |
102 | } |
103 | |
104 | static inline unsigned computeCurrencySortKey(const String& currency) |
105 | { |
106 | ASSERT(currency.length() == 3); |
107 | ASSERT(currency.isAllSpecialCharacters<isASCIIUpper>()); |
108 | return (currency[0] << 16) + (currency[1] << 8) + currency[2]; |
109 | } |
110 | |
111 | static inline unsigned computeCurrencySortKey(const char* currency) |
112 | { |
113 | ASSERT(strlen(currency) == 3); |
114 | ASSERT(isAllSpecialCharacters<isASCIIUpper>(currency, 3)); |
115 | return (currency[0] << 16) + (currency[1] << 8) + currency[2]; |
116 | } |
117 | |
118 | static unsigned (std::pair<const char*, unsigned>* currencyMinorUnit) |
119 | { |
120 | return computeCurrencySortKey(currencyMinorUnit->first); |
121 | } |
122 | |
123 | static unsigned computeCurrencyDigits(const String& currency) |
124 | { |
125 | // 11.1.1 The abstract operation CurrencyDigits (currency) |
126 | // "If the ISO 4217 currency and funds code list contains currency as an alphabetic code, |
127 | // then return the minor unit value corresponding to the currency from the list; else return 2. |
128 | std::pair<const char*, unsigned> currencyMinorUnits[] = { |
129 | { "BHD" , 3 }, |
130 | { "BIF" , 0 }, |
131 | { "BYR" , 0 }, |
132 | { "CLF" , 4 }, |
133 | { "CLP" , 0 }, |
134 | { "DJF" , 0 }, |
135 | { "GNF" , 0 }, |
136 | { "IQD" , 3 }, |
137 | { "ISK" , 0 }, |
138 | { "JOD" , 3 }, |
139 | { "JPY" , 0 }, |
140 | { "KMF" , 0 }, |
141 | { "KRW" , 0 }, |
142 | { "KWD" , 3 }, |
143 | { "LYD" , 3 }, |
144 | { "OMR" , 3 }, |
145 | { "PYG" , 0 }, |
146 | { "RWF" , 0 }, |
147 | { "TND" , 3 }, |
148 | { "UGX" , 0 }, |
149 | { "UYI" , 0 }, |
150 | { "VND" , 0 }, |
151 | { "VUV" , 0 }, |
152 | { "XAF" , 0 }, |
153 | { "XOF" , 0 }, |
154 | { "XPF" , 0 } |
155 | }; |
156 | auto* currencyMinorUnit = tryBinarySearch<std::pair<const char*, unsigned>>(currencyMinorUnits, WTF_ARRAY_LENGTH(currencyMinorUnits), computeCurrencySortKey(currency), extractCurrencySortKey); |
157 | if (currencyMinorUnit) |
158 | return currencyMinorUnit->second; |
159 | return 2; |
160 | } |
161 | |
162 | void IntlNumberFormat::initializeNumberFormat(JSGlobalObject* globalObject, JSValue locales, JSValue optionsValue) |
163 | { |
164 | VM& vm = globalObject->vm(); |
165 | auto scope = DECLARE_THROW_SCOPE(vm); |
166 | |
167 | // 11.1.2 InitializeNumberFormat (numberFormat, locales, options) (ECMA-402) |
168 | // https://tc39.github.io/ecma402/#sec-initializenumberformat |
169 | |
170 | auto requestedLocales = canonicalizeLocaleList(globalObject, locales); |
171 | RETURN_IF_EXCEPTION(scope, void()); |
172 | |
173 | JSObject* options; |
174 | if (optionsValue.isUndefined()) |
175 | options = constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); |
176 | else { |
177 | options = optionsValue.toObject(globalObject); |
178 | RETURN_IF_EXCEPTION(scope, void()); |
179 | } |
180 | |
181 | HashMap<String, String> opt; |
182 | |
183 | String matcher = intlStringOption(globalObject, options, vm.propertyNames->localeMatcher, { "lookup" , "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"" , "best fit" ); |
184 | RETURN_IF_EXCEPTION(scope, void()); |
185 | opt.add("localeMatcher"_s , matcher); |
186 | |
187 | auto& availableLocales = globalObject->intlNumberFormatAvailableLocales(); |
188 | auto result = resolveLocale(globalObject, availableLocales, requestedLocales, opt, relevantNumberExtensionKeys, WTF_ARRAY_LENGTH(relevantNumberExtensionKeys), IntlNFInternal::localeData); |
189 | |
190 | m_locale = result.get("locale"_s ); |
191 | if (m_locale.isEmpty()) { |
192 | throwTypeError(globalObject, scope, "failed to initialize NumberFormat due to invalid locale"_s ); |
193 | return; |
194 | } |
195 | |
196 | m_numberingSystem = result.get("nu"_s ); |
197 | |
198 | String styleString = intlStringOption(globalObject, options, Identifier::fromString(vm, "style" ), { "decimal" , "percent" , "currency" }, "style must be either \"decimal\", \"percent\", or \"currency\"" , "decimal" ); |
199 | RETURN_IF_EXCEPTION(scope, void()); |
200 | if (styleString == "decimal" ) |
201 | m_style = Style::Decimal; |
202 | else if (styleString == "percent" ) |
203 | m_style = Style::Percent; |
204 | else if (styleString == "currency" ) |
205 | m_style = Style::Currency; |
206 | else |
207 | ASSERT_NOT_REACHED(); |
208 | |
209 | String currency = intlStringOption(globalObject, options, Identifier::fromString(vm, "currency" ), { }, nullptr, nullptr); |
210 | RETURN_IF_EXCEPTION(scope, void()); |
211 | if (!currency.isNull()) { |
212 | if (currency.length() != 3 || !currency.isAllSpecialCharacters<isASCIIAlpha>()) { |
213 | throwException(globalObject, scope, createRangeError(globalObject, "currency is not a well-formed currency code"_s )); |
214 | return; |
215 | } |
216 | } |
217 | |
218 | unsigned currencyDigits = 0; |
219 | if (m_style == Style::Currency) { |
220 | if (currency.isNull()) { |
221 | throwTypeError(globalObject, scope, "currency must be a string"_s ); |
222 | return; |
223 | } |
224 | |
225 | currency = currency.convertToASCIIUppercase(); |
226 | m_currency = currency; |
227 | currencyDigits = computeCurrencyDigits(currency); |
228 | } |
229 | |
230 | String currencyDisplayString = intlStringOption(globalObject, options, Identifier::fromString(vm, "currencyDisplay" ), { "code" , "symbol" , "name" }, "currencyDisplay must be either \"code\", \"symbol\", or \"name\"" , "symbol" ); |
231 | RETURN_IF_EXCEPTION(scope, void()); |
232 | if (m_style == Style::Currency) { |
233 | if (currencyDisplayString == "code" ) |
234 | m_currencyDisplay = CurrencyDisplay::Code; |
235 | else if (currencyDisplayString == "symbol" ) |
236 | m_currencyDisplay = CurrencyDisplay::Symbol; |
237 | else if (currencyDisplayString == "name" ) |
238 | m_currencyDisplay = CurrencyDisplay::Name; |
239 | else |
240 | ASSERT_NOT_REACHED(); |
241 | } |
242 | |
243 | unsigned minimumIntegerDigits = intlNumberOption(globalObject, options, Identifier::fromString(vm, "minimumIntegerDigits" ), 1, 21, 1); |
244 | RETURN_IF_EXCEPTION(scope, void()); |
245 | m_minimumIntegerDigits = minimumIntegerDigits; |
246 | |
247 | unsigned minimumFractionDigitsDefault = (m_style == Style::Currency) ? currencyDigits : 0; |
248 | |
249 | unsigned minimumFractionDigits = intlNumberOption(globalObject, options, Identifier::fromString(vm, "minimumFractionDigits" ), 0, 20, minimumFractionDigitsDefault); |
250 | RETURN_IF_EXCEPTION(scope, void()); |
251 | m_minimumFractionDigits = minimumFractionDigits; |
252 | |
253 | unsigned maximumFractionDigitsDefault; |
254 | if (m_style == Style::Currency) |
255 | maximumFractionDigitsDefault = std::max(minimumFractionDigits, currencyDigits); |
256 | else if (m_style == Style::Percent) |
257 | maximumFractionDigitsDefault = minimumFractionDigits; |
258 | else |
259 | maximumFractionDigitsDefault = std::max(minimumFractionDigits, 3u); |
260 | |
261 | unsigned maximumFractionDigits = intlNumberOption(globalObject, options, Identifier::fromString(vm, "maximumFractionDigits" ), minimumFractionDigits, 20, maximumFractionDigitsDefault); |
262 | RETURN_IF_EXCEPTION(scope, void()); |
263 | m_maximumFractionDigits = maximumFractionDigits; |
264 | |
265 | JSValue minimumSignificantDigitsValue = options->get(globalObject, Identifier::fromString(vm, "minimumSignificantDigits" )); |
266 | RETURN_IF_EXCEPTION(scope, void()); |
267 | |
268 | JSValue maximumSignificantDigitsValue = options->get(globalObject, Identifier::fromString(vm, "maximumSignificantDigits" )); |
269 | RETURN_IF_EXCEPTION(scope, void()); |
270 | |
271 | if (!minimumSignificantDigitsValue.isUndefined() || !maximumSignificantDigitsValue.isUndefined()) { |
272 | unsigned minimumSignificantDigits = intlDefaultNumberOption(globalObject, minimumSignificantDigitsValue, Identifier::fromString(vm, "minimumSignificantDigits" ), 1, 21, 1); |
273 | RETURN_IF_EXCEPTION(scope, void()); |
274 | unsigned maximumSignificantDigits = intlDefaultNumberOption(globalObject, maximumSignificantDigitsValue, Identifier::fromString(vm, "maximumSignificantDigits" ), minimumSignificantDigits, 21, 21); |
275 | RETURN_IF_EXCEPTION(scope, void()); |
276 | m_minimumSignificantDigits = minimumSignificantDigits; |
277 | m_maximumSignificantDigits = maximumSignificantDigits; |
278 | } |
279 | |
280 | bool usesFallback; |
281 | bool useGrouping = intlBooleanOption(globalObject, options, Identifier::fromString(vm, "useGrouping" ), usesFallback); |
282 | if (usesFallback) |
283 | useGrouping = true; |
284 | RETURN_IF_EXCEPTION(scope, void()); |
285 | m_useGrouping = useGrouping; |
286 | |
287 | UNumberFormatStyle style = UNUM_DEFAULT; |
288 | switch (m_style) { |
289 | case Style::Decimal: |
290 | style = UNUM_DECIMAL; |
291 | break; |
292 | case Style::Percent: |
293 | style = UNUM_PERCENT; |
294 | break; |
295 | case Style::Currency: |
296 | switch (m_currencyDisplay) { |
297 | case CurrencyDisplay::Code: |
298 | style = UNUM_CURRENCY_ISO; |
299 | break; |
300 | case CurrencyDisplay::Symbol: |
301 | style = UNUM_CURRENCY; |
302 | break; |
303 | case CurrencyDisplay::Name: |
304 | style = UNUM_CURRENCY_PLURAL; |
305 | break; |
306 | default: |
307 | ASSERT_NOT_REACHED(); |
308 | } |
309 | break; |
310 | default: |
311 | ASSERT_NOT_REACHED(); |
312 | } |
313 | |
314 | UErrorCode status = U_ZERO_ERROR; |
315 | m_numberFormat = std::unique_ptr<UNumberFormat, UNumberFormatDeleter>(unum_open(style, nullptr, 0, m_locale.utf8().data(), nullptr, &status)); |
316 | if (U_FAILURE(status)) { |
317 | throwTypeError(globalObject, scope, "failed to initialize NumberFormat"_s ); |
318 | return; |
319 | } |
320 | |
321 | if (m_style == Style::Currency) { |
322 | unum_setTextAttribute(m_numberFormat.get(), UNUM_CURRENCY_CODE, StringView(m_currency).upconvertedCharacters(), m_currency.length(), &status); |
323 | if (U_FAILURE(status)) { |
324 | throwTypeError(globalObject, scope, "failed to initialize NumberFormat"_s ); |
325 | return; |
326 | } |
327 | } |
328 | if (!m_minimumSignificantDigits) { |
329 | unum_setAttribute(m_numberFormat.get(), UNUM_MIN_INTEGER_DIGITS, m_minimumIntegerDigits); |
330 | unum_setAttribute(m_numberFormat.get(), UNUM_MIN_FRACTION_DIGITS, m_minimumFractionDigits); |
331 | unum_setAttribute(m_numberFormat.get(), UNUM_MAX_FRACTION_DIGITS, m_maximumFractionDigits); |
332 | } else { |
333 | unum_setAttribute(m_numberFormat.get(), UNUM_SIGNIFICANT_DIGITS_USED, true); |
334 | unum_setAttribute(m_numberFormat.get(), UNUM_MIN_SIGNIFICANT_DIGITS, m_minimumSignificantDigits); |
335 | unum_setAttribute(m_numberFormat.get(), UNUM_MAX_SIGNIFICANT_DIGITS, m_maximumSignificantDigits); |
336 | } |
337 | unum_setAttribute(m_numberFormat.get(), UNUM_GROUPING_USED, m_useGrouping); |
338 | unum_setAttribute(m_numberFormat.get(), UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP); |
339 | |
340 | m_initializedNumberFormat = true; |
341 | } |
342 | |
343 | JSValue IntlNumberFormat::formatNumber(JSGlobalObject* globalObject, double number) |
344 | { |
345 | VM& vm = globalObject->vm(); |
346 | auto scope = DECLARE_THROW_SCOPE(vm); |
347 | |
348 | // 11.3.4 FormatNumber abstract operation (ECMA-402 2.0) |
349 | if (!m_initializedNumberFormat) |
350 | return throwTypeError(globalObject, scope, "Intl.NumberFormat.prototype.format called on value that's not an object initialized as a NumberFormat"_s ); |
351 | |
352 | // Map negative zero to positive zero. |
353 | if (!number) |
354 | number = 0.0; |
355 | |
356 | UErrorCode status = U_ZERO_ERROR; |
357 | Vector<UChar, 32> buffer(32); |
358 | auto length = unum_formatDouble(m_numberFormat.get(), number, buffer.data(), buffer.size(), nullptr, &status); |
359 | if (status == U_BUFFER_OVERFLOW_ERROR) { |
360 | buffer.grow(length); |
361 | status = U_ZERO_ERROR; |
362 | unum_formatDouble(m_numberFormat.get(), number, buffer.data(), length, nullptr, &status); |
363 | } |
364 | if (U_FAILURE(status)) |
365 | return throwException(globalObject, scope, createError(globalObject, "Failed to format a number."_s )); |
366 | |
367 | return jsString(vm, String(buffer.data(), length)); |
368 | } |
369 | |
370 | ASCIILiteral IntlNumberFormat::styleString(Style style) |
371 | { |
372 | switch (style) { |
373 | case Style::Decimal: |
374 | return "decimal"_s ; |
375 | case Style::Percent: |
376 | return "percent"_s ; |
377 | case Style::Currency: |
378 | return "currency"_s ; |
379 | } |
380 | ASSERT_NOT_REACHED(); |
381 | return ASCIILiteral::null(); |
382 | } |
383 | |
384 | ASCIILiteral IntlNumberFormat::currencyDisplayString(CurrencyDisplay currencyDisplay) |
385 | { |
386 | switch (currencyDisplay) { |
387 | case CurrencyDisplay::Code: |
388 | return "code"_s ; |
389 | case CurrencyDisplay::Symbol: |
390 | return "symbol"_s ; |
391 | case CurrencyDisplay::Name: |
392 | return "name"_s ; |
393 | } |
394 | ASSERT_NOT_REACHED(); |
395 | return ASCIILiteral::null(); |
396 | } |
397 | |
398 | JSObject* IntlNumberFormat::resolvedOptions(JSGlobalObject* globalObject) |
399 | { |
400 | VM& vm = globalObject->vm(); |
401 | auto scope = DECLARE_THROW_SCOPE(vm); |
402 | |
403 | // 11.3.5 Intl.NumberFormat.prototype.resolvedOptions() (ECMA-402 2.0) |
404 | // The function returns a new object whose properties and attributes are set as if |
405 | // constructed by an object literal assigning to each of the following properties the |
406 | // value of the corresponding internal slot of this NumberFormat object (see 11.4): |
407 | // locale, numberingSystem, style, currency, currencyDisplay, minimumIntegerDigits, |
408 | // minimumFractionDigits, maximumFractionDigits, minimumSignificantDigits, |
409 | // maximumSignificantDigits, and useGrouping. Properties whose corresponding internal |
410 | // slots are not present are not assigned. |
411 | |
412 | if (!m_initializedNumberFormat) { |
413 | initializeNumberFormat(globalObject, jsUndefined(), jsUndefined()); |
414 | scope.assertNoException(); |
415 | } |
416 | |
417 | JSObject* options = constructEmptyObject(globalObject); |
418 | options->putDirect(vm, vm.propertyNames->locale, jsString(vm, m_locale)); |
419 | options->putDirect(vm, Identifier::fromString(vm, "numberingSystem" ), jsString(vm, m_numberingSystem)); |
420 | options->putDirect(vm, Identifier::fromString(vm, "style" ), jsNontrivialString(vm, styleString(m_style))); |
421 | if (m_style == Style::Currency) { |
422 | options->putDirect(vm, Identifier::fromString(vm, "currency" ), jsNontrivialString(vm, m_currency)); |
423 | options->putDirect(vm, Identifier::fromString(vm, "currencyDisplay" ), jsNontrivialString(vm, currencyDisplayString(m_currencyDisplay))); |
424 | } |
425 | options->putDirect(vm, Identifier::fromString(vm, "minimumIntegerDigits" ), jsNumber(m_minimumIntegerDigits)); |
426 | options->putDirect(vm, Identifier::fromString(vm, "minimumFractionDigits" ), jsNumber(m_minimumFractionDigits)); |
427 | options->putDirect(vm, Identifier::fromString(vm, "maximumFractionDigits" ), jsNumber(m_maximumFractionDigits)); |
428 | if (m_minimumSignificantDigits) { |
429 | ASSERT(m_maximumSignificantDigits); |
430 | options->putDirect(vm, Identifier::fromString(vm, "minimumSignificantDigits" ), jsNumber(m_minimumSignificantDigits)); |
431 | options->putDirect(vm, Identifier::fromString(vm, "maximumSignificantDigits" ), jsNumber(m_maximumSignificantDigits)); |
432 | } |
433 | options->putDirect(vm, Identifier::fromString(vm, "useGrouping" ), jsBoolean(m_useGrouping)); |
434 | return options; |
435 | } |
436 | |
437 | void IntlNumberFormat::setBoundFormat(VM& vm, JSBoundFunction* format) |
438 | { |
439 | m_boundFormat.set(vm, this, format); |
440 | } |
441 | |
442 | #if HAVE(ICU_FORMAT_DOUBLE_FOR_FIELDS) |
443 | void IntlNumberFormat::UFieldPositionIteratorDeleter::operator()(UFieldPositionIterator* iterator) const |
444 | { |
445 | if (iterator) |
446 | ufieldpositer_close(iterator); |
447 | } |
448 | |
449 | ASCIILiteral IntlNumberFormat::partTypeString(UNumberFormatFields field, double value) |
450 | { |
451 | switch (field) { |
452 | case UNUM_INTEGER_FIELD: |
453 | if (std::isnan(value)) |
454 | return "nan"_s ; |
455 | if (!std::isfinite(value)) |
456 | return "infinity"_s ; |
457 | return "integer"_s ; |
458 | case UNUM_FRACTION_FIELD: |
459 | return "fraction"_s ; |
460 | case UNUM_DECIMAL_SEPARATOR_FIELD: |
461 | return "decimal"_s ; |
462 | case UNUM_GROUPING_SEPARATOR_FIELD: |
463 | return "group"_s ; |
464 | case UNUM_CURRENCY_FIELD: |
465 | return "currency"_s ; |
466 | case UNUM_PERCENT_FIELD: |
467 | return "percentSign"_s ; |
468 | case UNUM_SIGN_FIELD: |
469 | return value < 0 ? "minusSign"_s : "plusSign"_s ; |
470 | // These should not show up because there is no way to specify them in NumberFormat options. |
471 | // If they do, they don't fit well into any of known part types, so consider it an "unknown". |
472 | case UNUM_PERMILL_FIELD: |
473 | case UNUM_EXPONENT_SYMBOL_FIELD: |
474 | case UNUM_EXPONENT_SIGN_FIELD: |
475 | case UNUM_EXPONENT_FIELD: |
476 | #if !defined(U_HIDE_DEPRECATED_API) |
477 | case UNUM_FIELD_COUNT: |
478 | #endif |
479 | // Any newer additions to the UNumberFormatFields enum should just be considered an "unknown" part. |
480 | default: |
481 | return "unknown"_s ; |
482 | } |
483 | return "unknown"_s ; |
484 | } |
485 | |
486 | JSValue IntlNumberFormat::formatToParts(JSGlobalObject* globalObject, double value) |
487 | { |
488 | VM& vm = globalObject->vm(); |
489 | auto scope = DECLARE_THROW_SCOPE(vm); |
490 | |
491 | // FormatNumberToParts (ECMA-402) |
492 | // https://tc39.github.io/ecma402/#sec-formatnumbertoparts |
493 | // https://tc39.github.io/ecma402/#sec-partitionnumberpattern |
494 | |
495 | if (!m_initializedNumberFormat) |
496 | return throwTypeError(globalObject, scope, "Intl.NumberFormat.prototype.formatToParts called on value that's not an object initialized as a NumberFormat"_s ); |
497 | |
498 | UErrorCode status = U_ZERO_ERROR; |
499 | auto fieldItr = std::unique_ptr<UFieldPositionIterator, UFieldPositionIteratorDeleter>(ufieldpositer_open(&status)); |
500 | if (U_FAILURE(status)) |
501 | return throwTypeError(globalObject, scope, "failed to open field position iterator"_s ); |
502 | |
503 | status = U_ZERO_ERROR; |
504 | Vector<UChar, 32> result(32); |
505 | auto resultLength = unum_formatDoubleForFields(m_numberFormat.get(), value, result.data(), result.size(), fieldItr.get(), &status); |
506 | if (status == U_BUFFER_OVERFLOW_ERROR) { |
507 | status = U_ZERO_ERROR; |
508 | result.grow(resultLength); |
509 | unum_formatDoubleForFields(m_numberFormat.get(), value, result.data(), resultLength, fieldItr.get(), &status); |
510 | } |
511 | if (U_FAILURE(status)) |
512 | return throwTypeError(globalObject, scope, "failed to format a number."_s ); |
513 | |
514 | int32_t literalFieldType = -1; |
515 | auto literalField = IntlNumberFormatField(literalFieldType, resultLength); |
516 | Vector<IntlNumberFormatField> fields(resultLength, literalField); |
517 | int32_t beginIndex = 0; |
518 | int32_t endIndex = 0; |
519 | auto fieldType = ufieldpositer_next(fieldItr.get(), &beginIndex, &endIndex); |
520 | while (fieldType >= 0) { |
521 | auto size = endIndex - beginIndex; |
522 | for (auto i = beginIndex; i < endIndex; ++i) { |
523 | // Only override previous value if new value is more specific. |
524 | if (fields[i].size >= size) |
525 | fields[i] = IntlNumberFormatField(fieldType, size); |
526 | } |
527 | fieldType = ufieldpositer_next(fieldItr.get(), &beginIndex, &endIndex); |
528 | } |
529 | |
530 | JSArray* parts = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0); |
531 | if (!parts) |
532 | return throwOutOfMemoryError(globalObject, scope); |
533 | unsigned index = 0; |
534 | |
535 | auto resultString = String(result.data(), resultLength); |
536 | auto typePropertyName = Identifier::fromString(vm, "type" ); |
537 | auto literalString = jsString(vm, "literal"_s ); |
538 | |
539 | int32_t currentIndex = 0; |
540 | while (currentIndex < resultLength) { |
541 | auto startIndex = currentIndex; |
542 | auto fieldType = fields[currentIndex].type; |
543 | while (currentIndex < resultLength && fields[currentIndex].type == fieldType) |
544 | ++currentIndex; |
545 | auto partType = fieldType == literalFieldType ? literalString : jsString(vm, partTypeString(UNumberFormatFields(fieldType), value)); |
546 | auto partValue = jsSubstring(vm, resultString, startIndex, currentIndex - startIndex); |
547 | JSObject* part = constructEmptyObject(globalObject); |
548 | part->putDirect(vm, typePropertyName, partType); |
549 | part->putDirect(vm, vm.propertyNames->value, partValue); |
550 | parts->putDirectIndex(globalObject, index++, part); |
551 | RETURN_IF_EXCEPTION(scope, { }); |
552 | } |
553 | |
554 | return parts; |
555 | } |
556 | #endif |
557 | |
558 | } // namespace JSC |
559 | |
560 | #endif // ENABLE(INTL) |
561 | |