1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 /* Intl.NumberFormat implementation. */
8
9 #include "builtin/intl/NumberFormat.h"
10
11 #include "mozilla/Assertions.h"
12 #include "mozilla/Casting.h"
13 #include "mozilla/FloatingPoint.h"
14 #include "mozilla/intl/Locale.h"
15 #include "mozilla/intl/MeasureUnit.h"
16 #include "mozilla/intl/MeasureUnitGenerated.h"
17 #include "mozilla/intl/NumberFormat.h"
18 #include "mozilla/intl/NumberingSystem.h"
19 #include "mozilla/intl/NumberRangeFormat.h"
20 #include "mozilla/Span.h"
21 #include "mozilla/TextUtils.h"
22 #include "mozilla/UniquePtr.h"
23
24 #include <algorithm>
25 #include <cstring>
26 #include <iterator>
27 #include <stddef.h>
28 #include <stdint.h>
29 #include <string>
30 #include <string_view>
31 #include <type_traits>
32
33 #include "builtin/Array.h"
34 #include "builtin/intl/CommonFunctions.h"
35 #include "builtin/intl/DecimalNumber.h"
36 #include "builtin/intl/FormatBuffer.h"
37 #include "builtin/intl/LanguageTag.h"
38 #include "builtin/intl/RelativeTimeFormat.h"
39 #include "ds/Sort.h"
40 #include "gc/FreeOp.h"
41 #include "js/CharacterEncoding.h"
42 #include "js/PropertySpec.h"
43 #include "js/RootingAPI.h"
44 #include "js/TypeDecls.h"
45 #include "js/Vector.h"
46 #include "util/Text.h"
47 #include "vm/BigIntType.h"
48 #include "vm/GlobalObject.h"
49 #include "vm/JSContext.h"
50 #include "vm/PlainObject.h" // js::PlainObject
51 #include "vm/SelfHosting.h"
52 #include "vm/Stack.h"
53 #include "vm/StringType.h"
54 #include "vm/WellKnownAtom.h" // js_*_str
55
56 #include "vm/JSObject-inl.h"
57 #include "vm/NativeObject-inl.h"
58
59 using namespace js;
60
61 using mozilla::AssertedCast;
62
63 using js::intl::DateTimeFormatOptions;
64 using js::intl::FieldType;
65
66 const JSClassOps NumberFormatObject::classOps_ = {
67 nullptr, // addProperty
68 nullptr, // delProperty
69 nullptr, // enumerate
70 nullptr, // newEnumerate
71 nullptr, // resolve
72 nullptr, // mayResolve
73 NumberFormatObject::finalize, // finalize
74 nullptr, // call
75 nullptr, // hasInstance
76 nullptr, // construct
77 nullptr, // trace
78 };
79
80 const JSClass NumberFormatObject::class_ = {
81 "Intl.NumberFormat",
82 JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) |
83 JSCLASS_HAS_CACHED_PROTO(JSProto_NumberFormat) |
84 JSCLASS_FOREGROUND_FINALIZE,
85 &NumberFormatObject::classOps_, &NumberFormatObject::classSpec_};
86
87 const JSClass& NumberFormatObject::protoClass_ = PlainObject::class_;
88
numberFormat_toSource(JSContext * cx,unsigned argc,Value * vp)89 static bool numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
90 CallArgs args = CallArgsFromVp(argc, vp);
91 args.rval().setString(cx->names().NumberFormat);
92 return true;
93 }
94
95 static const JSFunctionSpec numberFormat_static_methods[] = {
96 JS_SELF_HOSTED_FN("supportedLocalesOf",
97 "Intl_NumberFormat_supportedLocalesOf", 1, 0),
98 JS_FS_END,
99 };
100
101 static const JSFunctionSpec numberFormat_methods[] = {
102 JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0,
103 0),
104 JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0),
105 #ifdef NIGHTLY_BUILD
106 JS_SELF_HOSTED_FN("formatRange", "Intl_NumberFormat_formatRange", 2, 0),
107 JS_SELF_HOSTED_FN("formatRangeToParts",
108 "Intl_NumberFormat_formatRangeToParts", 2, 0),
109 #endif
110 JS_FN(js_toSource_str, numberFormat_toSource, 0, 0),
111 JS_FS_END,
112 };
113
114 static const JSPropertySpec numberFormat_properties[] = {
115 JS_SELF_HOSTED_GET("format", "$Intl_NumberFormat_format_get", 0),
116 JS_STRING_SYM_PS(toStringTag, "Intl.NumberFormat", JSPROP_READONLY),
117 JS_PS_END,
118 };
119
120 static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp);
121
122 const ClassSpec NumberFormatObject::classSpec_ = {
123 GenericCreateConstructor<NumberFormat, 0, gc::AllocKind::FUNCTION>,
124 GenericCreatePrototype<NumberFormatObject>,
125 numberFormat_static_methods,
126 nullptr,
127 numberFormat_methods,
128 numberFormat_properties,
129 nullptr,
130 ClassSpec::DontDefineConstructor};
131
132 /**
133 * 11.2.1 Intl.NumberFormat([ locales [, options]])
134 *
135 * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
136 */
NumberFormat(JSContext * cx,const CallArgs & args,bool construct)137 static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) {
138 // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
139
140 // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
141 RootedObject proto(cx);
142 if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_NumberFormat,
143 &proto)) {
144 return false;
145 }
146
147 Rooted<NumberFormatObject*> numberFormat(cx);
148 numberFormat = NewObjectWithClassProto<NumberFormatObject>(cx, proto);
149 if (!numberFormat) {
150 return false;
151 }
152
153 RootedValue thisValue(cx,
154 construct ? ObjectValue(*numberFormat) : args.thisv());
155 HandleValue locales = args.get(0);
156 HandleValue options = args.get(1);
157
158 // Step 3.
159 return intl::LegacyInitializeObject(
160 cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, locales,
161 options, DateTimeFormatOptions::Standard, args.rval());
162 }
163
NumberFormat(JSContext * cx,unsigned argc,Value * vp)164 static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
165 CallArgs args = CallArgsFromVp(argc, vp);
166 return NumberFormat(cx, args, args.isConstructing());
167 }
168
intl_NumberFormat(JSContext * cx,unsigned argc,Value * vp)169 bool js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
170 CallArgs args = CallArgsFromVp(argc, vp);
171 MOZ_ASSERT(args.length() == 2);
172 MOZ_ASSERT(!args.isConstructing());
173 // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it
174 // cannot be used with "new", but it still has to be treated as a
175 // constructor.
176 return NumberFormat(cx, args, true);
177 }
178
finalize(JSFreeOp * fop,JSObject * obj)179 void js::NumberFormatObject::finalize(JSFreeOp* fop, JSObject* obj) {
180 MOZ_ASSERT(fop->onMainThread());
181
182 auto* numberFormat = &obj->as<NumberFormatObject>();
183 mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter();
184 mozilla::intl::NumberRangeFormat* nrf =
185 numberFormat->getNumberRangeFormatter();
186
187 if (nf) {
188 intl::RemoveICUCellMemory(fop, obj, NumberFormatObject::EstimatedMemoryUse);
189 // This was allocated using `new` in mozilla::intl::NumberFormat, so we
190 // delete here.
191 delete nf;
192 }
193
194 if (nrf) {
195 intl::RemoveICUCellMemory(fop, obj, EstimatedRangeFormatterMemoryUse);
196 // This was allocated using `new` in mozilla::intl::NumberRangeFormat, so we
197 // delete here.
198 delete nrf;
199 }
200 }
201
intl_numberingSystem(JSContext * cx,unsigned argc,Value * vp)202 bool js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) {
203 CallArgs args = CallArgsFromVp(argc, vp);
204 MOZ_ASSERT(args.length() == 1);
205 MOZ_ASSERT(args[0].isString());
206
207 UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
208 if (!locale) {
209 return false;
210 }
211
212 auto numberingSystem =
213 mozilla::intl::NumberingSystem::TryCreate(locale.get());
214 if (numberingSystem.isErr()) {
215 intl::ReportInternalError(cx, numberingSystem.unwrapErr());
216 return false;
217 }
218
219 auto name = numberingSystem.inspect()->GetName();
220 if (name.isErr()) {
221 intl::ReportInternalError(cx, name.unwrapErr());
222 return false;
223 }
224
225 JSString* jsname = NewStringCopy<CanGC>(cx, name.unwrap());
226 if (!jsname) {
227 return false;
228 }
229
230 args.rval().setString(jsname);
231 return true;
232 }
233
234 #if DEBUG || MOZ_SYSTEM_ICU
intl_availableMeasurementUnits(JSContext * cx,unsigned argc,Value * vp)235 bool js::intl_availableMeasurementUnits(JSContext* cx, unsigned argc,
236 Value* vp) {
237 CallArgs args = CallArgsFromVp(argc, vp);
238 MOZ_ASSERT(args.length() == 0);
239
240 RootedObject measurementUnits(cx, NewPlainObjectWithProto(cx, nullptr));
241 if (!measurementUnits) {
242 return false;
243 }
244
245 auto units = mozilla::intl::MeasureUnit::GetAvailable();
246 if (units.isErr()) {
247 intl::ReportInternalError(cx, units.unwrapErr());
248 return false;
249 }
250
251 RootedAtom unitAtom(cx);
252 for (auto unit : units.unwrap()) {
253 if (unit.isErr()) {
254 intl::ReportInternalError(cx);
255 return false;
256 }
257 auto unitIdentifier = unit.unwrap();
258
259 unitAtom = Atomize(cx, unitIdentifier.data(), unitIdentifier.size());
260 if (!unitAtom) {
261 return false;
262 }
263
264 if (!DefineDataProperty(cx, measurementUnits, unitAtom->asPropertyName(),
265 TrueHandleValue)) {
266 return false;
267 }
268 }
269
270 args.rval().setObject(*measurementUnits);
271 return true;
272 }
273 #endif
274
MaxUnitLength()275 static constexpr size_t MaxUnitLength() {
276 size_t length = 0;
277 for (const auto& unit : mozilla::intl::simpleMeasureUnits) {
278 length = std::max(length, std::char_traits<char>::length(unit.name));
279 }
280 return length * 2 + std::char_traits<char>::length("-per-");
281 }
282
NumberFormatLocale(JSContext * cx,HandleObject internals)283 static UniqueChars NumberFormatLocale(JSContext* cx, HandleObject internals) {
284 RootedValue value(cx);
285 if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
286 return nullptr;
287 }
288
289 // ICU expects numberingSystem as a Unicode locale extensions on locale.
290
291 mozilla::intl::Locale tag;
292 {
293 RootedLinearString locale(cx, value.toString()->ensureLinear(cx));
294 if (!locale) {
295 return nullptr;
296 }
297
298 if (!intl::ParseLocale(cx, locale, tag)) {
299 return nullptr;
300 }
301 }
302
303 JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
304
305 if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
306 &value)) {
307 return nullptr;
308 }
309
310 {
311 JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
312 if (!numberingSystem) {
313 return nullptr;
314 }
315
316 if (!keywords.emplaceBack("nu", numberingSystem)) {
317 return nullptr;
318 }
319 }
320
321 // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
322 // the Unicode extension subtag. We're then relying on ICU to follow RFC
323 // 6067, which states that any trailing keywords using the same key
324 // should be ignored.
325 if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
326 return nullptr;
327 }
328
329 intl::FormatBuffer<char> buffer(cx);
330 if (auto result = tag.ToString(buffer); result.isErr()) {
331 intl::ReportInternalError(cx, result.unwrapErr());
332 return nullptr;
333 }
334 return buffer.extractStringZ();
335 }
336
337 struct NumberFormatOptions : public mozilla::intl::NumberRangeFormatOptions {
338 static_assert(std::is_base_of_v<mozilla::intl::NumberFormatOptions,
339 mozilla::intl::NumberRangeFormatOptions>);
340
341 char currencyChars[3] = {};
342 char unitChars[MaxUnitLength()] = {};
343 };
344
FillNumberFormatOptions(JSContext * cx,HandleObject internals,NumberFormatOptions & options)345 static bool FillNumberFormatOptions(JSContext* cx, HandleObject internals,
346 NumberFormatOptions& options) {
347 RootedValue value(cx);
348 if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
349 return false;
350 }
351
352 bool accountingSign = false;
353 {
354 JSLinearString* style = value.toString()->ensureLinear(cx);
355 if (!style) {
356 return false;
357 }
358
359 if (StringEqualsLiteral(style, "currency")) {
360 if (!GetProperty(cx, internals, internals, cx->names().currency,
361 &value)) {
362 return false;
363 }
364 JSLinearString* currency = value.toString()->ensureLinear(cx);
365 if (!currency) {
366 return false;
367 }
368
369 MOZ_RELEASE_ASSERT(
370 currency->length() == 3,
371 "IsWellFormedCurrencyCode permits only length-3 strings");
372 MOZ_ASSERT(StringIsAscii(currency),
373 "IsWellFormedCurrencyCode permits only ASCII strings");
374 CopyChars(reinterpret_cast<Latin1Char*>(options.currencyChars),
375 *currency);
376
377 if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay,
378 &value)) {
379 return false;
380 }
381 JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx);
382 if (!currencyDisplay) {
383 return false;
384 }
385
386 using CurrencyDisplay =
387 mozilla::intl::NumberFormatOptions::CurrencyDisplay;
388
389 CurrencyDisplay display;
390 if (StringEqualsLiteral(currencyDisplay, "code")) {
391 display = CurrencyDisplay::Code;
392 } else if (StringEqualsLiteral(currencyDisplay, "symbol")) {
393 display = CurrencyDisplay::Symbol;
394 } else if (StringEqualsLiteral(currencyDisplay, "narrowSymbol")) {
395 display = CurrencyDisplay::NarrowSymbol;
396 } else {
397 MOZ_ASSERT(StringEqualsLiteral(currencyDisplay, "name"));
398 display = CurrencyDisplay::Name;
399 }
400
401 if (!GetProperty(cx, internals, internals, cx->names().currencySign,
402 &value)) {
403 return false;
404 }
405 JSLinearString* currencySign = value.toString()->ensureLinear(cx);
406 if (!currencySign) {
407 return false;
408 }
409
410 if (StringEqualsLiteral(currencySign, "accounting")) {
411 accountingSign = true;
412 } else {
413 MOZ_ASSERT(StringEqualsLiteral(currencySign, "standard"));
414 }
415
416 options.mCurrency = mozilla::Some(
417 std::make_pair(std::string_view(options.currencyChars, 3), display));
418 } else if (StringEqualsLiteral(style, "percent")) {
419 options.mPercent = true;
420 } else if (StringEqualsLiteral(style, "unit")) {
421 if (!GetProperty(cx, internals, internals, cx->names().unit, &value)) {
422 return false;
423 }
424 JSLinearString* unit = value.toString()->ensureLinear(cx);
425 if (!unit) {
426 return false;
427 }
428
429 size_t unit_str_length = unit->length();
430
431 MOZ_ASSERT(StringIsAscii(unit));
432 MOZ_RELEASE_ASSERT(unit_str_length <= MaxUnitLength());
433 CopyChars(reinterpret_cast<Latin1Char*>(options.unitChars), *unit);
434
435 if (!GetProperty(cx, internals, internals, cx->names().unitDisplay,
436 &value)) {
437 return false;
438 }
439 JSLinearString* unitDisplay = value.toString()->ensureLinear(cx);
440 if (!unitDisplay) {
441 return false;
442 }
443
444 using UnitDisplay = mozilla::intl::NumberFormatOptions::UnitDisplay;
445
446 UnitDisplay display;
447 if (StringEqualsLiteral(unitDisplay, "short")) {
448 display = UnitDisplay::Short;
449 } else if (StringEqualsLiteral(unitDisplay, "narrow")) {
450 display = UnitDisplay::Narrow;
451 } else {
452 MOZ_ASSERT(StringEqualsLiteral(unitDisplay, "long"));
453 display = UnitDisplay::Long;
454 }
455
456 options.mUnit = mozilla::Some(std::make_pair(
457 std::string_view(options.unitChars, unit_str_length), display));
458 } else {
459 MOZ_ASSERT(StringEqualsLiteral(style, "decimal"));
460 }
461 }
462
463 bool hasMinimumSignificantDigits;
464 if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits,
465 &hasMinimumSignificantDigits)) {
466 return false;
467 }
468
469 if (hasMinimumSignificantDigits) {
470 if (!GetProperty(cx, internals, internals,
471 cx->names().minimumSignificantDigits, &value)) {
472 return false;
473 }
474 uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
475
476 if (!GetProperty(cx, internals, internals,
477 cx->names().maximumSignificantDigits, &value)) {
478 return false;
479 }
480 uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
481
482 options.mSignificantDigits = mozilla::Some(
483 std::make_pair(minimumSignificantDigits, maximumSignificantDigits));
484 }
485
486 bool hasMinimumFractionDigits;
487 if (!HasProperty(cx, internals, cx->names().minimumFractionDigits,
488 &hasMinimumFractionDigits)) {
489 return false;
490 }
491
492 if (hasMinimumFractionDigits) {
493 if (!GetProperty(cx, internals, internals,
494 cx->names().minimumFractionDigits, &value)) {
495 return false;
496 }
497 uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
498
499 if (!GetProperty(cx, internals, internals,
500 cx->names().maximumFractionDigits, &value)) {
501 return false;
502 }
503 uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
504
505 options.mFractionDigits = mozilla::Some(
506 std::make_pair(minimumFractionDigits, maximumFractionDigits));
507 }
508
509 if (!GetProperty(cx, internals, internals, cx->names().roundingPriority,
510 &value)) {
511 return false;
512 }
513
514 {
515 JSLinearString* roundingPriority = value.toString()->ensureLinear(cx);
516 if (!roundingPriority) {
517 return false;
518 }
519
520 using RoundingPriority =
521 mozilla::intl::NumberFormatOptions::RoundingPriority;
522
523 RoundingPriority priority;
524 if (StringEqualsLiteral(roundingPriority, "auto")) {
525 priority = RoundingPriority::Auto;
526 } else if (StringEqualsLiteral(roundingPriority, "morePrecision")) {
527 priority = RoundingPriority::MorePrecision;
528 } else {
529 MOZ_ASSERT(StringEqualsLiteral(roundingPriority, "lessPrecision"));
530 priority = RoundingPriority::LessPrecision;
531 }
532
533 options.mRoundingPriority = priority;
534 }
535
536 if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
537 &value)) {
538 return false;
539 }
540 options.mMinIntegerDigits =
541 mozilla::Some(AssertedCast<uint32_t>(value.toInt32()));
542
543 if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) {
544 return false;
545 }
546
547 if (value.isString()) {
548 JSLinearString* useGrouping = value.toString()->ensureLinear(cx);
549 if (!useGrouping) {
550 return false;
551 }
552
553 using Grouping = mozilla::intl::NumberFormatOptions::Grouping;
554
555 Grouping grouping;
556 if (StringEqualsLiteral(useGrouping, "auto")) {
557 grouping = Grouping::Auto;
558 } else if (StringEqualsLiteral(useGrouping, "always")) {
559 grouping = Grouping::Always;
560 } else {
561 MOZ_ASSERT(StringEqualsLiteral(useGrouping, "min2"));
562 grouping = Grouping::Min2;
563 }
564
565 options.mGrouping = grouping;
566 } else {
567 MOZ_ASSERT(value.isBoolean());
568 #ifdef NIGHTLY_BUILD
569 // The caller passes the string "always" instead of |true| when the
570 // NumberFormat V3 spec is being used.
571 MOZ_ASSERT(value.toBoolean() == false);
572 #endif
573
574 using Grouping = mozilla::intl::NumberFormatOptions::Grouping;
575
576 Grouping grouping;
577 if (value.toBoolean()) {
578 grouping = Grouping::Auto;
579 } else {
580 grouping = Grouping::Never;
581 }
582
583 options.mGrouping = grouping;
584 }
585
586 if (!GetProperty(cx, internals, internals, cx->names().notation, &value)) {
587 return false;
588 }
589
590 {
591 JSLinearString* notation = value.toString()->ensureLinear(cx);
592 if (!notation) {
593 return false;
594 }
595
596 using Notation = mozilla::intl::NumberFormatOptions::Notation;
597
598 Notation style;
599 if (StringEqualsLiteral(notation, "standard")) {
600 style = Notation::Standard;
601 } else if (StringEqualsLiteral(notation, "scientific")) {
602 style = Notation::Scientific;
603 } else if (StringEqualsLiteral(notation, "engineering")) {
604 style = Notation::Engineering;
605 } else {
606 MOZ_ASSERT(StringEqualsLiteral(notation, "compact"));
607
608 if (!GetProperty(cx, internals, internals, cx->names().compactDisplay,
609 &value)) {
610 return false;
611 }
612
613 JSLinearString* compactDisplay = value.toString()->ensureLinear(cx);
614 if (!compactDisplay) {
615 return false;
616 }
617
618 if (StringEqualsLiteral(compactDisplay, "short")) {
619 style = Notation::CompactShort;
620 } else {
621 MOZ_ASSERT(StringEqualsLiteral(compactDisplay, "long"));
622 style = Notation::CompactLong;
623 }
624 }
625
626 options.mNotation = style;
627 }
628
629 if (!GetProperty(cx, internals, internals, cx->names().signDisplay, &value)) {
630 return false;
631 }
632
633 {
634 JSLinearString* signDisplay = value.toString()->ensureLinear(cx);
635 if (!signDisplay) {
636 return false;
637 }
638
639 using SignDisplay = mozilla::intl::NumberFormatOptions::SignDisplay;
640
641 SignDisplay display;
642 if (StringEqualsLiteral(signDisplay, "auto")) {
643 if (accountingSign) {
644 display = SignDisplay::Accounting;
645 } else {
646 display = SignDisplay::Auto;
647 }
648 } else if (StringEqualsLiteral(signDisplay, "never")) {
649 display = SignDisplay::Never;
650 } else if (StringEqualsLiteral(signDisplay, "always")) {
651 if (accountingSign) {
652 display = SignDisplay::AccountingAlways;
653 } else {
654 display = SignDisplay::Always;
655 }
656 } else if (StringEqualsLiteral(signDisplay, "exceptZero")) {
657 if (accountingSign) {
658 display = SignDisplay::AccountingExceptZero;
659 } else {
660 display = SignDisplay::ExceptZero;
661 }
662 } else {
663 MOZ_ASSERT(StringEqualsLiteral(signDisplay, "negative"));
664 if (accountingSign) {
665 display = SignDisplay::AccountingNegative;
666 } else {
667 display = SignDisplay::Negative;
668 }
669 }
670
671 options.mSignDisplay = display;
672 }
673
674 if (!GetProperty(cx, internals, internals, cx->names().roundingIncrement,
675 &value)) {
676 return false;
677 }
678 options.mRoundingIncrement = AssertedCast<uint32_t>(value.toInt32());
679
680 if (!GetProperty(cx, internals, internals, cx->names().roundingMode,
681 &value)) {
682 return false;
683 }
684
685 {
686 JSLinearString* roundingMode = value.toString()->ensureLinear(cx);
687 if (!roundingMode) {
688 return false;
689 }
690
691 using RoundingMode = mozilla::intl::NumberFormatOptions::RoundingMode;
692
693 RoundingMode rounding;
694 if (StringEqualsLiteral(roundingMode, "halfExpand")) {
695 // "halfExpand" is the default mode, so we handle it first.
696 rounding = RoundingMode::HalfExpand;
697 } else if (StringEqualsLiteral(roundingMode, "ceil")) {
698 rounding = RoundingMode::Ceil;
699 } else if (StringEqualsLiteral(roundingMode, "floor")) {
700 rounding = RoundingMode::Floor;
701 } else if (StringEqualsLiteral(roundingMode, "expand")) {
702 rounding = RoundingMode::Expand;
703 } else if (StringEqualsLiteral(roundingMode, "trunc")) {
704 rounding = RoundingMode::Trunc;
705 } else if (StringEqualsLiteral(roundingMode, "halfCeil")) {
706 rounding = RoundingMode::HalfCeil;
707 } else if (StringEqualsLiteral(roundingMode, "halfFloor")) {
708 rounding = RoundingMode::HalfFloor;
709 } else if (StringEqualsLiteral(roundingMode, "halfTrunc")) {
710 rounding = RoundingMode::HalfTrunc;
711 } else {
712 MOZ_ASSERT(StringEqualsLiteral(roundingMode, "halfEven"));
713 rounding = RoundingMode::HalfEven;
714 }
715
716 options.mRoundingMode = rounding;
717 }
718
719 if (!GetProperty(cx, internals, internals, cx->names().trailingZeroDisplay,
720 &value)) {
721 return false;
722 }
723
724 {
725 JSLinearString* trailingZeroDisplay = value.toString()->ensureLinear(cx);
726 if (!trailingZeroDisplay) {
727 return false;
728 }
729
730 if (StringEqualsLiteral(trailingZeroDisplay, "auto")) {
731 options.mStripTrailingZero = false;
732 } else {
733 MOZ_ASSERT(StringEqualsLiteral(trailingZeroDisplay, "stripIfInteger"));
734 options.mStripTrailingZero = true;
735 }
736 }
737
738 return true;
739 }
740
741 /**
742 * Returns a new mozilla::intl::Number[Range]Format with the locale and number
743 * formatting options of the given NumberFormat, or a nullptr if
744 * initialization failed.
745 */
746 template <class Formatter>
NewNumberFormat(JSContext * cx,Handle<NumberFormatObject * > numberFormat)747 static Formatter* NewNumberFormat(JSContext* cx,
748 Handle<NumberFormatObject*> numberFormat) {
749 RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat));
750 if (!internals) {
751 return nullptr;
752 }
753
754 UniqueChars locale = NumberFormatLocale(cx, internals);
755 if (!locale) {
756 return nullptr;
757 }
758
759 NumberFormatOptions options;
760 if (!FillNumberFormatOptions(cx, internals, options)) {
761 return nullptr;
762 }
763
764 options.mRangeCollapse = NumberFormatOptions::RangeCollapse::Auto;
765 options.mRangeIdentityFallback =
766 NumberFormatOptions::RangeIdentityFallback::Approximately;
767
768 mozilla::Result<mozilla::UniquePtr<Formatter>, mozilla::intl::ICUError>
769 result = Formatter::TryCreate(locale.get(), options);
770
771 if (result.isOk()) {
772 return result.unwrap().release();
773 }
774
775 intl::ReportInternalError(cx, result.unwrapErr());
776 return nullptr;
777 }
778
GetOrCreateNumberFormat(JSContext * cx,Handle<NumberFormatObject * > numberFormat)779 static mozilla::intl::NumberFormat* GetOrCreateNumberFormat(
780 JSContext* cx, Handle<NumberFormatObject*> numberFormat) {
781 // Obtain a cached mozilla::intl::NumberFormat object.
782 mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter();
783 if (nf) {
784 return nf;
785 }
786
787 nf = NewNumberFormat<mozilla::intl::NumberFormat>(cx, numberFormat);
788 if (!nf) {
789 return nullptr;
790 }
791 numberFormat->setNumberFormatter(nf);
792
793 intl::AddICUCellMemory(numberFormat, NumberFormatObject::EstimatedMemoryUse);
794 return nf;
795 }
796
GetOrCreateNumberRangeFormat(JSContext * cx,Handle<NumberFormatObject * > numberFormat)797 static mozilla::intl::NumberRangeFormat* GetOrCreateNumberRangeFormat(
798 JSContext* cx, Handle<NumberFormatObject*> numberFormat) {
799 // Obtain a cached mozilla::intl::NumberRangeFormat object.
800 mozilla::intl::NumberRangeFormat* nrf =
801 numberFormat->getNumberRangeFormatter();
802 if (nrf) {
803 return nrf;
804 }
805
806 nrf = NewNumberFormat<mozilla::intl::NumberRangeFormat>(cx, numberFormat);
807 if (!nrf) {
808 return nullptr;
809 }
810 numberFormat->setNumberRangeFormatter(nrf);
811
812 intl::AddICUCellMemory(numberFormat,
813 NumberFormatObject::EstimatedRangeFormatterMemoryUse);
814 return nrf;
815 }
816
GetFieldTypeForNumberPartType(mozilla::intl::NumberPartType type)817 static FieldType GetFieldTypeForNumberPartType(
818 mozilla::intl::NumberPartType type) {
819 switch (type) {
820 case mozilla::intl::NumberPartType::ApproximatelySign:
821 return &JSAtomState::approximatelySign;
822 case mozilla::intl::NumberPartType::Compact:
823 return &JSAtomState::compact;
824 case mozilla::intl::NumberPartType::Currency:
825 return &JSAtomState::currency;
826 case mozilla::intl::NumberPartType::Decimal:
827 return &JSAtomState::decimal;
828 case mozilla::intl::NumberPartType::ExponentInteger:
829 return &JSAtomState::exponentInteger;
830 case mozilla::intl::NumberPartType::ExponentMinusSign:
831 return &JSAtomState::exponentMinusSign;
832 case mozilla::intl::NumberPartType::ExponentSeparator:
833 return &JSAtomState::exponentSeparator;
834 case mozilla::intl::NumberPartType::Fraction:
835 return &JSAtomState::fraction;
836 case mozilla::intl::NumberPartType::Group:
837 return &JSAtomState::group;
838 case mozilla::intl::NumberPartType::Infinity:
839 return &JSAtomState::infinity;
840 case mozilla::intl::NumberPartType::Integer:
841 return &JSAtomState::integer;
842 case mozilla::intl::NumberPartType::Literal:
843 return &JSAtomState::literal;
844 case mozilla::intl::NumberPartType::MinusSign:
845 return &JSAtomState::minusSign;
846 case mozilla::intl::NumberPartType::Nan:
847 return &JSAtomState::nan;
848 case mozilla::intl::NumberPartType::Percent:
849 return &JSAtomState::percentSign;
850 case mozilla::intl::NumberPartType::PlusSign:
851 return &JSAtomState::plusSign;
852 case mozilla::intl::NumberPartType::Unit:
853 return &JSAtomState::unit;
854 }
855
856 MOZ_ASSERT_UNREACHABLE(
857 "unenumerated, undocumented format field returned by iterator");
858 return nullptr;
859 }
860
GetFieldTypeForNumberPartSource(mozilla::intl::NumberPartSource source)861 static FieldType GetFieldTypeForNumberPartSource(
862 mozilla::intl::NumberPartSource source) {
863 switch (source) {
864 case mozilla::intl::NumberPartSource::Shared:
865 return &JSAtomState::shared;
866 case mozilla::intl::NumberPartSource::Start:
867 return &JSAtomState::startRange;
868 case mozilla::intl::NumberPartSource::End:
869 return &JSAtomState::endRange;
870 }
871
872 MOZ_CRASH("unexpected number part source");
873 }
874
875 enum class DisplayNumberPartSource : bool { No, Yes };
876
FormattedNumberToParts(JSContext * cx,HandleString str,const mozilla::intl::NumberPartVector & parts,DisplayNumberPartSource displaySource,FieldType unitType,MutableHandleValue result)877 static bool FormattedNumberToParts(JSContext* cx, HandleString str,
878 const mozilla::intl::NumberPartVector& parts,
879 DisplayNumberPartSource displaySource,
880 FieldType unitType,
881 MutableHandleValue result) {
882 size_t lastEndIndex = 0;
883
884 RootedObject singlePart(cx);
885 RootedValue propVal(cx);
886
887 RootedArrayObject partsArray(cx,
888 NewDenseFullyAllocatedArray(cx, parts.length()));
889 if (!partsArray) {
890 return false;
891 }
892 partsArray->ensureDenseInitializedLength(0, parts.length());
893
894 size_t index = 0;
895 for (const auto& part : parts) {
896 FieldType type = GetFieldTypeForNumberPartType(part.type);
897 size_t endIndex = part.endIndex;
898
899 MOZ_ASSERT(lastEndIndex < endIndex);
900
901 singlePart = NewPlainObject(cx);
902 if (!singlePart) {
903 return false;
904 }
905
906 propVal.setString(cx->names().*type);
907 if (!DefineDataProperty(cx, singlePart, cx->names().type, propVal)) {
908 return false;
909 }
910
911 JSLinearString* partSubstr =
912 NewDependentString(cx, str, lastEndIndex, endIndex - lastEndIndex);
913 if (!partSubstr) {
914 return false;
915 }
916
917 propVal.setString(partSubstr);
918 if (!DefineDataProperty(cx, singlePart, cx->names().value, propVal)) {
919 return false;
920 }
921
922 if (displaySource == DisplayNumberPartSource::Yes) {
923 FieldType source = GetFieldTypeForNumberPartSource(part.source);
924
925 propVal.setString(cx->names().*source);
926 if (!DefineDataProperty(cx, singlePart, cx->names().source, propVal)) {
927 return false;
928 }
929 }
930
931 if (unitType != nullptr && type != &JSAtomState::literal) {
932 propVal.setString(cx->names().*unitType);
933 if (!DefineDataProperty(cx, singlePart, cx->names().unit, propVal)) {
934 return false;
935 }
936 }
937
938 partsArray->initDenseElement(index++, ObjectValue(*singlePart));
939
940 lastEndIndex = endIndex;
941 }
942
943 MOZ_ASSERT(index == parts.length());
944 MOZ_ASSERT(lastEndIndex == str->length(),
945 "result array must partition the entire string");
946
947 result.setObject(*partsArray);
948 return true;
949 }
950
FormattedRelativeTimeToParts(JSContext * cx,HandleString str,const mozilla::intl::NumberPartVector & parts,FieldType relativeTimeUnit,MutableHandleValue result)951 bool js::intl::FormattedRelativeTimeToParts(
952 JSContext* cx, HandleString str,
953 const mozilla::intl::NumberPartVector& parts, FieldType relativeTimeUnit,
954 MutableHandleValue result) {
955 return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No,
956 relativeTimeUnit, result);
957 }
958
959 // Return true if the string starts with "0[bBoOxX]", possibly skipping over
960 // leading whitespace.
961 template <typename CharT>
IsNonDecimalNumber(mozilla::Range<const CharT> chars)962 static bool IsNonDecimalNumber(mozilla::Range<const CharT> chars) {
963 const CharT* end = chars.begin().get() + chars.length();
964 const CharT* start = SkipSpace(chars.begin().get(), end);
965
966 if (end - start >= 2 && start[0] == '0') {
967 CharT ch = start[1];
968 return ch == 'b' || ch == 'B' || ch == 'o' || ch == 'O' || ch == 'x' ||
969 ch == 'X';
970 }
971 return false;
972 }
973
IsNonDecimalNumber(JSLinearString * str)974 static bool IsNonDecimalNumber(JSLinearString* str) {
975 JS::AutoCheckCannotGC nogc;
976 return str->hasLatin1Chars() ? IsNonDecimalNumber(str->latin1Range(nogc))
977 : IsNonDecimalNumber(str->twoByteRange(nogc));
978 }
979
ToIntlMathematicalValue(JSContext * cx,MutableHandleValue value,double * numberApproximation=nullptr)980 static bool ToIntlMathematicalValue(JSContext* cx, MutableHandleValue value,
981 double* numberApproximation = nullptr) {
982 if (!ToPrimitive(cx, JSTYPE_NUMBER, value)) {
983 return false;
984 }
985
986 // Maximum exponent supported by ICU. Exponents larger than this value will
987 // cause ICU to report an error.
988 // See also "intl/icu/source/i18n/decContext.h".
989 constexpr int32_t maximumExponent = 999'999'999;
990
991 // We further limit the maximum positive exponent to avoid spending multiple
992 // seconds or even minutes in ICU when formatting large numbers.
993 constexpr int32_t maximumPositiveExponent = 9'999'999;
994
995 // Compute the maximum BigInt digit length from the maximum positive exponent.
996 //
997 // BigInts are stored with base |2 ** BigInt::DigitBits|, so we have:
998 //
999 // |maximumPositiveExponent| * Log_DigitBase(10)
1000 // = |maximumPositiveExponent| * Log2(10) / Log2(2 ** BigInt::DigitBits)
1001 // = |maximumPositiveExponent| * Log2(10) / BigInt::DigitBits
1002 // = 33219277.626945525... / BigInt::DigitBits
1003 constexpr size_t maximumBigIntLength = 33219277.626945525 / BigInt::DigitBits;
1004
1005 if (!value.isString()) {
1006 if (!ToNumeric(cx, value)) {
1007 return false;
1008 }
1009
1010 if (value.isBigInt() &&
1011 value.toBigInt()->digitLength() > maximumBigIntLength) {
1012 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
1013 JSMSG_EXPONENT_TOO_LARGE);
1014 return false;
1015 }
1016
1017 return true;
1018 }
1019
1020 JSLinearString* str = value.toString()->ensureLinear(cx);
1021 if (!str) {
1022 return false;
1023 }
1024
1025 // Call StringToNumber to validate the input can be parsed as a number.
1026 double number;
1027 if (!StringToNumber(cx, str, &number)) {
1028 return false;
1029 }
1030 if (numberApproximation) {
1031 *numberApproximation = number;
1032 }
1033
1034 bool exponentTooLarge = false;
1035 if (mozilla::IsNaN(number)) {
1036 // Set to NaN if the input can't be parsed as a number.
1037 value.setNaN();
1038 } else if (IsNonDecimalNumber(str)) {
1039 // ICU doesn't accept non-decimal numbers, so we have to convert the input
1040 // into a base-10 string.
1041
1042 MOZ_ASSERT(!mozilla::IsNegative(number),
1043 "non-decimal numbers can't be negative");
1044
1045 if (number < DOUBLE_INTEGRAL_PRECISION_LIMIT) {
1046 // Fast-path if we can guarantee there was no loss of precision.
1047 value.setDouble(number);
1048 } else {
1049 // For the slow-path convert the string into a BigInt.
1050
1051 // StringToBigInt can't fail (other than OOM) when StringToNumber already
1052 // succeeded.
1053 RootedString rooted(cx, str);
1054 BigInt* bi;
1055 JS_TRY_VAR_OR_RETURN_FALSE(cx, bi, StringToBigInt(cx, rooted));
1056 MOZ_ASSERT(bi);
1057
1058 if (bi->digitLength() > maximumBigIntLength) {
1059 exponentTooLarge = true;
1060 } else {
1061 value.setBigInt(bi);
1062 }
1063 }
1064 } else {
1065 JS::AutoCheckCannotGC nogc;
1066 if (auto decimal = intl::DecimalNumber::from(str, nogc)) {
1067 if (decimal->isZero()) {
1068 // Normalize positive/negative zero.
1069 MOZ_ASSERT(number == 0);
1070
1071 value.setDouble(number);
1072 } else if (decimal->exponentTooLarge() ||
1073 std::abs(decimal->exponent()) >= maximumExponent ||
1074 decimal->exponent() > maximumPositiveExponent) {
1075 exponentTooLarge = true;
1076 }
1077 } else {
1078 // If we can't parse the string as a decimal, it must be ±Infinity.
1079 MOZ_ASSERT(mozilla::IsInfinite(number));
1080 MOZ_ASSERT(StringFindPattern(str, cx->names().Infinity, 0) >= 0);
1081
1082 value.setDouble(number);
1083 }
1084 }
1085
1086 if (exponentTooLarge) {
1087 // Throw an error if the exponent is too large.
1088 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
1089 JSMSG_EXPONENT_TOO_LARGE);
1090 return false;
1091 }
1092
1093 return true;
1094 }
1095
1096 // Return the number part of the input by removing leading and trailing
1097 // whitespace.
1098 template <typename CharT>
NumberPart(const CharT * chars,size_t length)1099 static mozilla::Span<const CharT> NumberPart(const CharT* chars,
1100 size_t length) {
1101 const CharT* start = chars;
1102 const CharT* end = chars + length;
1103
1104 start = SkipSpace(start, end);
1105
1106 // |SkipSpace| only supports forward iteration, so inline the backwards
1107 // iteration here.
1108 MOZ_ASSERT(start <= end);
1109 while (end > start && unicode::IsSpace(end[-1])) {
1110 end--;
1111 }
1112
1113 // The number part is a non-empty, ASCII-only substring.
1114 MOZ_ASSERT(start < end);
1115 MOZ_ASSERT(mozilla::IsAscii(mozilla::Span(start, end)));
1116
1117 return {start, end};
1118 }
1119
NumberPart(JSContext * cx,JSLinearString * str,const JS::AutoCheckCannotGC & nogc,JS::UniqueChars & latin1,std::string_view & result)1120 static bool NumberPart(JSContext* cx, JSLinearString* str,
1121 const JS::AutoCheckCannotGC& nogc,
1122 JS::UniqueChars& latin1, std::string_view& result) {
1123 if (str->hasLatin1Chars()) {
1124 auto span = NumberPart(
1125 reinterpret_cast<const char*>(str->latin1Chars(nogc)), str->length());
1126
1127 result = {span.data(), span.size()};
1128 return true;
1129 }
1130
1131 auto span = NumberPart(str->twoByteChars(nogc), str->length());
1132
1133 latin1.reset(JS::LossyTwoByteCharsToNewLatin1CharsZ(cx, span).c_str());
1134 if (!latin1) {
1135 return false;
1136 }
1137
1138 result = {latin1.get(), span.size()};
1139 return true;
1140 }
1141
intl_FormatNumber(JSContext * cx,unsigned argc,Value * vp)1142 bool js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) {
1143 CallArgs args = CallArgsFromVp(argc, vp);
1144 MOZ_ASSERT(args.length() == 3);
1145 MOZ_ASSERT(args[0].isObject());
1146 #ifndef NIGHTLY_BUILD
1147 MOZ_ASSERT(args[1].isNumeric());
1148 #endif
1149 MOZ_ASSERT(args[2].isBoolean());
1150
1151 Rooted<NumberFormatObject*> numberFormat(
1152 cx, &args[0].toObject().as<NumberFormatObject>());
1153
1154 RootedValue value(cx, args[1]);
1155 #ifdef NIGHTLY_BUILD
1156 if (!ToIntlMathematicalValue(cx, &value)) {
1157 return false;
1158 }
1159 #endif
1160
1161 mozilla::intl::NumberFormat* nf = GetOrCreateNumberFormat(cx, numberFormat);
1162 if (!nf) {
1163 return false;
1164 }
1165
1166 // Actually format the number
1167 using ICUError = mozilla::intl::ICUError;
1168
1169 bool formatToParts = args[2].toBoolean();
1170 mozilla::Result<std::u16string_view, ICUError> result =
1171 mozilla::Err(ICUError::InternalError);
1172 mozilla::intl::NumberPartVector parts;
1173 if (value.isNumber()) {
1174 double num = value.toNumber();
1175 if (formatToParts) {
1176 result = nf->formatToParts(num, parts);
1177 } else {
1178 result = nf->format(num);
1179 }
1180 } else if (value.isBigInt()) {
1181 RootedBigInt bi(cx, value.toBigInt());
1182
1183 int64_t num;
1184 if (BigInt::isInt64(bi, &num)) {
1185 if (formatToParts) {
1186 result = nf->formatToParts(num, parts);
1187 } else {
1188 result = nf->format(num);
1189 }
1190 } else {
1191 JSLinearString* str = BigInt::toString<CanGC>(cx, bi, 10);
1192 if (!str) {
1193 return false;
1194 }
1195 MOZ_RELEASE_ASSERT(str->hasLatin1Chars());
1196
1197 JS::AutoCheckCannotGC nogc;
1198
1199 const char* chars = reinterpret_cast<const char*>(str->latin1Chars(nogc));
1200 if (formatToParts) {
1201 result =
1202 nf->formatToParts(std::string_view(chars, str->length()), parts);
1203 } else {
1204 result = nf->format(std::string_view(chars, str->length()));
1205 }
1206 }
1207 } else {
1208 JSLinearString* str = value.toString()->ensureLinear(cx);
1209 if (!str) {
1210 return false;
1211 }
1212
1213 JS::AutoCheckCannotGC nogc;
1214
1215 // Two-byte strings have to be copied into a separate |char| buffer.
1216 JS::UniqueChars latin1;
1217
1218 std::string_view sv;
1219 if (!NumberPart(cx, str, nogc, latin1, sv)) {
1220 return false;
1221 }
1222
1223 if (formatToParts) {
1224 result = nf->formatToParts(sv, parts);
1225 } else {
1226 result = nf->format(sv);
1227 }
1228 }
1229
1230 if (result.isErr()) {
1231 intl::ReportInternalError(cx, result.unwrapErr());
1232 return false;
1233 }
1234
1235 RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap()));
1236 if (!str) {
1237 return false;
1238 }
1239
1240 if (formatToParts) {
1241 return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No,
1242 nullptr, args.rval());
1243 }
1244
1245 args.rval().setString(str);
1246 return true;
1247 }
1248
ToLinearString(JSContext * cx,HandleValue val)1249 static JSLinearString* ToLinearString(JSContext* cx, HandleValue val) {
1250 // Special case to preserve negative zero.
1251 if (val.isDouble() && mozilla::IsNegativeZero(val.toDouble())) {
1252 constexpr std::string_view negativeZero = "-0";
1253 return NewStringCopy<CanGC>(cx, negativeZero);
1254 }
1255
1256 JSString* str = ToString(cx, val);
1257 return str ? str->ensureLinear(cx) : nullptr;
1258 };
1259
ValidateNumberRange(JSContext * cx,MutableHandleValue start,double startApprox,MutableHandleValue end,double endApprox,bool formatToParts)1260 static bool ValidateNumberRange(JSContext* cx, MutableHandleValue start,
1261 double startApprox, MutableHandleValue end,
1262 double endApprox, bool formatToParts) {
1263 static auto isSpecificDouble = [](const Value& val, auto fn) {
1264 return val.isDouble() && fn(val.toDouble());
1265 };
1266
1267 static auto isNaN = [](const Value& val) {
1268 return isSpecificDouble(val, mozilla::IsNaN<double>);
1269 };
1270
1271 static auto isPositiveInfinity = [](const Value& val) {
1272 return isSpecificDouble(
1273 val, [](double num) { return num > 0 && mozilla::IsInfinite(num); });
1274 };
1275
1276 static auto isNegativeInfinity = [](const Value& val) {
1277 return isSpecificDouble(
1278 val, [](double num) { return num < 0 && mozilla::IsInfinite(num); });
1279 };
1280
1281 static auto isNegativeZero = [](const Value& val) {
1282 return isSpecificDouble(val, mozilla::IsNegativeZero<double>);
1283 };
1284
1285 static auto isMathematicalValue = [](const Value& val) {
1286 // |ToIntlMathematicalValue()| normalizes non-finite values and negative
1287 // zero to Double values, so any string is guaranteed to be a mathematical
1288 // value at this point.
1289 if (!val.isDouble()) {
1290 return true;
1291 }
1292 double num = val.toDouble();
1293 return mozilla::IsFinite(num) && !mozilla::IsNegativeZero(num);
1294 };
1295
1296 static auto isPositiveOrZero = [](const Value& val, double approx) {
1297 MOZ_ASSERT(isMathematicalValue(val));
1298
1299 if (val.isNumber()) {
1300 return val.toNumber() >= 0;
1301 }
1302 if (val.isBigInt()) {
1303 return !val.toBigInt()->isNegative();
1304 }
1305 return approx >= 0;
1306 };
1307
1308 auto throwRangeError = [&]() {
1309 JS_ReportErrorNumberASCII(
1310 cx, GetErrorMessage, nullptr, JSMSG_START_AFTER_END_NUMBER,
1311 "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange");
1312 return false;
1313 };
1314
1315 // PartitionNumberRangePattern, step 1.
1316 if (isNaN(start)) {
1317 JS_ReportErrorNumberASCII(
1318 cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "start",
1319 formatToParts ? "formatRangeToParts" : "formatRange");
1320 return false;
1321 }
1322 if (isNaN(end)) {
1323 JS_ReportErrorNumberASCII(
1324 cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "end",
1325 formatToParts ? "formatRangeToParts" : "formatRange");
1326 return false;
1327 }
1328
1329 // Make sure |start| and |end| can be correctly classified.
1330 MOZ_ASSERT(isMathematicalValue(start) || isNegativeZero(start) ||
1331 isNegativeInfinity(start) || isPositiveInfinity(start));
1332 MOZ_ASSERT(isMathematicalValue(end) || isNegativeZero(end) ||
1333 isNegativeInfinity(end) || isPositiveInfinity(end));
1334
1335 // PartitionNumberRangePattern, step 2.
1336 if (isMathematicalValue(start)) {
1337 // PartitionNumberRangePattern, step 2.a.
1338 if (isMathematicalValue(end)) {
1339 if (!start.isString() && !end.isString()) {
1340 MOZ_ASSERT(start.isNumeric() && end.isNumeric());
1341
1342 bool isLessThan;
1343 if (!LessThan(cx, end, start, &isLessThan)) {
1344 return false;
1345 }
1346 if (isLessThan) {
1347 return throwRangeError();
1348 }
1349 } else {
1350 // |startApprox| and |endApprox| are only initially computed for string
1351 // numbers.
1352 if (start.isNumber()) {
1353 startApprox = start.toNumber();
1354 } else if (start.isBigInt()) {
1355 startApprox = BigInt::numberValue(start.toBigInt());
1356 }
1357 if (end.isNumber()) {
1358 endApprox = end.toNumber();
1359 } else if (end.isBigInt()) {
1360 endApprox = BigInt::numberValue(end.toBigInt());
1361 }
1362
1363 // If the approximation is smaller, the actual value is definitely
1364 // smaller, too.
1365 if (endApprox < startApprox) {
1366 return throwRangeError();
1367 }
1368
1369 // If both approximations are equal to each other, we have to perform
1370 // more work.
1371 if (endApprox == startApprox) {
1372 RootedLinearString strStart(cx, ToLinearString(cx, start));
1373 if (!strStart) {
1374 return false;
1375 }
1376
1377 RootedLinearString strEnd(cx, ToLinearString(cx, end));
1378 if (!strEnd) {
1379 return false;
1380 }
1381
1382 bool endLessThanStart;
1383 {
1384 JS::AutoCheckCannotGC nogc;
1385
1386 auto decStart = intl::DecimalNumber::from(strStart, nogc);
1387 MOZ_ASSERT(decStart);
1388
1389 auto decEnd = intl::DecimalNumber::from(strEnd, nogc);
1390 MOZ_ASSERT(decEnd);
1391
1392 endLessThanStart = decEnd->compareTo(*decStart) < 0;
1393 }
1394 if (endLessThanStart) {
1395 return throwRangeError();
1396 }
1397
1398 // If either value is a string, we end up passing both values as
1399 // strings to the formatter. So let's save the string representation
1400 // here, because then we don't have to recompute them later on.
1401 start.setString(strStart);
1402 end.setString(strEnd);
1403 }
1404 }
1405 }
1406
1407 // PartitionNumberRangePattern, step 2.b.
1408 else if (isNegativeInfinity(end)) {
1409 return throwRangeError();
1410 }
1411
1412 // PartitionNumberRangePattern, step 2.c.
1413 else if (isNegativeZero(end)) {
1414 if (isPositiveOrZero(start, startApprox)) {
1415 return throwRangeError();
1416 }
1417 }
1418
1419 // No range restrictions when the end is positive infinity.
1420 else {
1421 MOZ_ASSERT(isPositiveInfinity(end));
1422 }
1423 }
1424
1425 // PartitionNumberRangePattern, step 3.
1426 else if (isPositiveInfinity(start)) {
1427 // PartitionNumberRangePattern, steps 3.a-c.
1428 if (!isPositiveInfinity(end)) {
1429 return throwRangeError();
1430 }
1431 }
1432
1433 // PartitionNumberRangePattern, step 4.
1434 else if (isNegativeZero(start)) {
1435 // PartitionNumberRangePattern, step 4.a.
1436 if (isMathematicalValue(end)) {
1437 if (!isPositiveOrZero(end, endApprox)) {
1438 return throwRangeError();
1439 }
1440 }
1441
1442 // PartitionNumberRangePattern, step 4.b.
1443 else if (isNegativeInfinity(end)) {
1444 return throwRangeError();
1445 }
1446
1447 // No range restrictions when the end is negative zero or positive infinity.
1448 else {
1449 MOZ_ASSERT(isNegativeZero(end) || isPositiveInfinity(end));
1450 }
1451 }
1452
1453 // No range restrictions when the start is negative infinity.
1454 else {
1455 MOZ_ASSERT(isNegativeInfinity(start));
1456 }
1457
1458 return true;
1459 }
1460
intl_FormatNumberRange(JSContext * cx,unsigned argc,Value * vp)1461 bool js::intl_FormatNumberRange(JSContext* cx, unsigned argc, Value* vp) {
1462 CallArgs args = CallArgsFromVp(argc, vp);
1463 MOZ_ASSERT(args.length() == 4);
1464 MOZ_ASSERT(args[0].isObject());
1465 MOZ_ASSERT(!args[1].isUndefined());
1466 MOZ_ASSERT(!args[2].isUndefined());
1467 MOZ_ASSERT(args[3].isBoolean());
1468
1469 Rooted<NumberFormatObject*> numberFormat(
1470 cx, &args[0].toObject().as<NumberFormatObject>());
1471 bool formatToParts = args[3].toBoolean();
1472
1473 RootedValue start(cx, args[1]);
1474 double startApprox = mozilla::UnspecifiedNaN<double>();
1475 if (!ToIntlMathematicalValue(cx, &start, &startApprox)) {
1476 return false;
1477 }
1478
1479 RootedValue end(cx, args[2]);
1480 double endApprox = mozilla::UnspecifiedNaN<double>();
1481 if (!ToIntlMathematicalValue(cx, &end, &endApprox)) {
1482 return false;
1483 }
1484
1485 if (!ValidateNumberRange(cx, &start, startApprox, &end, endApprox,
1486 formatToParts)) {
1487 return false;
1488 }
1489
1490 using NumberRangeFormat = mozilla::intl::NumberRangeFormat;
1491 NumberRangeFormat* nf = GetOrCreateNumberRangeFormat(cx, numberFormat);
1492 if (!nf) {
1493 return false;
1494 }
1495
1496 auto valueRepresentableAsDouble = [](const Value& val, double* num) {
1497 if (val.isNumber()) {
1498 *num = val.toNumber();
1499 return true;
1500 }
1501 if (val.isBigInt()) {
1502 int64_t i64;
1503 if (BigInt::isInt64(val.toBigInt(), &i64) &&
1504 i64 < int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT) &&
1505 i64 > -int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) {
1506 *num = double(i64);
1507 return true;
1508 }
1509 }
1510 return false;
1511 };
1512
1513 // Actually format the number range.
1514 using ICUError = mozilla::intl::ICUError;
1515
1516 mozilla::Result<std::u16string_view, ICUError> result =
1517 mozilla::Err(ICUError::InternalError);
1518 mozilla::intl::NumberPartVector parts;
1519
1520 double numStart, numEnd;
1521 if (valueRepresentableAsDouble(start, &numStart) &&
1522 valueRepresentableAsDouble(end, &numEnd)) {
1523 if (formatToParts) {
1524 result = nf->formatToParts(numStart, numEnd, parts);
1525 } else {
1526 result = nf->format(numStart, numEnd);
1527 }
1528 } else {
1529 RootedLinearString strStart(cx, ToLinearString(cx, start));
1530 if (!strStart) {
1531 return false;
1532 }
1533
1534 RootedLinearString strEnd(cx, ToLinearString(cx, end));
1535 if (!strEnd) {
1536 return false;
1537 }
1538
1539 JS::AutoCheckCannotGC nogc;
1540
1541 // Two-byte strings have to be copied into a separate |char| buffer.
1542 JS::UniqueChars latin1Start;
1543 JS::UniqueChars latin1End;
1544
1545 std::string_view svStart;
1546 if (!NumberPart(cx, strStart, nogc, latin1Start, svStart)) {
1547 return false;
1548 }
1549
1550 std::string_view svEnd;
1551 if (!NumberPart(cx, strEnd, nogc, latin1End, svEnd)) {
1552 return false;
1553 }
1554
1555 if (formatToParts) {
1556 result = nf->formatToParts(svStart, svEnd, parts);
1557 } else {
1558 result = nf->format(svStart, svEnd);
1559 }
1560 }
1561
1562 if (result.isErr()) {
1563 intl::ReportInternalError(cx, result.unwrapErr());
1564 return false;
1565 }
1566
1567 RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap()));
1568 if (!str) {
1569 return false;
1570 }
1571
1572 if (formatToParts) {
1573 return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::Yes,
1574 nullptr, args.rval());
1575 }
1576
1577 args.rval().setString(str);
1578 return true;
1579 }
1580