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