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
/* Implementation of the Intl.RelativeTimeFormat proposal. */
8
9
#include "builtin/intl/RelativeTimeFormat.h"
10
11
#include "mozilla/Assertions.h"
12
#include "mozilla/FloatingPoint.h"
13
14
#include "builtin/intl/CommonFunctions.h"
15
#include "builtin/intl/LanguageTag.h"
16
#include "builtin/intl/ScopedICUObject.h"
17
#include "gc/FreeOp.h"
18
#include "js/CharacterEncoding.h"
19
#include "js/PropertySpec.h"
20
#include "unicode/udisplaycontext.h"
21
#include "unicode/uloc.h"
22
#include "unicode/ureldatefmt.h"
23
#include "unicode/utypes.h"
24
#include "vm/GlobalObject.h"
25
#include "vm/JSContext.h"
26
#include "vm/Printer.h"
27
#include "vm/StringType.h"
28
29
#include "vm/NativeObject-inl.h"
30
31
using namespace js;
32
33
using js::intl::CallICU;
34
using js::intl::IcuLocale;
35
36
/**************** RelativeTimeFormat *****************/
37
38
const JSClassOps RelativeTimeFormatObject::classOps_ = {
39
nullptr, // addProperty
40
nullptr, // delProperty
41
nullptr, // enumerate
42
nullptr, // newEnumerate
43
nullptr, // resolve
44
nullptr, // mayResolve
45
RelativeTimeFormatObject::finalize, // finalize
46
nullptr, // call
47
nullptr, // hasInstance
48
nullptr, // construct
49
nullptr, // trace
50
};
51
52
const JSClass RelativeTimeFormatObject::class_ = {
53
js_Object_str,
54
JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) |
55
JSCLASS_HAS_CACHED_PROTO(JSProto_RelativeTimeFormat) |
56
JSCLASS_FOREGROUND_FINALIZE,
57
&RelativeTimeFormatObject::classOps_,
58
&RelativeTimeFormatObject::classSpec_};
59
60
const JSClass& RelativeTimeFormatObject::protoClass_ = PlainObject::class_;
61
62
static bool relativeTimeFormat_toSource(JSContext* cx, unsigned argc,
63
Value* vp) {
64
CallArgs args = CallArgsFromVp(argc, vp);
65
args.rval().setString(cx->names().RelativeTimeFormat);
66
return true;
67
}
68
69
static const JSFunctionSpec relativeTimeFormat_static_methods[] = {
70
JS_SELF_HOSTED_FN("supportedLocalesOf",
71
"Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0),
72
JS_FS_END};
73
74
static const JSFunctionSpec relativeTimeFormat_methods[] = {
75
JS_SELF_HOSTED_FN("resolvedOptions",
76
"Intl_RelativeTimeFormat_resolvedOptions", 0, 0),
77
JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0),
78
#ifndef U_HIDE_DRAFT_API
79
JS_SELF_HOSTED_FN("formatToParts", "Intl_RelativeTimeFormat_formatToParts",
80
2, 0),
81
#endif
82
JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), JS_FS_END};
83
84
static const JSPropertySpec relativeTimeFormat_properties[] = {
85
JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY),
86
JS_PS_END};
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
* RelativeTimeFormat constructor.
102
* Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1
103
*/
104
static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
105
CallArgs args = CallArgsFromVp(argc, vp);
106
107
// Step 1.
108
if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) {
109
return false;
110
}
111
112
// Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
113
RootedObject proto(cx);
114
if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_RelativeTimeFormat,
115
&proto)) {
116
return false;
117
}
118
119
Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx);
120
relativeTimeFormat =
121
NewObjectWithClassProto<RelativeTimeFormatObject>(cx, proto);
122
if (!relativeTimeFormat) {
123
return false;
124
}
125
126
HandleValue locales = args.get(0);
127
HandleValue options = args.get(1);
128
129
// Step 3.
130
if (!intl::InitializeObject(cx, relativeTimeFormat,
131
cx->names().InitializeRelativeTimeFormat, locales,
132
options)) {
133
return false;
134
}
135
136
args.rval().setObject(*relativeTimeFormat);
137
return true;
138
}
139
140
void js::RelativeTimeFormatObject::finalize(JSFreeOp* fop, JSObject* obj) {
141
MOZ_ASSERT(fop->onMainThread());
142
143
if (URelativeDateTimeFormatter* rtf =
144
obj->as<RelativeTimeFormatObject>().getRelativeDateTimeFormatter()) {
145
intl::RemoveICUCellMemory(fop, obj,
146
RelativeTimeFormatObject::EstimatedMemoryUse);
147
148
ureldatefmt_close(rtf);
149
}
150
}
151
152
/**
153
* Returns a new URelativeDateTimeFormatter with the locale and options of the
154
* given RelativeTimeFormatObject.
155
*/
156
static URelativeDateTimeFormatter* NewURelativeDateTimeFormatter(
157
JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) {
158
RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat));
159
if (!internals) {
160
return nullptr;
161
}
162
163
RootedValue value(cx);
164
165
if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
166
return nullptr;
167
}
168
169
// ICU expects numberingSystem as a Unicode locale extensions on locale.
170
171
intl::LanguageTag tag(cx);
172
{
173
JSLinearString* locale = value.toString()->ensureLinear(cx);
174
if (!locale) {
175
return nullptr;
176
}
177
178
if (!intl::LanguageTagParser::parse(cx, locale, tag)) {
179
return nullptr;
180
}
181
}
182
183
JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
184
185
if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
186
&value)) {
187
return nullptr;
188
}
189
190
{
191
JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
192
if (!numberingSystem) {
193
return nullptr;
194
}
195
196
if (!keywords.emplaceBack("nu", numberingSystem)) {
197
return nullptr;
198
}
199
}
200
201
// |ApplyUnicodeExtensionToTag| applies the new keywords to the front of the
202
// Unicode extension subtag. We're then relying on ICU to follow RFC 6067,
203
// which states that any trailing keywords using the same key should be
204
// ignored.
205
if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
206
return nullptr;
207
}
208
209
UniqueChars locale = tag.toStringZ(cx);
210
if (!locale) {
211
return nullptr;
212
}
213
214
if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
215
return nullptr;
216
}
217
218
UDateRelativeDateTimeFormatterStyle relDateTimeStyle;
219
{
220
JSLinearString* style = value.toString()->ensureLinear(cx);
221
if (!style) {
222
return nullptr;
223
}
224
225
if (StringEqualsLiteral(style, "short")) {
226
relDateTimeStyle = UDAT_STYLE_SHORT;
227
} else if (StringEqualsLiteral(style, "narrow")) {
228
relDateTimeStyle = UDAT_STYLE_NARROW;
229
} else {
230
MOZ_ASSERT(StringEqualsLiteral(style, "long"));
231
relDateTimeStyle = UDAT_STYLE_LONG;
232
}
233
}
234
235
UErrorCode status = U_ZERO_ERROR;
236
URelativeDateTimeFormatter* rtf =
237
ureldatefmt_open(IcuLocale(locale.get()), nullptr, relDateTimeStyle,
238
UDISPCTX_CAPITALIZATION_FOR_STANDALONE, &status);
239
if (U_FAILURE(status)) {
240
intl::ReportInternalError(cx);
241
return nullptr;
242
}
243
return rtf;
244
}
245
246
enum class RelativeTimeNumeric {
247
/**
248
* Only strings with numeric components like `1 day ago`.
249
*/
250
Always,
251
/**
252
* Natural-language strings like `yesterday` when possible,
253
* otherwise strings with numeric components as in `7 months ago`.
254
*/
255
Auto,
256
};
257
258
static bool intl_FormatRelativeTime(JSContext* cx,
259
URelativeDateTimeFormatter* rtf, double t,
260
URelativeDateTimeUnit unit,
261
RelativeTimeNumeric numeric,
262
MutableHandleValue result) {
263
JSString* str = CallICU(
264
cx,
265
[rtf, t, unit, numeric](UChar* chars, int32_t size, UErrorCode* status) {
266
auto fmt = numeric == RelativeTimeNumeric::Auto
267
? ureldatefmt_format
268
: ureldatefmt_formatNumeric;
269
return fmt(rtf, t, unit, chars, size, status);
270
});
271
if (!str) {
272
return false;
273
}
274
275
result.setString(str);
276
return true;
277
}
278
279
#ifndef U_HIDE_DRAFT_API
280
static bool intl_FormatToPartsRelativeTime(JSContext* cx,
281
URelativeDateTimeFormatter* rtf,
282
double t, URelativeDateTimeUnit unit,
283
RelativeTimeNumeric numeric,
284
MutableHandleValue result) {
285
UErrorCode status = U_ZERO_ERROR;
286
UFormattedRelativeDateTime* formatted = ureldatefmt_openResult(&status);
287
if (U_FAILURE(status)) {
288
intl::ReportInternalError(cx);
289
return false;
290
}
291
ScopedICUObject<UFormattedRelativeDateTime, ureldatefmt_closeResult> toClose(
292
formatted);
293
294
if (numeric == RelativeTimeNumeric::Auto) {
295
ureldatefmt_formatToResult(rtf, t, unit, formatted, &status);
296
} else {
297
ureldatefmt_formatNumericToResult(rtf, t, unit, formatted, &status);
298
}
299
if (U_FAILURE(status)) {
300
intl::ReportInternalError(cx);
301
return false;
302
}
303
304
const UFormattedValue* formattedValue =
305
ureldatefmt_resultAsValue(formatted, &status);
306
if (U_FAILURE(status)) {
307
intl::ReportInternalError(cx);
308
return false;
309
}
310
311
intl::FieldType unitType;
312
switch (unit) {
313
case UDAT_REL_UNIT_SECOND:
314
unitType = &JSAtomState::second;
315
break;
316
case UDAT_REL_UNIT_MINUTE:
317
unitType = &JSAtomState::minute;
318
break;
319
case UDAT_REL_UNIT_HOUR:
320
unitType = &JSAtomState::hour;
321
break;
322
case UDAT_REL_UNIT_DAY:
323
unitType = &JSAtomState::day;
324
break;
325
case UDAT_REL_UNIT_WEEK:
326
unitType = &JSAtomState::week;
327
break;
328
case UDAT_REL_UNIT_MONTH:
329
unitType = &JSAtomState::month;
330
break;
331
case UDAT_REL_UNIT_QUARTER:
332
unitType = &JSAtomState::quarter;
333
break;
334
case UDAT_REL_UNIT_YEAR:
335
unitType = &JSAtomState::year;
336
break;
337
default:
338
MOZ_CRASH("unexpected relative time unit");
339
}
340
341
return intl::FormattedRelativeTimeToParts(cx, formattedValue, t, unitType,
342
result);
343
}
344
#endif
345
346
bool js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) {
347
CallArgs args = CallArgsFromVp(argc, vp);
348
MOZ_ASSERT(args.length() == 5);
349
350
Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx);
351
relativeTimeFormat = &args[0].toObject().as<RelativeTimeFormatObject>();
352
353
bool formatToParts = args[4].toBoolean();
354
355
// PartitionRelativeTimePattern, step 4.
356
double t = args[1].toNumber();
357
if (!mozilla::IsFinite(t)) {
358
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
359
JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat",
360
formatToParts ? "formatToParts" : "format");
361
return false;
362
}
363
364
// Obtain a cached URelativeDateTimeFormatter object.
365
URelativeDateTimeFormatter* rtf =
366
relativeTimeFormat->getRelativeDateTimeFormatter();
367
if (!rtf) {
368
rtf = NewURelativeDateTimeFormatter(cx, relativeTimeFormat);
369
if (!rtf) {
370
return false;
371
}
372
relativeTimeFormat->setRelativeDateTimeFormatter(rtf);
373
374
intl::AddICUCellMemory(relativeTimeFormat,
375
RelativeTimeFormatObject::EstimatedMemoryUse);
376
}
377
378
URelativeDateTimeUnit relDateTimeUnit;
379
{
380
JSLinearString* unit = args[2].toString()->ensureLinear(cx);
381
if (!unit) {
382
return false;
383
}
384
385
// PartitionRelativeTimePattern, step 5.
386
if (StringEqualsLiteral(unit, "second") ||
387
StringEqualsLiteral(unit, "seconds")) {
388
relDateTimeUnit = UDAT_REL_UNIT_SECOND;
389
} else if (StringEqualsLiteral(unit, "minute") ||
390
StringEqualsLiteral(unit, "minutes")) {
391
relDateTimeUnit = UDAT_REL_UNIT_MINUTE;
392
} else if (StringEqualsLiteral(unit, "hour") ||
393
StringEqualsLiteral(unit, "hours")) {
394
relDateTimeUnit = UDAT_REL_UNIT_HOUR;
395
} else if (StringEqualsLiteral(unit, "day") ||
396
StringEqualsLiteral(unit, "days")) {
397
relDateTimeUnit = UDAT_REL_UNIT_DAY;
398
} else if (StringEqualsLiteral(unit, "week") ||
399
StringEqualsLiteral(unit, "weeks")) {
400
relDateTimeUnit = UDAT_REL_UNIT_WEEK;
401
} else if (StringEqualsLiteral(unit, "month") ||
402
StringEqualsLiteral(unit, "months")) {
403
relDateTimeUnit = UDAT_REL_UNIT_MONTH;
404
} else if (StringEqualsLiteral(unit, "quarter") ||
405
StringEqualsLiteral(unit, "quarters")) {
406
relDateTimeUnit = UDAT_REL_UNIT_QUARTER;
407
} else if (StringEqualsLiteral(unit, "year") ||
408
StringEqualsLiteral(unit, "years")) {
409
relDateTimeUnit = UDAT_REL_UNIT_YEAR;
410
} else {
411
if (auto unitChars = QuoteString(cx, unit, '"')) {
412
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
413
JSMSG_INVALID_OPTION_VALUE, "unit",
414
unitChars.get());
415
}
416
return false;
417
}
418
}
419
420
RelativeTimeNumeric relDateTimeNumeric;
421
{
422
JSLinearString* numeric = args[3].toString()->ensureLinear(cx);
423
if (!numeric) {
424
return false;
425
}
426
427
if (StringEqualsLiteral(numeric, "auto")) {
428
relDateTimeNumeric = RelativeTimeNumeric::Auto;
429
} else {
430
MOZ_ASSERT(StringEqualsLiteral(numeric, "always"));
431
relDateTimeNumeric = RelativeTimeNumeric::Always;
432
}
433
}
434
435
#ifndef U_HIDE_DRAFT_API
436
return formatToParts
437
? intl_FormatToPartsRelativeTime(cx, rtf, t, relDateTimeUnit,
438
relDateTimeNumeric, args.rval())
439
: intl_FormatRelativeTime(cx, rtf, t, relDateTimeUnit,
440
relDateTimeNumeric, args.rval());
441
#else
442
MOZ_ASSERT(!formatToParts);
443
return intl_FormatRelativeTime(cx, rtf, t, relDateTimeUnit,
444
relDateTimeNumeric, args.rval());
445
#endif
446
}