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 /* Implementation of the Intl.PluralRules proposal. */
8
9 #include "builtin/intl/PluralRules.h"
10
11 #include "mozilla/Assertions.h"
12 #include "mozilla/Casting.h"
13
14 #include "builtin/Array.h"
15 #include "builtin/intl/CommonFunctions.h"
16 #include "builtin/intl/NumberFormat.h"
17 #include "builtin/intl/ScopedICUObject.h"
18 #include "gc/FreeOp.h"
19 #include "js/CharacterEncoding.h"
20 #include "js/PropertySpec.h"
21 #include "unicode/uenum.h"
22 #include "unicode/uloc.h"
23 #include "unicode/unumberformatter.h"
24 #include "unicode/upluralrules.h"
25 #include "unicode/utypes.h"
26 #include "vm/GlobalObject.h"
27 #include "vm/JSContext.h"
28 #include "vm/PlainObject.h" // js::PlainObject
29 #include "vm/StringType.h"
30
31 #include "vm/JSObject-inl.h"
32 #include "vm/NativeObject-inl.h"
33
34 using namespace js;
35
36 using mozilla::AssertedCast;
37
38 using js::intl::CallICU;
39 using js::intl::IcuLocale;
40
41 const JSClassOps PluralRulesObject::classOps_ = {
42 nullptr, // addProperty
43 nullptr, // delProperty
44 nullptr, // enumerate
45 nullptr, // newEnumerate
46 nullptr, // resolve
47 nullptr, // mayResolve
48 PluralRulesObject::finalize, // finalize
49 nullptr, // call
50 nullptr, // hasInstance
51 nullptr, // construct
52 nullptr, // trace
53 };
54
55 const JSClass PluralRulesObject::class_ = {
56 js_Object_str,
57 JSCLASS_HAS_RESERVED_SLOTS(PluralRulesObject::SLOT_COUNT) |
58 JSCLASS_HAS_CACHED_PROTO(JSProto_PluralRules) |
59 JSCLASS_FOREGROUND_FINALIZE,
60 &PluralRulesObject::classOps_, &PluralRulesObject::classSpec_};
61
62 const JSClass& PluralRulesObject::protoClass_ = PlainObject::class_;
63
pluralRules_toSource(JSContext * cx,unsigned argc,Value * vp)64 static bool pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) {
65 CallArgs args = CallArgsFromVp(argc, vp);
66 args.rval().setString(cx->names().PluralRules);
67 return true;
68 }
69
70 static const JSFunctionSpec pluralRules_static_methods[] = {
71 JS_SELF_HOSTED_FN("supportedLocalesOf",
72 "Intl_PluralRules_supportedLocalesOf", 1, 0),
73 JS_FS_END};
74
75 static const JSFunctionSpec pluralRules_methods[] = {
76 JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0,
77 0),
78 JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0),
79 JS_FN(js_toSource_str, pluralRules_toSource, 0, 0), JS_FS_END};
80
81 static bool PluralRules(JSContext* cx, unsigned argc, Value* vp);
82
83 const ClassSpec PluralRulesObject::classSpec_ = {
84 GenericCreateConstructor<PluralRules, 0, gc::AllocKind::FUNCTION>,
85 GenericCreatePrototype<PluralRulesObject>,
86 pluralRules_static_methods,
87 nullptr,
88 pluralRules_methods,
89 nullptr,
90 nullptr,
91 ClassSpec::DontDefineConstructor};
92
93 /**
94 * PluralRules constructor.
95 * Spec: ECMAScript 402 API, PluralRules, 13.2.1
96 */
PluralRules(JSContext * cx,unsigned argc,Value * vp)97 static bool PluralRules(JSContext* cx, unsigned argc, Value* vp) {
98 CallArgs args = CallArgsFromVp(argc, vp);
99
100 // Step 1.
101 if (!ThrowIfNotConstructing(cx, args, "Intl.PluralRules")) {
102 return false;
103 }
104
105 // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
106 RootedObject proto(cx);
107 if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PluralRules,
108 &proto)) {
109 return false;
110 }
111
112 Rooted<PluralRulesObject*> pluralRules(cx);
113 pluralRules = NewObjectWithClassProto<PluralRulesObject>(cx, proto);
114 if (!pluralRules) {
115 return false;
116 }
117
118 HandleValue locales = args.get(0);
119 HandleValue options = args.get(1);
120
121 // Step 3.
122 if (!intl::InitializeObject(cx, pluralRules,
123 cx->names().InitializePluralRules, locales,
124 options)) {
125 return false;
126 }
127
128 args.rval().setObject(*pluralRules);
129 return true;
130 }
131
finalize(JSFreeOp * fop,JSObject * obj)132 void js::PluralRulesObject::finalize(JSFreeOp* fop, JSObject* obj) {
133 MOZ_ASSERT(fop->onMainThread());
134
135 auto* pluralRules = &obj->as<PluralRulesObject>();
136 UPluralRules* pr = pluralRules->getPluralRules();
137 UNumberFormatter* nf = pluralRules->getNumberFormatter();
138 UFormattedNumber* formatted = pluralRules->getFormattedNumber();
139
140 if (pr) {
141 intl::RemoveICUCellMemory(
142 fop, obj, PluralRulesObject::UPluralRulesEstimatedMemoryUse);
143 }
144 if (nf) {
145 intl::RemoveICUCellMemory(
146 fop, obj, PluralRulesObject::UNumberFormatterEstimatedMemoryUse);
147
148 // UFormattedNumber memory tracked as part of UNumberFormatter.
149 }
150
151 if (pr) {
152 uplrules_close(pr);
153 }
154 if (nf) {
155 unumf_close(nf);
156 }
157 if (formatted) {
158 unumf_closeResult(formatted);
159 }
160 }
161
162 /**
163 * This creates a new UNumberFormatter with calculated digit formatting
164 * properties for PluralRules.
165 *
166 * This is similar to NewUNumberFormatter but doesn't allow for currency or
167 * percent types.
168 */
NewUNumberFormatterForPluralRules(JSContext * cx,Handle<PluralRulesObject * > pluralRules)169 static UNumberFormatter* NewUNumberFormatterForPluralRules(
170 JSContext* cx, Handle<PluralRulesObject*> pluralRules) {
171 RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules));
172 if (!internals) {
173 return nullptr;
174 }
175
176 RootedValue value(cx);
177
178 if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
179 return nullptr;
180 }
181 UniqueChars locale = intl::EncodeLocale(cx, value.toString());
182 if (!locale) {
183 return nullptr;
184 }
185
186 intl::NumberFormatterSkeleton skeleton(cx);
187
188 bool hasMinimumSignificantDigits;
189 if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits,
190 &hasMinimumSignificantDigits)) {
191 return nullptr;
192 }
193
194 if (hasMinimumSignificantDigits) {
195 if (!GetProperty(cx, internals, internals,
196 cx->names().minimumSignificantDigits, &value)) {
197 return nullptr;
198 }
199 uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
200
201 if (!GetProperty(cx, internals, internals,
202 cx->names().maximumSignificantDigits, &value)) {
203 return nullptr;
204 }
205 uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
206
207 if (!skeleton.significantDigits(minimumSignificantDigits,
208 maximumSignificantDigits)) {
209 return nullptr;
210 }
211 } else {
212 if (!GetProperty(cx, internals, internals,
213 cx->names().minimumFractionDigits, &value)) {
214 return nullptr;
215 }
216 uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
217
218 if (!GetProperty(cx, internals, internals,
219 cx->names().maximumFractionDigits, &value)) {
220 return nullptr;
221 }
222 uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
223
224 if (!skeleton.fractionDigits(minimumFractionDigits,
225 maximumFractionDigits)) {
226 return nullptr;
227 }
228 }
229
230 if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
231 &value)) {
232 return nullptr;
233 }
234 uint32_t minimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32());
235
236 if (!skeleton.integerWidth(minimumIntegerDigits)) {
237 return nullptr;
238 }
239
240 if (!skeleton.roundingModeHalfUp()) {
241 return nullptr;
242 }
243
244 return skeleton.toFormatter(cx, locale.get());
245 }
246
NewUFormattedNumberForPluralRules(JSContext * cx)247 static UFormattedNumber* NewUFormattedNumberForPluralRules(JSContext* cx) {
248 UErrorCode status = U_ZERO_ERROR;
249 UFormattedNumber* formatted = unumf_openResult(&status);
250 if (U_FAILURE(status)) {
251 intl::ReportInternalError(cx);
252 return nullptr;
253 }
254 return formatted;
255 }
256
257 /**
258 * Returns a new UPluralRules with the locale and type options of the given
259 * PluralRules.
260 */
NewUPluralRules(JSContext * cx,Handle<PluralRulesObject * > pluralRules)261 static UPluralRules* NewUPluralRules(JSContext* cx,
262 Handle<PluralRulesObject*> pluralRules) {
263 RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules));
264 if (!internals) {
265 return nullptr;
266 }
267
268 RootedValue value(cx);
269
270 if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
271 return nullptr;
272 }
273 UniqueChars locale = intl::EncodeLocale(cx, value.toString());
274 if (!locale) {
275 return nullptr;
276 }
277
278 if (!GetProperty(cx, internals, internals, cx->names().type, &value)) {
279 return nullptr;
280 }
281
282 UPluralType category;
283 {
284 JSLinearString* type = value.toString()->ensureLinear(cx);
285 if (!type) {
286 return nullptr;
287 }
288
289 if (StringEqualsLiteral(type, "cardinal")) {
290 category = UPLURAL_TYPE_CARDINAL;
291 } else {
292 MOZ_ASSERT(StringEqualsLiteral(type, "ordinal"));
293 category = UPLURAL_TYPE_ORDINAL;
294 }
295 }
296
297 UErrorCode status = U_ZERO_ERROR;
298 UPluralRules* pr =
299 uplrules_openForType(IcuLocale(locale.get()), category, &status);
300 if (U_FAILURE(status)) {
301 intl::ReportInternalError(cx);
302 return nullptr;
303 }
304 return pr;
305 }
306
intl_SelectPluralRule(JSContext * cx,unsigned argc,Value * vp)307 bool js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) {
308 CallArgs args = CallArgsFromVp(argc, vp);
309 MOZ_ASSERT(args.length() == 2);
310
311 Rooted<PluralRulesObject*> pluralRules(
312 cx, &args[0].toObject().as<PluralRulesObject>());
313
314 double x = args[1].toNumber();
315
316 // Obtain a cached UPluralRules object.
317 UPluralRules* pr = pluralRules->getPluralRules();
318 if (!pr) {
319 pr = NewUPluralRules(cx, pluralRules);
320 if (!pr) {
321 return false;
322 }
323 pluralRules->setPluralRules(pr);
324
325 intl::AddICUCellMemory(pluralRules,
326 PluralRulesObject::UPluralRulesEstimatedMemoryUse);
327 }
328
329 // Obtain a cached UNumberFormat object.
330 UNumberFormatter* nf = pluralRules->getNumberFormatter();
331 if (!nf) {
332 nf = NewUNumberFormatterForPluralRules(cx, pluralRules);
333 if (!nf) {
334 return false;
335 }
336 pluralRules->setNumberFormatter(nf);
337
338 intl::AddICUCellMemory(
339 pluralRules, PluralRulesObject::UNumberFormatterEstimatedMemoryUse);
340 }
341
342 // Obtain a cached UFormattedNumber object.
343 UFormattedNumber* formatted = pluralRules->getFormattedNumber();
344 if (!formatted) {
345 formatted = NewUFormattedNumberForPluralRules(cx);
346 if (!formatted) {
347 return false;
348 }
349 pluralRules->setFormattedNumber(formatted);
350
351 // UFormattedNumber memory tracked as part of UNumberFormatter.
352 }
353
354 UErrorCode status = U_ZERO_ERROR;
355 unumf_formatDouble(nf, x, formatted, &status);
356 if (U_FAILURE(status)) {
357 intl::ReportInternalError(cx);
358 return false;
359 }
360
361 JSString* str = CallICU(
362 cx, [pr, formatted](UChar* chars, int32_t size, UErrorCode* status) {
363 return uplrules_selectFormatted(pr, formatted, chars, size, status);
364 });
365 if (!str) {
366 return false;
367 }
368
369 args.rval().setString(str);
370 return true;
371 }
372
intl_GetPluralCategories(JSContext * cx,unsigned argc,Value * vp)373 bool js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) {
374 CallArgs args = CallArgsFromVp(argc, vp);
375 MOZ_ASSERT(args.length() == 1);
376
377 Rooted<PluralRulesObject*> pluralRules(
378 cx, &args[0].toObject().as<PluralRulesObject>());
379
380 // Obtain a cached UPluralRules object.
381 UPluralRules* pr = pluralRules->getPluralRules();
382 if (!pr) {
383 pr = NewUPluralRules(cx, pluralRules);
384 if (!pr) {
385 return false;
386 }
387 pluralRules->setPluralRules(pr);
388
389 intl::AddICUCellMemory(pluralRules,
390 PluralRulesObject::UPluralRulesEstimatedMemoryUse);
391 }
392
393 UErrorCode status = U_ZERO_ERROR;
394 UEnumeration* ue = uplrules_getKeywords(pr, &status);
395 if (U_FAILURE(status)) {
396 intl::ReportInternalError(cx);
397 return false;
398 }
399 ScopedICUObject<UEnumeration, uenum_close> closeEnum(ue);
400
401 RootedObject res(cx, NewDenseEmptyArray(cx));
402 if (!res) {
403 return false;
404 }
405
406 do {
407 int32_t catSize;
408 const char* cat = uenum_next(ue, &catSize, &status);
409 if (U_FAILURE(status)) {
410 intl::ReportInternalError(cx);
411 return false;
412 }
413
414 if (!cat) {
415 break;
416 }
417
418 MOZ_ASSERT(catSize >= 0);
419 JSString* str = NewStringCopyN<CanGC>(cx, cat, catSize);
420 if (!str) {
421 return false;
422 }
423
424 if (!NewbornArrayPush(cx, res, StringValue(str))) {
425 return false;
426 }
427 } while (true);
428
429 args.rval().setObject(*res);
430 return true;
431 }
432