RelativeTimeFormat.cpp (12994B)
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.RelativeTimeFormat proposal. */ 8 9 #include "builtin/intl/RelativeTimeFormat.h" 10 11 #include "mozilla/Assertions.h" 12 #include "mozilla/intl/RelativeTimeFormat.h" 13 14 #include "builtin/intl/CommonFunctions.h" 15 #include "builtin/intl/FormatBuffer.h" 16 #include "builtin/intl/LanguageTag.h" 17 #include "builtin/intl/LocaleNegotiation.h" 18 #include "gc/GCContext.h" 19 #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* 20 #include "js/Printer.h" 21 #include "js/PropertySpec.h" 22 #include "vm/GlobalObject.h" 23 #include "vm/JSContext.h" 24 #include "vm/PlainObject.h" // js::PlainObject 25 #include "vm/StringType.h" 26 27 #include "vm/NativeObject-inl.h" 28 29 using namespace js; 30 using namespace js::intl; 31 32 /**************** RelativeTimeFormat *****************/ 33 34 const JSClassOps RelativeTimeFormatObject::classOps_ = { 35 nullptr, // addProperty 36 nullptr, // delProperty 37 nullptr, // enumerate 38 nullptr, // newEnumerate 39 nullptr, // resolve 40 nullptr, // mayResolve 41 RelativeTimeFormatObject::finalize, // finalize 42 nullptr, // call 43 nullptr, // construct 44 nullptr, // trace 45 }; 46 47 const JSClass RelativeTimeFormatObject::class_ = { 48 "Intl.RelativeTimeFormat", 49 JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) | 50 JSCLASS_HAS_CACHED_PROTO(JSProto_RelativeTimeFormat) | 51 JSCLASS_FOREGROUND_FINALIZE, 52 &RelativeTimeFormatObject::classOps_, 53 &RelativeTimeFormatObject::classSpec_, 54 }; 55 56 const JSClass& RelativeTimeFormatObject::protoClass_ = PlainObject::class_; 57 58 static bool relativeTimeFormat_supportedLocalesOf(JSContext* cx, unsigned argc, 59 Value* vp); 60 61 static bool relativeTimeFormat_toSource(JSContext* cx, unsigned argc, 62 Value* vp) { 63 CallArgs args = CallArgsFromVp(argc, vp); 64 args.rval().setString(cx->names().RelativeTimeFormat); 65 return true; 66 } 67 68 static const JSFunctionSpec relativeTimeFormat_static_methods[] = { 69 JS_FN("supportedLocalesOf", relativeTimeFormat_supportedLocalesOf, 1, 0), 70 JS_FS_END, 71 }; 72 73 static const JSFunctionSpec relativeTimeFormat_methods[] = { 74 JS_SELF_HOSTED_FN("resolvedOptions", 75 "Intl_RelativeTimeFormat_resolvedOptions", 0, 0), 76 JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0), 77 JS_SELF_HOSTED_FN("formatToParts", "Intl_RelativeTimeFormat_formatToParts", 78 2, 0), 79 JS_FN("toSource", relativeTimeFormat_toSource, 0, 0), 80 JS_FS_END, 81 }; 82 83 static const JSPropertySpec relativeTimeFormat_properties[] = { 84 JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), 85 JS_PS_END, 86 }; 87 88 static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp); 89 90 const ClassSpec RelativeTimeFormatObject::classSpec_ = { 91 GenericCreateConstructor<RelativeTimeFormat, 0, gc::AllocKind::FUNCTION>, 92 GenericCreatePrototype<RelativeTimeFormatObject>, 93 relativeTimeFormat_static_methods, 94 nullptr, 95 relativeTimeFormat_methods, 96 relativeTimeFormat_properties, 97 nullptr, 98 ClassSpec::DontDefineConstructor, 99 }; 100 101 /** 102 * RelativeTimeFormat constructor. 103 * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1 104 */ 105 static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) { 106 CallArgs args = CallArgsFromVp(argc, vp); 107 108 // Step 1. 109 if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) { 110 return false; 111 } 112 113 // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). 114 RootedObject proto(cx); 115 if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_RelativeTimeFormat, 116 &proto)) { 117 return false; 118 } 119 120 Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx); 121 relativeTimeFormat = 122 NewObjectWithClassProto<RelativeTimeFormatObject>(cx, proto); 123 if (!relativeTimeFormat) { 124 return false; 125 } 126 127 HandleValue locales = args.get(0); 128 HandleValue options = args.get(1); 129 130 // Step 3. 131 if (!intl::InitializeObject(cx, relativeTimeFormat, 132 cx->names().InitializeRelativeTimeFormat, locales, 133 options)) { 134 return false; 135 } 136 137 args.rval().setObject(*relativeTimeFormat); 138 return true; 139 } 140 141 void js::RelativeTimeFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) { 142 MOZ_ASSERT(gcx->onMainThread()); 143 144 if (mozilla::intl::RelativeTimeFormat* rtf = 145 obj->as<RelativeTimeFormatObject>().getRelativeTimeFormatter()) { 146 intl::RemoveICUCellMemory(gcx, obj, 147 RelativeTimeFormatObject::EstimatedMemoryUse); 148 149 // This was allocated using `new` in mozilla::intl::RelativeTimeFormat, 150 // so we delete here. 151 delete rtf; 152 } 153 } 154 155 /** 156 * Returns a new URelativeDateTimeFormatter with the locale and options of the 157 * given RelativeTimeFormatObject. 158 */ 159 static mozilla::intl::RelativeTimeFormat* NewRelativeTimeFormatter( 160 JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) { 161 RootedValue value(cx); 162 RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat)); 163 if (!internals) { 164 return nullptr; 165 } 166 167 // ICU expects numberingSystem as a Unicode locale extensions on locale. 168 169 JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); 170 171 if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, 172 &value)) { 173 return nullptr; 174 } 175 176 { 177 JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); 178 if (!numberingSystem) { 179 return nullptr; 180 } 181 182 if (!keywords.emplaceBack("nu", numberingSystem)) { 183 return nullptr; 184 } 185 } 186 187 UniqueChars locale = intl::FormatLocale(cx, internals, keywords); 188 if (!locale) { 189 return nullptr; 190 } 191 192 if (!GetProperty(cx, internals, internals, cx->names().style, &value)) { 193 return nullptr; 194 } 195 196 using RelativeTimeFormatOptions = mozilla::intl::RelativeTimeFormatOptions; 197 RelativeTimeFormatOptions options; 198 { 199 JSLinearString* style = value.toString()->ensureLinear(cx); 200 if (!style) { 201 return nullptr; 202 } 203 204 if (StringEqualsLiteral(style, "short")) { 205 options.style = RelativeTimeFormatOptions::Style::Short; 206 } else if (StringEqualsLiteral(style, "narrow")) { 207 options.style = RelativeTimeFormatOptions::Style::Narrow; 208 } else { 209 MOZ_ASSERT(StringEqualsLiteral(style, "long")); 210 options.style = RelativeTimeFormatOptions::Style::Long; 211 } 212 } 213 214 if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) { 215 return nullptr; 216 } 217 218 { 219 JSLinearString* numeric = value.toString()->ensureLinear(cx); 220 if (!numeric) { 221 return nullptr; 222 } 223 224 if (StringEqualsLiteral(numeric, "auto")) { 225 options.numeric = RelativeTimeFormatOptions::Numeric::Auto; 226 } else { 227 MOZ_ASSERT(StringEqualsLiteral(numeric, "always")); 228 options.numeric = RelativeTimeFormatOptions::Numeric::Always; 229 } 230 } 231 232 using RelativeTimeFormat = mozilla::intl::RelativeTimeFormat; 233 mozilla::Result<mozilla::UniquePtr<RelativeTimeFormat>, 234 mozilla::intl::ICUError> 235 result = RelativeTimeFormat::TryCreate(locale.get(), options); 236 237 if (result.isOk()) { 238 return result.unwrap().release(); 239 } 240 241 intl::ReportInternalError(cx, result.unwrapErr()); 242 return nullptr; 243 } 244 245 static mozilla::intl::RelativeTimeFormat* GetOrCreateRelativeTimeFormat( 246 JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) { 247 // Obtain a cached RelativeDateTimeFormatter object. 248 mozilla::intl::RelativeTimeFormat* rtf = 249 relativeTimeFormat->getRelativeTimeFormatter(); 250 if (rtf) { 251 return rtf; 252 } 253 254 rtf = NewRelativeTimeFormatter(cx, relativeTimeFormat); 255 if (!rtf) { 256 return nullptr; 257 } 258 relativeTimeFormat->setRelativeTimeFormatter(rtf); 259 260 intl::AddICUCellMemory(relativeTimeFormat, 261 RelativeTimeFormatObject::EstimatedMemoryUse); 262 return rtf; 263 } 264 265 bool js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) { 266 CallArgs args = CallArgsFromVp(argc, vp); 267 MOZ_ASSERT(args.length() == 4); 268 MOZ_ASSERT(args[0].isObject()); 269 MOZ_ASSERT(args[1].isNumber()); 270 MOZ_ASSERT(args[2].isString()); 271 MOZ_ASSERT(args[3].isBoolean()); 272 273 Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx); 274 relativeTimeFormat = &args[0].toObject().as<RelativeTimeFormatObject>(); 275 276 bool formatToParts = args[3].toBoolean(); 277 278 // PartitionRelativeTimePattern, step 4. 279 double t = args[1].toNumber(); 280 if (!std::isfinite(t)) { 281 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 282 JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat", 283 formatToParts ? "formatToParts" : "format"); 284 return false; 285 } 286 287 mozilla::intl::RelativeTimeFormat* rtf = 288 GetOrCreateRelativeTimeFormat(cx, relativeTimeFormat); 289 if (!rtf) { 290 return false; 291 } 292 293 intl::RelativeTimeFormatUnit jsUnitType; 294 using FormatUnit = mozilla::intl::RelativeTimeFormat::FormatUnit; 295 FormatUnit relTimeUnit; 296 { 297 JSLinearString* unit = args[2].toString()->ensureLinear(cx); 298 if (!unit) { 299 return false; 300 } 301 302 // PartitionRelativeTimePattern, step 5. 303 if (StringEqualsLiteral(unit, "second") || 304 StringEqualsLiteral(unit, "seconds")) { 305 jsUnitType = &JSAtomState::second; 306 relTimeUnit = FormatUnit::Second; 307 } else if (StringEqualsLiteral(unit, "minute") || 308 StringEqualsLiteral(unit, "minutes")) { 309 jsUnitType = &JSAtomState::minute; 310 relTimeUnit = FormatUnit::Minute; 311 } else if (StringEqualsLiteral(unit, "hour") || 312 StringEqualsLiteral(unit, "hours")) { 313 jsUnitType = &JSAtomState::hour; 314 relTimeUnit = FormatUnit::Hour; 315 } else if (StringEqualsLiteral(unit, "day") || 316 StringEqualsLiteral(unit, "days")) { 317 jsUnitType = &JSAtomState::day; 318 relTimeUnit = FormatUnit::Day; 319 } else if (StringEqualsLiteral(unit, "week") || 320 StringEqualsLiteral(unit, "weeks")) { 321 jsUnitType = &JSAtomState::week; 322 relTimeUnit = FormatUnit::Week; 323 } else if (StringEqualsLiteral(unit, "month") || 324 StringEqualsLiteral(unit, "months")) { 325 jsUnitType = &JSAtomState::month; 326 relTimeUnit = FormatUnit::Month; 327 } else if (StringEqualsLiteral(unit, "quarter") || 328 StringEqualsLiteral(unit, "quarters")) { 329 jsUnitType = &JSAtomState::quarter; 330 relTimeUnit = FormatUnit::Quarter; 331 } else if (StringEqualsLiteral(unit, "year") || 332 StringEqualsLiteral(unit, "years")) { 333 jsUnitType = &JSAtomState::year; 334 relTimeUnit = FormatUnit::Year; 335 } else { 336 if (auto unitChars = QuoteString(cx, unit, '"')) { 337 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 338 JSMSG_INVALID_OPTION_VALUE, "unit", 339 unitChars.get()); 340 } 341 return false; 342 } 343 } 344 345 using ICUError = mozilla::intl::ICUError; 346 if (formatToParts) { 347 mozilla::intl::NumberPartVector parts; 348 mozilla::Result<mozilla::Span<const char16_t>, ICUError> result = 349 rtf->formatToParts(t, relTimeUnit, parts); 350 351 if (result.isErr()) { 352 intl::ReportInternalError(cx, result.unwrapErr()); 353 return false; 354 } 355 356 RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap())); 357 if (!str) { 358 return false; 359 } 360 361 return js::intl::FormattedRelativeTimeToParts(cx, str, parts, jsUnitType, 362 args.rval()); 363 } 364 365 js::intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); 366 mozilla::Result<Ok, ICUError> result = rtf->format(t, relTimeUnit, buffer); 367 368 if (result.isErr()) { 369 intl::ReportInternalError(cx, result.unwrapErr()); 370 return false; 371 } 372 373 JSString* str = buffer.toString(cx); 374 if (!str) { 375 return false; 376 } 377 378 args.rval().setString(str); 379 return true; 380 } 381 382 /** 383 * Intl.RelativeTimeFormat.supportedLocalesOf ( locales [ , options ] ) 384 */ 385 static bool relativeTimeFormat_supportedLocalesOf(JSContext* cx, unsigned argc, 386 Value* vp) { 387 CallArgs args = CallArgsFromVp(argc, vp); 388 389 // Steps 1-3. 390 auto* array = SupportedLocalesOf(cx, AvailableLocaleKind::RelativeTimeFormat, 391 args.get(0), args.get(1)); 392 if (!array) { 393 return false; 394 } 395 args.rval().setObject(*array); 396 return true; 397 }