[INTL] Implement Intl.DateTimeFormat.prototype.resolvedOptions ()
[WebKit.git] / Source / JavaScriptCore / runtime / IntlDateTimeFormat.cpp
index dc5b2ac..1167eb3 100644 (file)
@@ -28,7 +28,6 @@
 
 #if ENABLE(INTL)
 
-#include "DateConstructor.h"
 #include "DateInstance.h"
 #include "Error.h"
 #include "IntlDateTimeFormatConstructor.h"
 #include "JSBoundFunction.h"
 #include "JSCJSValueInlines.h"
 #include "JSCellInlines.h"
+#include "ObjectConstructor.h"
 #include "SlotVisitorInlines.h"
 #include "StructureInlines.h"
+#include <unicode/ucal.h>
+#include <unicode/udat.h>
+#include <unicode/udatpg.h>
+#include <unicode/uenum.h>
 
 namespace JSC {
 
 const ClassInfo IntlDateTimeFormat::s_info = { "Object", &Base::s_info, 0, CREATE_METHOD_TABLE(IntlDateTimeFormat) };
 
+static const char* const relevantExtensionKeys[2] = { "ca", "nu" };
+static const size_t indexOfExtensionKeyCa = 0;
+static const size_t indexOfExtensionKeyNu = 1;
+
+enum class DateTimeOptionRequired { Date, Time, Any };
+enum class DateTimeOptionDefaults { Date, Time, All };
+
 IntlDateTimeFormat* IntlDateTimeFormat::create(VM& vm, IntlDateTimeFormatConstructor* constructor)
 {
     IntlDateTimeFormat* format = new (NotNull, allocateCell<IntlDateTimeFormat>(vm.heap)) IntlDateTimeFormat(vm, constructor->dateTimeFormatStructure());
@@ -86,46 +97,816 @@ void IntlDateTimeFormat::setBoundFormat(VM& vm, JSBoundFunction* format)
     m_boundFormat.set(vm, this, format);
 }
 
-EncodedJSValue JSC_HOST_CALL IntlDateTimeFormatFuncFormatDateTime(ExecState* state)
+static String defaultTimeZone()
+{
+    // 6.4.3 DefaultTimeZone () (ECMA-402 2.0)
+    // 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.
+
+    UErrorCode status = U_ZERO_ERROR;
+    Vector<UChar, 32> buffer(32);
+    auto bufferLength = ucal_getDefaultTimeZone(buffer.data(), buffer.capacity(), &status);
+    if (status == U_BUFFER_OVERFLOW_ERROR) {
+        status = U_ZERO_ERROR;
+        buffer = Vector<UChar, 32>(bufferLength);
+        ucal_getDefaultTimeZone(buffer.data(), bufferLength, &status);
+    }
+    if (U_SUCCESS(status)) {
+        status = U_ZERO_ERROR;
+        UBool isSystemID = false;
+        Vector<UChar, 32> canonicalBuffer(32);
+        auto canonicalLength = ucal_getCanonicalTimeZoneID(buffer.data(), bufferLength, canonicalBuffer.data(), canonicalBuffer.capacity(), &isSystemID, &status);
+        if (status == U_BUFFER_OVERFLOW_ERROR) {
+            status = U_ZERO_ERROR;
+            canonicalBuffer = Vector<UChar, 32>(canonicalLength);
+            ucal_getCanonicalTimeZoneID(buffer.data(), bufferLength, canonicalBuffer.data(), canonicalLength, &isSystemID, &status);
+        }
+        if (U_SUCCESS(status))
+            return String(canonicalBuffer.data(), canonicalLength);
+    }
+
+    return ASCIILiteral("UTC");
+}
+
+static String canonicalizeTimeZoneName(const String& timeZoneName)
+{
+    // 6.4.1 IsValidTimeZoneName (timeZone)
+    // 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.
+    UErrorCode status = U_ZERO_ERROR;
+    UEnumeration* timeZones = ucal_openTimeZones(&status);
+    ASSERT(U_SUCCESS(status));
+
+    String canonical;
+    do {
+        status = U_ZERO_ERROR;
+        int32_t ianaTimeZoneLength;
+        // Time zone names are respresented as UChar[] in all related ICU apis.
+        const UChar* ianaTimeZone = uenum_unext(timeZones, &ianaTimeZoneLength, &status);
+        ASSERT(U_SUCCESS(status));
+
+        // End of enumeration.
+        if (!ianaTimeZone)
+            break;
+
+        StringView ianaTimeZoneView(ianaTimeZone, ianaTimeZoneLength);
+        if (!equalIgnoringASCIICase(timeZoneName, ianaTimeZoneView))
+            continue;
+
+        // Found a match, now canonicalize.
+        // 6.4.2 CanonicalizeTimeZoneName (timeZone) (ECMA-402 2.0)
+        // 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.
+        // 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.
+
+        Vector<UChar, 32> buffer(ianaTimeZoneLength);
+        UBool isSystemID = false;
+        status = U_ZERO_ERROR;
+        auto canonicalLength = ucal_getCanonicalTimeZoneID(ianaTimeZone, ianaTimeZoneLength, buffer.data(), ianaTimeZoneLength, &isSystemID, &status);
+        if (status == U_BUFFER_OVERFLOW_ERROR) {
+            buffer = Vector<UChar, 32>(canonicalLength);
+            isSystemID = false;
+            status = U_ZERO_ERROR;
+            ucal_getCanonicalTimeZoneID(ianaTimeZone, ianaTimeZoneLength, buffer.data(), canonicalLength, &isSystemID, &status);
+        }
+        ASSERT(U_SUCCESS(status));
+        canonical = String(buffer.data(), canonicalLength);
+    } while (canonical.isNull());
+    uenum_close(timeZones);
+
+    // 3. If ianaTimeZone is "Etc/UTC" or "Etc/GMT", then return "UTC".
+    if (canonical == "Etc/UTC" || canonical == "Etc/GMT")
+        canonical = ASCIILiteral("UTC");
+
+    // 4. Return ianaTimeZone.
+    return canonical;
+}
+
+static Vector<String> localeData(const String& locale, size_t keyIndex)
 {
-    // 12.3.4 DateTime Format Functions (ECMA-402 2.0)
-    // 1. Let dtf be the this value.
-    IntlDateTimeFormat* format = jsDynamicCast<IntlDateTimeFormat*>(state->thisValue());
-    // 2. Assert: Type(dtf) is Object and dtf has an [[initializedDateTimeFormat]] internal slot whose value is true.
-    if (!format)
-        return JSValue::encode(throwTypeError(state));
+    Vector<String> keyLocaleData;
+    switch (keyIndex) {
+    case indexOfExtensionKeyCa: {
+        UErrorCode status = U_ZERO_ERROR;
+        UEnumeration* calendars = ucal_getKeywordValuesForLocale("calendar", locale.utf8().data(), false, &status);
+        ASSERT(U_SUCCESS(status));
+
+        status = U_ZERO_ERROR;
+        int32_t nameLength;
+        while (const char* availableName = uenum_next(calendars, &nameLength, &status)) {
+            ASSERT(U_SUCCESS(status));
+            status = U_ZERO_ERROR;
+            String calendar = String(availableName, nameLength);
+            keyLocaleData.append(calendar);
+            // Ensure aliases used in language tag are allowed.
+            if (calendar == "gregorian")
+                keyLocaleData.append(ASCIILiteral("gregory"));
+            else if (calendar == "islamic-civil")
+                keyLocaleData.append(ASCIILiteral("islamicc"));
+            else if (calendar == "ethiopic-amete-alem")
+                keyLocaleData.append(ASCIILiteral("ethioaa"));
+        }
+        uenum_close(calendars);
+        break;
+    }
+    case indexOfExtensionKeyNu:
+        keyLocaleData = getNumberingSystemsForLocale(locale);
+        break;
+    default:
+        ASSERT_NOT_REACHED();
+    }
+    return keyLocaleData;
+}
+
+static JSObject* toDateTimeOptions(ExecState& exec, JSValue originalOptions, const DateTimeOptionRequired required, const DateTimeOptionDefaults defaults)
+{
+    // 12.1.1 ToDateTimeOptions abstract operation (ECMA-402 2.0)
+    VM& vm = exec.vm();
+
+    // 1. If options is undefined, then let options be null, else let options be ToObject(options).
+    // 2. ReturnIfAbrupt(options).
+    // 3. Let options be ObjectCreate(options).
+    JSObject* options;
+    if (originalOptions.isUndefinedOrNull())
+        options = constructEmptyObject(&exec, exec.lexicalGlobalObject()->nullPrototypeObjectStructure());
+    else {
+        JSObject* originalToObject = originalOptions.toObject(&exec);
+        if (exec.hadException())
+            return nullptr;
+        options = constructEmptyObject(&exec, originalToObject);
+    }
+
+    // 4. Let needDefaults be true.
+    bool needDefaults = true;
+
+    // 5. If required is "date" or "any",
+    if (required == DateTimeOptionRequired::Date || required == DateTimeOptionRequired::Any) {
+        // a. For each of the property names "weekday", "year", "month", "day":
+        // i. Let prop be the property name.
+        // ii. Let value be Get(options, prop).
+        // iii. ReturnIfAbrupt(value).
+        // iv. If value is not undefined, then let needDefaults be false.
+        JSValue weekday = options->get(&exec, vm.propertyNames->weekday);
+        if (exec.hadException())
+            return nullptr;
+        if (!weekday.isUndefined())
+            needDefaults = false;
+
+        JSValue year = options->get(&exec, vm.propertyNames->year);
+        if (exec.hadException())
+            return nullptr;
+        if (!year.isUndefined())
+            needDefaults = false;
+
+        JSValue month = options->get(&exec, vm.propertyNames->month);
+        if (exec.hadException())
+            return nullptr;
+        if (!month.isUndefined())
+            needDefaults = false;
+
+        JSValue day = options->get(&exec, vm.propertyNames->day);
+        if (exec.hadException())
+            return nullptr;
+        if (!day.isUndefined())
+            needDefaults = false;
+    }
+
+    // 6. If required is "time" or "any",
+    if (required == DateTimeOptionRequired::Time || required == DateTimeOptionRequired::Any) {
+        // a. For each of the property names "hour", "minute", "second":
+        // i. Let prop be the property name.
+        // ii. Let value be Get(options, prop).
+        // iii. ReturnIfAbrupt(value).
+        // iv. If value is not undefined, then let needDefaults be false.
+        JSValue hour = options->get(&exec, vm.propertyNames->hour);
+        if (exec.hadException())
+            return nullptr;
+        if (!hour.isUndefined())
+            needDefaults = false;
+
+        JSValue minute = options->get(&exec, vm.propertyNames->minute);
+        if (exec.hadException())
+            return nullptr;
+        if (!minute.isUndefined())
+            needDefaults = false;
+
+        JSValue second = options->get(&exec, vm.propertyNames->second);
+        if (exec.hadException())
+            return nullptr;
+        if (!second.isUndefined())
+            needDefaults = false;
+    }
+
+    // 7. If needDefaults is true and defaults is either "date" or "all", then
+    if (needDefaults && (defaults == DateTimeOptionDefaults::Date || defaults == DateTimeOptionDefaults::All)) {
+        // a. For each of the property names "year", "month", "day":
+        // i. Let status be CreateDatePropertyOrThrow(options, prop, "numeric").
+        // ii. ReturnIfAbrupt(status).
+        options->putDirect(vm, vm.propertyNames->year, jsString(&exec, "numeric"));
+        if (exec.hadException())
+            return nullptr;
 
-    JSValue date = state->argument(0);
-    double value;
+        options->putDirect(vm, vm.propertyNames->month, jsString(&exec, "numeric"));
+        if (exec.hadException())
+            return nullptr;
 
-    // 3. If date is not provided or is undefined, then
-    if (date.isUndefined()) {
-        // a. Let x be %Date_now%().
-        value = JSValue::decode(dateNow(state)).toNumber(state);
+        options->putDirect(vm, vm.propertyNames->day, jsString(&exec, "numeric"));
+        if (exec.hadException())
+            return nullptr;
+    }
+
+    // 8. If needDefaults is true and defaults is either "time" or "all", then
+    if (needDefaults && (defaults == DateTimeOptionDefaults::Time || defaults == DateTimeOptionDefaults::All)) {
+        // a. For each of the property names "hour", "minute", "second":
+        // i. Let status be CreateDatePropertyOrThrow(options, prop, "numeric").
+        // ii. ReturnIfAbrupt(status).
+        options->putDirect(vm, vm.propertyNames->hour, jsString(&exec, "numeric"));
+        if (exec.hadException())
+            return nullptr;
+
+        options->putDirect(vm, vm.propertyNames->minute, jsString(&exec, "numeric"));
+        if (exec.hadException())
+            return nullptr;
+
+        options->putDirect(vm, vm.propertyNames->second, jsString(&exec, "numeric"));
+        if (exec.hadException())
+            return nullptr;
+    }
+
+    // 9. Return options.
+    return options;
+}
+
+void IntlDateTimeFormat::setFormatsFromPattern(const StringView& pattern)
+{
+    // Get all symbols from the pattern, and set format fields accordingly.
+    // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
+    unsigned length = pattern.length();
+    for (unsigned i = 0; i < length; ++i) {
+        UChar currentCharacter = pattern[i];
+        if (!isASCIIAlpha(currentCharacter))
+            continue;
+
+        unsigned count = 1;
+        while (i + 1 < length && pattern[i + 1] == currentCharacter) {
+            ++count;
+            ++i;
+        }
+
+        if (currentCharacter == 'h' || currentCharacter == 'K')
+            m_hour12 = true;
+        else if (currentCharacter == 'H' || currentCharacter == 'k')
+            m_hour12 = false;
+
+        switch (currentCharacter) {
+        case 'G':
+            if (count <= 3)
+                m_era = Era::Short;
+            else if (count == 4)
+                m_era = Era::Long;
+            else if (count == 5)
+                m_era = Era::Narrow;
+            break;
+        case 'y':
+            if (count == 1)
+                m_year = Year::Numeric;
+            else if (count == 2)
+                m_year = Year::TwoDigit;
+            break;
+        case 'M':
+        case 'L':
+            if (count == 1)
+                m_month = Month::Numeric;
+            else if (count == 2)
+                m_month = Month::TwoDigit;
+            else if (count == 3)
+                m_month = Month::Short;
+            else if (count == 4)
+                m_month = Month::Long;
+            else if (count == 5)
+                m_month = Month::Narrow;
+            break;
+        case 'E':
+        case 'e':
+        case 'c':
+            if (count <= 3)
+                m_weekday = Weekday::Short;
+            else if (count == 4)
+                m_weekday = Weekday::Long;
+            else if (count == 5)
+                m_weekday = Weekday::Narrow;
+            break;
+        case 'd':
+            if (count == 1)
+                m_day = Day::Numeric;
+            else if (count == 2)
+                m_day = Day::TwoDigit;
+            break;
+        case 'h':
+        case 'H':
+        case 'k':
+        case 'K':
+            if (count == 1)
+                m_hour = Hour::Numeric;
+            else if (count == 2)
+                m_hour = Hour::TwoDigit;
+            break;
+        case 'm':
+            if (count == 1)
+                m_minute = Minute::Numeric;
+            else if (count == 2)
+                m_minute = Minute::TwoDigit;
+            break;
+        case 's':
+            if (count == 1)
+                m_second = Second::Numeric;
+            else if (count == 2)
+                m_second = Second::TwoDigit;
+            break;
+        case 'z':
+        case 'v':
+        case 'V':
+            if (count == 1)
+                m_timeZoneName = TimeZoneName::Short;
+            else if (count == 4)
+                m_timeZoneName = TimeZoneName::Long;
+            break;
+        }
+    }
+}
+
+void IntlDateTimeFormat::initializeDateTimeFormat(ExecState& exec, JSValue locales, JSValue originalOptions)
+{
+    // 12.1.1 InitializeDateTimeFormat (dateTimeFormat, locales, options) (ECMA-402 2.0)
+    // 1. If dateTimeFormat.[[initializedIntlObject]] is true, throw a TypeError exception.
+    // 2. Set dateTimeFormat.[[initializedIntlObject]] to true.
+
+    // 3. Let requestedLocales be CanonicalizeLocaleList(locales).
+    Vector<String> requestedLocales = canonicalizeLocaleList(exec, locales);
+    // 4. ReturnIfAbrupt(requestedLocales),
+    if (exec.hadException())
+        return;
+
+    // 5. Let options be ToDateTimeOptions(options, "any", "date").
+    JSObject* options = toDateTimeOptions(exec, originalOptions, DateTimeOptionRequired::Any, DateTimeOptionDefaults::Date);
+    // 6. ReturnIfAbrupt(options).
+    if (exec.hadException())
+        return;
+
+    // 7. Let opt be a new Record.
+    HashMap<String, String> localeOpt;
+
+    // 8. Let matcher be GetOption(options, "localeMatcher", "string", «"lookup", "best fit"», "best fit").
+    VM& vm = exec.vm();
+    String localeMatcher = intlStringOption(exec, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit");
+    // 9. ReturnIfAbrupt(matcher).
+    if (exec.hadException())
+        return;
+    // 10. Set opt.[[localeMatcher]] to matcher.
+    localeOpt.set(vm.propertyNames->localeMatcher.string(), localeMatcher);
+
+    // 11. Let localeData be the value of %DateTimeFormat%.[[localeData]].
+    // 12. Let r be ResolveLocale( %DateTimeFormat%.[[availableLocales]], requestedLocales, opt, %DateTimeFormat%.[[relevantExtensionKeys]], localeData).
+    const HashSet<String> availableLocales = exec.lexicalGlobalObject()->intlDateTimeFormatAvailableLocales();
+    HashMap<String, String> resolved = resolveLocale(availableLocales, requestedLocales, localeOpt, relevantExtensionKeys, WTF_ARRAY_LENGTH(relevantExtensionKeys), localeData);
+
+    // 13. Set dateTimeFormat.[[locale]] to the value of r.[[locale]].
+    m_locale = resolved.get(vm.propertyNames->locale.string());
+    // 14. Set dateTimeFormat.[[calendar]] to the value of r.[[ca]].
+    m_calendar = resolved.get(ASCIILiteral("ca"));
+    // Switch to preferred aliases.
+    if (m_calendar == "gregory")
+        m_calendar = ASCIILiteral("gregorian");
+    else if (m_calendar == "islamicc")
+        m_calendar = ASCIILiteral("islamic-civil");
+    else if (m_calendar == "ethioaa")
+        m_calendar = ASCIILiteral("ethiopic-amete-alem");
+    // 15. Set dateTimeFormat.[[numberingSystem]] to the value of r.[[nu]].
+    m_numberingSystem = resolved.get(ASCIILiteral("nu"));
+    // 16. Let dataLocale be the value of r.[[dataLocale]].
+    String dataLocale = resolved.get(ASCIILiteral("dataLocale"));
+
+    // 17. Let tz be Get(options, "timeZone").
+    JSValue tzValue = options->get(&exec, vm.propertyNames->timeZone);
+    // 18. ReturnIfAbrupt(tz).
+    if (exec.hadException())
+        return;
+
+    // 19. If tz is not undefined, then
+    String tz;
+    if (!tzValue.isUndefined()) {
+        // a. Let tz be ToString(tz).
+        String originalTz = tzValue.toWTFString(&exec);
+        // b. ReturnIfAbrupt(tz).
+        if (exec.hadException())
+            return;
+        // c. If the result of IsValidTimeZoneName(tz) is false, then i. Throw a RangeError exception.
+        // d. Let tz be CanonicalizeTimeZoneName(tz).
+        tz = canonicalizeTimeZoneName(originalTz);
+        if (tz.isNull()) {
+            throwRangeError(&exec, String::format("invalid time zone: %s", originalTz.utf8().data()));
+            return;
+        }
     } else {
-        // 4. Else
-        // a. Let x be ToNumber(date).
-        value = date.toNumber(state);
-        // b. ReturnIfAbrupt(x).
-        if (state->hadException())
-            return JSValue::encode(jsUndefined());
+        // 20. Else,
+        // a. Let tz be DefaultTimeZone().
+        tz = defaultTimeZone();
+    }
+
+    // 21. Set dateTimeFormat.[[timeZone]] to tz.
+    m_timeZone = tz;
+
+    // 22. Let opt be a new Record.
+    // Rather than building a record, build the skeleton pattern.
+    StringBuilder skeletonBuilder;
+
+    // 23. For each row of Table 3, except the header row, do:
+    // a. Let prop be the name given in the Property column of the row.
+    // b. Let value be GetOption(options, prop, "string", «the strings given in the Values column of the row», undefined).
+    // c. ReturnIfAbrupt(value).
+    // d. Set opt.[[<prop>]] to value.
+    auto narrowShortLong = { "narrow", "short", "long" };
+    auto twoDigitNumeric = { "2-digit", "numeric" };
+    auto twoDigitNumericNarrowShortLong = { "2-digit", "numeric", "narrow", "short", "long" };
+    auto shortLong = { "short", "long" };
+
+    String weekday = intlStringOption(exec, options, vm.propertyNames->weekday, narrowShortLong, "weekday must be \"narrow\", \"short\", or \"long\"", nullptr);
+    if (exec.hadException())
+        return;
+    if (!weekday.isNull()) {
+        if (weekday == "narrow")
+            skeletonBuilder.appendLiteral("EEEEE");
+        else if (weekday == "short")
+            skeletonBuilder.appendLiteral("EEE");
+        else if (weekday == "long")
+            skeletonBuilder.appendLiteral("EEEE");
+    }
+
+    String era = intlStringOption(exec, options, vm.propertyNames->era, narrowShortLong, "era must be \"narrow\", \"short\", or \"long\"", nullptr);
+    if (exec.hadException())
+        return;
+    if (!era.isNull()) {
+        if (era == "narrow")
+            skeletonBuilder.appendLiteral("GGGGG");
+        else if (era == "short")
+            skeletonBuilder.appendLiteral("GGG");
+        else if (era == "long")
+            skeletonBuilder.appendLiteral("GGGG");
+    }
+
+    String year = intlStringOption(exec, options, vm.propertyNames->year, twoDigitNumeric, "year must be \"2-digit\" or \"numeric\"", nullptr);
+    if (exec.hadException())
+        return;
+    if (!year.isNull()) {
+        if (year == "2-digit")
+            skeletonBuilder.appendLiteral("yy");
+        else if (year == "numeric")
+            skeletonBuilder.append('y');
+    }
+
+    String month = intlStringOption(exec, options, vm.propertyNames->month, twoDigitNumericNarrowShortLong, "month must be \"2-digit\", \"numeric\", \"narrow\", \"short\", or \"long\"", nullptr);
+    if (exec.hadException())
+        return;
+    if (!month.isNull()) {
+        if (month == "2-digit")
+            skeletonBuilder.appendLiteral("MM");
+        else if (month == "numeric")
+            skeletonBuilder.append('M');
+        else if (month == "narrow")
+            skeletonBuilder.appendLiteral("MMMMM");
+        else if (month == "short")
+            skeletonBuilder.appendLiteral("MMM");
+        else if (month == "long")
+            skeletonBuilder.appendLiteral("MMMM");
+    }
+
+    String day = intlStringOption(exec, options, vm.propertyNames->day, twoDigitNumeric, "day must be \"2-digit\" or \"numeric\"", nullptr);
+    if (exec.hadException())
+        return;
+    if (!day.isNull()) {
+        if (day == "2-digit")
+            skeletonBuilder.appendLiteral("dd");
+        else if (day == "numeric")
+            skeletonBuilder.append('d');
     }
 
-    // 5. Return FormatDateTime(dtf, x).
+    String hour = intlStringOption(exec, options, vm.propertyNames->hour, twoDigitNumeric, "hour must be \"2-digit\" or \"numeric\"", nullptr);
+    if (exec.hadException())
+        return;
 
+    // We need hour12 to make the hour skeleton pattern decision, so do this early.
+    // 32. Let hr12 be GetOption(options, "hour12", "boolean", undefined, undefined).
+    bool isHour12Undefined = true;
+    bool hr12 = intlBooleanOption(exec, options, vm.propertyNames->hour12, isHour12Undefined);
+    // 33. ReturnIfAbrupt(hr12).
+    if (exec.hadException())
+        return;
+
+    if (!hour.isNull()) {
+        if (isHour12Undefined) {
+            if (hour == "2-digit")
+                skeletonBuilder.appendLiteral("jj");
+            else if (hour == "numeric")
+                skeletonBuilder.append('j');
+        } else if (hr12) {
+            if (hour == "2-digit")
+                skeletonBuilder.appendLiteral("hh");
+            else if (hour == "numeric")
+                skeletonBuilder.append('h');
+        } else {
+            if (hour == "2-digit")
+                skeletonBuilder.appendLiteral("HH");
+            else if (hour == "numeric")
+                skeletonBuilder.append('H');
+        }
+    }
+
+    String minute = intlStringOption(exec, options, vm.propertyNames->minute, twoDigitNumeric, "minute must be \"2-digit\" or \"numeric\"", nullptr);
+    if (exec.hadException())
+        return;
+    if (!minute.isNull()) {
+        if (minute == "2-digit")
+            skeletonBuilder.appendLiteral("mm");
+        else if (minute == "numeric")
+            skeletonBuilder.append('m');
+    }
+
+    String second = intlStringOption(exec, options, vm.propertyNames->second, twoDigitNumeric, "second must be \"2-digit\" or \"numeric\"", nullptr);
+    if (exec.hadException())
+        return;
+    if (!second.isNull()) {
+        if (second == "2-digit")
+            skeletonBuilder.appendLiteral("ss");
+        else if (second == "numeric")
+            skeletonBuilder.append('s');
+    }
+
+    String timeZoneName = intlStringOption(exec, options, vm.propertyNames->timeZoneName, shortLong, "timeZoneName must be \"short\" or \"long\"", nullptr);
+    if (exec.hadException())
+        return;
+    if (!timeZoneName.isNull()) {
+        if (timeZoneName == "short")
+            skeletonBuilder.append('z');
+        else if (timeZoneName == "long")
+            skeletonBuilder.appendLiteral("zzzz");
+    }
+
+    // 24. Let dataLocaleData be Get(localeData, dataLocale).
+    // 25. Let formats be Get(dataLocaleData, "formats").
+    // 26. Let matcher be GetOption(options, "formatMatcher", "string", «"basic", "best fit"», "best fit").
+    intlStringOption(exec, options, vm.propertyNames->formatMatcher, { "basic", "best fit" }, "formatMatcher must be either \"basic\" or \"best fit\"", "best fit");
+    // 27. ReturnIfAbrupt(matcher).
+    if (exec.hadException())
+        return;
+
+    // Always use ICU date format generator, rather than our own pattern list and matcher.
+    // Covers steps 28-36.
+    UErrorCode status = U_ZERO_ERROR;
+    UDateTimePatternGenerator* generator = udatpg_open(dataLocale.utf8().data(), &status);
+    if (U_FAILURE(status)) {
+        throwTypeError(&exec, ASCIILiteral("failed to initialize DateTimeFormat"));
+        return;
+    }
+
+    String skeleton = skeletonBuilder.toString();
+    StringView skeletonView(skeleton);
+    Vector<UChar, 32> patternBuffer(32);
+    status = U_ZERO_ERROR;
+    auto patternLength = udatpg_getBestPattern(generator, skeletonView.upconvertedCharacters(), skeletonView.length(), patternBuffer.data(), patternBuffer.capacity(), &status);
+    if (status == U_BUFFER_OVERFLOW_ERROR) {
+        status = U_ZERO_ERROR;
+        patternBuffer = Vector<UChar, 32>(patternLength);
+        udatpg_getBestPattern(generator, skeletonView.upconvertedCharacters(), skeletonView.length(), patternBuffer.data(), patternLength, &status);
+    }
+    udatpg_close(generator);
+    if (U_FAILURE(status)) {
+        throwTypeError(&exec, ASCIILiteral("failed to initialize DateTimeFormat"));
+        return;
+    }
+
+    StringView pattern(patternBuffer.data(), patternLength);
+    setFormatsFromPattern(pattern);
+
+    // 37. Set dateTimeFormat.[[boundFormat]] to undefined.
+    // Already undefined.
+
+    // 38. Set dateTimeFormat.[[initializedDateTimeFormat]] to true.
+    m_initializedDateTimeFormat = true;
+
+    // 39. Return dateTimeFormat.
+    return;
+}
+
+const char* IntlDateTimeFormat::weekdayString(Weekday weekday)
+{
+    switch (weekday) {
+    case Weekday::Narrow:
+        return ASCIILiteral("narrow");
+    case Weekday::Short:
+        return ASCIILiteral("short");
+    case Weekday::Long:
+        return ASCIILiteral("long");
+    case Weekday::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+const char* IntlDateTimeFormat::eraString(Era era)
+{
+    switch (era) {
+    case Era::Narrow:
+        return "narrow";
+    case Era::Short:
+        return "short";
+    case Era::Long:
+        return "long";
+    case Era::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+const char* IntlDateTimeFormat::yearString(Year year)
+{
+    switch (year) {
+    case Year::TwoDigit:
+        return "2-digit";
+    case Year::Numeric:
+        return "numeric";
+    case Year::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+const char* IntlDateTimeFormat::monthString(Month month)
+{
+    switch (month) {
+    case Month::TwoDigit:
+        return "2-digit";
+    case Month::Numeric:
+        return "numeric";
+    case Month::Narrow:
+        return "narrow";
+    case Month::Short:
+        return "short";
+    case Month::Long:
+        return "long";
+    case Month::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+const char* IntlDateTimeFormat::dayString(Day day)
+{
+    switch (day) {
+    case Day::TwoDigit:
+        return "2-digit";
+    case Day::Numeric:
+        return "numeric";
+    case Day::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+const char* IntlDateTimeFormat::hourString(Hour hour)
+{
+    switch (hour) {
+    case Hour::TwoDigit:
+        return "2-digit";
+    case Hour::Numeric:
+        return "numeric";
+    case Hour::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+const char* IntlDateTimeFormat::minuteString(Minute minute)
+{
+    switch (minute) {
+    case Minute::TwoDigit:
+        return "2-digit";
+    case Minute::Numeric:
+        return "numeric";
+    case Minute::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+const char* IntlDateTimeFormat::secondString(Second second)
+{
+    switch (second) {
+    case Second::TwoDigit:
+        return "2-digit";
+    case Second::Numeric:
+        return "numeric";
+    case Second::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+const char* IntlDateTimeFormat::timeZoneNameString(TimeZoneName timeZoneName)
+{
+    switch (timeZoneName) {
+    case TimeZoneName::Short:
+        return "short";
+    case TimeZoneName::Long:
+        return "long";
+    case TimeZoneName::None:
+        ASSERT_NOT_REACHED();
+        return nullptr;
+    }
+    ASSERT_NOT_REACHED();
+    return nullptr;
+}
+
+JSObject* IntlDateTimeFormat::resolvedOptions(ExecState& exec)
+{
+    // 12.3.5 Intl.DateTimeFormat.prototype.resolvedOptions() (ECMA-402 2.0)
+    // 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.
+    // 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.
+    if (!m_initializedDateTimeFormat) {
+        initializeDateTimeFormat(exec, jsUndefined(), jsUndefined());
+        ASSERT(!exec.hadException());
+    }
+
+    VM& vm = exec.vm();
+    JSObject* options = constructEmptyObject(&exec);
+    options->putDirect(vm, vm.propertyNames->locale, jsNontrivialString(&exec, m_locale));
+    options->putDirect(vm, vm.propertyNames->calendar, jsNontrivialString(&exec, m_calendar));
+    options->putDirect(vm, vm.propertyNames->numberingSystem, jsNontrivialString(&exec, m_numberingSystem));
+    options->putDirect(vm, vm.propertyNames->timeZone, jsNontrivialString(&exec, m_timeZone));
+
+    if (m_weekday != Weekday::None)
+        options->putDirect(vm, vm.propertyNames->weekday, jsNontrivialString(&exec, ASCIILiteral(weekdayString(m_weekday))));
+
+    if (m_era != Era::None)
+        options->putDirect(vm, vm.propertyNames->era, jsNontrivialString(&exec, ASCIILiteral(eraString(m_era))));
+
+    if (m_year != Year::None)
+        options->putDirect(vm, vm.propertyNames->year, jsNontrivialString(&exec, ASCIILiteral(yearString(m_year))));
+
+    if (m_month != Month::None)
+        options->putDirect(vm, vm.propertyNames->month, jsNontrivialString(&exec, ASCIILiteral(monthString(m_month))));
+
+    if (m_day != Day::None)
+        options->putDirect(vm, vm.propertyNames->day, jsNontrivialString(&exec, ASCIILiteral(dayString(m_day))));
+
+    if (m_hour != Hour::None) {
+        options->putDirect(vm, vm.propertyNames->hour, jsNontrivialString(&exec, ASCIILiteral(hourString(m_hour))));
+        options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hour12));
+    }
+
+    if (m_minute != Minute::None)
+        options->putDirect(vm, vm.propertyNames->minute, jsNontrivialString(&exec, ASCIILiteral(minuteString(m_minute))));
+
+    if (m_second != Second::None)
+        options->putDirect(vm, vm.propertyNames->second, jsNontrivialString(&exec, ASCIILiteral(secondString(m_second))));
+
+    if (m_timeZoneName != TimeZoneName::None)
+        options->putDirect(vm, vm.propertyNames->timeZoneName, jsNontrivialString(&exec, ASCIILiteral(timeZoneNameString(m_timeZoneName))));
+
+    return options;
+}
+
+JSValue IntlDateTimeFormat::format(ExecState& exec, double value)
+{
     // 12.3.4 FormatDateTime abstract operation (ECMA-402 2.0)
+    if (!m_initializedDateTimeFormat) {
+        initializeDateTimeFormat(exec, jsUndefined(), jsUndefined());
+        ASSERT(!exec.hadException());
+    }
 
     // 1. If x is not a finite Number, then throw a RangeError exception.
     if (!std::isfinite(value))
-        return JSValue::encode(throwRangeError(state, ASCIILiteral("date value is not finite in DateTimeFormat.format()")));
+        return throwRangeError(&exec, ASCIILiteral("date value is not finite in DateTimeFormat format()"));
 
     // FIXME: implement 2 - 9
 
     // Return new Date(value).toString() until properly implemented.
-    VM& vm = state->vm();
-    JSGlobalObject* globalObject = state->callee()->globalObject();
+    VM& vm = exec.vm();
+    JSGlobalObject* globalObject = exec.callee()->globalObject();
     DateInstance* d = DateInstance::create(vm, globalObject->dateStructure(), value);
-    return JSValue::encode(JSValue(d).toString(state));
+    return JSValue(d).toString(&exec);
 }
 
 } // namespace JSC