Source code

Revision control

Other Tools

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/FloatingPoint.h"
13
14
#include <algorithm>
15
#include <stddef.h>
16
#include <stdint.h>
17
18
#include "builtin/intl/CommonFunctions.h"
19
#include "builtin/intl/ICUStubs.h"
20
#include "builtin/intl/ScopedICUObject.h"
21
#include "ds/Sort.h"
22
#include "gc/FreeOp.h"
23
#include "js/CharacterEncoding.h"
24
#include "js/PropertySpec.h"
25
#include "js/RootingAPI.h"
26
#include "js/StableStringChars.h"
27
#include "js/TypeDecls.h"
28
#include "vm/JSContext.h"
29
#include "vm/SelfHosting.h"
30
#include "vm/Stack.h"
31
32
#include "vm/JSObject-inl.h"
33
34
using namespace js;
35
36
using mozilla::AssertedCast;
37
using mozilla::IsFinite;
38
using mozilla::IsNaN;
39
using mozilla::IsNegative;
40
using mozilla::SpecificNaN;
41
42
using js::intl::CallICU;
43
using js::intl::DateTimeFormatOptions;
44
using js::intl::GetAvailableLocales;
45
using js::intl::IcuLocale;
46
47
using JS::AutoStableStringChars;
48
49
const ClassOps NumberFormatObject::classOps_ = {nullptr, /* addProperty */
50
nullptr, /* delProperty */
51
nullptr, /* enumerate */
52
nullptr, /* newEnumerate */
53
nullptr, /* resolve */
54
nullptr, /* mayResolve */
55
NumberFormatObject::finalize};
56
57
const Class NumberFormatObject::class_ = {
58
js_Object_str,
59
JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) |
60
JSCLASS_FOREGROUND_FINALIZE,
61
&NumberFormatObject::classOps_};
62
63
static bool numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
64
CallArgs args = CallArgsFromVp(argc, vp);
65
args.rval().setString(cx->names().NumberFormat);
66
return true;
67
}
68
69
static const JSFunctionSpec numberFormat_static_methods[] = {
70
JS_SELF_HOSTED_FN("supportedLocalesOf",
71
"Intl_NumberFormat_supportedLocalesOf", 1, 0),
72
JS_FS_END};
73
74
static const JSFunctionSpec numberFormat_methods[] = {
75
JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0,
76
0),
77
JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0),
78
JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), JS_FS_END};
79
80
static const JSPropertySpec numberFormat_properties[] = {
81
JS_SELF_HOSTED_GET("format", "$Intl_NumberFormat_format_get", 0),
82
JS_STRING_SYM_PS(toStringTag, "Object", JSPROP_READONLY), JS_PS_END};
83
84
/**
85
* 11.2.1 Intl.NumberFormat([ locales [, options]])
86
*
87
* ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
88
*/
89
static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) {
90
// Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
91
92
// Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
93
RootedObject proto(cx);
94
if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Null, &proto)) {
95
return false;
96
}
97
98
if (!proto) {
99
proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global());
100
if (!proto) {
101
return false;
102
}
103
}
104
105
Rooted<NumberFormatObject*> numberFormat(cx);
106
numberFormat = NewObjectWithGivenProto<NumberFormatObject>(cx, proto);
107
if (!numberFormat) {
108
return false;
109
}
110
111
numberFormat->setFixedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue());
112
numberFormat->setNumberFormatter(nullptr);
113
numberFormat->setFormattedNumber(nullptr);
114
115
RootedValue thisValue(cx,
116
construct ? ObjectValue(*numberFormat) : args.thisv());
117
HandleValue locales = args.get(0);
118
HandleValue options = args.get(1);
119
120
// Step 3.
121
return intl::LegacyInitializeObject(
122
cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, locales,
123
options, DateTimeFormatOptions::Standard, args.rval());
124
}
125
126
static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
127
CallArgs args = CallArgsFromVp(argc, vp);
128
return NumberFormat(cx, args, args.isConstructing());
129
}
130
131
bool js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) {
132
CallArgs args = CallArgsFromVp(argc, vp);
133
MOZ_ASSERT(args.length() == 2);
134
MOZ_ASSERT(!args.isConstructing());
135
// intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it
136
// cannot be used with "new", but it still has to be treated as a
137
// constructor.
138
return NumberFormat(cx, args, true);
139
}
140
141
void js::NumberFormatObject::finalize(FreeOp* fop, JSObject* obj) {
142
MOZ_ASSERT(fop->onMainThread());
143
144
auto* numberFormat = &obj->as<NumberFormatObject>();
145
UNumberFormatter* nf = numberFormat->getNumberFormatter();
146
UFormattedNumber* formatted = numberFormat->getFormattedNumber();
147
148
if (nf) {
149
unumf_close(nf);
150
}
151
if (formatted) {
152
unumf_closeResult(formatted);
153
}
154
}
155
156
JSObject* js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl,
157
Handle<GlobalObject*> global,
158
MutableHandleObject constructor) {
159
RootedFunction ctor(cx);
160
ctor = GlobalObject::createConstructor(cx, &NumberFormat,
161
cx->names().NumberFormat, 0);
162
if (!ctor) {
163
return nullptr;
164
}
165
166
RootedObject proto(
167
cx, GlobalObject::createBlankPrototype<PlainObject>(cx, global));
168
if (!proto) {
169
return nullptr;
170
}
171
172
if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
173
return nullptr;
174
}
175
176
// 11.3.2
177
if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods)) {
178
return nullptr;
179
}
180
181
// 11.4.4
182
if (!JS_DefineFunctions(cx, proto, numberFormat_methods)) {
183
return nullptr;
184
}
185
186
// 11.4.2 and 11.4.3
187
if (!JS_DefineProperties(cx, proto, numberFormat_properties)) {
188
return nullptr;
189
}
190
191
// 8.1
192
RootedValue ctorValue(cx, ObjectValue(*ctor));
193
if (!DefineDataProperty(cx, Intl, cx->names().NumberFormat, ctorValue, 0)) {
194
return nullptr;
195
}
196
197
constructor.set(ctor);
198
return proto;
199
}
200
201
bool js::intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc,
202
Value* vp) {
203
CallArgs args = CallArgsFromVp(argc, vp);
204
MOZ_ASSERT(args.length() == 0);
205
206
RootedValue result(cx);
207
if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable,
208
&result)) {
209
return false;
210
}
211
args.rval().set(result);
212
return true;
213
}
214
215
bool js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) {
216
CallArgs args = CallArgsFromVp(argc, vp);
217
MOZ_ASSERT(args.length() == 1);
218
MOZ_ASSERT(args[0].isString());
219
220
UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
221
if (!locale) {
222
return false;
223
}
224
225
UErrorCode status = U_ZERO_ERROR;
226
UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.get()), &status);
227
if (U_FAILURE(status)) {
228
intl::ReportInternalError(cx);
229
return false;
230
}
231
232
ScopedICUObject<UNumberingSystem, unumsys_close> toClose(numbers);
233
234
const char* name = unumsys_getName(numbers);
235
if (!name) {
236
intl::ReportInternalError(cx);
237
return false;
238
}
239
240
JSString* jsname = NewStringCopyZ<CanGC>(cx, name);
241
if (!jsname) {
242
return false;
243
}
244
245
args.rval().setString(jsname);
246
return true;
247
}
248
249
bool js::intl::NumberFormatterSkeleton::currency(CurrencyDisplay display,
250
JSLinearString* currency) {
251
MOZ_ASSERT(currency->length() == 3,
252
"IsWellFormedCurrencyCode permits only length-3 strings");
253
254
char16_t currencyChars[] = {currency->latin1OrTwoByteChar(0),
255
currency->latin1OrTwoByteChar(1),
256
currency->latin1OrTwoByteChar(2), '\0'};
257
258
if (!(append(u"currency/") && append(currencyChars) && append(' '))) {
259
return false;
260
}
261
262
switch (display) {
263
case CurrencyDisplay::Code:
264
return appendToken(u"unit-width-iso-code");
265
case CurrencyDisplay::Name:
266
return appendToken(u"unit-width-full-name");
267
case CurrencyDisplay::Symbol:
268
// Default, no additional tokens needed.
269
return true;
270
}
271
272
MOZ_CRASH("unexpected currency display type");
273
}
274
275
bool js::intl::NumberFormatterSkeleton::percent() {
276
return appendToken(u"percent scale/100");
277
}
278
279
bool js::intl::NumberFormatterSkeleton::fractionDigits(uint32_t min,
280
uint32_t max) {
281
// Note: |min| can be zero here.
282
MOZ_ASSERT(min <= max);
283
return append('.') && appendN('0', min) && appendN('#', max - min) &&
284
append(' ');
285
}
286
287
bool js::intl::NumberFormatterSkeleton::integerWidth(uint32_t min) {
288
MOZ_ASSERT(min > 0);
289
return append(u"integer-width/+") && appendN('0', min) && append(' ');
290
}
291
292
bool js::intl::NumberFormatterSkeleton::significantDigits(uint32_t min,
293
uint32_t max) {
294
MOZ_ASSERT(min > 0);
295
MOZ_ASSERT(min <= max);
296
return appendN('@', min) && appendN('#', max - min) && append(' ');
297
}
298
299
bool js::intl::NumberFormatterSkeleton::useGrouping(bool on) {
300
return on || appendToken(u"group-off");
301
}
302
303
bool js::intl::NumberFormatterSkeleton::roundingModeHalfUp() {
304
return appendToken(u"rounding-mode-half-up");
305
}
306
307
UNumberFormatter* js::intl::NumberFormatterSkeleton::toFormatter(
308
JSContext* cx, const char* locale) {
309
UErrorCode status = U_ZERO_ERROR;
310
UNumberFormatter* nf = unumf_openForSkeletonAndLocale(
311
vector_.begin(), vector_.length(), locale, &status);
312
if (U_FAILURE(status)) {
313
intl::ReportInternalError(cx);
314
return nullptr;
315
}
316
return nf;
317
}
318
319
/**
320
* Returns a new UNumberFormatter with the locale and number formatting options
321
* of the given NumberFormat.
322
*/
323
static UNumberFormatter* NewUNumberFormatter(
324
JSContext* cx, Handle<NumberFormatObject*> numberFormat) {
325
RootedValue value(cx);
326
327
RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat));
328
if (!internals) {
329
return nullptr;
330
}
331
332
if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
333
return nullptr;
334
}
335
UniqueChars locale = intl::EncodeLocale(cx, value.toString());
336
if (!locale) {
337
return nullptr;
338
}
339
340
intl::NumberFormatterSkeleton skeleton(cx);
341
342
// We don't need to look at numberingSystem - it can only be set via
343
// the Unicode locale extension and is therefore already set on locale.
344
345
if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
346
return nullptr;
347
}
348
349
{
350
JSLinearString* style = value.toString()->ensureLinear(cx);
351
if (!style) {
352
return nullptr;
353
}
354
355
if (StringEqualsAscii(style, "currency")) {
356
using CurrencyDisplay = intl::NumberFormatterSkeleton::CurrencyDisplay;
357
358
if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay,
359
&value)) {
360
return nullptr;
361
}
362
JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx);
363
if (!currencyDisplay) {
364
return nullptr;
365
}
366
367
CurrencyDisplay display;
368
if (StringEqualsAscii(currencyDisplay, "code")) {
369
display = CurrencyDisplay::Code;
370
} else if (StringEqualsAscii(currencyDisplay, "symbol")) {
371
display = CurrencyDisplay::Symbol;
372
} else {
373
MOZ_ASSERT(StringEqualsAscii(currencyDisplay, "name"));
374
display = CurrencyDisplay::Name;
375
}
376
377
if (!GetProperty(cx, internals, internals, cx->names().currency,
378
&value)) {
379
return nullptr;
380
}
381
JSLinearString* currency = value.toString()->ensureLinear(cx);
382
if (!currency) {
383
return nullptr;
384
}
385
386
if (!skeleton.currency(display, currency)) {
387
return nullptr;
388
}
389
} else if (StringEqualsAscii(style, "percent")) {
390
if (!skeleton.percent()) {
391
return nullptr;
392
}
393
} else {
394
MOZ_ASSERT(StringEqualsAscii(style, "decimal"));
395
}
396
}
397
398
bool hasMinimumSignificantDigits;
399
if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits,
400
&hasMinimumSignificantDigits)) {
401
return nullptr;
402
}
403
404
if (hasMinimumSignificantDigits) {
405
if (!GetProperty(cx, internals, internals,
406
cx->names().minimumSignificantDigits, &value)) {
407
return nullptr;
408
}
409
uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
410
411
if (!GetProperty(cx, internals, internals,
412
cx->names().maximumSignificantDigits, &value)) {
413
return nullptr;
414
}
415
uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
416
417
if (!skeleton.significantDigits(minimumSignificantDigits,
418
maximumSignificantDigits)) {
419
return nullptr;
420
}
421
} else {
422
if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
423
&value)) {
424
return nullptr;
425
}
426
uint32_t minimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32());
427
428
if (!GetProperty(cx, internals, internals,
429
cx->names().minimumFractionDigits, &value)) {
430
return nullptr;
431
}
432
uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
433
434
if (!GetProperty(cx, internals, internals,
435
cx->names().maximumFractionDigits, &value)) {
436
return nullptr;
437
}
438
uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
439
440
if (!skeleton.fractionDigits(minimumFractionDigits,
441
maximumFractionDigits)) {
442
return nullptr;
443
}
444
if (!skeleton.integerWidth(minimumIntegerDigits)) {
445
return nullptr;
446
}
447
}
448
449
if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) {
450
return nullptr;
451
}
452
if (!skeleton.useGrouping(value.toBoolean())) {
453
return nullptr;
454
}
455
456
if (!skeleton.roundingModeHalfUp()) {
457
return nullptr;
458
}
459
460
return skeleton.toFormatter(cx, locale.get());
461
}
462
463
static UFormattedNumber* NewUFormattedNumber(JSContext* cx) {
464
UErrorCode status = U_ZERO_ERROR;
465
UFormattedNumber* formatted = unumf_openResult(&status);
466
if (U_FAILURE(status)) {
467
intl::ReportInternalError(cx);
468
return nullptr;
469
}
470
return formatted;
471
}
472
473
static JSString* PartitionNumberPattern(JSContext* cx, UNumberFormatter* nf,
474
UFormattedNumber* formatted,
475
double* x) {
476
// ICU incorrectly formats NaN values with the sign bit set, as if they
477
// were negative. Replace all NaNs with a single pattern with sign bit
478
// unset ("positive", that is) until ICU is fixed.
479
if (MOZ_UNLIKELY(IsNaN(*x))) {
480
*x = SpecificNaN<double>(0, 1);
481
}
482
483
UErrorCode status = U_ZERO_ERROR;
484
unumf_formatDouble(nf, *x, formatted, &status);
485
if (U_FAILURE(status)) {
486
intl::ReportInternalError(cx);
487
return nullptr;
488
}
489
490
return CallICU(cx,
491
[formatted](UChar* chars, int32_t size, UErrorCode* status) {
492
return unumf_resultToString(formatted, chars, size, status);
493
});
494
}
495
496
static bool intl_FormatNumber(JSContext* cx, UNumberFormatter* nf,
497
UFormattedNumber* formatted, double x,
498
MutableHandleValue result) {
499
JSString* str = PartitionNumberPattern(cx, nf, formatted, &x);
500
if (!str) {
501
return false;
502
}
503
504
result.setString(str);
505
return true;
506
}
507
508
static intl::FieldType GetFieldTypeForNumberField(UNumberFormatFields fieldName,
509
double d) {
510
// See intl/icu/source/i18n/unicode/unum.h for a detailed field list. This
511
// list is deliberately exhaustive: cases might have to be added/removed if
512
// this code is compiled with a different ICU with more UNumberFormatFields
513
// enum initializers. Please guard such cases with appropriate ICU
514
// version-testing #ifdefs, should cross-version divergence occur.
515
switch (fieldName) {
516
case UNUM_INTEGER_FIELD:
517
if (IsNaN(d)) {
518
return &JSAtomState::nan;
519
}
520
if (!IsFinite(d)) {
521
return &JSAtomState::infinity;
522
}
523
return &JSAtomState::integer;
524
525
case UNUM_GROUPING_SEPARATOR_FIELD:
526
return &JSAtomState::group;
527
528
case UNUM_DECIMAL_SEPARATOR_FIELD:
529
return &JSAtomState::decimal;
530
531
case UNUM_FRACTION_FIELD:
532
return &JSAtomState::fraction;
533
534
case UNUM_SIGN_FIELD: {
535
// Manual trawling through the ICU call graph appears to indicate that
536
// the basic formatting we request will never include a positive sign.
537
// But this analysis may be mistaken, so don't absolutely trust it.
538
MOZ_ASSERT(!IsNaN(d),
539
"ICU appearing not to produce positive-sign among fields, "
540
"plus our coercing all NaNs to one with sign bit unset "
541
"(i.e. \"positive\"), means we shouldn't reach here with a "
542
"NaN value");
543
return IsNegative(d) ? &JSAtomState::minusSign : &JSAtomState::plusSign;
544
}
545
546
case UNUM_PERCENT_FIELD:
547
return &JSAtomState::percentSign;
548
549
case UNUM_CURRENCY_FIELD:
550
return &JSAtomState::currency;
551
552
case UNUM_PERMILL_FIELD:
553
MOZ_ASSERT_UNREACHABLE(
554
"unexpected permill field found, even though "
555
"we don't use any user-defined patterns that "
556
"would require a permill field");
557
break;
558
559
case UNUM_EXPONENT_SYMBOL_FIELD:
560
case UNUM_EXPONENT_SIGN_FIELD:
561
case UNUM_EXPONENT_FIELD:
562
MOZ_ASSERT_UNREACHABLE(
563
"exponent field unexpectedly found in "
564
"formatted number, even though UNUM_SCIENTIFIC "
565
"and scientific notation were never requested");
566
break;
567
568
#ifndef U_HIDE_DRAFT_API
569
case UNUM_MEASURE_UNIT_FIELD:
570
MOZ_ASSERT_UNREACHABLE(
571
"unexpected measure unit field found, even though "
572
"we don't use any user-defined patterns that "
573
"would require a measure unit field");
574
break;
575
576
case UNUM_COMPACT_FIELD:
577
MOZ_ASSERT_UNREACHABLE(
578
"unexpected compact field found, even though "
579
"we don't use any user-defined patterns that "
580
"would require a compact number notation");
581
break;
582
#endif
583
584
#ifndef U_HIDE_DEPRECATED_API
585
case UNUM_FIELD_COUNT:
586
MOZ_ASSERT_UNREACHABLE(
587
"format field sentinel value returned by "
588
"iterator!");
589
break;
590
#endif
591
}
592
593
MOZ_ASSERT_UNREACHABLE(
594
"unenumerated, undocumented format field returned "
595
"by iterator");
596
return nullptr;
597
}
598
599
bool js::intl::NumberFormatFields::append(int32_t field, int32_t begin,
600
int32_t end) {
601
MOZ_ASSERT(begin >= 0);
602
MOZ_ASSERT(end >= 0);
603
MOZ_ASSERT(begin < end, "erm, aren't fields always non-empty?");
604
605
FieldType type =
606
GetFieldTypeForNumberField(UNumberFormatFields(field), number_);
607
return fields_.emplaceBack(uint32_t(begin), uint32_t(end), type);
608
}
609
610
ArrayObject* js::intl::NumberFormatFields::toArray(JSContext* cx,
611
HandleString overallResult,
612
FieldType unitType) {
613
// Merge sort the fields vector. Expand the vector to have scratch space for
614
// performing the sort.
615
size_t fieldsLen = fields_.length();
616
if (!fields_.resizeUninitialized(fieldsLen * 2)) {
617
return nullptr;
618
}
619
620
MOZ_ALWAYS_TRUE(MergeSort(
621
fields_.begin(), fieldsLen, fields_.begin() + fieldsLen,
622
[](const Field& left, const Field& right, bool* lessOrEqual) {
623
// Sort first by begin index, then to place
624
// enclosing fields before nested fields.
625
*lessOrEqual = left.begin < right.begin ||
626
(left.begin == right.begin && left.end > right.end);
627
return true;
628
}));
629
630
// Deallocate the scratch space.
631
if (!fields_.resize(fieldsLen)) {
632
return nullptr;
633
}
634
635
// Then iterate over the sorted field list to generate a sequence of parts
636
// (what ECMA-402 actually exposes). A part is a maximal character sequence
637
// entirely within no field or a single most-nested field.
638
//
639
// Diagrams may be helpful to illustrate how fields map to parts. Consider
640
// formatting -19,766,580,028,249.41, the US national surplus (negative
641
// because it's actually a debt) on October 18, 2016.
642
//
643
// var options =
644
// { style: "currency", currency: "USD", currencyDisplay: "name" };
645
// var usdFormatter = new Intl.NumberFormat("en-US", options);
646
// usdFormatter.format(-19766580028249.41);
647
//
648
// The formatted result is "-19,766,580,028,249.41 US dollars". ICU
649
// identifies these fields in the string:
650
//
651
// UNUM_GROUPING_SEPARATOR_FIELD
652
// |
653
// UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD
654
// | __________/| |
655
// | / | | | |
656
// "-19,766,580,028,249.41 US dollars"
657
// \________________/ |/ \_______/
658
// | | |
659
// UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD
660
// |
661
// UNUM_FRACTION_FIELD
662
//
663
// These fields map to parts as follows:
664
//
665
// integer decimal
666
// _____|________ |
667
// / /| |\ |\ |\ | literal
668
// /| / | | \ | \ | \| |
669
// "-19,766,580,028,249.41 US dollars"
670
// | \___|___|___/ |/ \________/
671
// | | | |
672
// | group | currency
673
// | |
674
// minusSign fraction
675
//
676
// The sign is a part. Each comma is a part, splitting the integer field
677
// into parts for trillions/billions/&c. digits. The decimal point is a
678
// part. Cents are a part. The space between cents and currency is a part
679
// (outside any field). Last, the currency field is a part.
680
//
681
// Because parts fully partition the formatted string, we only track the
682
// end of each part -- the beginning is implicitly the last part's end.
683
struct Part {
684
uint32_t end;
685
FieldType type;
686
};
687
688
class PartGenerator {
689
// The fields in order from start to end, then least to most nested.
690
const FieldsVector& fields;
691
692
// Index of the current field, in |fields|, being considered to
693
// determine part boundaries. |lastEnd <= fields[index].begin| is an
694
// invariant.
695
size_t index;
696
697
// The end index of the last part produced, always less than or equal
698
// to |limit|, strictly increasing.
699
uint32_t lastEnd;
700
701
// The length of the overall formatted string.
702
const uint32_t limit;
703
704
Vector<size_t, 4> enclosingFields;
705
706
void popEnclosingFieldsEndingAt(uint32_t end) {
707
MOZ_ASSERT_IF(enclosingFields.length() > 0,
708
fields[enclosingFields.back()].end >= end);
709
710
while (enclosingFields.length() > 0 &&
711
fields[enclosingFields.back()].end == end) {
712
enclosingFields.popBack();
713
}
714
}
715
716
bool nextPartInternal(Part* part) {
717
size_t len = fields.length();
718
MOZ_ASSERT(index <= len);
719
720
// If we're out of fields, all that remains are part(s) consisting
721
// of trailing portions of enclosing fields, and maybe a final
722
// literal part.
723
if (index == len) {
724
if (enclosingFields.length() > 0) {
725
const auto& enclosing = fields[enclosingFields.popCopy()];
726
part->end = enclosing.end;
727
part->type = enclosing.type;
728
729
// If additional enclosing fields end where this part ends,
730
// pop them as well.
731
popEnclosingFieldsEndingAt(part->end);
732
} else {
733
part->end = limit;
734
part->type = &JSAtomState::literal;
735
}
736
737
return true;
738
}
739
740
// Otherwise we still have a field to process.
741
const Field* current = &fields[index];
742
MOZ_ASSERT(lastEnd <= current->begin);
743
MOZ_ASSERT(current->begin < current->end);
744
745
// But first, deal with inter-field space.
746
if (lastEnd < current->begin) {
747
if (enclosingFields.length() > 0) {
748
// Space between fields, within an enclosing field, is part
749
// of that enclosing field, until the start of the current
750
// field or the end of the enclosing field, whichever is
751
// earlier.
752
const auto& enclosing = fields[enclosingFields.back()];
753
part->end = std::min(enclosing.end, current->begin);
754
part->type = enclosing.type;
755
popEnclosingFieldsEndingAt(part->end);
756
} else {
757
// If there's no enclosing field, the space is a literal.
758
part->end = current->begin;
759
part->type = &JSAtomState::literal;
760
}
761
762
return true;
763
}
764
765
// Otherwise, the part spans a prefix of the current field. Find
766
// the most-nested field containing that prefix.
767
const Field* next;
768
do {
769
current = &fields[index];
770
771
// If the current field is last, the part extends to its end.
772
if (++index == len) {
773
part->end = current->end;
774
part->type = current->type;
775
return true;
776
}
777
778
next = &fields[index];
779
MOZ_ASSERT(current->begin <= next->begin);
780
MOZ_ASSERT(current->begin < next->end);
781
782
// If the next field nests within the current field, push an
783
// enclosing field. (If there are no nested fields, don't
784
// bother pushing a field that'd be immediately popped.)
785
if (current->end > next->begin) {
786
if (!enclosingFields.append(index - 1)) {
787
return false;
788
}
789
}
790
791
// Do so until the next field begins after this one.
792
} while (current->begin == next->begin);
793
794
part->type = current->type;
795
796
if (current->end <= next->begin) {
797
// The next field begins after the current field ends. Therefore
798
// the current part ends at the end of the current field.
799
part->end = current->end;
800
popEnclosingFieldsEndingAt(part->end);
801
} else {
802
// The current field encloses the next one. The current part
803
// ends where the next field/part will start.
804
part->end = next->begin;
805
}
806
807
return true;
808
}
809
810
public:
811
PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit)
812
: fields(vec),
813
index(0),
814
lastEnd(0),
815
limit(limit),
816
enclosingFields(cx) {}
817
818
bool nextPart(bool* hasPart, Part* part) {
819
// There are no parts left if we've partitioned the entire string.
820
if (lastEnd == limit) {
821
MOZ_ASSERT(enclosingFields.length() == 0);
822
*hasPart = false;
823
return true;
824
}
825
826
if (!nextPartInternal(part)) {
827
return false;
828
}
829
830
*hasPart = true;
831
lastEnd = part->end;
832
return true;
833
}
834
};
835
836
// Finally, generate the result array.
837
size_t lastEndIndex = 0;
838
uint32_t partIndex = 0;
839
RootedObject singlePart(cx);
840
RootedValue propVal(cx);
841
842
RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
843
if (!partsArray) {
844
return nullptr;
845
}
846
847
PartGenerator gen(cx, fields_, overallResult->length());
848
do {
849
bool hasPart;
850
Part part;
851
if (!gen.nextPart(&hasPart, &part)) {
852
return nullptr;
853
}
854
855
if (!hasPart) {
856
break;
857
}
858
859
FieldType type = part.type;
860
size_t endIndex = part.end;
861
862
MOZ_ASSERT(lastEndIndex < endIndex);
863
864
singlePart = NewBuiltinClassInstance<PlainObject>(cx);
865
if (!singlePart) {
866
return nullptr;
867
}
868
869
propVal.setString(cx->names().*type);
870
if (!DefineDataProperty(cx, singlePart, cx->names().type, propVal)) {
871
return nullptr;
872
}
873
874
JSLinearString* partSubstr = NewDependentString(
875
cx, overallResult, lastEndIndex, endIndex - lastEndIndex);
876
if (!partSubstr) {
877
return nullptr;
878
}
879
880
propVal.setString(partSubstr);
881
if (!DefineDataProperty(cx, singlePart, cx->names().value, propVal)) {
882
return nullptr;
883
}
884
885
if (unitType != nullptr && type != &JSAtomState::literal) {
886
propVal.setString(cx->names().*unitType);
887
if (!DefineDataProperty(cx, singlePart, cx->names().unit, propVal)) {
888
return nullptr;
889
}
890
}
891
892
propVal.setObject(*singlePart);
893
if (!DefineDataElement(cx, partsArray, partIndex, propVal)) {
894
return nullptr;
895
}
896
897
lastEndIndex = endIndex;
898
partIndex++;
899
} while (true);
900
901
MOZ_ASSERT(lastEndIndex == overallResult->length(),
902
"result array must partition the entire string");
903
904
return partsArray;
905
}
906
907
static bool intl_FormatNumberToParts(JSContext* cx, UNumberFormatter* nf,
908
UFormattedNumber* formatted, double x,
909
MutableHandleValue result) {
910
RootedString overallResult(cx, PartitionNumberPattern(cx, nf, formatted, &x));
911
if (!overallResult) {
912
return false;
913
}
914
915
UErrorCode status = U_ZERO_ERROR;
916
UFieldPositionIterator* fpositer = ufieldpositer_open(&status);
917
if (U_FAILURE(status)) {
918
intl::ReportInternalError(cx);
919
return false;
920
}
921
922
MOZ_ASSERT(fpositer);
923
ScopedICUObject<UFieldPositionIterator, ufieldpositer_close> toClose(
924
fpositer);
925
926
unumf_resultGetAllFieldPositions(formatted, fpositer, &status);
927
if (U_FAILURE(status)) {
928
intl::ReportInternalError(cx);
929
return false;
930
}
931
932
// Vacuum up fields in the overall formatted string.
933
934
intl::NumberFormatFields fields(cx, x);
935
936
int32_t field, beginIndex, endIndex;
937
while ((field = ufieldpositer_next(fpositer, &beginIndex, &endIndex)) >= 0) {
938
if (!fields.append(field, beginIndex, endIndex)) {
939
return false;
940
}
941
}
942
943
ArrayObject* array = fields.toArray(cx, overallResult, nullptr);
944
if (!array) {
945
return false;
946
}
947
948
result.setObject(*array);
949
return true;
950
}
951
952
bool js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) {
953
CallArgs args = CallArgsFromVp(argc, vp);
954
MOZ_ASSERT(args.length() == 3);
955
MOZ_ASSERT(args[0].isObject());
956
MOZ_ASSERT(args[1].isNumber());
957
MOZ_ASSERT(args[2].isBoolean());
958
959
Rooted<NumberFormatObject*> numberFormat(
960
cx, &args[0].toObject().as<NumberFormatObject>());
961
962
// Obtain a cached UNumberFormatter object.
963
UNumberFormatter* nf = numberFormat->getNumberFormatter();
964
if (!nf) {
965
nf = NewUNumberFormatter(cx, numberFormat);
966
if (!nf) {
967
return false;
968
}
969
numberFormat->setNumberFormatter(nf);
970
}
971
972
// Obtain a cached UFormattedNumber object.
973
UFormattedNumber* formatted = numberFormat->getFormattedNumber();
974
if (!formatted) {
975
formatted = NewUFormattedNumber(cx);
976
if (!formatted) {
977
return false;
978
}
979
numberFormat->setFormattedNumber(formatted);
980
}
981
982
// Use the UNumberFormatter to actually format the number.
983
if (args[2].toBoolean()) {
984
return intl_FormatNumberToParts(cx, nf, formatted, args[1].toNumber(),
985
args.rval());
986
}
987
988
return intl_FormatNumber(cx, nf, formatted, args[1].toNumber(), args.rval());
989
}