8204224a27cac2608eb1e2fd4b6047df67cc029b
[WebKit-https.git] / Source / JavaScriptCore / runtime / IntlDateTimeFormat.cpp
1 /*
2  * Copyright (C) 2015 Andy VanWagoner (thetalecrafter@gmail.com)
3  * Copyright (C) 2016 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 namespace JSC {
45
46 const ClassInfo IntlDateTimeFormat::s_info = { "Object", &Base::s_info, 0, CREATE_METHOD_TABLE(IntlDateTimeFormat) };
47
48 static const char* const relevantExtensionKeys[2] = { "ca", "nu" };
49 static const size_t indexOfExtensionKeyCa = 0;
50 static const size_t indexOfExtensionKeyNu = 1;
51
52 void IntlDateTimeFormat::UDateFormatDeleter::operator()(UDateFormat* dateFormat) const
53 {
54     if (dateFormat)
55         udat_close(dateFormat);
56 }
57
58 IntlDateTimeFormat* IntlDateTimeFormat::create(VM& vm, Structure* structure)
59 {
60     IntlDateTimeFormat* format = new (NotNull, allocateCell<IntlDateTimeFormat>(vm.heap)) IntlDateTimeFormat(vm, structure);
61     format->finishCreation(vm);
62     return format;
63 }
64
65 Structure* IntlDateTimeFormat::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
66 {
67     return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
68 }
69
70 IntlDateTimeFormat::IntlDateTimeFormat(VM& vm, Structure* structure)
71     : JSDestructibleObject(vm, structure)
72 {
73 }
74
75 void IntlDateTimeFormat::finishCreation(VM& vm)
76 {
77     Base::finishCreation(vm);
78     ASSERT(inherits(info()));
79 }
80
81 void IntlDateTimeFormat::destroy(JSCell* cell)
82 {
83     static_cast<IntlDateTimeFormat*>(cell)->IntlDateTimeFormat::~IntlDateTimeFormat();
84 }
85
86 void IntlDateTimeFormat::visitChildren(JSCell* cell, SlotVisitor& visitor)
87 {
88     IntlDateTimeFormat* thisObject = jsCast<IntlDateTimeFormat*>(cell);
89     ASSERT_GC_OBJECT_INHERITS(thisObject, info());
90
91     Base::visitChildren(thisObject, visitor);
92
93     visitor.append(&thisObject->m_boundFormat);
94 }
95
96 void IntlDateTimeFormat::setBoundFormat(VM& vm, JSBoundFunction* format)
97 {
98     m_boundFormat.set(vm, this, format);
99 }
100
101 static String defaultTimeZone()
102 {
103     // 6.4.3 DefaultTimeZone () (ECMA-402 2.0)
104     // 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.
105
106     UErrorCode status = U_ZERO_ERROR;
107     Vector<UChar, 32> buffer(32);
108     auto bufferLength = ucal_getDefaultTimeZone(buffer.data(), buffer.size(), &status);
109     if (status == U_BUFFER_OVERFLOW_ERROR) {
110         status = U_ZERO_ERROR;
111         buffer.grow(bufferLength);
112         ucal_getDefaultTimeZone(buffer.data(), bufferLength, &status);
113     }
114     if (U_SUCCESS(status)) {
115         status = U_ZERO_ERROR;
116         Vector<UChar, 32> canonicalBuffer(32);
117         auto canonicalLength = ucal_getCanonicalTimeZoneID(buffer.data(), bufferLength, canonicalBuffer.data(), canonicalBuffer.size(), nullptr, &status);
118         if (status == U_BUFFER_OVERFLOW_ERROR) {
119             status = U_ZERO_ERROR;
120             canonicalBuffer.grow(canonicalLength);
121             ucal_getCanonicalTimeZoneID(buffer.data(), bufferLength, canonicalBuffer.data(), canonicalLength, nullptr, &status);
122         }
123         if (U_SUCCESS(status))
124             return String(canonicalBuffer.data(), canonicalLength);
125     }
126
127     return ASCIILiteral("UTC");
128 }
129
130 static String canonicalizeTimeZoneName(const String& timeZoneName)
131 {
132     // 6.4.1 IsValidTimeZoneName (timeZone)
133     // 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.
134     UErrorCode status = U_ZERO_ERROR;
135     UEnumeration* timeZones = ucal_openTimeZones(&status);
136     ASSERT(U_SUCCESS(status));
137
138     String canonical;
139     do {
140         status = U_ZERO_ERROR;
141         int32_t ianaTimeZoneLength;
142         // Time zone names are respresented as UChar[] in all related ICU apis.
143         const UChar* ianaTimeZone = uenum_unext(timeZones, &ianaTimeZoneLength, &status);
144         ASSERT(U_SUCCESS(status));
145
146         // End of enumeration.
147         if (!ianaTimeZone)
148             break;
149
150         StringView ianaTimeZoneView(ianaTimeZone, ianaTimeZoneLength);
151         if (!equalIgnoringASCIICase(timeZoneName, ianaTimeZoneView))
152             continue;
153
154         // Found a match, now canonicalize.
155         // 6.4.2 CanonicalizeTimeZoneName (timeZone) (ECMA-402 2.0)
156         // 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.
157         // 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.
158
159         Vector<UChar, 32> buffer(ianaTimeZoneLength);
160         status = U_ZERO_ERROR;
161         auto canonicalLength = ucal_getCanonicalTimeZoneID(ianaTimeZone, ianaTimeZoneLength, buffer.data(), ianaTimeZoneLength, nullptr, &status);
162         if (status == U_BUFFER_OVERFLOW_ERROR) {
163             buffer.grow(canonicalLength);
164             status = U_ZERO_ERROR;
165             ucal_getCanonicalTimeZoneID(ianaTimeZone, ianaTimeZoneLength, buffer.data(), canonicalLength, nullptr, &status);
166         }
167         ASSERT(U_SUCCESS(status));
168         canonical = String(buffer.data(), canonicalLength);
169     } while (canonical.isNull());
170     uenum_close(timeZones);
171
172     // 3. If ianaTimeZone is "Etc/UTC" or "Etc/GMT", then return "UTC".
173     if (canonical == "Etc/UTC" || canonical == "Etc/GMT")
174         canonical = ASCIILiteral("UTC");
175
176     // 4. Return ianaTimeZone.
177     return canonical;
178 }
179
180 static Vector<String> localeData(const String& locale, size_t keyIndex)
181 {
182     Vector<String> keyLocaleData;
183     switch (keyIndex) {
184     case indexOfExtensionKeyCa: {
185         UErrorCode status = U_ZERO_ERROR;
186         UEnumeration* calendars = ucal_getKeywordValuesForLocale("calendar", locale.utf8().data(), false, &status);
187         ASSERT(U_SUCCESS(status));
188
189         int32_t nameLength;
190         while (const char* availableName = uenum_next(calendars, &nameLength, &status)) {
191             ASSERT(U_SUCCESS(status));
192             String calendar = String(availableName, nameLength);
193             keyLocaleData.append(calendar);
194             // Ensure aliases used in language tag are allowed.
195             if (calendar == "gregorian")
196                 keyLocaleData.append(ASCIILiteral("gregory"));
197             else if (calendar == "islamic-civil")
198                 keyLocaleData.append(ASCIILiteral("islamicc"));
199             else if (calendar == "ethiopic-amete-alem")
200                 keyLocaleData.append(ASCIILiteral("ethioaa"));
201         }
202         uenum_close(calendars);
203         break;
204     }
205     case indexOfExtensionKeyNu:
206         keyLocaleData = numberingSystemsForLocale(locale);
207         break;
208     default:
209         ASSERT_NOT_REACHED();
210     }
211     return keyLocaleData;
212 }
213
214 static JSObject* toDateTimeOptionsAnyDate(ExecState& exec, JSValue originalOptions)
215 {
216     // 12.1.1 ToDateTimeOptions abstract operation (ECMA-402 2.0)
217     VM& vm = exec.vm();
218     auto scope = DECLARE_THROW_SCOPE(vm);
219
220     // 1. If options is undefined, then let options be null, else let options be ToObject(options).
221     // 2. ReturnIfAbrupt(options).
222     // 3. Let options be ObjectCreate(options).
223     JSObject* options;
224     if (originalOptions.isUndefined())
225         options = constructEmptyObject(&exec, exec.lexicalGlobalObject()->nullPrototypeObjectStructure());
226     else {
227         JSObject* originalToObject = originalOptions.toObject(&exec);
228         if (UNLIKELY(scope.exception()))
229             return nullptr;
230         options = constructEmptyObject(&exec, originalToObject);
231     }
232
233     // 4. Let needDefaults be true.
234     bool needDefaults = true;
235
236     // 5. If required is "date" or "any",
237     // Always "any".
238
239     // a. For each of the property names "weekday", "year", "month", "day":
240     // i. Let prop be the property name.
241     // ii. Let value be Get(options, prop).
242     // iii. ReturnIfAbrupt(value).
243     // iv. If value is not undefined, then let needDefaults be false.
244     JSValue weekday = options->get(&exec, vm.propertyNames->weekday);
245     if (UNLIKELY(scope.exception()))
246         return nullptr;
247     if (!weekday.isUndefined())
248         needDefaults = false;
249
250     JSValue year = options->get(&exec, vm.propertyNames->year);
251     if (UNLIKELY(scope.exception()))
252         return nullptr;
253     if (!year.isUndefined())
254         needDefaults = false;
255
256     JSValue month = options->get(&exec, vm.propertyNames->month);
257     if (UNLIKELY(scope.exception()))
258         return nullptr;
259     if (!month.isUndefined())
260         needDefaults = false;
261
262     JSValue day = options->get(&exec, vm.propertyNames->day);
263     if (UNLIKELY(scope.exception()))
264         return nullptr;
265     if (!day.isUndefined())
266         needDefaults = false;
267
268     // 6. If required is "time" or "any",
269     // Always "any".
270
271     // a. For each of the property names "hour", "minute", "second":
272     // i. Let prop be the property name.
273     // ii. Let value be Get(options, prop).
274     // iii. ReturnIfAbrupt(value).
275     // iv. If value is not undefined, then let needDefaults be false.
276     JSValue hour = options->get(&exec, vm.propertyNames->hour);
277     if (UNLIKELY(scope.exception()))
278         return nullptr;
279     if (!hour.isUndefined())
280         needDefaults = false;
281
282     JSValue minute = options->get(&exec, vm.propertyNames->minute);
283     if (UNLIKELY(scope.exception()))
284         return nullptr;
285     if (!minute.isUndefined())
286         needDefaults = false;
287
288     JSValue second = options->get(&exec, vm.propertyNames->second);
289     if (UNLIKELY(scope.exception()))
290         return nullptr;
291     if (!second.isUndefined())
292         needDefaults = false;
293
294     // 7. If needDefaults is true and defaults is either "date" or "all", then
295     // Defaults is always "date".
296     if (needDefaults) {
297         // a. For each of the property names "year", "month", "day":
298         // i. Let status be CreateDatePropertyOrThrow(options, prop, "numeric").
299         // ii. ReturnIfAbrupt(status).
300         JSString* numeric = jsNontrivialString(&exec, ASCIILiteral("numeric"));
301
302         options->putDirect(vm, vm.propertyNames->year, numeric);
303         if (UNLIKELY(scope.exception()))
304             return nullptr;
305
306         options->putDirect(vm, vm.propertyNames->month, numeric);
307         if (UNLIKELY(scope.exception()))
308             return nullptr;
309
310         options->putDirect(vm, vm.propertyNames->day, numeric);
311         if (UNLIKELY(scope.exception()))
312             return nullptr;
313     }
314
315     // 8. If needDefaults is true and defaults is either "time" or "all", then
316     // Defaults is always "date". Ignore this branch.
317
318     // 9. Return options.
319     return options;
320 }
321
322 void IntlDateTimeFormat::setFormatsFromPattern(const StringView& pattern)
323 {
324     // Get all symbols from the pattern, and set format fields accordingly.
325     // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
326     unsigned length = pattern.length();
327     for (unsigned i = 0; i < length; ++i) {
328         UChar currentCharacter = pattern[i];
329         if (!isASCIIAlpha(currentCharacter))
330             continue;
331
332         unsigned count = 1;
333         while (i + 1 < length && pattern[i + 1] == currentCharacter) {
334             ++count;
335             ++i;
336         }
337
338         if (currentCharacter == 'h' || currentCharacter == 'K')
339             m_hour12 = true;
340         else if (currentCharacter == 'H' || currentCharacter == 'k')
341             m_hour12 = false;
342
343         switch (currentCharacter) {
344         case 'G':
345             if (count <= 3)
346                 m_era = Era::Short;
347             else if (count == 4)
348                 m_era = Era::Long;
349             else if (count == 5)
350                 m_era = Era::Narrow;
351             break;
352         case 'y':
353             if (count == 1)
354                 m_year = Year::Numeric;
355             else if (count == 2)
356                 m_year = Year::TwoDigit;
357             break;
358         case 'M':
359         case 'L':
360             if (count == 1)
361                 m_month = Month::Numeric;
362             else if (count == 2)
363                 m_month = Month::TwoDigit;
364             else if (count == 3)
365                 m_month = Month::Short;
366             else if (count == 4)
367                 m_month = Month::Long;
368             else if (count == 5)
369                 m_month = Month::Narrow;
370             break;
371         case 'E':
372         case 'e':
373         case 'c':
374             if (count <= 3)
375                 m_weekday = Weekday::Short;
376             else if (count == 4)
377                 m_weekday = Weekday::Long;
378             else if (count == 5)
379                 m_weekday = Weekday::Narrow;
380             break;
381         case 'd':
382             if (count == 1)
383                 m_day = Day::Numeric;
384             else if (count == 2)
385                 m_day = Day::TwoDigit;
386             break;
387         case 'h':
388         case 'H':
389         case 'k':
390         case 'K':
391             if (count == 1)
392                 m_hour = Hour::Numeric;
393             else if (count == 2)
394                 m_hour = Hour::TwoDigit;
395             break;
396         case 'm':
397             if (count == 1)
398                 m_minute = Minute::Numeric;
399             else if (count == 2)
400                 m_minute = Minute::TwoDigit;
401             break;
402         case 's':
403             if (count == 1)
404                 m_second = Second::Numeric;
405             else if (count == 2)
406                 m_second = Second::TwoDigit;
407             break;
408         case 'z':
409         case 'v':
410         case 'V':
411             if (count == 1)
412                 m_timeZoneName = TimeZoneName::Short;
413             else if (count == 4)
414                 m_timeZoneName = TimeZoneName::Long;
415             break;
416         }
417     }
418 }
419
420 void IntlDateTimeFormat::initializeDateTimeFormat(ExecState& exec, JSValue locales, JSValue originalOptions)
421 {
422     VM& vm = exec.vm();
423     auto scope = DECLARE_THROW_SCOPE(vm);
424
425     // 12.1.1 InitializeDateTimeFormat (dateTimeFormat, locales, options) (ECMA-402 2.0)
426     // 1. If dateTimeFormat.[[initializedIntlObject]] is true, throw a TypeError exception.
427     // 2. Set dateTimeFormat.[[initializedIntlObject]] to true.
428
429     // 3. Let requestedLocales be CanonicalizeLocaleList(locales).
430     Vector<String> requestedLocales = canonicalizeLocaleList(exec, locales);
431     // 4. ReturnIfAbrupt(requestedLocales),
432     if (UNLIKELY(scope.exception()))
433         return;
434
435     // 5. Let options be ToDateTimeOptions(options, "any", "date").
436     JSObject* options = toDateTimeOptionsAnyDate(exec, originalOptions);
437     // 6. ReturnIfAbrupt(options).
438     if (UNLIKELY(scope.exception()))
439         return;
440
441     // 7. Let opt be a new Record.
442     HashMap<String, String> localeOpt;
443
444     // 8. Let matcher be GetOption(options, "localeMatcher", "string", «"lookup", "best fit"», "best fit").
445     String localeMatcher = intlStringOption(exec, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit");
446     // 9. ReturnIfAbrupt(matcher).
447     if (UNLIKELY(scope.exception()))
448         return;
449     // 10. Set opt.[[localeMatcher]] to matcher.
450     localeOpt.add(vm.propertyNames->localeMatcher.string(), localeMatcher);
451
452     // 11. Let localeData be the value of %DateTimeFormat%.[[localeData]].
453     // 12. Let r be ResolveLocale( %DateTimeFormat%.[[availableLocales]], requestedLocales, opt, %DateTimeFormat%.[[relevantExtensionKeys]], localeData).
454     const HashSet<String> availableLocales = exec.callee()->globalObject()->intlDateTimeFormatAvailableLocales();
455     HashMap<String, String> resolved = resolveLocale(exec, availableLocales, requestedLocales, localeOpt, relevantExtensionKeys, WTF_ARRAY_LENGTH(relevantExtensionKeys), localeData);
456
457     // 13. Set dateTimeFormat.[[locale]] to the value of r.[[locale]].
458     m_locale = resolved.get(vm.propertyNames->locale.string());
459     if (m_locale.isEmpty()) {
460         throwTypeError(&exec, scope, ASCIILiteral("failed to initialize DateTimeFormat due to invalid locale"));
461         return;
462     }
463     // 14. Set dateTimeFormat.[[calendar]] to the value of r.[[ca]].
464     m_calendar = resolved.get(ASCIILiteral("ca"));
465     // Switch to preferred aliases.
466     if (m_calendar == "gregory")
467         m_calendar = ASCIILiteral("gregorian");
468     else if (m_calendar == "islamicc")
469         m_calendar = ASCIILiteral("islamic-civil");
470     else if (m_calendar == "ethioaa")
471         m_calendar = ASCIILiteral("ethiopic-amete-alem");
472     // 15. Set dateTimeFormat.[[numberingSystem]] to the value of r.[[nu]].
473     m_numberingSystem = resolved.get(ASCIILiteral("nu"));
474     // 16. Let dataLocale be the value of r.[[dataLocale]].
475     String dataLocale = resolved.get(ASCIILiteral("dataLocale"));
476
477     // 17. Let tz be Get(options, "timeZone").
478     JSValue tzValue = options->get(&exec, vm.propertyNames->timeZone);
479     // 18. ReturnIfAbrupt(tz).
480     if (UNLIKELY(scope.exception()))
481         return;
482
483     // 19. If tz is not undefined, then
484     String tz;
485     if (!tzValue.isUndefined()) {
486         // a. Let tz be ToString(tz).
487         String originalTz = tzValue.toWTFString(&exec);
488         // b. ReturnIfAbrupt(tz).
489         if (UNLIKELY(scope.exception()))
490             return;
491         // c. If the result of IsValidTimeZoneName(tz) is false, then i. Throw a RangeError exception.
492         // d. Let tz be CanonicalizeTimeZoneName(tz).
493         tz = canonicalizeTimeZoneName(originalTz);
494         if (tz.isNull()) {
495             throwRangeError(&exec, scope, String::format("invalid time zone: %s", originalTz.utf8().data()));
496             return;
497         }
498     } else {
499         // 20. Else,
500         // a. Let tz be DefaultTimeZone().
501         tz = defaultTimeZone();
502     }
503
504     // 21. Set dateTimeFormat.[[timeZone]] to tz.
505     m_timeZone = tz;
506
507     // 22. Let opt be a new Record.
508     // Rather than building a record, build the skeleton pattern.
509     StringBuilder skeletonBuilder;
510
511     // 23. For each row of Table 3, except the header row, do:
512     // a. Let prop be the name given in the Property column of the row.
513     // b. Let value be GetOption(options, prop, "string", «the strings given in the Values column of the row», undefined).
514     // c. ReturnIfAbrupt(value).
515     // d. Set opt.[[<prop>]] to value.
516     auto narrowShortLong = { "narrow", "short", "long" };
517     auto twoDigitNumeric = { "2-digit", "numeric" };
518     auto twoDigitNumericNarrowShortLong = { "2-digit", "numeric", "narrow", "short", "long" };
519     auto shortLong = { "short", "long" };
520
521     String weekday = intlStringOption(exec, options, vm.propertyNames->weekday, narrowShortLong, "weekday must be \"narrow\", \"short\", or \"long\"", nullptr);
522     if (UNLIKELY(scope.exception()))
523         return;
524     if (!weekday.isNull()) {
525         if (weekday == "narrow")
526             skeletonBuilder.appendLiteral("EEEEE");
527         else if (weekday == "short")
528             skeletonBuilder.appendLiteral("EEE");
529         else if (weekday == "long")
530             skeletonBuilder.appendLiteral("EEEE");
531     }
532
533     String era = intlStringOption(exec, options, vm.propertyNames->era, narrowShortLong, "era must be \"narrow\", \"short\", or \"long\"", nullptr);
534     if (UNLIKELY(scope.exception()))
535         return;
536     if (!era.isNull()) {
537         if (era == "narrow")
538             skeletonBuilder.appendLiteral("GGGGG");
539         else if (era == "short")
540             skeletonBuilder.appendLiteral("GGG");
541         else if (era == "long")
542             skeletonBuilder.appendLiteral("GGGG");
543     }
544
545     String year = intlStringOption(exec, options, vm.propertyNames->year, twoDigitNumeric, "year must be \"2-digit\" or \"numeric\"", nullptr);
546     if (UNLIKELY(scope.exception()))
547         return;
548     if (!year.isNull()) {
549         if (year == "2-digit")
550             skeletonBuilder.appendLiteral("yy");
551         else if (year == "numeric")
552             skeletonBuilder.append('y');
553     }
554
555     String month = intlStringOption(exec, options, vm.propertyNames->month, twoDigitNumericNarrowShortLong, "month must be \"2-digit\", \"numeric\", \"narrow\", \"short\", or \"long\"", nullptr);
556     if (UNLIKELY(scope.exception()))
557         return;
558     if (!month.isNull()) {
559         if (month == "2-digit")
560             skeletonBuilder.appendLiteral("MM");
561         else if (month == "numeric")
562             skeletonBuilder.append('M');
563         else if (month == "narrow")
564             skeletonBuilder.appendLiteral("MMMMM");
565         else if (month == "short")
566             skeletonBuilder.appendLiteral("MMM");
567         else if (month == "long")
568             skeletonBuilder.appendLiteral("MMMM");
569     }
570
571     String day = intlStringOption(exec, options, vm.propertyNames->day, twoDigitNumeric, "day must be \"2-digit\" or \"numeric\"", nullptr);
572     if (UNLIKELY(scope.exception()))
573         return;
574     if (!day.isNull()) {
575         if (day == "2-digit")
576             skeletonBuilder.appendLiteral("dd");
577         else if (day == "numeric")
578             skeletonBuilder.append('d');
579     }
580
581     String hour = intlStringOption(exec, options, vm.propertyNames->hour, twoDigitNumeric, "hour must be \"2-digit\" or \"numeric\"", nullptr);
582     if (UNLIKELY(scope.exception()))
583         return;
584
585     // We need hour12 to make the hour skeleton pattern decision, so do this early.
586     // 32. Let hr12 be GetOption(options, "hour12", "boolean", undefined, undefined).
587     bool isHour12Undefined;
588     bool hr12 = intlBooleanOption(exec, options, vm.propertyNames->hour12, isHour12Undefined);
589     // 33. ReturnIfAbrupt(hr12).
590     if (UNLIKELY(scope.exception()))
591         return;
592
593     if (!hour.isNull()) {
594         if (isHour12Undefined) {
595             if (hour == "2-digit")
596                 skeletonBuilder.appendLiteral("jj");
597             else if (hour == "numeric")
598                 skeletonBuilder.append('j');
599         } else if (hr12) {
600             if (hour == "2-digit")
601                 skeletonBuilder.appendLiteral("hh");
602             else if (hour == "numeric")
603                 skeletonBuilder.append('h');
604         } else {
605             if (hour == "2-digit")
606                 skeletonBuilder.appendLiteral("HH");
607             else if (hour == "numeric")
608                 skeletonBuilder.append('H');
609         }
610     }
611
612     String minute = intlStringOption(exec, options, vm.propertyNames->minute, twoDigitNumeric, "minute must be \"2-digit\" or \"numeric\"", nullptr);
613     if (UNLIKELY(scope.exception()))
614         return;
615     if (!minute.isNull()) {
616         if (minute == "2-digit")
617             skeletonBuilder.appendLiteral("mm");
618         else if (minute == "numeric")
619             skeletonBuilder.append('m');
620     }
621
622     String second = intlStringOption(exec, options, vm.propertyNames->second, twoDigitNumeric, "second must be \"2-digit\" or \"numeric\"", nullptr);
623     if (UNLIKELY(scope.exception()))
624         return;
625     if (!second.isNull()) {
626         if (second == "2-digit")
627             skeletonBuilder.appendLiteral("ss");
628         else if (second == "numeric")
629             skeletonBuilder.append('s');
630     }
631
632     String timeZoneName = intlStringOption(exec, options, vm.propertyNames->timeZoneName, shortLong, "timeZoneName must be \"short\" or \"long\"", nullptr);
633     if (UNLIKELY(scope.exception()))
634         return;
635     if (!timeZoneName.isNull()) {
636         if (timeZoneName == "short")
637             skeletonBuilder.append('z');
638         else if (timeZoneName == "long")
639             skeletonBuilder.appendLiteral("zzzz");
640     }
641
642     // 24. Let dataLocaleData be Get(localeData, dataLocale).
643     // 25. Let formats be Get(dataLocaleData, "formats").
644     // 26. Let matcher be GetOption(options, "formatMatcher", "string", «"basic", "best fit"», "best fit").
645     intlStringOption(exec, options, vm.propertyNames->formatMatcher, { "basic", "best fit" }, "formatMatcher must be either \"basic\" or \"best fit\"", "best fit");
646     // 27. ReturnIfAbrupt(matcher).
647     if (UNLIKELY(scope.exception()))
648         return;
649
650     // Always use ICU date format generator, rather than our own pattern list and matcher.
651     // Covers steps 28-36.
652     UErrorCode status = U_ZERO_ERROR;
653     UDateTimePatternGenerator* generator = udatpg_open(dataLocale.utf8().data(), &status);
654     if (U_FAILURE(status)) {
655         throwTypeError(&exec, scope, ASCIILiteral("failed to initialize DateTimeFormat"));
656         return;
657     }
658
659     String skeleton = skeletonBuilder.toString();
660     StringView skeletonView(skeleton);
661     Vector<UChar, 32> patternBuffer(32);
662     status = U_ZERO_ERROR;
663     auto patternLength = udatpg_getBestPattern(generator, skeletonView.upconvertedCharacters(), skeletonView.length(), patternBuffer.data(), patternBuffer.size(), &status);
664     if (status == U_BUFFER_OVERFLOW_ERROR) {
665         status = U_ZERO_ERROR;
666         patternBuffer.grow(patternLength);
667         udatpg_getBestPattern(generator, skeletonView.upconvertedCharacters(), skeletonView.length(), patternBuffer.data(), patternLength, &status);
668     }
669     udatpg_close(generator);
670     if (U_FAILURE(status)) {
671         throwTypeError(&exec, scope, ASCIILiteral("failed to initialize DateTimeFormat"));
672         return;
673     }
674
675     StringView pattern(patternBuffer.data(), patternLength);
676     setFormatsFromPattern(pattern);
677
678     status = U_ZERO_ERROR;
679     StringView timeZoneView(m_timeZone);
680     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));
681     if (U_FAILURE(status)) {
682         throwTypeError(&exec, scope, ASCIILiteral("failed to initialize DateTimeFormat"));
683         return;
684     }
685
686     // 37. Set dateTimeFormat.[[boundFormat]] to undefined.
687     // Already undefined.
688
689     // 38. Set dateTimeFormat.[[initializedDateTimeFormat]] to true.
690     m_initializedDateTimeFormat = true;
691
692     // 39. Return dateTimeFormat.
693 }
694
695 const char* IntlDateTimeFormat::weekdayString(Weekday weekday)
696 {
697     switch (weekday) {
698     case Weekday::Narrow:
699         return "narrow";
700     case Weekday::Short:
701         return "short";
702     case Weekday::Long:
703         return "long";
704     case Weekday::None:
705         ASSERT_NOT_REACHED();
706         return nullptr;
707     }
708     ASSERT_NOT_REACHED();
709     return nullptr;
710 }
711
712 const char* IntlDateTimeFormat::eraString(Era era)
713 {
714     switch (era) {
715     case Era::Narrow:
716         return "narrow";
717     case Era::Short:
718         return "short";
719     case Era::Long:
720         return "long";
721     case Era::None:
722         ASSERT_NOT_REACHED();
723         return nullptr;
724     }
725     ASSERT_NOT_REACHED();
726     return nullptr;
727 }
728
729 const char* IntlDateTimeFormat::yearString(Year year)
730 {
731     switch (year) {
732     case Year::TwoDigit:
733         return "2-digit";
734     case Year::Numeric:
735         return "numeric";
736     case Year::None:
737         ASSERT_NOT_REACHED();
738         return nullptr;
739     }
740     ASSERT_NOT_REACHED();
741     return nullptr;
742 }
743
744 const char* IntlDateTimeFormat::monthString(Month month)
745 {
746     switch (month) {
747     case Month::TwoDigit:
748         return "2-digit";
749     case Month::Numeric:
750         return "numeric";
751     case Month::Narrow:
752         return "narrow";
753     case Month::Short:
754         return "short";
755     case Month::Long:
756         return "long";
757     case Month::None:
758         ASSERT_NOT_REACHED();
759         return nullptr;
760     }
761     ASSERT_NOT_REACHED();
762     return nullptr;
763 }
764
765 const char* IntlDateTimeFormat::dayString(Day day)
766 {
767     switch (day) {
768     case Day::TwoDigit:
769         return "2-digit";
770     case Day::Numeric:
771         return "numeric";
772     case Day::None:
773         ASSERT_NOT_REACHED();
774         return nullptr;
775     }
776     ASSERT_NOT_REACHED();
777     return nullptr;
778 }
779
780 const char* IntlDateTimeFormat::hourString(Hour hour)
781 {
782     switch (hour) {
783     case Hour::TwoDigit:
784         return "2-digit";
785     case Hour::Numeric:
786         return "numeric";
787     case Hour::None:
788         ASSERT_NOT_REACHED();
789         return nullptr;
790     }
791     ASSERT_NOT_REACHED();
792     return nullptr;
793 }
794
795 const char* IntlDateTimeFormat::minuteString(Minute minute)
796 {
797     switch (minute) {
798     case Minute::TwoDigit:
799         return "2-digit";
800     case Minute::Numeric:
801         return "numeric";
802     case Minute::None:
803         ASSERT_NOT_REACHED();
804         return nullptr;
805     }
806     ASSERT_NOT_REACHED();
807     return nullptr;
808 }
809
810 const char* IntlDateTimeFormat::secondString(Second second)
811 {
812     switch (second) {
813     case Second::TwoDigit:
814         return "2-digit";
815     case Second::Numeric:
816         return "numeric";
817     case Second::None:
818         ASSERT_NOT_REACHED();
819         return nullptr;
820     }
821     ASSERT_NOT_REACHED();
822     return nullptr;
823 }
824
825 const char* IntlDateTimeFormat::timeZoneNameString(TimeZoneName timeZoneName)
826 {
827     switch (timeZoneName) {
828     case TimeZoneName::Short:
829         return "short";
830     case TimeZoneName::Long:
831         return "long";
832     case TimeZoneName::None:
833         ASSERT_NOT_REACHED();
834         return nullptr;
835     }
836     ASSERT_NOT_REACHED();
837     return nullptr;
838 }
839
840 JSObject* IntlDateTimeFormat::resolvedOptions(ExecState& exec)
841 {
842     VM& vm = exec.vm();
843     auto scope = DECLARE_THROW_SCOPE(vm);
844
845     // 12.3.5 Intl.DateTimeFormat.prototype.resolvedOptions() (ECMA-402 2.0)
846     // 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.
847     // 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.
848     if (!m_initializedDateTimeFormat) {
849         initializeDateTimeFormat(exec, jsUndefined(), jsUndefined());
850         ASSERT_UNUSED(scope, !scope.exception());
851     }
852
853     JSObject* options = constructEmptyObject(&exec);
854     options->putDirect(vm, vm.propertyNames->locale, jsNontrivialString(&exec, m_locale));
855     options->putDirect(vm, vm.propertyNames->calendar, jsNontrivialString(&exec, m_calendar));
856     options->putDirect(vm, vm.propertyNames->numberingSystem, jsNontrivialString(&exec, m_numberingSystem));
857     options->putDirect(vm, vm.propertyNames->timeZone, jsNontrivialString(&exec, m_timeZone));
858
859     if (m_weekday != Weekday::None)
860         options->putDirect(vm, vm.propertyNames->weekday, jsNontrivialString(&exec, ASCIILiteral(weekdayString(m_weekday))));
861
862     if (m_era != Era::None)
863         options->putDirect(vm, vm.propertyNames->era, jsNontrivialString(&exec, ASCIILiteral(eraString(m_era))));
864
865     if (m_year != Year::None)
866         options->putDirect(vm, vm.propertyNames->year, jsNontrivialString(&exec, ASCIILiteral(yearString(m_year))));
867
868     if (m_month != Month::None)
869         options->putDirect(vm, vm.propertyNames->month, jsNontrivialString(&exec, ASCIILiteral(monthString(m_month))));
870
871     if (m_day != Day::None)
872         options->putDirect(vm, vm.propertyNames->day, jsNontrivialString(&exec, ASCIILiteral(dayString(m_day))));
873
874     if (m_hour != Hour::None) {
875         options->putDirect(vm, vm.propertyNames->hour, jsNontrivialString(&exec, ASCIILiteral(hourString(m_hour))));
876         options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hour12));
877     }
878
879     if (m_minute != Minute::None)
880         options->putDirect(vm, vm.propertyNames->minute, jsNontrivialString(&exec, ASCIILiteral(minuteString(m_minute))));
881
882     if (m_second != Second::None)
883         options->putDirect(vm, vm.propertyNames->second, jsNontrivialString(&exec, ASCIILiteral(secondString(m_second))));
884
885     if (m_timeZoneName != TimeZoneName::None)
886         options->putDirect(vm, vm.propertyNames->timeZoneName, jsNontrivialString(&exec, ASCIILiteral(timeZoneNameString(m_timeZoneName))));
887
888     return options;
889 }
890
891 JSValue IntlDateTimeFormat::format(ExecState& exec, double value)
892 {
893     VM& vm = exec.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(exec, jsUndefined(), jsUndefined());
899         ASSERT(!scope.exception());
900     }
901
902     // 1. If x is not a finite Number, then throw a RangeError exception.
903     if (!std::isfinite(value))
904         return throwRangeError(&exec, scope, ASCIILiteral("date value is not finite in DateTimeFormat format()"));
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(&exec, scope, ASCIILiteral("failed to format date value"));
917
918     return jsString(&exec, String(result.data(), resultLength));
919 }
920
921 } // namespace JSC
922
923 #endif // ENABLE(INTL)