PluralRules.cpp (16086B)
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 #include "mozilla/intl/PluralRules.h" 14 15 #include "builtin/Array.h" 16 #include "builtin/intl/CommonFunctions.h" 17 #include "builtin/intl/LocaleNegotiation.h" 18 #include "gc/GCContext.h" 19 #include "js/PropertySpec.h" 20 #include "vm/GlobalObject.h" 21 #include "vm/JSContext.h" 22 #include "vm/PlainObject.h" // js::PlainObject 23 #include "vm/StringType.h" 24 25 #include "vm/JSObject-inl.h" 26 #include "vm/NativeObject-inl.h" 27 28 using namespace js; 29 using namespace js::intl; 30 31 using mozilla::AssertedCast; 32 33 const JSClassOps PluralRulesObject::classOps_ = { 34 nullptr, // addProperty 35 nullptr, // delProperty 36 nullptr, // enumerate 37 nullptr, // newEnumerate 38 nullptr, // resolve 39 nullptr, // mayResolve 40 PluralRulesObject::finalize, // finalize 41 nullptr, // call 42 nullptr, // construct 43 nullptr, // trace 44 }; 45 46 const JSClass PluralRulesObject::class_ = { 47 "Intl.PluralRules", 48 JSCLASS_HAS_RESERVED_SLOTS(PluralRulesObject::SLOT_COUNT) | 49 JSCLASS_HAS_CACHED_PROTO(JSProto_PluralRules) | 50 JSCLASS_FOREGROUND_FINALIZE, 51 &PluralRulesObject::classOps_, 52 &PluralRulesObject::classSpec_, 53 }; 54 55 const JSClass& PluralRulesObject::protoClass_ = PlainObject::class_; 56 57 static bool pluralRules_supportedLocalesOf(JSContext* cx, unsigned argc, 58 Value* vp); 59 60 static bool pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) { 61 CallArgs args = CallArgsFromVp(argc, vp); 62 args.rval().setString(cx->names().PluralRules); 63 return true; 64 } 65 66 static const JSFunctionSpec pluralRules_static_methods[] = { 67 JS_FN("supportedLocalesOf", pluralRules_supportedLocalesOf, 1, 0), 68 JS_FS_END, 69 }; 70 71 static const JSFunctionSpec pluralRules_methods[] = { 72 JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0, 73 0), 74 JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0), 75 JS_SELF_HOSTED_FN("selectRange", "Intl_PluralRules_selectRange", 2, 0), 76 JS_FN("toSource", pluralRules_toSource, 0, 0), 77 JS_FS_END, 78 }; 79 80 static const JSPropertySpec pluralRules_properties[] = { 81 JS_STRING_SYM_PS(toStringTag, "Intl.PluralRules", JSPROP_READONLY), 82 JS_PS_END, 83 }; 84 85 static bool PluralRules(JSContext* cx, unsigned argc, Value* vp); 86 87 const ClassSpec PluralRulesObject::classSpec_ = { 88 GenericCreateConstructor<PluralRules, 0, gc::AllocKind::FUNCTION>, 89 GenericCreatePrototype<PluralRulesObject>, 90 pluralRules_static_methods, 91 nullptr, 92 pluralRules_methods, 93 pluralRules_properties, 94 nullptr, 95 ClassSpec::DontDefineConstructor, 96 }; 97 98 /** 99 * 16.1.1 Intl.PluralRules ( [ locales [ , options ] ] ) 100 * 101 * ES2024 Intl draft rev 74ca7099f103d143431b2ea422ae640c6f43e3e6 102 */ 103 static bool PluralRules(JSContext* cx, unsigned argc, Value* vp) { 104 CallArgs args = CallArgsFromVp(argc, vp); 105 106 // Step 1. 107 if (!ThrowIfNotConstructing(cx, args, "Intl.PluralRules")) { 108 return false; 109 } 110 111 // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). 112 RootedObject proto(cx); 113 if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PluralRules, 114 &proto)) { 115 return false; 116 } 117 118 Rooted<PluralRulesObject*> pluralRules(cx); 119 pluralRules = NewObjectWithClassProto<PluralRulesObject>(cx, proto); 120 if (!pluralRules) { 121 return false; 122 } 123 124 HandleValue locales = args.get(0); 125 HandleValue options = args.get(1); 126 127 // Step 3. 128 if (!intl::InitializeObject(cx, pluralRules, 129 cx->names().InitializePluralRules, locales, 130 options)) { 131 return false; 132 } 133 134 args.rval().setObject(*pluralRules); 135 return true; 136 } 137 138 void js::PluralRulesObject::finalize(JS::GCContext* gcx, JSObject* obj) { 139 MOZ_ASSERT(gcx->onMainThread()); 140 141 auto* pluralRules = &obj->as<PluralRulesObject>(); 142 if (mozilla::intl::PluralRules* pr = pluralRules->getPluralRules()) { 143 intl::RemoveICUCellMemory( 144 gcx, obj, PluralRulesObject::UPluralRulesEstimatedMemoryUse); 145 delete pr; 146 } 147 } 148 149 static JSString* KeywordToString(mozilla::intl::PluralRules::Keyword keyword, 150 JSContext* cx) { 151 using Keyword = mozilla::intl::PluralRules::Keyword; 152 switch (keyword) { 153 case Keyword::Zero: { 154 return cx->names().zero; 155 } 156 case Keyword::One: { 157 return cx->names().one; 158 } 159 case Keyword::Two: { 160 return cx->names().two; 161 } 162 case Keyword::Few: { 163 return cx->names().few; 164 } 165 case Keyword::Many: { 166 return cx->names().many; 167 } 168 case Keyword::Other: { 169 return cx->names().other; 170 } 171 } 172 MOZ_CRASH("Unexpected PluralRules keyword"); 173 } 174 175 /** 176 * Returns a new intl::PluralRules with the locale and type options of the given 177 * PluralRules. 178 */ 179 static mozilla::intl::PluralRules* NewPluralRules( 180 JSContext* cx, Handle<PluralRulesObject*> pluralRules) { 181 RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); 182 if (!internals) { 183 return nullptr; 184 } 185 186 RootedValue value(cx); 187 188 if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { 189 return nullptr; 190 } 191 UniqueChars locale = intl::EncodeLocale(cx, value.toString()); 192 if (!locale) { 193 return nullptr; 194 } 195 196 using PluralRules = mozilla::intl::PluralRules; 197 mozilla::intl::PluralRulesOptions options; 198 199 if (!GetProperty(cx, internals, internals, cx->names().type, &value)) { 200 return nullptr; 201 } 202 203 { 204 JSLinearString* type = value.toString()->ensureLinear(cx); 205 if (!type) { 206 return nullptr; 207 } 208 209 if (StringEqualsLiteral(type, "ordinal")) { 210 options.mPluralType = PluralRules::Type::Ordinal; 211 } else { 212 MOZ_ASSERT(StringEqualsLiteral(type, "cardinal")); 213 options.mPluralType = PluralRules::Type::Cardinal; 214 } 215 } 216 217 bool hasMinimumSignificantDigits; 218 if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits, 219 &hasMinimumSignificantDigits)) { 220 return nullptr; 221 } 222 223 if (hasMinimumSignificantDigits) { 224 if (!GetProperty(cx, internals, internals, 225 cx->names().minimumSignificantDigits, &value)) { 226 return nullptr; 227 } 228 uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32()); 229 230 if (!GetProperty(cx, internals, internals, 231 cx->names().maximumSignificantDigits, &value)) { 232 return nullptr; 233 } 234 uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32()); 235 236 options.mSignificantDigits = mozilla::Some( 237 std::make_pair(minimumSignificantDigits, maximumSignificantDigits)); 238 } 239 240 bool hasMinimumFractionDigits; 241 if (!HasProperty(cx, internals, cx->names().minimumFractionDigits, 242 &hasMinimumFractionDigits)) { 243 return nullptr; 244 } 245 246 if (hasMinimumFractionDigits) { 247 if (!GetProperty(cx, internals, internals, 248 cx->names().minimumFractionDigits, &value)) { 249 return nullptr; 250 } 251 uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32()); 252 253 if (!GetProperty(cx, internals, internals, 254 cx->names().maximumFractionDigits, &value)) { 255 return nullptr; 256 } 257 uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32()); 258 259 options.mFractionDigits = mozilla::Some( 260 std::make_pair(minimumFractionDigits, maximumFractionDigits)); 261 } 262 263 if (!GetProperty(cx, internals, internals, cx->names().roundingPriority, 264 &value)) { 265 return nullptr; 266 } 267 268 { 269 JSLinearString* roundingPriority = value.toString()->ensureLinear(cx); 270 if (!roundingPriority) { 271 return nullptr; 272 } 273 274 using RoundingPriority = 275 mozilla::intl::PluralRulesOptions::RoundingPriority; 276 277 RoundingPriority priority; 278 if (StringEqualsLiteral(roundingPriority, "auto")) { 279 priority = RoundingPriority::Auto; 280 } else if (StringEqualsLiteral(roundingPriority, "morePrecision")) { 281 priority = RoundingPriority::MorePrecision; 282 } else { 283 MOZ_ASSERT(StringEqualsLiteral(roundingPriority, "lessPrecision")); 284 priority = RoundingPriority::LessPrecision; 285 } 286 287 options.mRoundingPriority = priority; 288 } 289 290 if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, 291 &value)) { 292 return nullptr; 293 } 294 options.mMinIntegerDigits = 295 mozilla::Some(AssertedCast<uint32_t>(value.toInt32())); 296 297 if (!GetProperty(cx, internals, internals, cx->names().roundingIncrement, 298 &value)) { 299 return nullptr; 300 } 301 options.mRoundingIncrement = AssertedCast<uint32_t>(value.toInt32()); 302 303 if (!GetProperty(cx, internals, internals, cx->names().roundingMode, 304 &value)) { 305 return nullptr; 306 } 307 308 { 309 JSLinearString* roundingMode = value.toString()->ensureLinear(cx); 310 if (!roundingMode) { 311 return nullptr; 312 } 313 314 using RoundingMode = mozilla::intl::PluralRulesOptions::RoundingMode; 315 316 RoundingMode rounding; 317 if (StringEqualsLiteral(roundingMode, "halfExpand")) { 318 // "halfExpand" is the default mode, so we handle it first. 319 rounding = RoundingMode::HalfExpand; 320 } else if (StringEqualsLiteral(roundingMode, "ceil")) { 321 rounding = RoundingMode::Ceil; 322 } else if (StringEqualsLiteral(roundingMode, "floor")) { 323 rounding = RoundingMode::Floor; 324 } else if (StringEqualsLiteral(roundingMode, "expand")) { 325 rounding = RoundingMode::Expand; 326 } else if (StringEqualsLiteral(roundingMode, "trunc")) { 327 rounding = RoundingMode::Trunc; 328 } else if (StringEqualsLiteral(roundingMode, "halfCeil")) { 329 rounding = RoundingMode::HalfCeil; 330 } else if (StringEqualsLiteral(roundingMode, "halfFloor")) { 331 rounding = RoundingMode::HalfFloor; 332 } else if (StringEqualsLiteral(roundingMode, "halfTrunc")) { 333 rounding = RoundingMode::HalfTrunc; 334 } else { 335 MOZ_ASSERT(StringEqualsLiteral(roundingMode, "halfEven")); 336 rounding = RoundingMode::HalfEven; 337 } 338 339 options.mRoundingMode = rounding; 340 } 341 342 if (!GetProperty(cx, internals, internals, cx->names().trailingZeroDisplay, 343 &value)) { 344 return nullptr; 345 } 346 347 { 348 JSLinearString* trailingZeroDisplay = value.toString()->ensureLinear(cx); 349 if (!trailingZeroDisplay) { 350 return nullptr; 351 } 352 353 if (StringEqualsLiteral(trailingZeroDisplay, "auto")) { 354 options.mStripTrailingZero = false; 355 } else { 356 MOZ_ASSERT(StringEqualsLiteral(trailingZeroDisplay, "stripIfInteger")); 357 options.mStripTrailingZero = true; 358 } 359 } 360 361 auto result = PluralRules::TryCreate(locale.get(), options); 362 if (result.isErr()) { 363 intl::ReportInternalError(cx, result.unwrapErr()); 364 return nullptr; 365 } 366 367 return result.unwrap().release(); 368 } 369 370 static mozilla::intl::PluralRules* GetOrCreatePluralRules( 371 JSContext* cx, Handle<PluralRulesObject*> pluralRules) { 372 // Obtain a cached PluralRules object. 373 mozilla::intl::PluralRules* pr = pluralRules->getPluralRules(); 374 if (pr) { 375 return pr; 376 } 377 378 pr = NewPluralRules(cx, pluralRules); 379 if (!pr) { 380 return nullptr; 381 } 382 pluralRules->setPluralRules(pr); 383 384 intl::AddICUCellMemory(pluralRules, 385 PluralRulesObject::UPluralRulesEstimatedMemoryUse); 386 return pr; 387 } 388 389 /** 390 * 16.5.3 ResolvePlural ( pluralRules, n ) 391 * 16.5.2 PluralRuleSelect ( locale, type, n, operands ) 392 * 393 * ES2024 Intl draft rev 74ca7099f103d143431b2ea422ae640c6f43e3e6 394 */ 395 bool js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) { 396 CallArgs args = CallArgsFromVp(argc, vp); 397 MOZ_ASSERT(args.length() == 2); 398 399 // Steps 1-2. 400 Rooted<PluralRulesObject*> pluralRules( 401 cx, &args[0].toObject().as<PluralRulesObject>()); 402 403 // Step 3. 404 double x = args[1].toNumber(); 405 406 // Steps 4-11. 407 using PluralRules = mozilla::intl::PluralRules; 408 PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules); 409 if (!pr) { 410 return false; 411 } 412 413 auto keywordResult = pr->Select(x); 414 if (keywordResult.isErr()) { 415 intl::ReportInternalError(cx, keywordResult.unwrapErr()); 416 return false; 417 } 418 419 JSString* str = KeywordToString(keywordResult.unwrap(), cx); 420 MOZ_ASSERT(str); 421 422 args.rval().setString(str); 423 return true; 424 } 425 426 /** 427 * 16.5.5 ResolvePluralRange ( pluralRules, x, y ) 428 * 16.5.4 PluralRuleSelectRange ( locale, type, xp, yp ) 429 * 430 * ES2024 Intl draft rev 74ca7099f103d143431b2ea422ae640c6f43e3e6 431 */ 432 bool js::intl_SelectPluralRuleRange(JSContext* cx, unsigned argc, Value* vp) { 433 CallArgs args = CallArgsFromVp(argc, vp); 434 MOZ_ASSERT(args.length() == 3); 435 436 // Steps 1-2. 437 Rooted<PluralRulesObject*> pluralRules( 438 cx, &args[0].toObject().as<PluralRulesObject>()); 439 440 // Steps 3-4. 441 double x = args[1].toNumber(); 442 double y = args[2].toNumber(); 443 444 // Step 5. 445 if (std::isnan(x)) { 446 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 447 JSMSG_NAN_NUMBER_RANGE, "start", "PluralRules", 448 "selectRange"); 449 return false; 450 } 451 if (std::isnan(y)) { 452 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 453 JSMSG_NAN_NUMBER_RANGE, "end", "PluralRules", 454 "selectRange"); 455 return false; 456 } 457 458 using PluralRules = mozilla::intl::PluralRules; 459 PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules); 460 if (!pr) { 461 return false; 462 } 463 464 // Steps 6-11. 465 auto keywordResult = pr->SelectRange(x, y); 466 if (keywordResult.isErr()) { 467 intl::ReportInternalError(cx, keywordResult.unwrapErr()); 468 return false; 469 } 470 471 JSString* str = KeywordToString(keywordResult.unwrap(), cx); 472 MOZ_ASSERT(str); 473 474 args.rval().setString(str); 475 return true; 476 } 477 478 bool js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) { 479 CallArgs args = CallArgsFromVp(argc, vp); 480 MOZ_ASSERT(args.length() == 1); 481 482 Rooted<PluralRulesObject*> pluralRules( 483 cx, &args[0].toObject().as<PluralRulesObject>()); 484 485 using PluralRules = mozilla::intl::PluralRules; 486 PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules); 487 if (!pr) { 488 return false; 489 } 490 491 auto categoriesResult = pr->Categories(); 492 if (categoriesResult.isErr()) { 493 intl::ReportInternalError(cx, categoriesResult.unwrapErr()); 494 return false; 495 } 496 auto categories = categoriesResult.unwrap(); 497 498 ArrayObject* res = NewDenseFullyAllocatedArray(cx, categories.size()); 499 if (!res) { 500 return false; 501 } 502 res->setDenseInitializedLength(categories.size()); 503 504 size_t index = 0; 505 for (auto keyword : { 506 PluralRules::Keyword::Zero, 507 PluralRules::Keyword::One, 508 PluralRules::Keyword::Two, 509 PluralRules::Keyword::Few, 510 PluralRules::Keyword::Many, 511 PluralRules::Keyword::Other, 512 }) { 513 if (categories.contains(keyword)) { 514 JSString* str = KeywordToString(keyword, cx); 515 MOZ_ASSERT(str); 516 517 res->initDenseElement(index++, StringValue(str)); 518 } 519 } 520 MOZ_ASSERT(index == categories.size()); 521 522 args.rval().setObject(*res); 523 return true; 524 } 525 526 /** 527 * Intl.PluralRules.supportedLocalesOf ( locales [ , options ] ) 528 */ 529 static bool pluralRules_supportedLocalesOf(JSContext* cx, unsigned argc, 530 Value* vp) { 531 CallArgs args = CallArgsFromVp(argc, vp); 532 533 // Steps 1-3. 534 auto* array = SupportedLocalesOf(cx, AvailableLocaleKind::PluralRules, 535 args.get(0), args.get(1)); 536 if (!array) { 537 return false; 538 } 539 args.rval().setObject(*array); 540 return true; 541 }