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.DateTimeFormat implementation. */
8
9 #include "builtin/intl/DateTimeFormat.h"
10
11 #include "mozilla/Assertions.h"
12 #include "mozilla/EnumSet.h"
13 #include "mozilla/intl/Calendar.h"
14 #include "mozilla/intl/DateIntervalFormat.h"
15 #include "mozilla/intl/DateTimeFormat.h"
16 #include "mozilla/intl/DateTimePart.h"
17 #include "mozilla/intl/DateTimePatternGenerator.h"
18 #include "mozilla/intl/Locale.h"
19 #include "mozilla/intl/TimeZone.h"
20 #include "mozilla/Range.h"
21 #include "mozilla/Span.h"
22
23 #include "builtin/Array.h"
24 #include "builtin/intl/CommonFunctions.h"
25 #include "builtin/intl/FormatBuffer.h"
26 #include "builtin/intl/LanguageTag.h"
27 #include "builtin/intl/SharedIntlData.h"
28 #include "builtin/intl/TimeZoneDataGenerated.h"
29 #include "gc/FreeOp.h"
30 #include "js/CharacterEncoding.h"
31 #include "js/Date.h"
32 #include "js/experimental/Intl.h" // JS::AddMozDateTimeFormatConstructor
33 #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
34 #include "js/GCAPI.h"
35 #include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties
36 #include "js/PropertySpec.h"
37 #include "js/StableStringChars.h"
38 #include "vm/DateTime.h"
39 #include "vm/GlobalObject.h"
40 #include "vm/JSContext.h"
41 #include "vm/PlainObject.h" // js::PlainObject
42 #include "vm/Runtime.h"
43 #include "vm/WellKnownAtom.h" // js_*_str
44
45 #include "vm/JSObject-inl.h"
46 #include "vm/NativeObject-inl.h"
47
48 using namespace js;
49
50 using JS::AutoStableStringChars;
51 using JS::ClippedTime;
52 using JS::TimeClip;
53
54 using js::intl::DateTimeFormatOptions;
55 using js::intl::FormatBuffer;
56 using js::intl::INITIAL_CHAR_BUFFER_SIZE;
57 using js::intl::SharedIntlData;
58
59 const JSClassOps DateTimeFormatObject::classOps_ = {
60 nullptr, // addProperty
61 nullptr, // delProperty
62 nullptr, // enumerate
63 nullptr, // newEnumerate
64 nullptr, // resolve
65 nullptr, // mayResolve
66 DateTimeFormatObject::finalize, // finalize
67 nullptr, // call
68 nullptr, // hasInstance
69 nullptr, // construct
70 nullptr, // trace
71 };
72
73 const JSClass DateTimeFormatObject::class_ = {
74 "Intl.DateTimeFormat",
75 JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) |
76 JSCLASS_HAS_CACHED_PROTO(JSProto_DateTimeFormat) |
77 JSCLASS_FOREGROUND_FINALIZE,
78 &DateTimeFormatObject::classOps_, &DateTimeFormatObject::classSpec_};
79
80 const JSClass& DateTimeFormatObject::protoClass_ = PlainObject::class_;
81
dateTimeFormat_toSource(JSContext * cx,unsigned argc,Value * vp)82 static bool dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
83 CallArgs args = CallArgsFromVp(argc, vp);
84 args.rval().setString(cx->names().DateTimeFormat);
85 return true;
86 }
87
88 static const JSFunctionSpec dateTimeFormat_static_methods[] = {
89 JS_SELF_HOSTED_FN("supportedLocalesOf",
90 "Intl_DateTimeFormat_supportedLocalesOf", 1, 0),
91 JS_FS_END};
92
93 static const JSFunctionSpec dateTimeFormat_methods[] = {
94 JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions",
95 0, 0),
96 JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 1,
97 0),
98 JS_SELF_HOSTED_FN("formatRange", "Intl_DateTimeFormat_formatRange", 2, 0),
99 JS_SELF_HOSTED_FN("formatRangeToParts",
100 "Intl_DateTimeFormat_formatRangeToParts", 2, 0),
101 JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0),
102 JS_FS_END};
103
104 static const JSPropertySpec dateTimeFormat_properties[] = {
105 JS_SELF_HOSTED_GET("format", "$Intl_DateTimeFormat_format_get", 0),
106 JS_STRING_SYM_PS(toStringTag, "Intl.DateTimeFormat", JSPROP_READONLY),
107 JS_PS_END};
108
109 static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp);
110
111 const ClassSpec DateTimeFormatObject::classSpec_ = {
112 GenericCreateConstructor<DateTimeFormat, 0, gc::AllocKind::FUNCTION>,
113 GenericCreatePrototype<DateTimeFormatObject>,
114 dateTimeFormat_static_methods,
115 nullptr,
116 dateTimeFormat_methods,
117 dateTimeFormat_properties,
118 nullptr,
119 ClassSpec::DontDefineConstructor};
120
121 /**
122 * 12.2.1 Intl.DateTimeFormat([ locales [, options]])
123 *
124 * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
125 */
DateTimeFormat(JSContext * cx,const CallArgs & args,bool construct,DateTimeFormatOptions dtfOptions)126 static bool DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct,
127 DateTimeFormatOptions dtfOptions) {
128 // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
129
130 // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
131 JSProtoKey protoKey = dtfOptions == DateTimeFormatOptions::Standard
132 ? JSProto_DateTimeFormat
133 : JSProto_Null;
134 RootedObject proto(cx);
135 if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey, &proto)) {
136 return false;
137 }
138
139 Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
140 dateTimeFormat = NewObjectWithClassProto<DateTimeFormatObject>(cx, proto);
141 if (!dateTimeFormat) {
142 return false;
143 }
144
145 RootedValue thisValue(
146 cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv());
147 HandleValue locales = args.get(0);
148 HandleValue options = args.get(1);
149
150 // Step 3.
151 return intl::LegacyInitializeObject(
152 cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, thisValue,
153 locales, options, dtfOptions, args.rval());
154 }
155
DateTimeFormat(JSContext * cx,unsigned argc,Value * vp)156 static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
157 CallArgs args = CallArgsFromVp(argc, vp);
158 return DateTimeFormat(cx, args, args.isConstructing(),
159 DateTimeFormatOptions::Standard);
160 }
161
MozDateTimeFormat(JSContext * cx,unsigned argc,Value * vp)162 static bool MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
163 CallArgs args = CallArgsFromVp(argc, vp);
164
165 // Don't allow to call mozIntl.DateTimeFormat as a function. That way we
166 // don't need to worry how to handle the legacy initialization semantics
167 // when applied on mozIntl.DateTimeFormat.
168 if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) {
169 return false;
170 }
171
172 return DateTimeFormat(cx, args, true,
173 DateTimeFormatOptions::EnableMozExtensions);
174 }
175
intl_DateTimeFormat(JSContext * cx,unsigned argc,Value * vp)176 bool js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
177 CallArgs args = CallArgsFromVp(argc, vp);
178 MOZ_ASSERT(args.length() == 2);
179 MOZ_ASSERT(!args.isConstructing());
180 // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it
181 // cannot be used with "new", but it still has to be treated as a
182 // constructor.
183 return DateTimeFormat(cx, args, true, DateTimeFormatOptions::Standard);
184 }
185
finalize(JSFreeOp * fop,JSObject * obj)186 void js::DateTimeFormatObject::finalize(JSFreeOp* fop, JSObject* obj) {
187 MOZ_ASSERT(fop->onMainThread());
188
189 auto* dateTimeFormat = &obj->as<DateTimeFormatObject>();
190 mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat();
191 mozilla::intl::DateIntervalFormat* dif =
192 dateTimeFormat->getDateIntervalFormat();
193
194 if (df) {
195 intl::RemoveICUCellMemory(
196 fop, obj, DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
197
198 delete df;
199 }
200
201 if (dif) {
202 intl::RemoveICUCellMemory(
203 fop, obj, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
204
205 delete dif;
206 }
207 }
208
AddMozDateTimeFormatConstructor(JSContext * cx,JS::Handle<JSObject * > intl)209 bool JS::AddMozDateTimeFormatConstructor(JSContext* cx,
210 JS::Handle<JSObject*> intl) {
211 RootedObject ctor(
212 cx, GlobalObject::createConstructor(cx, MozDateTimeFormat,
213 cx->names().DateTimeFormat, 0));
214 if (!ctor) {
215 return false;
216 }
217
218 RootedObject proto(
219 cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global()));
220 if (!proto) {
221 return false;
222 }
223
224 if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
225 return false;
226 }
227
228 // 12.3.2
229 if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) {
230 return false;
231 }
232
233 // 12.4.4 and 12.4.5
234 if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) {
235 return false;
236 }
237
238 // 12.4.2 and 12.4.3
239 if (!JS_DefineProperties(cx, proto, dateTimeFormat_properties)) {
240 return false;
241 }
242
243 RootedValue ctorValue(cx, ObjectValue(*ctor));
244 return DefineDataProperty(cx, intl, cx->names().DateTimeFormat, ctorValue, 0);
245 }
246
DefaultCalendar(JSContext * cx,const UniqueChars & locale,MutableHandleValue rval)247 static bool DefaultCalendar(JSContext* cx, const UniqueChars& locale,
248 MutableHandleValue rval) {
249 auto calendar = mozilla::intl::Calendar::TryCreate(locale.get());
250 if (calendar.isErr()) {
251 intl::ReportInternalError(cx, calendar.unwrapErr());
252 return false;
253 }
254
255 auto type = calendar.unwrap()->GetBcp47Type();
256 if (type.isErr()) {
257 intl::ReportInternalError(cx, type.unwrapErr());
258 return false;
259 }
260
261 JSString* str = NewStringCopy<CanGC>(cx, type.unwrap());
262 if (!str) {
263 return false;
264 }
265
266 rval.setString(str);
267 return true;
268 }
269
intl_availableCalendars(JSContext * cx,unsigned argc,Value * vp)270 bool js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) {
271 CallArgs args = CallArgsFromVp(argc, vp);
272 MOZ_ASSERT(args.length() == 1);
273 MOZ_ASSERT(args[0].isString());
274
275 UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
276 if (!locale) {
277 return false;
278 }
279
280 RootedObject calendars(cx, NewDenseEmptyArray(cx));
281 if (!calendars) {
282 return false;
283 }
284
285 // We need the default calendar for the locale as the first result.
286 RootedValue defaultCalendar(cx);
287 if (!DefaultCalendar(cx, locale, &defaultCalendar)) {
288 return false;
289 }
290
291 if (!NewbornArrayPush(cx, calendars, defaultCalendar)) {
292 return false;
293 }
294
295 // Now get the calendars that "would make a difference", i.e., not the
296 // default.
297 auto keywords =
298 mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale(locale.get());
299 if (keywords.isErr()) {
300 intl::ReportInternalError(cx, keywords.unwrapErr());
301 return false;
302 }
303
304 for (auto keyword : keywords.unwrap()) {
305 if (keyword.isErr()) {
306 intl::ReportInternalError(cx);
307 return false;
308 }
309
310 JSString* jscalendar = NewStringCopy<CanGC>(cx, keyword.unwrap());
311 if (!jscalendar) {
312 return false;
313 }
314 if (!NewbornArrayPush(cx, calendars, StringValue(jscalendar))) {
315 return false;
316 }
317 }
318
319 args.rval().setObject(*calendars);
320 return true;
321 }
322
intl_defaultCalendar(JSContext * cx,unsigned argc,Value * vp)323 bool js::intl_defaultCalendar(JSContext* cx, unsigned argc, Value* vp) {
324 CallArgs args = CallArgsFromVp(argc, vp);
325 MOZ_ASSERT(args.length() == 1);
326 MOZ_ASSERT(args[0].isString());
327
328 UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
329 if (!locale) {
330 return false;
331 }
332
333 return DefaultCalendar(cx, locale, args.rval());
334 }
335
intl_IsValidTimeZoneName(JSContext * cx,unsigned argc,Value * vp)336 bool js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) {
337 CallArgs args = CallArgsFromVp(argc, vp);
338 MOZ_ASSERT(args.length() == 1);
339 MOZ_ASSERT(args[0].isString());
340
341 SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
342
343 RootedString timeZone(cx, args[0].toString());
344 RootedAtom validatedTimeZone(cx);
345 if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) {
346 return false;
347 }
348
349 if (validatedTimeZone) {
350 cx->markAtom(validatedTimeZone);
351 args.rval().setString(validatedTimeZone);
352 } else {
353 args.rval().setNull();
354 }
355
356 return true;
357 }
358
intl_canonicalizeTimeZone(JSContext * cx,unsigned argc,Value * vp)359 bool js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) {
360 CallArgs args = CallArgsFromVp(argc, vp);
361 MOZ_ASSERT(args.length() == 1);
362 MOZ_ASSERT(args[0].isString());
363
364 SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
365
366 // Some time zone names are canonicalized differently by ICU -- handle
367 // those first:
368 RootedString timeZone(cx, args[0].toString());
369 RootedAtom ianaTimeZone(cx);
370 if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(
371 cx, timeZone, &ianaTimeZone)) {
372 return false;
373 }
374
375 if (ianaTimeZone) {
376 cx->markAtom(ianaTimeZone);
377 args.rval().setString(ianaTimeZone);
378 return true;
379 }
380
381 AutoStableStringChars stableChars(cx);
382 if (!stableChars.initTwoByte(cx, timeZone)) {
383 return false;
384 }
385
386 FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> canonicalTimeZone(cx);
387 auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID(
388 stableChars.twoByteRange(), canonicalTimeZone);
389 if (result.isErr()) {
390 intl::ReportInternalError(cx, result.unwrapErr());
391 return false;
392 }
393
394 JSString* str = canonicalTimeZone.toString(cx);
395 if (!str) {
396 return false;
397 }
398
399 args.rval().setString(str);
400 return true;
401 }
402
intl_defaultTimeZone(JSContext * cx,unsigned argc,Value * vp)403 bool js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) {
404 CallArgs args = CallArgsFromVp(argc, vp);
405 MOZ_ASSERT(args.length() == 0);
406
407 // The current default might be stale, because JS::ResetTimeZone() doesn't
408 // immediately update ICU's default time zone. So perform an update if
409 // needed.
410 js::ResyncICUDefaultTimeZone();
411
412 FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> timeZone(cx);
413 auto result = mozilla::intl::TimeZone::GetDefaultTimeZone(timeZone);
414 if (result.isErr()) {
415 intl::ReportInternalError(cx, result.unwrapErr());
416 return false;
417 }
418
419 JSString* str = timeZone.toString(cx);
420 if (!str) {
421 return false;
422 }
423
424 args.rval().setString(str);
425 return true;
426 }
427
intl_defaultTimeZoneOffset(JSContext * cx,unsigned argc,Value * vp)428 bool js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) {
429 CallArgs args = CallArgsFromVp(argc, vp);
430 MOZ_ASSERT(args.length() == 0);
431
432 auto timeZone = mozilla::intl::TimeZone::TryCreate();
433 if (timeZone.isErr()) {
434 intl::ReportInternalError(cx, timeZone.unwrapErr());
435 return false;
436 }
437
438 auto offset = timeZone.unwrap()->GetRawOffsetMs();
439 if (offset.isErr()) {
440 intl::ReportInternalError(cx, offset.unwrapErr());
441 return false;
442 }
443
444 args.rval().setInt32(offset.unwrap());
445 return true;
446 }
447
intl_isDefaultTimeZone(JSContext * cx,unsigned argc,Value * vp)448 bool js::intl_isDefaultTimeZone(JSContext* cx, unsigned argc, Value* vp) {
449 CallArgs args = CallArgsFromVp(argc, vp);
450 MOZ_ASSERT(args.length() == 1);
451 MOZ_ASSERT(args[0].isString() || args[0].isUndefined());
452
453 // |undefined| is the default value when the Intl runtime caches haven't
454 // yet been initialized. Handle it the same way as a cache miss.
455 if (args[0].isUndefined()) {
456 args.rval().setBoolean(false);
457 return true;
458 }
459
460 // The current default might be stale, because JS::ResetTimeZone() doesn't
461 // immediately update ICU's default time zone. So perform an update if
462 // needed.
463 js::ResyncICUDefaultTimeZone();
464
465 FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> chars(cx);
466 auto result = mozilla::intl::TimeZone::GetDefaultTimeZone(chars);
467 if (result.isErr()) {
468 intl::ReportInternalError(cx, result.unwrapErr());
469 return false;
470 }
471
472 JSLinearString* str = args[0].toString()->ensureLinear(cx);
473 if (!str) {
474 return false;
475 }
476
477 bool equals;
478 if (str->length() == chars.length()) {
479 JS::AutoCheckCannotGC nogc;
480 equals =
481 str->hasLatin1Chars()
482 ? EqualChars(str->latin1Chars(nogc), chars.data(), str->length())
483 : EqualChars(str->twoByteChars(nogc), chars.data(), str->length());
484 } else {
485 equals = false;
486 }
487
488 args.rval().setBoolean(equals);
489 return true;
490 }
491
492 enum class HourCycle {
493 // 12 hour cycle, from 0 to 11.
494 H11,
495
496 // 12 hour cycle, from 1 to 12.
497 H12,
498
499 // 24 hour cycle, from 0 to 23.
500 H23,
501
502 // 24 hour cycle, from 1 to 24.
503 H24
504 };
505
DateTimeFormatLocale(JSContext * cx,HandleObject internals,mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hourCycle=mozilla::Nothing ())506 static UniqueChars DateTimeFormatLocale(
507 JSContext* cx, HandleObject internals,
508 mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hourCycle =
509 mozilla::Nothing()) {
510 RootedValue value(cx);
511 if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
512 return nullptr;
513 }
514
515 // ICU expects calendar, numberingSystem, and hourCycle as Unicode locale
516 // extensions on locale.
517
518 mozilla::intl::Locale tag;
519 {
520 RootedLinearString locale(cx, value.toString()->ensureLinear(cx));
521 if (!locale) {
522 return nullptr;
523 }
524
525 if (!intl::ParseLocale(cx, locale, tag)) {
526 return nullptr;
527 }
528 }
529
530 JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
531
532 if (!GetProperty(cx, internals, internals, cx->names().calendar, &value)) {
533 return nullptr;
534 }
535
536 {
537 JSLinearString* calendar = value.toString()->ensureLinear(cx);
538 if (!calendar) {
539 return nullptr;
540 }
541
542 if (!keywords.emplaceBack("ca", calendar)) {
543 return nullptr;
544 }
545 }
546
547 if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
548 &value)) {
549 return nullptr;
550 }
551
552 {
553 JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
554 if (!numberingSystem) {
555 return nullptr;
556 }
557
558 if (!keywords.emplaceBack("nu", numberingSystem)) {
559 return nullptr;
560 }
561 }
562
563 if (hourCycle) {
564 JSAtom* hourCycleStr;
565 switch (*hourCycle) {
566 case mozilla::intl::DateTimeFormat::HourCycle::H11:
567 hourCycleStr = cx->names().h11;
568 break;
569 case mozilla::intl::DateTimeFormat::HourCycle::H12:
570 hourCycleStr = cx->names().h12;
571 break;
572 case mozilla::intl::DateTimeFormat::HourCycle::H23:
573 hourCycleStr = cx->names().h23;
574 break;
575 case mozilla::intl::DateTimeFormat::HourCycle::H24:
576 hourCycleStr = cx->names().h24;
577 break;
578 }
579
580 if (!keywords.emplaceBack("hc", hourCycleStr)) {
581 return nullptr;
582 }
583 }
584
585 // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
586 // the Unicode extension subtag. We're then relying on ICU to follow RFC
587 // 6067, which states that any trailing keywords using the same key
588 // should be ignored.
589 if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
590 return nullptr;
591 }
592
593 FormatBuffer<char> buffer(cx);
594 if (auto result = tag.ToString(buffer); result.isErr()) {
595 intl::ReportInternalError(cx, result.unwrapErr());
596 return nullptr;
597 }
598 return buffer.extractStringZ();
599 }
600
AssignTextComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::Text> * text)601 static bool AssignTextComponent(
602 JSContext* cx, HandleObject internals, HandlePropertyName property,
603 mozilla::Maybe<mozilla::intl::DateTimeFormat::Text>* text) {
604 RootedValue value(cx);
605 if (!GetProperty(cx, internals, internals, property, &value)) {
606 return false;
607 }
608
609 if (value.isString()) {
610 JSLinearString* string = value.toString()->ensureLinear(cx);
611 if (!string) {
612 return false;
613 }
614 if (StringEqualsLiteral(string, "narrow")) {
615 *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Narrow);
616 } else if (StringEqualsLiteral(string, "short")) {
617 *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short);
618 } else {
619 MOZ_ASSERT(StringEqualsLiteral(string, "long"));
620 *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Long);
621 }
622 } else {
623 MOZ_ASSERT(value.isUndefined());
624 }
625
626 return true;
627 }
628
AssignNumericComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::Numeric> * numeric)629 static bool AssignNumericComponent(
630 JSContext* cx, HandleObject internals, HandlePropertyName property,
631 mozilla::Maybe<mozilla::intl::DateTimeFormat::Numeric>* numeric) {
632 RootedValue value(cx);
633 if (!GetProperty(cx, internals, internals, property, &value)) {
634 return false;
635 }
636
637 if (value.isString()) {
638 JSLinearString* string = value.toString()->ensureLinear(cx);
639 if (!string) {
640 return false;
641 }
642 if (StringEqualsLiteral(string, "numeric")) {
643 *numeric = mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::Numeric);
644 } else {
645 MOZ_ASSERT(StringEqualsLiteral(string, "2-digit"));
646 *numeric =
647 mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::TwoDigit);
648 }
649 } else {
650 MOZ_ASSERT(value.isUndefined());
651 }
652
653 return true;
654 }
655
AssignMonthComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::Month> * month)656 static bool AssignMonthComponent(
657 JSContext* cx, HandleObject internals, HandlePropertyName property,
658 mozilla::Maybe<mozilla::intl::DateTimeFormat::Month>* month) {
659 RootedValue value(cx);
660 if (!GetProperty(cx, internals, internals, property, &value)) {
661 return false;
662 }
663
664 if (value.isString()) {
665 JSLinearString* string = value.toString()->ensureLinear(cx);
666 if (!string) {
667 return false;
668 }
669 if (StringEqualsLiteral(string, "numeric")) {
670 *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Numeric);
671 } else if (StringEqualsLiteral(string, "2-digit")) {
672 *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::TwoDigit);
673 } else if (StringEqualsLiteral(string, "long")) {
674 *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Long);
675 } else if (StringEqualsLiteral(string, "short")) {
676 *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Short);
677 } else {
678 MOZ_ASSERT(StringEqualsLiteral(string, "narrow"));
679 *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Narrow);
680 }
681 } else {
682 MOZ_ASSERT(value.isUndefined());
683 }
684
685 return true;
686 }
687
AssignTimeZoneNameComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::TimeZoneName> * tzName)688 static bool AssignTimeZoneNameComponent(
689 JSContext* cx, HandleObject internals, HandlePropertyName property,
690 mozilla::Maybe<mozilla::intl::DateTimeFormat::TimeZoneName>* tzName) {
691 RootedValue value(cx);
692 if (!GetProperty(cx, internals, internals, property, &value)) {
693 return false;
694 }
695
696 if (value.isString()) {
697 JSLinearString* string = value.toString()->ensureLinear(cx);
698 if (!string) {
699 return false;
700 }
701 if (StringEqualsLiteral(string, "long")) {
702 *tzName =
703 mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Long);
704 } else if (StringEqualsLiteral(string, "short")) {
705 *tzName =
706 mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Short);
707 } else if (StringEqualsLiteral(string, "shortOffset")) {
708 *tzName = mozilla::Some(
709 mozilla::intl::DateTimeFormat::TimeZoneName::ShortOffset);
710 } else if (StringEqualsLiteral(string, "longOffset")) {
711 *tzName = mozilla::Some(
712 mozilla::intl::DateTimeFormat::TimeZoneName::LongOffset);
713 } else if (StringEqualsLiteral(string, "shortGeneric")) {
714 *tzName = mozilla::Some(
715 mozilla::intl::DateTimeFormat::TimeZoneName::ShortGeneric);
716 } else {
717 MOZ_ASSERT(StringEqualsLiteral(string, "longGeneric"));
718 *tzName = mozilla::Some(
719 mozilla::intl::DateTimeFormat::TimeZoneName::LongGeneric);
720 }
721 } else {
722 MOZ_ASSERT(value.isUndefined());
723 }
724
725 return true;
726 }
727
AssignHourCycleComponent(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> * hourCycle)728 static bool AssignHourCycleComponent(
729 JSContext* cx, HandleObject internals, HandlePropertyName property,
730 mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle>* hourCycle) {
731 RootedValue value(cx);
732 if (!GetProperty(cx, internals, internals, property, &value)) {
733 return false;
734 }
735
736 if (value.isString()) {
737 JSLinearString* string = value.toString()->ensureLinear(cx);
738 if (!string) {
739 return false;
740 }
741 if (StringEqualsLiteral(string, "h11")) {
742 *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H11);
743 } else if (StringEqualsLiteral(string, "h12")) {
744 *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H12);
745 } else if (StringEqualsLiteral(string, "h23")) {
746 *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H23);
747 } else {
748 MOZ_ASSERT(StringEqualsLiteral(string, "h24"));
749 *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H24);
750 }
751 } else {
752 MOZ_ASSERT(value.isUndefined());
753 }
754
755 return true;
756 }
757
AssignHour12Component(JSContext * cx,HandleObject internals,mozilla::Maybe<bool> * hour12)758 static bool AssignHour12Component(JSContext* cx, HandleObject internals,
759 mozilla::Maybe<bool>* hour12) {
760 RootedValue value(cx);
761 if (!GetProperty(cx, internals, internals, cx->names().hour12, &value)) {
762 return false;
763 }
764 if (value.isBoolean()) {
765 *hour12 = mozilla::Some(value.toBoolean());
766 } else {
767 MOZ_ASSERT(value.isUndefined());
768 }
769
770 return true;
771 }
772
AssignDateTimeLength(JSContext * cx,HandleObject internals,HandlePropertyName property,mozilla::Maybe<mozilla::intl::DateTimeFormat::Style> * style)773 static bool AssignDateTimeLength(
774 JSContext* cx, HandleObject internals, HandlePropertyName property,
775 mozilla::Maybe<mozilla::intl::DateTimeFormat::Style>* style) {
776 RootedValue value(cx);
777 if (!GetProperty(cx, internals, internals, property, &value)) {
778 return false;
779 }
780
781 if (value.isString()) {
782 JSLinearString* string = value.toString()->ensureLinear(cx);
783 if (!string) {
784 return false;
785 }
786 if (StringEqualsLiteral(string, "full")) {
787 *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Full);
788 } else if (StringEqualsLiteral(string, "long")) {
789 *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
790 } else if (StringEqualsLiteral(string, "medium")) {
791 *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Medium);
792 } else {
793 MOZ_ASSERT(StringEqualsLiteral(string, "short"));
794 *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
795 }
796 } else {
797 MOZ_ASSERT(value.isUndefined());
798 }
799
800 return true;
801 }
802
803 /**
804 * Returns a new mozilla::intl::DateTimeFormat with the locale and date-time
805 * formatting options of the given DateTimeFormat.
806 */
NewDateTimeFormat(JSContext * cx,Handle<DateTimeFormatObject * > dateTimeFormat)807 static mozilla::intl::DateTimeFormat* NewDateTimeFormat(
808 JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) {
809 RootedValue value(cx);
810
811 RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat));
812 if (!internals) {
813 return nullptr;
814 }
815
816 UniqueChars locale = DateTimeFormatLocale(cx, internals);
817 if (!locale) {
818 return nullptr;
819 }
820
821 if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) {
822 return nullptr;
823 }
824
825 AutoStableStringChars timeZone(cx);
826 if (!timeZone.initTwoByte(cx, value.toString())) {
827 return nullptr;
828 }
829
830 mozilla::Range<const char16_t> timeZoneChars = timeZone.twoByteRange();
831
832 if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) {
833 return nullptr;
834 }
835 bool hasPattern = value.isString();
836
837 if (!GetProperty(cx, internals, internals, cx->names().timeStyle, &value)) {
838 return nullptr;
839 }
840 bool hasStyle = value.isString();
841 if (!hasStyle) {
842 if (!GetProperty(cx, internals, internals, cx->names().dateStyle, &value)) {
843 return nullptr;
844 }
845 hasStyle = value.isString();
846 }
847
848 mozilla::UniquePtr<mozilla::intl::DateTimeFormat> df = nullptr;
849 if (hasPattern) {
850 // This is a DateTimeFormat defined by a pattern option. This is internal
851 // to Mozilla, and not part of the ECMA-402 API.
852 if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) {
853 return nullptr;
854 }
855
856 AutoStableStringChars pattern(cx);
857 if (!pattern.initTwoByte(cx, value.toString())) {
858 return nullptr;
859 }
860
861 auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromPattern(
862 mozilla::MakeStringSpan(locale.get()), pattern.twoByteRange(),
863 mozilla::Some(timeZoneChars));
864 if (dfResult.isErr()) {
865 intl::ReportInternalError(cx, dfResult.unwrapErr());
866 return nullptr;
867 }
868
869 df = dfResult.unwrap();
870 } else if (hasStyle) {
871 // This is a DateTimeFormat defined by a time style or date style.
872 mozilla::intl::DateTimeFormat::StyleBag style;
873 if (!AssignDateTimeLength(cx, internals, cx->names().timeStyle,
874 &style.time)) {
875 return nullptr;
876 }
877 if (!AssignDateTimeLength(cx, internals, cx->names().dateStyle,
878 &style.date)) {
879 return nullptr;
880 }
881 if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle,
882 &style.hourCycle)) {
883 return nullptr;
884 }
885
886 if (!AssignHour12Component(cx, internals, &style.hour12)) {
887 return nullptr;
888 }
889
890 SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
891
892 mozilla::intl::DateTimePatternGenerator* gen =
893 sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
894 if (!gen) {
895 return nullptr;
896 }
897 auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromStyle(
898 mozilla::MakeStringSpan(locale.get()), style, gen,
899 mozilla::Some(timeZoneChars));
900 if (dfResult.isErr()) {
901 intl::ReportInternalError(cx, dfResult.unwrapErr());
902 return nullptr;
903 }
904 df = dfResult.unwrap();
905 } else {
906 // This is a DateTimeFormat defined by a components bag.
907 mozilla::intl::DateTimeFormat::ComponentsBag bag;
908
909 if (!AssignTextComponent(cx, internals, cx->names().era, &bag.era)) {
910 return nullptr;
911 }
912 if (!AssignNumericComponent(cx, internals, cx->names().year, &bag.year)) {
913 return nullptr;
914 }
915 if (!AssignMonthComponent(cx, internals, cx->names().month, &bag.month)) {
916 return nullptr;
917 }
918 if (!AssignNumericComponent(cx, internals, cx->names().day, &bag.day)) {
919 return nullptr;
920 }
921 if (!AssignTextComponent(cx, internals, cx->names().weekday,
922 &bag.weekday)) {
923 return nullptr;
924 }
925 if (!AssignNumericComponent(cx, internals, cx->names().hour, &bag.hour)) {
926 return nullptr;
927 }
928 if (!AssignNumericComponent(cx, internals, cx->names().minute,
929 &bag.minute)) {
930 return nullptr;
931 }
932 if (!AssignNumericComponent(cx, internals, cx->names().second,
933 &bag.second)) {
934 return nullptr;
935 }
936 if (!AssignTimeZoneNameComponent(cx, internals, cx->names().timeZoneName,
937 &bag.timeZoneName)) {
938 return nullptr;
939 }
940 if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle,
941 &bag.hourCycle)) {
942 return nullptr;
943 }
944 if (!AssignTextComponent(cx, internals, cx->names().dayPeriod,
945 &bag.dayPeriod)) {
946 return nullptr;
947 }
948 if (!AssignHour12Component(cx, internals, &bag.hour12)) {
949 return nullptr;
950 }
951
952 if (!GetProperty(cx, internals, internals,
953 cx->names().fractionalSecondDigits, &value)) {
954 return nullptr;
955 }
956 if (value.isInt32()) {
957 bag.fractionalSecondDigits = mozilla::Some(value.toInt32());
958 } else {
959 MOZ_ASSERT(value.isUndefined());
960 }
961
962 SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
963 auto* dtpg = sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
964 if (!dtpg) {
965 return nullptr;
966 }
967
968 auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromComponents(
969 mozilla::MakeStringSpan(locale.get()), bag, dtpg,
970 mozilla::Some(timeZoneChars));
971 if (dfResult.isErr()) {
972 intl::ReportInternalError(cx, dfResult.unwrapErr());
973 return nullptr;
974 }
975 df = dfResult.unwrap();
976 }
977
978 // ECMAScript requires the Gregorian calendar to be used from the beginning
979 // of ECMAScript time.
980 df->SetStartTimeIfGregorian(StartOfTime);
981
982 return df.release();
983 }
984
GetOrCreateDateTimeFormat(JSContext * cx,Handle<DateTimeFormatObject * > dateTimeFormat)985 static mozilla::intl::DateTimeFormat* GetOrCreateDateTimeFormat(
986 JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) {
987 // Obtain a cached mozilla::intl::DateTimeFormat object.
988 mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat();
989 if (df) {
990 return df;
991 }
992
993 df = NewDateTimeFormat(cx, dateTimeFormat);
994 if (!df) {
995 return nullptr;
996 }
997 dateTimeFormat->setDateFormat(df);
998
999 intl::AddICUCellMemory(dateTimeFormat,
1000 DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
1001 return df;
1002 }
1003
1004 template <typename T>
SetResolvedProperty(JSContext * cx,HandleObject resolved,HandlePropertyName name,mozilla::Maybe<T> intlProp)1005 static bool SetResolvedProperty(JSContext* cx, HandleObject resolved,
1006 HandlePropertyName name,
1007 mozilla::Maybe<T> intlProp) {
1008 if (!intlProp) {
1009 return true;
1010 }
1011 JSString* str = NewStringCopyZ<CanGC>(
1012 cx, mozilla::intl::DateTimeFormat::ToString(*intlProp));
1013 if (!str) {
1014 return false;
1015 }
1016 RootedValue value(cx, StringValue(str));
1017 return DefineDataProperty(cx, resolved, name, value);
1018 }
1019
intl_resolveDateTimeFormatComponents(JSContext * cx,unsigned argc,Value * vp)1020 bool js::intl_resolveDateTimeFormatComponents(JSContext* cx, unsigned argc,
1021 Value* vp) {
1022 CallArgs args = CallArgsFromVp(argc, vp);
1023 MOZ_ASSERT(args.length() == 3);
1024 MOZ_ASSERT(args[0].isObject());
1025 MOZ_ASSERT(args[1].isObject());
1026 MOZ_ASSERT(args[2].isBoolean());
1027
1028 Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
1029 dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
1030
1031 RootedObject resolved(cx, &args[1].toObject());
1032
1033 bool includeDateTimeFields = args[2].toBoolean();
1034
1035 mozilla::intl::DateTimeFormat* df =
1036 GetOrCreateDateTimeFormat(cx, dateTimeFormat);
1037 if (!df) {
1038 return false;
1039 }
1040
1041 auto result = df->ResolveComponents();
1042 if (result.isErr()) {
1043 intl::ReportInternalError(cx, result.unwrapErr());
1044 return false;
1045 }
1046
1047 mozilla::intl::DateTimeFormat::ComponentsBag components = result.unwrap();
1048
1049 // Map the resolved mozilla::intl::DateTimeFormat::ComponentsBag to the
1050 // options object as returned by DateTimeFormat.prototype.resolvedOptions.
1051 //
1052 // Resolved options must match the ordering as defined in:
1053 // https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions
1054
1055 if (!SetResolvedProperty(cx, resolved, cx->names().hourCycle,
1056 components.hourCycle)) {
1057 return false;
1058 }
1059
1060 if (components.hour12) {
1061 RootedValue value(cx, BooleanValue(*components.hour12));
1062 if (!DefineDataProperty(cx, resolved, cx->names().hour12, value)) {
1063 return false;
1064 }
1065 }
1066
1067 if (!includeDateTimeFields) {
1068 args.rval().setUndefined();
1069 // Do not include date time fields.
1070 return true;
1071 }
1072
1073 if (!SetResolvedProperty(cx, resolved, cx->names().weekday,
1074 components.weekday)) {
1075 return false;
1076 }
1077 if (!SetResolvedProperty(cx, resolved, cx->names().era, components.era)) {
1078 return false;
1079 }
1080 if (!SetResolvedProperty(cx, resolved, cx->names().year, components.year)) {
1081 return false;
1082 }
1083 if (!SetResolvedProperty(cx, resolved, cx->names().month, components.month)) {
1084 return false;
1085 }
1086 if (!SetResolvedProperty(cx, resolved, cx->names().day, components.day)) {
1087 return false;
1088 }
1089 if (!SetResolvedProperty(cx, resolved, cx->names().dayPeriod,
1090 components.dayPeriod)) {
1091 return false;
1092 }
1093 if (!SetResolvedProperty(cx, resolved, cx->names().hour, components.hour)) {
1094 return false;
1095 }
1096 if (!SetResolvedProperty(cx, resolved, cx->names().minute,
1097 components.minute)) {
1098 return false;
1099 }
1100 if (!SetResolvedProperty(cx, resolved, cx->names().second,
1101 components.second)) {
1102 return false;
1103 }
1104 if (!SetResolvedProperty(cx, resolved, cx->names().timeZoneName,
1105 components.timeZoneName)) {
1106 return false;
1107 }
1108
1109 if (components.fractionalSecondDigits) {
1110 RootedValue value(cx, Int32Value(*components.fractionalSecondDigits));
1111 if (!DefineDataProperty(cx, resolved, cx->names().fractionalSecondDigits,
1112 value)) {
1113 return false;
1114 }
1115 }
1116
1117 args.rval().setUndefined();
1118 return true;
1119 }
1120
intl_FormatDateTime(JSContext * cx,const mozilla::intl::DateTimeFormat * df,ClippedTime x,MutableHandleValue result)1121 static bool intl_FormatDateTime(JSContext* cx,
1122 const mozilla::intl::DateTimeFormat* df,
1123 ClippedTime x, MutableHandleValue result) {
1124 MOZ_ASSERT(x.isValid());
1125
1126 FormatBuffer<char16_t, INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
1127 auto dfResult = df->TryFormat(x.toDouble(), buffer);
1128 if (dfResult.isErr()) {
1129 intl::ReportInternalError(cx, dfResult.unwrapErr());
1130 return false;
1131 }
1132
1133 JSString* str = buffer.toString(cx);
1134 if (!str) {
1135 return false;
1136 }
1137
1138 result.setString(str);
1139 return true;
1140 }
1141
1142 using FieldType = js::ImmutablePropertyNamePtr JSAtomState::*;
1143
GetFieldTypeForPartType(mozilla::intl::DateTimePartType type)1144 static FieldType GetFieldTypeForPartType(mozilla::intl::DateTimePartType type) {
1145 switch (type) {
1146 case mozilla::intl::DateTimePartType::Literal:
1147 return &JSAtomState::literal;
1148 case mozilla::intl::DateTimePartType::Era:
1149 return &JSAtomState::era;
1150 case mozilla::intl::DateTimePartType::Year:
1151 return &JSAtomState::year;
1152 case mozilla::intl::DateTimePartType::YearName:
1153 return &JSAtomState::yearName;
1154 case mozilla::intl::DateTimePartType::RelatedYear:
1155 return &JSAtomState::relatedYear;
1156 case mozilla::intl::DateTimePartType::Month:
1157 return &JSAtomState::month;
1158 case mozilla::intl::DateTimePartType::Day:
1159 return &JSAtomState::day;
1160 case mozilla::intl::DateTimePartType::Hour:
1161 return &JSAtomState::hour;
1162 case mozilla::intl::DateTimePartType::Minute:
1163 return &JSAtomState::minute;
1164 case mozilla::intl::DateTimePartType::Second:
1165 return &JSAtomState::second;
1166 case mozilla::intl::DateTimePartType::Weekday:
1167 return &JSAtomState::weekday;
1168 case mozilla::intl::DateTimePartType::DayPeriod:
1169 return &JSAtomState::dayPeriod;
1170 case mozilla::intl::DateTimePartType::TimeZoneName:
1171 return &JSAtomState::timeZoneName;
1172 case mozilla::intl::DateTimePartType::FractionalSecondDigits:
1173 return &JSAtomState::fractionalSecond;
1174 case mozilla::intl::DateTimePartType::Unknown:
1175 return &JSAtomState::unknown;
1176 }
1177
1178 MOZ_CRASH(
1179 "unenumerated, undocumented format field returned "
1180 "by iterator");
1181 }
1182
GetFieldTypeForPartSource(mozilla::intl::DateTimePartSource source)1183 static FieldType GetFieldTypeForPartSource(
1184 mozilla::intl::DateTimePartSource source) {
1185 switch (source) {
1186 case mozilla::intl::DateTimePartSource::Shared:
1187 return &JSAtomState::shared;
1188 case mozilla::intl::DateTimePartSource::StartRange:
1189 return &JSAtomState::startRange;
1190 case mozilla::intl::DateTimePartSource::EndRange:
1191 return &JSAtomState::endRange;
1192 }
1193
1194 MOZ_CRASH(
1195 "unenumerated, undocumented format field returned "
1196 "by iterator");
1197 }
1198
1199 // A helper function to create an ArrayObject from DateTimePart objects.
1200 // When hasNoSource is true, we don't need to create the ||Source|| property for
1201 // the DateTimePart object.
CreateDateTimePartArray(JSContext * cx,mozilla::Span<const char16_t> formattedSpan,bool hasNoSource,const mozilla::intl::DateTimePartVector & parts,MutableHandleValue result)1202 static bool CreateDateTimePartArray(
1203 JSContext* cx, mozilla::Span<const char16_t> formattedSpan,
1204 bool hasNoSource, const mozilla::intl::DateTimePartVector& parts,
1205 MutableHandleValue result) {
1206 RootedString overallResult(cx, NewStringCopy<CanGC>(cx, formattedSpan));
1207 if (!overallResult) {
1208 return false;
1209 }
1210
1211 RootedArrayObject partsArray(cx,
1212 NewDenseFullyAllocatedArray(cx, parts.length()));
1213 if (!partsArray) {
1214 return false;
1215 }
1216 partsArray->ensureDenseInitializedLength(0, parts.length());
1217
1218 if (overallResult->length() == 0) {
1219 // An empty string contains no parts, so avoid extra work below.
1220 result.setObject(*partsArray);
1221 return true;
1222 }
1223
1224 RootedObject singlePart(cx);
1225 RootedValue val(cx);
1226
1227 size_t index = 0;
1228 size_t beginIndex = 0;
1229 for (const mozilla::intl::DateTimePart& part : parts) {
1230 singlePart = NewPlainObject(cx);
1231 if (!singlePart) {
1232 return false;
1233 }
1234
1235 FieldType type = GetFieldTypeForPartType(part.mType);
1236 val = StringValue(cx->names().*type);
1237 if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) {
1238 return false;
1239 }
1240
1241 MOZ_ASSERT(part.mEndIndex > beginIndex);
1242 JSLinearString* partStr = NewDependentString(cx, overallResult, beginIndex,
1243 part.mEndIndex - beginIndex);
1244 if (!partStr) {
1245 return false;
1246 }
1247 val = StringValue(partStr);
1248 if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) {
1249 return false;
1250 }
1251
1252 if (!hasNoSource) {
1253 FieldType source = GetFieldTypeForPartSource(part.mSource);
1254 val = StringValue(cx->names().*source);
1255 if (!DefineDataProperty(cx, singlePart, cx->names().source, val)) {
1256 return false;
1257 }
1258 }
1259
1260 beginIndex = part.mEndIndex;
1261 partsArray->initDenseElement(index++, ObjectValue(*singlePart));
1262 }
1263
1264 MOZ_ASSERT(index == parts.length());
1265 MOZ_ASSERT(beginIndex == formattedSpan.size());
1266 result.setObject(*partsArray);
1267 return true;
1268 }
1269
intl_FormatToPartsDateTime(JSContext * cx,const mozilla::intl::DateTimeFormat * df,ClippedTime x,bool hasNoSource,MutableHandleValue result)1270 static bool intl_FormatToPartsDateTime(JSContext* cx,
1271 const mozilla::intl::DateTimeFormat* df,
1272 ClippedTime x, bool hasNoSource,
1273 MutableHandleValue result) {
1274 MOZ_ASSERT(x.isValid());
1275
1276 FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
1277 mozilla::intl::DateTimePartVector parts;
1278 auto r = df->TryFormatToParts(x.toDouble(), buffer, parts);
1279 if (r.isErr()) {
1280 intl::ReportInternalError(cx, r.unwrapErr());
1281 return false;
1282 }
1283
1284 return CreateDateTimePartArray(cx, buffer, hasNoSource, parts, result);
1285 }
1286
intl_FormatDateTime(JSContext * cx,unsigned argc,Value * vp)1287 bool js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) {
1288 CallArgs args = CallArgsFromVp(argc, vp);
1289 MOZ_ASSERT(args.length() == 3);
1290 MOZ_ASSERT(args[0].isObject());
1291 MOZ_ASSERT(args[1].isNumber());
1292 MOZ_ASSERT(args[2].isBoolean());
1293
1294 Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
1295 dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
1296
1297 bool formatToParts = args[2].toBoolean();
1298
1299 ClippedTime x = TimeClip(args[1].toNumber());
1300 if (!x.isValid()) {
1301 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
1302 JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
1303 formatToParts ? "formatToParts" : "format");
1304 return false;
1305 }
1306
1307 mozilla::intl::DateTimeFormat* df =
1308 GetOrCreateDateTimeFormat(cx, dateTimeFormat);
1309 if (!df) {
1310 return false;
1311 }
1312
1313 // Use the DateTimeFormat to actually format the time stamp.
1314 return formatToParts ? intl_FormatToPartsDateTime(
1315 cx, df, x, /* hasNoSource */ true, args.rval())
1316 : intl_FormatDateTime(cx, df, x, args.rval());
1317 }
1318
1319 /**
1320 * Returns a new DateIntervalFormat with the locale and date-time formatting
1321 * options of the given DateTimeFormat.
1322 */
NewDateIntervalFormat(JSContext * cx,Handle<DateTimeFormatObject * > dateTimeFormat,mozilla::intl::DateTimeFormat & mozDtf)1323 static mozilla::intl::DateIntervalFormat* NewDateIntervalFormat(
1324 JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
1325 mozilla::intl::DateTimeFormat& mozDtf) {
1326 RootedValue value(cx);
1327 RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat));
1328 if (!internals) {
1329 return nullptr;
1330 }
1331
1332 FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> pattern(cx);
1333 auto result = mozDtf.GetPattern(pattern);
1334 if (result.isErr()) {
1335 intl::ReportInternalError(cx, result.unwrapErr());
1336 return nullptr;
1337 }
1338
1339 // Determine the hour cycle used in the resolved pattern. This is needed to
1340 // workaround <https://unicode-org.atlassian.net/browse/ICU-21154> and
1341 // <https://unicode-org.atlassian.net/browse/ICU-21155>.
1342 mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hcPattern =
1343 mozilla::intl::DateTimeFormat::HourCycleFromPattern(pattern);
1344
1345 UniqueChars locale = DateTimeFormatLocale(cx, internals, hcPattern);
1346 if (!locale) {
1347 return nullptr;
1348 }
1349
1350 if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) {
1351 return nullptr;
1352 }
1353
1354 AutoStableStringChars timeZone(cx);
1355 if (!timeZone.initTwoByte(cx, value.toString())) {
1356 return nullptr;
1357 }
1358 mozilla::Span<const char16_t> timeZoneChars = timeZone.twoByteRange();
1359
1360 FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> skeleton(cx);
1361 auto skelResult = mozDtf.GetOriginalSkeleton(skeleton, hcPattern);
1362 if (skelResult.isErr()) {
1363 intl::ReportInternalError(cx, skelResult.unwrapErr());
1364 return nullptr;
1365 }
1366
1367 auto dif = mozilla::intl::DateIntervalFormat::TryCreate(
1368 mozilla::MakeStringSpan(locale.get()), skeleton, timeZoneChars);
1369
1370 if (dif.isErr()) {
1371 js::intl::ReportInternalError(cx, dif.unwrapErr());
1372 return nullptr;
1373 }
1374
1375 return dif.unwrap().release();
1376 }
1377
GetOrCreateDateIntervalFormat(JSContext * cx,Handle<DateTimeFormatObject * > dateTimeFormat,mozilla::intl::DateTimeFormat & mozDtf)1378 static mozilla::intl::DateIntervalFormat* GetOrCreateDateIntervalFormat(
1379 JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
1380 mozilla::intl::DateTimeFormat& mozDtf) {
1381 // Obtain a cached DateIntervalFormat object.
1382 mozilla::intl::DateIntervalFormat* dif =
1383 dateTimeFormat->getDateIntervalFormat();
1384 if (dif) {
1385 return dif;
1386 }
1387
1388 dif = NewDateIntervalFormat(cx, dateTimeFormat, mozDtf);
1389 if (!dif) {
1390 return nullptr;
1391 }
1392 dateTimeFormat->setDateIntervalFormat(dif);
1393
1394 intl::AddICUCellMemory(
1395 dateTimeFormat,
1396 DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
1397 return dif;
1398 }
1399
1400 /**
1401 * PartitionDateTimeRangePattern ( dateTimeFormat, x, y )
1402 */
PartitionDateTimeRangePattern(JSContext * cx,const mozilla::intl::DateTimeFormat * df,const mozilla::intl::DateIntervalFormat * dif,mozilla::intl::AutoFormattedDateInterval & formatted,ClippedTime x,ClippedTime y,bool * equal)1403 static bool PartitionDateTimeRangePattern(
1404 JSContext* cx, const mozilla::intl::DateTimeFormat* df,
1405 const mozilla::intl::DateIntervalFormat* dif,
1406 mozilla::intl::AutoFormattedDateInterval& formatted, ClippedTime x,
1407 ClippedTime y, bool* equal) {
1408 MOZ_ASSERT(x.isValid());
1409 MOZ_ASSERT(y.isValid());
1410 MOZ_ASSERT(x.toDouble() <= y.toDouble());
1411
1412 // We can't access the calendar used by UDateIntervalFormat to change it to a
1413 // proleptic Gregorian calendar. Instead we need to call a different formatter
1414 // function which accepts UCalendar instead of UDate.
1415 // But creating new UCalendar objects for each call is slow, so when we can
1416 // ensure that the input dates are later than the Gregorian change date,
1417 // directly call the formatter functions taking UDate.
1418
1419 // The Gregorian change date "1582-10-15T00:00:00.000Z".
1420 constexpr double GregorianChangeDate = -12219292800000.0;
1421
1422 // Add a full day to account for time zone offsets.
1423 constexpr double GregorianChangeDatePlusOneDay =
1424 GregorianChangeDate + msPerDay;
1425
1426 mozilla::intl::ICUResult result = Ok();
1427 if (x.toDouble() < GregorianChangeDatePlusOneDay) {
1428 // Create calendar objects for the start and end date by cloning the date
1429 // formatter calendar. The date formatter calendar already has the correct
1430 // time zone set and was changed to use a proleptic Gregorian calendar.
1431 auto startCal = df->CloneCalendar(x.toDouble());
1432 if (startCal.isErr()) {
1433 intl::ReportInternalError(cx, startCal.unwrapErr());
1434 return false;
1435 }
1436
1437 auto endCal = df->CloneCalendar(y.toDouble());
1438 if (endCal.isErr()) {
1439 intl::ReportInternalError(cx, endCal.unwrapErr());
1440 return false;
1441 }
1442
1443 result = dif->TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(),
1444 formatted, equal);
1445 } else {
1446 // The common fast path which doesn't require creating calendar objects.
1447 result =
1448 dif->TryFormatDateTime(x.toDouble(), y.toDouble(), formatted, equal);
1449 }
1450
1451 if (result.isErr()) {
1452 intl::ReportInternalError(cx, result.unwrapErr());
1453 return false;
1454 }
1455
1456 return true;
1457 }
1458
1459 /**
1460 * FormatDateTimeRange( dateTimeFormat, x, y )
1461 */
FormatDateTimeRange(JSContext * cx,const mozilla::intl::DateTimeFormat * df,const mozilla::intl::DateIntervalFormat * dif,ClippedTime x,ClippedTime y,MutableHandleValue result)1462 static bool FormatDateTimeRange(JSContext* cx,
1463 const mozilla::intl::DateTimeFormat* df,
1464 const mozilla::intl::DateIntervalFormat* dif,
1465 ClippedTime x, ClippedTime y,
1466 MutableHandleValue result) {
1467 mozilla::intl::AutoFormattedDateInterval formatted;
1468 if (!formatted.IsValid()) {
1469 intl::ReportInternalError(cx, formatted.GetError());
1470 return false;
1471 }
1472
1473 bool equal;
1474 if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) {
1475 return false;
1476 }
1477
1478 // PartitionDateTimeRangePattern, step 12.
1479 if (equal) {
1480 return intl_FormatDateTime(cx, df, x, result);
1481 }
1482
1483 auto spanResult = formatted.ToSpan();
1484 if (spanResult.isErr()) {
1485 intl::ReportInternalError(cx, spanResult.unwrapErr());
1486 return false;
1487 }
1488 JSString* resultStr = NewStringCopy<CanGC>(cx, spanResult.unwrap());
1489 if (!resultStr) {
1490 return false;
1491 }
1492
1493 result.setString(resultStr);
1494 return true;
1495 }
1496
1497 /**
1498 * FormatDateTimeRangeToParts ( dateTimeFormat, x, y )
1499 */
FormatDateTimeRangeToParts(JSContext * cx,const mozilla::intl::DateTimeFormat * df,const mozilla::intl::DateIntervalFormat * dif,ClippedTime x,ClippedTime y,MutableHandleValue result)1500 static bool FormatDateTimeRangeToParts(
1501 JSContext* cx, const mozilla::intl::DateTimeFormat* df,
1502 const mozilla::intl::DateIntervalFormat* dif, ClippedTime x, ClippedTime y,
1503 MutableHandleValue result) {
1504 mozilla::intl::AutoFormattedDateInterval formatted;
1505 if (!formatted.IsValid()) {
1506 intl::ReportInternalError(cx, formatted.GetError());
1507 return false;
1508 }
1509
1510 bool equal;
1511 if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) {
1512 return false;
1513 }
1514
1515 // PartitionDateTimeRangePattern, step 12.
1516 if (equal) {
1517 return intl_FormatToPartsDateTime(cx, df, x, /* hasNoSource */ false,
1518 result);
1519 }
1520
1521 mozilla::intl::DateTimePartVector parts;
1522 auto r = dif->TryFormattedToParts(formatted, parts);
1523 if (r.isErr()) {
1524 intl::ReportInternalError(cx, r.unwrapErr());
1525 return false;
1526 }
1527
1528 auto spanResult = formatted.ToSpan();
1529 if (spanResult.isErr()) {
1530 intl::ReportInternalError(cx, spanResult.unwrapErr());
1531 return false;
1532 }
1533 return CreateDateTimePartArray(cx, spanResult.unwrap(),
1534 /* hasNoSource */ false, parts, result);
1535 }
1536
intl_FormatDateTimeRange(JSContext * cx,unsigned argc,Value * vp)1537 bool js::intl_FormatDateTimeRange(JSContext* cx, unsigned argc, Value* vp) {
1538 CallArgs args = CallArgsFromVp(argc, vp);
1539 MOZ_ASSERT(args.length() == 4);
1540 MOZ_ASSERT(args[0].isObject());
1541 MOZ_ASSERT(args[1].isNumber());
1542 MOZ_ASSERT(args[2].isNumber());
1543 MOZ_ASSERT(args[3].isBoolean());
1544
1545 Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
1546 dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
1547
1548 bool formatToParts = args[3].toBoolean();
1549
1550 // PartitionDateTimeRangePattern, steps 1-2.
1551 ClippedTime x = TimeClip(args[1].toNumber());
1552 if (!x.isValid()) {
1553 JS_ReportErrorNumberASCII(
1554 cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
1555 formatToParts ? "formatRangeToParts" : "formatRange");
1556 return false;
1557 }
1558
1559 // PartitionDateTimeRangePattern, steps 3-4.
1560 ClippedTime y = TimeClip(args[2].toNumber());
1561 if (!y.isValid()) {
1562 JS_ReportErrorNumberASCII(
1563 cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
1564 formatToParts ? "formatRangeToParts" : "formatRange");
1565 return false;
1566 }
1567
1568 // Self-hosted code should have checked this condition.
1569 MOZ_ASSERT(x.toDouble() <= y.toDouble(),
1570 "start date mustn't be after the end date");
1571
1572 mozilla::intl::DateTimeFormat* df =
1573 GetOrCreateDateTimeFormat(cx, dateTimeFormat);
1574 if (!df) {
1575 return false;
1576 }
1577
1578 mozilla::intl::DateIntervalFormat* dif =
1579 GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df);
1580 if (!dif) {
1581 return false;
1582 }
1583
1584 // Use the DateIntervalFormat to actually format the time range.
1585 return formatToParts
1586 ? FormatDateTimeRangeToParts(cx, df, dif, x, y, args.rval())
1587 : FormatDateTimeRange(cx, df, dif, x, y, args.rval());
1588 }
1589