1/*
2 * Copyright (C) 2015 Andy VanWagoner ([email protected])
3 * Copyright (C) 2016-2019 Apple Inc. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24 * THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "config.h"
28#include "IntlDateTimeFormat.h"
29
30#if ENABLE(INTL)
31
32#include "DateInstance.h"
33#include "Error.h"
34#include "IntlDateTimeFormatConstructor.h"
35#include "IntlObject.h"
36#include "JSBoundFunction.h"
37#include "JSCInlines.h"
38#include "ObjectConstructor.h"
39#include <unicode/ucal.h>
40#include <unicode/udatpg.h>
41#include <unicode/uenum.h>
42#include <wtf/text/StringBuilder.h>
43
44#if JSC_ICU_HAS_UFIELDPOSITER
45#include <unicode/ufieldpositer.h>
46#endif
47
48namespace JSC {
49
50static const double minECMAScriptTime = -8.64E15;
51
52const ClassInfo IntlDateTimeFormat::s_info = { "Object", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(IntlDateTimeFormat) };
53
54namespace IntlDTFInternal {
55static const char* const relevantExtensionKeys[3] = { "ca", "nu", "hc" };
56}
57
58static const size_t indexOfExtensionKeyCa = 0;
59static const size_t indexOfExtensionKeyNu = 1;
60static const size_t indexOfExtensionKeyHc = 2;
61
62void IntlDateTimeFormat::UDateFormatDeleter::operator()(UDateFormat* dateFormat) const
63{
64 if (dateFormat)
65 udat_close(dateFormat);
66}
67
68#if JSC_ICU_HAS_UFIELDPOSITER
69void IntlDateTimeFormat::UFieldPositionIteratorDeleter::operator()(UFieldPositionIterator* iterator) const
70{
71 if (iterator)
72 ufieldpositer_close(iterator);
73}
74#endif
75
76IntlDateTimeFormat* IntlDateTimeFormat::create(VM& vm, Structure* structure)
77{
78 IntlDateTimeFormat* format = new (NotNull, allocateCell<IntlDateTimeFormat>(vm.heap)) IntlDateTimeFormat(vm, structure);
79 format->finishCreation(vm);
80 return format;
81}
82
83Structure* IntlDateTimeFormat::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
84{
85 return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
86}
87
88IntlDateTimeFormat::IntlDateTimeFormat(VM& vm, Structure* structure)
89 : JSDestructibleObject(vm, structure)
90{
91}
92
93void IntlDateTimeFormat::finishCreation(VM& vm)
94{
95 Base::finishCreation(vm);
96 ASSERT(inherits(vm, info()));
97}
98
99void IntlDateTimeFormat::destroy(JSCell* cell)
100{
101 static_cast<IntlDateTimeFormat*>(cell)->IntlDateTimeFormat::~IntlDateTimeFormat();
102}
103
104void IntlDateTimeFormat::visitChildren(JSCell* cell, SlotVisitor& visitor)
105{
106 IntlDateTimeFormat* thisObject = jsCast<IntlDateTimeFormat*>(cell);
107 ASSERT_GC_OBJECT_INHERITS(thisObject, info());
108
109 Base::visitChildren(thisObject, visitor);
110
111 visitor.append(thisObject->m_boundFormat);
112}
113
114void IntlDateTimeFormat::setBoundFormat(VM& vm, JSBoundFunction* format)
115{
116 m_boundFormat.set(vm, this, format);
117}
118
119static String defaultTimeZone()
120{
121 // 6.4.3 DefaultTimeZone () (ECMA-402 2.0)
122 // The DefaultTimeZone abstract operation returns a String value representing the valid (6.4.1) and canonicalized (6.4.2) time zone name for the host environment’s current time zone.
123
124 UErrorCode status = U_ZERO_ERROR;
125 Vector<UChar, 32> buffer(32);
126 auto bufferLength = ucal_getDefaultTimeZone(buffer.data(), buffer.size(), &status);
127 if (status == U_BUFFER_OVERFLOW_ERROR) {
128 status = U_ZERO_ERROR;
129 buffer.grow(bufferLength);
130 ucal_getDefaultTimeZone(buffer.data(), bufferLength, &status);
131 }
132 if (U_SUCCESS(status)) {
133 status = U_ZERO_ERROR;
134 Vector<UChar, 32> canonicalBuffer(32);
135 auto canonicalLength = ucal_getCanonicalTimeZoneID(buffer.data(), bufferLength, canonicalBuffer.data(), canonicalBuffer.size(), nullptr, &status);
136 if (status == U_BUFFER_OVERFLOW_ERROR) {
137 status = U_ZERO_ERROR;
138 canonicalBuffer.grow(canonicalLength);
139 ucal_getCanonicalTimeZoneID(buffer.data(), bufferLength, canonicalBuffer.data(), canonicalLength, nullptr, &status);
140 }
141 if (U_SUCCESS(status))
142 return String(canonicalBuffer.data(), canonicalLength);
143 }
144
145 return "UTC"_s;
146}
147
148static String canonicalizeTimeZoneName(const String& timeZoneName)
149{
150 // 6.4.1 IsValidTimeZoneName (timeZone)
151 // The abstract operation returns true if timeZone, converted to upper case as described in 6.1, is equal to one of the Zone or Link names of the IANA Time Zone Database, converted to upper case as described in 6.1. It returns false otherwise.
152 UErrorCode status = U_ZERO_ERROR;
153 UEnumeration* timeZones = ucal_openTimeZones(&status);
154 ASSERT(U_SUCCESS(status));
155
156 String canonical;
157 do {
158 status = U_ZERO_ERROR;
159 int32_t ianaTimeZoneLength;
160 // Time zone names are respresented as UChar[] in all related ICU apis.
161 const UChar* ianaTimeZone = uenum_unext(timeZones, &ianaTimeZoneLength, &status);
162 ASSERT(U_SUCCESS(status));
163
164 // End of enumeration.
165 if (!ianaTimeZone)
166 break;
167
168 StringView ianaTimeZoneView(ianaTimeZone, ianaTimeZoneLength);
169 if (!equalIgnoringASCIICase(timeZoneName, ianaTimeZoneView))
170 continue;
171
172 // Found a match, now canonicalize.
173 // 6.4.2 CanonicalizeTimeZoneName (timeZone) (ECMA-402 2.0)
174 // 1. Let ianaTimeZone be the Zone or Link name of the IANA Time Zone Database such that timeZone, converted to upper case as described in 6.1, is equal to ianaTimeZone, converted to upper case as described in 6.1.
175 // 2. If ianaTimeZone is a Link name, then let ianaTimeZone be the corresponding Zone name as specified in the “backward” file of the IANA Time Zone Database.
176
177 Vector<UChar, 32> buffer(ianaTimeZoneLength);
178 status = U_ZERO_ERROR;
179 auto canonicalLength = ucal_getCanonicalTimeZoneID(ianaTimeZone, ianaTimeZoneLength, buffer.data(), ianaTimeZoneLength, nullptr, &status);
180 if (status == U_BUFFER_OVERFLOW_ERROR) {
181 buffer.grow(canonicalLength);
182 status = U_ZERO_ERROR;
183 ucal_getCanonicalTimeZoneID(ianaTimeZone, ianaTimeZoneLength, buffer.data(), canonicalLength, nullptr, &status);
184 }
185 ASSERT(U_SUCCESS(status));
186 canonical = String(buffer.data(), canonicalLength);
187 } while (canonical.isNull());
188 uenum_close(timeZones);
189
190 // 3. If ianaTimeZone is "Etc/UTC" or "Etc/GMT", then return "UTC".
191 if (canonical == "Etc/UTC" || canonical == "Etc/GMT")
192 canonical = "UTC"_s;
193
194 // 4. Return ianaTimeZone.
195 return canonical;
196}
197
198namespace IntlDTFInternal {
199static Vector<String> localeData(const String& locale, size_t keyIndex)
200{
201 Vector<String> keyLocaleData;
202 switch (keyIndex) {
203 case indexOfExtensionKeyCa: {
204 UErrorCode status = U_ZERO_ERROR;
205 UEnumeration* calendars = ucal_getKeywordValuesForLocale("calendar", locale.utf8().data(), false, &status);
206 ASSERT(U_SUCCESS(status));
207
208 int32_t nameLength;
209 while (const char* availableName = uenum_next(calendars, &nameLength, &status)) {
210 ASSERT(U_SUCCESS(status));
211 String calendar = String(availableName, nameLength);
212 keyLocaleData.append(calendar);
213 // Ensure aliases used in language tag are allowed.
214 if (calendar == "gregorian")
215 keyLocaleData.append("gregory"_s);
216 else if (calendar == "islamic-civil")
217 keyLocaleData.append("islamicc"_s);
218 else if (calendar == "ethiopic-amete-alem")
219 keyLocaleData.append("ethioaa"_s);
220 }
221 uenum_close(calendars);
222 break;
223 }
224 case indexOfExtensionKeyNu:
225 keyLocaleData = numberingSystemsForLocale(locale);
226 break;
227 case indexOfExtensionKeyHc:
228 // Null default so we know to use 'j' in pattern.
229 keyLocaleData.append(String());
230 keyLocaleData.append("h11"_s);
231 keyLocaleData.append("h12"_s);
232 keyLocaleData.append("h23"_s);
233 keyLocaleData.append("h24"_s);
234 break;
235 default:
236 ASSERT_NOT_REACHED();
237 }
238 return keyLocaleData;
239}
240
241static JSObject* toDateTimeOptionsAnyDate(JSGlobalObject* globalObject, JSValue originalOptions)
242{
243 // 12.1.1 ToDateTimeOptions abstract operation (ECMA-402 2.0)
244 VM& vm = globalObject->vm();
245 auto scope = DECLARE_THROW_SCOPE(vm);
246
247 // 1. If options is undefined, then let options be null, else let options be ToObject(options).
248 // 2. ReturnIfAbrupt(options).
249 // 3. Let options be ObjectCreate(options).
250 JSObject* options;
251 if (originalOptions.isUndefined())
252 options = constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure());
253 else {
254 JSObject* originalToObject = originalOptions.toObject(globalObject);
255 RETURN_IF_EXCEPTION(scope, nullptr);
256 options = constructEmptyObject(globalObject, originalToObject);
257 }
258
259 // 4. Let needDefaults be true.
260 bool needDefaults = true;
261
262 // 5. If required is "date" or "any",
263 // Always "any".
264
265 // a. For each of the property names "weekday", "year", "month", "day":
266 // i. Let prop be the property name.
267 // ii. Let value be Get(options, prop).
268 // iii. ReturnIfAbrupt(value).
269 // iv. If value is not undefined, then let needDefaults be false.
270 JSValue weekday = options->get(globalObject, vm.propertyNames->weekday);
271 RETURN_IF_EXCEPTION(scope, nullptr);
272 if (!weekday.isUndefined())
273 needDefaults = false;
274
275 JSValue year = options->get(globalObject, vm.propertyNames->year);
276 RETURN_IF_EXCEPTION(scope, nullptr);
277 if (!year.isUndefined())
278 needDefaults = false;
279
280 JSValue month = options->get(globalObject, vm.propertyNames->month);
281 RETURN_IF_EXCEPTION(scope, nullptr);
282 if (!month.isUndefined())
283 needDefaults = false;
284
285 JSValue day = options->get(globalObject, vm.propertyNames->day);
286 RETURN_IF_EXCEPTION(scope, nullptr);
287 if (!day.isUndefined())
288 needDefaults = false;
289
290 // 6. If required is "time" or "any",
291 // Always "any".
292
293 // a. For each of the property names "hour", "minute", "second":
294 // i. Let prop be the property name.
295 // ii. Let value be Get(options, prop).
296 // iii. ReturnIfAbrupt(value).
297 // iv. If value is not undefined, then let needDefaults be false.
298 JSValue hour = options->get(globalObject, vm.propertyNames->hour);
299 RETURN_IF_EXCEPTION(scope, nullptr);
300 if (!hour.isUndefined())
301 needDefaults = false;
302
303 JSValue minute = options->get(globalObject, vm.propertyNames->minute);
304 RETURN_IF_EXCEPTION(scope, nullptr);
305 if (!minute.isUndefined())
306 needDefaults = false;
307
308 JSValue second = options->get(globalObject, vm.propertyNames->second);
309 RETURN_IF_EXCEPTION(scope, nullptr);
310 if (!second.isUndefined())
311 needDefaults = false;
312
313 // 7. If needDefaults is true and defaults is either "date" or "all", then
314 // Defaults is always "date".
315 if (needDefaults) {
316 // a. For each of the property names "year", "month", "day":
317 // i. Let status be CreateDatePropertyOrThrow(options, prop, "numeric").
318 // ii. ReturnIfAbrupt(status).
319 JSString* numeric = jsNontrivialString(vm, "numeric"_s);
320
321 options->putDirect(vm, vm.propertyNames->year, numeric);
322 RETURN_IF_EXCEPTION(scope, nullptr);
323
324 options->putDirect(vm, vm.propertyNames->month, numeric);
325 RETURN_IF_EXCEPTION(scope, nullptr);
326
327 options->putDirect(vm, vm.propertyNames->day, numeric);
328 RETURN_IF_EXCEPTION(scope, nullptr);
329 }
330
331 // 8. If needDefaults is true and defaults is either "time" or "all", then
332 // Defaults is always "date". Ignore this branch.
333
334 // 9. Return options.
335 return options;
336}
337}
338
339void IntlDateTimeFormat::setFormatsFromPattern(const StringView& pattern)
340{
341 // Get all symbols from the pattern, and set format fields accordingly.
342 // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
343 unsigned length = pattern.length();
344 for (unsigned i = 0; i < length; ++i) {
345 UChar currentCharacter = pattern[i];
346 if (!isASCIIAlpha(currentCharacter))
347 continue;
348
349 unsigned count = 1;
350 while (i + 1 < length && pattern[i + 1] == currentCharacter) {
351 ++count;
352 ++i;
353 }
354
355 // If hourCycle was null, this sets it to the locale default.
356 if (m_hourCycle.isNull()) {
357 if (currentCharacter == 'h')
358 m_hourCycle = "h12"_s;
359 else if (currentCharacter == 'H')
360 m_hourCycle = "h23"_s;
361 else if (currentCharacter == 'k')
362 m_hourCycle = "h24"_s;
363 else if (currentCharacter == 'K')
364 m_hourCycle = "h11"_s;
365 }
366
367 switch (currentCharacter) {
368 case 'G':
369 if (count <= 3)
370 m_era = Era::Short;
371 else if (count == 4)
372 m_era = Era::Long;
373 else if (count == 5)
374 m_era = Era::Narrow;
375 break;
376 case 'y':
377 if (count == 1)
378 m_year = Year::Numeric;
379 else if (count == 2)
380 m_year = Year::TwoDigit;
381 break;
382 case 'M':
383 case 'L':
384 if (count == 1)
385 m_month = Month::Numeric;
386 else if (count == 2)
387 m_month = Month::TwoDigit;
388 else if (count == 3)
389 m_month = Month::Short;
390 else if (count == 4)
391 m_month = Month::Long;
392 else if (count == 5)
393 m_month = Month::Narrow;
394 break;
395 case 'E':
396 case 'e':
397 case 'c':
398 if (count <= 3)
399 m_weekday = Weekday::Short;
400 else if (count == 4)
401 m_weekday = Weekday::Long;
402 else if (count == 5)
403 m_weekday = Weekday::Narrow;
404 break;
405 case 'd':
406 if (count == 1)
407 m_day = Day::Numeric;
408 else if (count == 2)
409 m_day = Day::TwoDigit;
410 break;
411 case 'h':
412 case 'H':
413 case 'k':
414 case 'K':
415 if (count == 1)
416 m_hour = Hour::Numeric;
417 else if (count == 2)
418 m_hour = Hour::TwoDigit;
419 break;
420 case 'm':
421 if (count == 1)
422 m_minute = Minute::Numeric;
423 else if (count == 2)
424 m_minute = Minute::TwoDigit;
425 break;
426 case 's':
427 if (count == 1)
428 m_second = Second::Numeric;
429 else if (count == 2)
430 m_second = Second::TwoDigit;
431 break;
432 case 'z':
433 case 'v':
434 case 'V':
435 if (count == 1)
436 m_timeZoneName = TimeZoneName::Short;
437 else if (count == 4)
438 m_timeZoneName = TimeZoneName::Long;
439 break;
440 }
441 }
442}
443
444void IntlDateTimeFormat::initializeDateTimeFormat(JSGlobalObject* globalObject, JSValue locales, JSValue originalOptions)
445{
446 VM& vm = globalObject->vm();
447 auto scope = DECLARE_THROW_SCOPE(vm);
448
449 // 12.1.1 InitializeDateTimeFormat (dateTimeFormat, locales, options) (ECMA-402)
450 // https://tc39.github.io/ecma402/#sec-initializedatetimeformat
451
452 Vector<String> requestedLocales = canonicalizeLocaleList(globalObject, locales);
453 RETURN_IF_EXCEPTION(scope, void());
454
455 JSObject* options = IntlDTFInternal::toDateTimeOptionsAnyDate(globalObject, originalOptions);
456 RETURN_IF_EXCEPTION(scope, void());
457
458 HashMap<String, String> opt;
459
460 String localeMatcher = intlStringOption(globalObject, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit");
461 RETURN_IF_EXCEPTION(scope, void());
462 opt.add(vm.propertyNames->localeMatcher.string(), localeMatcher);
463
464 bool isHour12Undefined;
465 bool hour12 = intlBooleanOption(globalObject, options, vm.propertyNames->hour12, isHour12Undefined);
466 RETURN_IF_EXCEPTION(scope, void());
467
468 String hourCycle = intlStringOption(globalObject, options, vm.propertyNames->hourCycle, { "h11", "h12", "h23", "h24" }, "hourCycle must be \"h11\", \"h12\", \"h23\", or \"h24\"", nullptr);
469 RETURN_IF_EXCEPTION(scope, void());
470 if (isHour12Undefined) {
471 // Set hour12 here to simplify hour logic later.
472 hour12 = (hourCycle == "h11" || hourCycle == "h12");
473 if (!hourCycle.isNull())
474 opt.add("hc"_s, hourCycle);
475 } else
476 opt.add("hc"_s, String());
477
478 const HashSet<String> availableLocales = globalObject->intlDateTimeFormatAvailableLocales();
479 HashMap<String, String> resolved = resolveLocale(globalObject, availableLocales, requestedLocales, opt, IntlDTFInternal::relevantExtensionKeys, WTF_ARRAY_LENGTH(IntlDTFInternal::relevantExtensionKeys), IntlDTFInternal::localeData);
480
481 m_locale = resolved.get(vm.propertyNames->locale.string());
482 if (m_locale.isEmpty()) {
483 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat due to invalid locale"_s);
484 return;
485 }
486
487 m_calendar = resolved.get("ca"_s);
488 if (m_calendar == "gregorian")
489 m_calendar = "gregory"_s;
490 else if (m_calendar == "islamicc")
491 m_calendar = "islamic-civil"_s;
492 else if (m_calendar == "ethioaa")
493 m_calendar = "ethiopic-amete-alem"_s;
494
495 m_hourCycle = resolved.get("hc"_s);
496 m_numberingSystem = resolved.get("nu"_s);
497 String dataLocale = resolved.get("dataLocale"_s);
498
499 JSValue tzValue = options->get(globalObject, vm.propertyNames->timeZone);
500 RETURN_IF_EXCEPTION(scope, void());
501 String tz;
502 if (!tzValue.isUndefined()) {
503 String originalTz = tzValue.toWTFString(globalObject);
504 RETURN_IF_EXCEPTION(scope, void());
505 tz = canonicalizeTimeZoneName(originalTz);
506 if (tz.isNull()) {
507 throwRangeError(globalObject, scope, "invalid time zone: " + originalTz);
508 return;
509 }
510 } else
511 tz = defaultTimeZone();
512 m_timeZone = tz;
513
514 StringBuilder skeletonBuilder;
515 auto narrowShortLong = { "narrow", "short", "long" };
516 auto twoDigitNumeric = { "2-digit", "numeric" };
517 auto twoDigitNumericNarrowShortLong = { "2-digit", "numeric", "narrow", "short", "long" };
518 auto shortLong = { "short", "long" };
519
520 String weekday = intlStringOption(globalObject, options, vm.propertyNames->weekday, narrowShortLong, "weekday must be \"narrow\", \"short\", or \"long\"", nullptr);
521 RETURN_IF_EXCEPTION(scope, void());
522 if (!weekday.isNull()) {
523 if (weekday == "narrow")
524 skeletonBuilder.appendLiteral("EEEEE");
525 else if (weekday == "short")
526 skeletonBuilder.appendLiteral("EEE");
527 else if (weekday == "long")
528 skeletonBuilder.appendLiteral("EEEE");
529 }
530
531 String era = intlStringOption(globalObject, options, vm.propertyNames->era, narrowShortLong, "era must be \"narrow\", \"short\", or \"long\"", nullptr);
532 RETURN_IF_EXCEPTION(scope, void());
533 if (!era.isNull()) {
534 if (era == "narrow")
535 skeletonBuilder.appendLiteral("GGGGG");
536 else if (era == "short")
537 skeletonBuilder.appendLiteral("GGG");
538 else if (era == "long")
539 skeletonBuilder.appendLiteral("GGGG");
540 }
541
542 String year = intlStringOption(globalObject, options, vm.propertyNames->year, twoDigitNumeric, "year must be \"2-digit\" or \"numeric\"", nullptr);
543 RETURN_IF_EXCEPTION(scope, void());
544 if (!year.isNull()) {
545 if (year == "2-digit")
546 skeletonBuilder.appendLiteral("yy");
547 else if (year == "numeric")
548 skeletonBuilder.append('y');
549 }
550
551 String month = intlStringOption(globalObject, options, vm.propertyNames->month, twoDigitNumericNarrowShortLong, "month must be \"2-digit\", \"numeric\", \"narrow\", \"short\", or \"long\"", nullptr);
552 RETURN_IF_EXCEPTION(scope, void());
553 if (!month.isNull()) {
554 if (month == "2-digit")
555 skeletonBuilder.appendLiteral("MM");
556 else if (month == "numeric")
557 skeletonBuilder.append('M');
558 else if (month == "narrow")
559 skeletonBuilder.appendLiteral("MMMMM");
560 else if (month == "short")
561 skeletonBuilder.appendLiteral("MMM");
562 else if (month == "long")
563 skeletonBuilder.appendLiteral("MMMM");
564 }
565
566 String day = intlStringOption(globalObject, options, vm.propertyNames->day, twoDigitNumeric, "day must be \"2-digit\" or \"numeric\"", nullptr);
567 RETURN_IF_EXCEPTION(scope, void());
568 if (!day.isNull()) {
569 if (day == "2-digit")
570 skeletonBuilder.appendLiteral("dd");
571 else if (day == "numeric")
572 skeletonBuilder.append('d');
573 }
574
575 String hour = intlStringOption(globalObject, options, vm.propertyNames->hour, twoDigitNumeric, "hour must be \"2-digit\" or \"numeric\"", nullptr);
576 RETURN_IF_EXCEPTION(scope, void());
577 if (hour == "2-digit") {
578 if (isHour12Undefined && m_hourCycle.isNull())
579 skeletonBuilder.appendLiteral("jj");
580 else if (hour12)
581 skeletonBuilder.appendLiteral("hh");
582 else
583 skeletonBuilder.appendLiteral("HH");
584 } else if (hour == "numeric") {
585 if (isHour12Undefined && m_hourCycle.isNull())
586 skeletonBuilder.append('j');
587 else if (hour12)
588 skeletonBuilder.append('h');
589 else
590 skeletonBuilder.append('H');
591 } else
592 m_hourCycle = String();
593
594 String minute = intlStringOption(globalObject, options, vm.propertyNames->minute, twoDigitNumeric, "minute must be \"2-digit\" or \"numeric\"", nullptr);
595 RETURN_IF_EXCEPTION(scope, void());
596 if (!minute.isNull()) {
597 if (minute == "2-digit")
598 skeletonBuilder.appendLiteral("mm");
599 else if (minute == "numeric")
600 skeletonBuilder.append('m');
601 }
602
603 String second = intlStringOption(globalObject, options, vm.propertyNames->second, twoDigitNumeric, "second must be \"2-digit\" or \"numeric\"", nullptr);
604 RETURN_IF_EXCEPTION(scope, void());
605 if (!second.isNull()) {
606 if (second == "2-digit")
607 skeletonBuilder.appendLiteral("ss");
608 else if (second == "numeric")
609 skeletonBuilder.append('s');
610 }
611
612 String timeZoneName = intlStringOption(globalObject, options, vm.propertyNames->timeZoneName, shortLong, "timeZoneName must be \"short\" or \"long\"", nullptr);
613 RETURN_IF_EXCEPTION(scope, void());
614 if (!timeZoneName.isNull()) {
615 if (timeZoneName == "short")
616 skeletonBuilder.append('z');
617 else if (timeZoneName == "long")
618 skeletonBuilder.appendLiteral("zzzz");
619 }
620
621 intlStringOption(globalObject, options, vm.propertyNames->formatMatcher, { "basic", "best fit" }, "formatMatcher must be either \"basic\" or \"best fit\"", "best fit");
622 RETURN_IF_EXCEPTION(scope, void());
623
624 // Always use ICU date format generator, rather than our own pattern list and matcher.
625 UErrorCode status = U_ZERO_ERROR;
626 UDateTimePatternGenerator* generator = udatpg_open(dataLocale.utf8().data(), &status);
627 if (U_FAILURE(status)) {
628 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
629 return;
630 }
631
632 String skeleton = skeletonBuilder.toString();
633 StringView skeletonView(skeleton);
634 Vector<UChar, 32> patternBuffer(32);
635 status = U_ZERO_ERROR;
636 auto patternLength = udatpg_getBestPatternWithOptions(generator, skeletonView.upconvertedCharacters(), skeletonView.length(), UDATPG_MATCH_HOUR_FIELD_LENGTH, patternBuffer.data(), patternBuffer.size(), &status);
637 if (status == U_BUFFER_OVERFLOW_ERROR) {
638 status = U_ZERO_ERROR;
639 patternBuffer.grow(patternLength);
640 udatpg_getBestPattern(generator, skeletonView.upconvertedCharacters(), skeletonView.length(), patternBuffer.data(), patternLength, &status);
641 }
642 udatpg_close(generator);
643 if (U_FAILURE(status)) {
644 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
645 return;
646 }
647
648 // Enforce our hourCycle, replacing hour characters in pattern.
649 if (!m_hourCycle.isNull()) {
650 UChar hour = 'H';
651 if (m_hourCycle == "h11")
652 hour = 'K';
653 else if (m_hourCycle == "h12")
654 hour = 'h';
655 else if (m_hourCycle == "h24")
656 hour = 'k';
657
658 bool isEscaped = false;
659 bool hasHour = false;
660 for (auto i = 0; i < patternLength; ++i) {
661 UChar c = patternBuffer[i];
662 if (c == '\'')
663 isEscaped = !isEscaped;
664 else if (!isEscaped && (c == 'h' || c == 'H' || c == 'k' || c == 'K')) {
665 patternBuffer[i] = hour;
666 hasHour = true;
667 }
668 }
669 if (!hasHour)
670 m_hourCycle = String();
671 }
672
673 StringView pattern(patternBuffer.data(), patternLength);
674 setFormatsFromPattern(pattern);
675
676 status = U_ZERO_ERROR;
677 StringView timeZoneView(m_timeZone);
678 m_dateFormat = std::unique_ptr<UDateFormat, UDateFormatDeleter>(udat_open(UDAT_PATTERN, UDAT_PATTERN, m_locale.utf8().data(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), pattern.upconvertedCharacters(), pattern.length(), &status));
679 if (U_FAILURE(status)) {
680 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s);
681 return;
682 }
683
684 // Gregorian calendar should be used from the beginning of ECMAScript time.
685 // Failure here means unsupported calendar, and can safely be ignored.
686 UCalendar* cal = const_cast<UCalendar*>(udat_getCalendar(m_dateFormat.get()));
687 ucal_setGregorianChange(cal, minECMAScriptTime, &status);
688
689 m_initializedDateTimeFormat = true;
690}
691
692ASCIILiteral IntlDateTimeFormat::weekdayString(Weekday weekday)
693{
694 switch (weekday) {
695 case Weekday::Narrow:
696 return "narrow"_s;
697 case Weekday::Short:
698 return "short"_s;
699 case Weekday::Long:
700 return "long"_s;
701 case Weekday::None:
702 ASSERT_NOT_REACHED();
703 return ASCIILiteral::null();
704 }
705 ASSERT_NOT_REACHED();
706 return ASCIILiteral::null();
707}
708
709ASCIILiteral IntlDateTimeFormat::eraString(Era era)
710{
711 switch (era) {
712 case Era::Narrow:
713 return "narrow"_s;
714 case Era::Short:
715 return "short"_s;
716 case Era::Long:
717 return "long"_s;
718 case Era::None:
719 ASSERT_NOT_REACHED();
720 return ASCIILiteral::null();
721 }
722 ASSERT_NOT_REACHED();
723 return ASCIILiteral::null();
724}
725
726ASCIILiteral IntlDateTimeFormat::yearString(Year year)
727{
728 switch (year) {
729 case Year::TwoDigit:
730 return "2-digit"_s;
731 case Year::Numeric:
732 return "numeric"_s;
733 case Year::None:
734 ASSERT_NOT_REACHED();
735 return ASCIILiteral::null();
736 }
737 ASSERT_NOT_REACHED();
738 return ASCIILiteral::null();
739}
740
741ASCIILiteral IntlDateTimeFormat::monthString(Month month)
742{
743 switch (month) {
744 case Month::TwoDigit:
745 return "2-digit"_s;
746 case Month::Numeric:
747 return "numeric"_s;
748 case Month::Narrow:
749 return "narrow"_s;
750 case Month::Short:
751 return "short"_s;
752 case Month::Long:
753 return "long"_s;
754 case Month::None:
755 ASSERT_NOT_REACHED();
756 return ASCIILiteral::null();
757 }
758 ASSERT_NOT_REACHED();
759 return ASCIILiteral::null();
760}
761
762ASCIILiteral IntlDateTimeFormat::dayString(Day day)
763{
764 switch (day) {
765 case Day::TwoDigit:
766 return "2-digit"_s;
767 case Day::Numeric:
768 return "numeric"_s;
769 case Day::None:
770 ASSERT_NOT_REACHED();
771 return ASCIILiteral::null();
772 }
773 ASSERT_NOT_REACHED();
774 return ASCIILiteral::null();
775}
776
777ASCIILiteral IntlDateTimeFormat::hourString(Hour hour)
778{
779 switch (hour) {
780 case Hour::TwoDigit:
781 return "2-digit"_s;
782 case Hour::Numeric:
783 return "numeric"_s;
784 case Hour::None:
785 ASSERT_NOT_REACHED();
786 return ASCIILiteral::null();
787 }
788 ASSERT_NOT_REACHED();
789 return ASCIILiteral::null();
790}
791
792ASCIILiteral IntlDateTimeFormat::minuteString(Minute minute)
793{
794 switch (minute) {
795 case Minute::TwoDigit:
796 return "2-digit"_s;
797 case Minute::Numeric:
798 return "numeric"_s;
799 case Minute::None:
800 ASSERT_NOT_REACHED();
801 return ASCIILiteral::null();
802 }
803 ASSERT_NOT_REACHED();
804 return ASCIILiteral::null();
805}
806
807ASCIILiteral IntlDateTimeFormat::secondString(Second second)
808{
809 switch (second) {
810 case Second::TwoDigit:
811 return "2-digit"_s;
812 case Second::Numeric:
813 return "numeric"_s;
814 case Second::None:
815 ASSERT_NOT_REACHED();
816 return ASCIILiteral::null();
817 }
818 ASSERT_NOT_REACHED();
819 return ASCIILiteral::null();
820}
821
822ASCIILiteral IntlDateTimeFormat::timeZoneNameString(TimeZoneName timeZoneName)
823{
824 switch (timeZoneName) {
825 case TimeZoneName::Short:
826 return "short"_s;
827 case TimeZoneName::Long:
828 return "long"_s;
829 case TimeZoneName::None:
830 ASSERT_NOT_REACHED();
831 return ASCIILiteral::null();
832 }
833 ASSERT_NOT_REACHED();
834 return ASCIILiteral::null();
835}
836
837JSObject* IntlDateTimeFormat::resolvedOptions(JSGlobalObject* globalObject)
838{
839 VM& vm = globalObject->vm();
840 auto scope = DECLARE_THROW_SCOPE(vm);
841
842 // 12.3.5 Intl.DateTimeFormat.prototype.resolvedOptions() (ECMA-402 2.0)
843 // The function returns a new object whose properties and attributes are set as if constructed by an object literal assigning to each of the following properties the value of the corresponding internal slot of this DateTimeFormat object (see 12.4): locale, calendar, numberingSystem, timeZone, hour12, weekday, era, year, month, day, hour, minute, second, and timeZoneName. Properties whose corresponding internal slots are not present are not assigned.
844 // Note: In this version of the ECMAScript 2015 Internationalization API, the timeZone property will be the name of the default time zone if no timeZone property was provided in the options object provided to the Intl.DateTimeFormat constructor. The previous version left the timeZone property undefined in this case.
845 if (!m_initializedDateTimeFormat) {
846 initializeDateTimeFormat(globalObject, jsUndefined(), jsUndefined());
847 scope.assertNoException();
848 }
849
850 JSObject* options = constructEmptyObject(globalObject);
851 options->putDirect(vm, vm.propertyNames->locale, jsNontrivialString(vm, m_locale));
852 options->putDirect(vm, vm.propertyNames->calendar, jsNontrivialString(vm, m_calendar));
853 options->putDirect(vm, vm.propertyNames->numberingSystem, jsNontrivialString(vm, m_numberingSystem));
854 options->putDirect(vm, vm.propertyNames->timeZone, jsNontrivialString(vm, m_timeZone));
855
856 if (!m_hourCycle.isNull()) {
857 options->putDirect(vm, vm.propertyNames->hourCycle, jsNontrivialString(vm, m_hourCycle));
858 options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hourCycle == "h11" || m_hourCycle == "h12"));
859 }
860
861 if (m_weekday != Weekday::None)
862 options->putDirect(vm, vm.propertyNames->weekday, jsNontrivialString(vm, weekdayString(m_weekday)));
863
864 if (m_era != Era::None)
865 options->putDirect(vm, vm.propertyNames->era, jsNontrivialString(vm, eraString(m_era)));
866
867 if (m_year != Year::None)
868 options->putDirect(vm, vm.propertyNames->year, jsNontrivialString(vm, yearString(m_year)));
869
870 if (m_month != Month::None)
871 options->putDirect(vm, vm.propertyNames->month, jsNontrivialString(vm, monthString(m_month)));
872
873 if (m_day != Day::None)
874 options->putDirect(vm, vm.propertyNames->day, jsNontrivialString(vm, dayString(m_day)));
875
876 if (m_hour != Hour::None)
877 options->putDirect(vm, vm.propertyNames->hour, jsNontrivialString(vm, hourString(m_hour)));
878
879 if (m_minute != Minute::None)
880 options->putDirect(vm, vm.propertyNames->minute, jsNontrivialString(vm, minuteString(m_minute)));
881
882 if (m_second != Second::None)
883 options->putDirect(vm, vm.propertyNames->second, jsNontrivialString(vm, secondString(m_second)));
884
885 if (m_timeZoneName != TimeZoneName::None)
886 options->putDirect(vm, vm.propertyNames->timeZoneName, jsNontrivialString(vm, timeZoneNameString(m_timeZoneName)));
887
888 return options;
889}
890
891JSValue IntlDateTimeFormat::format(JSGlobalObject* globalObject, double value)
892{
893 VM& vm = globalObject->vm();
894 auto scope = DECLARE_THROW_SCOPE(vm);
895
896 // 12.3.4 FormatDateTime abstract operation (ECMA-402 2.0)
897 if (!m_initializedDateTimeFormat) {
898 initializeDateTimeFormat(globalObject, jsUndefined(), jsUndefined());
899 scope.assertNoException();
900 }
901
902 // 1. If x is not a finite Number, then throw a RangeError exception.
903 if (!std::isfinite(value))
904 return throwRangeError(globalObject, scope, "date value is not finite in DateTimeFormat format()"_s);
905
906 // Delegate remaining steps to ICU.
907 UErrorCode status = U_ZERO_ERROR;
908 Vector<UChar, 32> result(32);
909 auto resultLength = udat_format(m_dateFormat.get(), value, result.data(), result.size(), nullptr, &status);
910 if (status == U_BUFFER_OVERFLOW_ERROR) {
911 status = U_ZERO_ERROR;
912 result.grow(resultLength);
913 udat_format(m_dateFormat.get(), value, result.data(), resultLength, nullptr, &status);
914 }
915 if (U_FAILURE(status))
916 return throwTypeError(globalObject, scope, "failed to format date value"_s);
917
918 return jsString(vm, String(result.data(), resultLength));
919}
920
921#if JSC_ICU_HAS_UFIELDPOSITER
922ASCIILiteral IntlDateTimeFormat::partTypeString(UDateFormatField field)
923{
924 switch (field) {
925 case UDAT_ERA_FIELD:
926 return "era"_s;
927 case UDAT_YEAR_FIELD:
928 case UDAT_YEAR_NAME_FIELD:
929 case UDAT_EXTENDED_YEAR_FIELD:
930 return "year"_s;
931 case UDAT_MONTH_FIELD:
932 case UDAT_STANDALONE_MONTH_FIELD:
933 return "month"_s;
934 case UDAT_DATE_FIELD:
935 return "day"_s;
936 case UDAT_HOUR_OF_DAY1_FIELD:
937 case UDAT_HOUR_OF_DAY0_FIELD:
938 case UDAT_HOUR1_FIELD:
939 case UDAT_HOUR0_FIELD:
940 return "hour"_s;
941 case UDAT_MINUTE_FIELD:
942 return "minute"_s;
943 case UDAT_SECOND_FIELD:
944 case UDAT_FRACTIONAL_SECOND_FIELD:
945 return "second"_s;
946 case UDAT_DAY_OF_WEEK_FIELD:
947 case UDAT_DOW_LOCAL_FIELD:
948 case UDAT_STANDALONE_DAY_FIELD:
949 return "weekday"_s;
950 case UDAT_AM_PM_FIELD:
951#if U_ICU_VERSION_MAJOR_NUM >= 57
952 case UDAT_AM_PM_MIDNIGHT_NOON_FIELD:
953 case UDAT_FLEXIBLE_DAY_PERIOD_FIELD:
954#endif
955 return "dayPeriod"_s;
956 case UDAT_TIMEZONE_FIELD:
957 case UDAT_TIMEZONE_RFC_FIELD:
958 case UDAT_TIMEZONE_GENERIC_FIELD:
959 case UDAT_TIMEZONE_SPECIAL_FIELD:
960 case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD:
961 case UDAT_TIMEZONE_ISO_FIELD:
962 case UDAT_TIMEZONE_ISO_LOCAL_FIELD:
963 return "timeZoneName"_s;
964 // These should not show up because there is no way to specify them in DateTimeFormat options.
965 // If they do, they don't fit well into any of known part types, so consider it an "unknown".
966 case UDAT_DAY_OF_YEAR_FIELD:
967 case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD:
968 case UDAT_WEEK_OF_YEAR_FIELD:
969 case UDAT_WEEK_OF_MONTH_FIELD:
970 case UDAT_YEAR_WOY_FIELD:
971 case UDAT_JULIAN_DAY_FIELD:
972 case UDAT_MILLISECONDS_IN_DAY_FIELD:
973 case UDAT_QUARTER_FIELD:
974 case UDAT_STANDALONE_QUARTER_FIELD:
975 case UDAT_RELATED_YEAR_FIELD:
976 case UDAT_TIME_SEPARATOR_FIELD:
977#if U_ICU_VERSION_MAJOR_NUM < 58 || !defined(U_HIDE_DEPRECATED_API)
978 case UDAT_FIELD_COUNT:
979#endif
980 // Any newer additions to the UDateFormatField enum should just be considered an "unknown" part.
981 default:
982 return "unknown"_s;
983 }
984 return "unknown"_s;
985}
986
987
988JSValue IntlDateTimeFormat::formatToParts(JSGlobalObject* globalObject, double value)
989{
990 VM& vm = globalObject->vm();
991 auto scope = DECLARE_THROW_SCOPE(vm);
992
993 // 12.1.8 FormatDateTimeToParts (ECMA-402 4.0)
994 // https://tc39.github.io/ecma402/#sec-formatdatetimetoparts
995
996 if (!std::isfinite(value))
997 return throwRangeError(globalObject, scope, "date value is not finite in DateTimeFormat formatToParts()"_s);
998
999 UErrorCode status = U_ZERO_ERROR;
1000 auto fields = std::unique_ptr<UFieldPositionIterator, UFieldPositionIteratorDeleter>(ufieldpositer_open(&status));
1001 if (U_FAILURE(status))
1002 return throwTypeError(globalObject, scope, "failed to open field position iterator"_s);
1003
1004 status = U_ZERO_ERROR;
1005 Vector<UChar, 32> result(32);
1006 auto resultLength = udat_formatForFields(m_dateFormat.get(), value, result.data(), result.size(), fields.get(), &status);
1007 if (status == U_BUFFER_OVERFLOW_ERROR) {
1008 status = U_ZERO_ERROR;
1009 result.grow(resultLength);
1010 udat_formatForFields(m_dateFormat.get(), value, result.data(), resultLength, fields.get(), &status);
1011 }
1012 if (U_FAILURE(status))
1013 return throwTypeError(globalObject, scope, "failed to format date value"_s);
1014
1015 JSArray* parts = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0);
1016 if (!parts)
1017 return throwOutOfMemoryError(globalObject, scope);
1018
1019 auto resultString = String(result.data(), resultLength);
1020 auto typePropertyName = Identifier::fromString(vm, "type");
1021 auto literalString = jsString(vm, "literal"_s);
1022
1023 int32_t previousEndIndex = 0;
1024 int32_t beginIndex = 0;
1025 int32_t endIndex = 0;
1026 while (previousEndIndex < resultLength) {
1027 auto fieldType = ufieldpositer_next(fields.get(), &beginIndex, &endIndex);
1028 if (fieldType < 0)
1029 beginIndex = endIndex = resultLength;
1030
1031 if (previousEndIndex < beginIndex) {
1032 auto value = jsString(vm, resultString.substring(previousEndIndex, beginIndex - previousEndIndex));
1033 JSObject* part = constructEmptyObject(globalObject);
1034 part->putDirect(vm, typePropertyName, literalString);
1035 part->putDirect(vm, vm.propertyNames->value, value);
1036 parts->push(globalObject, part);
1037 RETURN_IF_EXCEPTION(scope, { });
1038 }
1039 previousEndIndex = endIndex;
1040
1041 if (fieldType >= 0) {
1042 auto type = jsString(vm, partTypeString(UDateFormatField(fieldType)));
1043 auto value = jsString(vm, resultString.substring(beginIndex, endIndex - beginIndex));
1044 JSObject* part = constructEmptyObject(globalObject);
1045 part->putDirect(vm, typePropertyName, type);
1046 part->putDirect(vm, vm.propertyNames->value, value);
1047 parts->push(globalObject, part);
1048 RETURN_IF_EXCEPTION(scope, { });
1049 }
1050 }
1051
1052
1053 return parts;
1054}
1055#endif
1056
1057} // namespace JSC
1058
1059#endif // ENABLE(INTL)
1060