[INTL] Implement hourCycle in DateTimeFormat
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 1 Aug 2018 21:19:27 +0000 (21:19 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 1 Aug 2018 21:19:27 +0000 (21:19 +0000)
https://bugs.webkit.org/show_bug.cgi?id=188006

Patch by Andy VanWagoner <andy@vanwagoner.family> on 2018-08-01
Reviewed by Darin Adler.

JSTests:

Removed fixed hourCycle expectations.

* test262/expectations.yaml:

Source/JavaScriptCore:

Implemented hourCycle, updating both the skeleton and the final pattern.
Changed resolveLocale to assume undefined options are not given and null
strings actually mean null, which removes the tag extension.

* runtime/CommonIdentifiers.h:
* runtime/IntlCollator.cpp:
(JSC::IntlCollator::initializeCollator):
* runtime/IntlDateTimeFormat.cpp:
(JSC::IntlDTFInternal::localeData):
(JSC::IntlDateTimeFormat::setFormatsFromPattern):
(JSC::IntlDateTimeFormat::initializeDateTimeFormat):
(JSC::IntlDateTimeFormat::resolvedOptions):
* runtime/IntlDateTimeFormat.h:
* runtime/IntlObject.cpp:
(JSC::resolveLocale):

LayoutTests:

Added tests for hourCycle.

* js/intl-datetimeformat-expected.txt:
* js/script-tests/intl-datetimeformat.js:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@234475 268f45cc-cd09-0410-ab3c-d52691b4dbfc

JSTests/ChangeLog
JSTests/test262/expectations.yaml
LayoutTests/ChangeLog
LayoutTests/js/intl-datetimeformat-expected.txt
LayoutTests/js/script-tests/intl-datetimeformat.js
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/runtime/CommonIdentifiers.h
Source/JavaScriptCore/runtime/IntlCollator.cpp
Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp
Source/JavaScriptCore/runtime/IntlDateTimeFormat.h
Source/JavaScriptCore/runtime/IntlObject.cpp

index f869655..952ef3a 100644 (file)
@@ -1,3 +1,14 @@
+2018-08-01  Andy VanWagoner  <andy@vanwagoner.family>
+
+        [INTL] Implement hourCycle in DateTimeFormat
+        https://bugs.webkit.org/show_bug.cgi?id=188006
+
+        Reviewed by Darin Adler.
+
+        Removed fixed hourCycle expectations.
+
+        * test262/expectations.yaml:
+
 2018-07-25  Yusuke Suzuki  <utatane.tea@gmail.com>
 
         [JSC] Record CoW status in ArrayProfile correctly
index 1e8cc83..908cec0 100644 (file)
@@ -1471,12 +1471,6 @@ test/intl402/Collator/prototype/compare/compare-function-name.js:
 test/intl402/DateTimeFormat/prototype/format/format-function-name.js:
   default: 'Test262Error: Expected SameValue(«true», Â«false») to be true'
   strict mode: 'Test262Error: Expected SameValue(«true», Â«false») to be true'
-test/intl402/DateTimeFormat/prototype/resolvedOptions/hourCycle.js:
-  default: 'Test262Error: Expected SameValue(«undefined», Â«h11») to be true'
-  strict mode: 'Test262Error: Expected SameValue(«undefined», Â«h11») to be true'
-test/intl402/DateTimeFormat/prototype/resolvedOptions/resolved-locale-with-hc-unicode.js:
-  default: 'Test262Error: Only hc Unicode extension value is present. (With hour option.) Expected SameValue(«en-US», Â«en-US-u-hc-h11») to be true'
-  strict mode: 'Test262Error: Only hc Unicode extension value is present. (With hour option.) Expected SameValue(«en-US», Â«en-US-u-hc-h11») to be true'
 test/intl402/Intl/getCanonicalLocales/Locale-object.js:
   default: "TypeError: undefined is not a constructor (evaluating 'new Intl.Locale(\"en-gb-oed\")')"
   strict mode: "TypeError: undefined is not a constructor (evaluating 'new Intl.Locale(\"en-gb-oed\")')"
index a81dc84..924b139 100644 (file)
@@ -1,5 +1,17 @@
 2018-08-01  Andy VanWagoner  <andy@vanwagoner.family>
 
+        [INTL] Implement hourCycle in DateTimeFormat
+        https://bugs.webkit.org/show_bug.cgi?id=188006
+
+        Reviewed by Darin Adler.
+
+        Added tests for hourCycle.
+
+        * js/intl-datetimeformat-expected.txt:
+        * js/script-tests/intl-datetimeformat.js:
+
+2018-08-01  Andy VanWagoner  <andy@vanwagoner.family>
+
         number-toLocaleString.js test fails on ARM Linux buildbots
         https://bugs.webkit.org/show_bug.cgi?id=154533
 
index 0ccebb0..ad68fba 100644 (file)
@@ -98,6 +98,7 @@ PASS Intl.DateTimeFormat('en').resolvedOptions().month is 'numeric'
 PASS Intl.DateTimeFormat('en').resolvedOptions().day is 'numeric'
 PASS Intl.DateTimeFormat('en').resolvedOptions().year is 'numeric'
 PASS Intl.DateTimeFormat('en').resolvedOptions().hour is undefined
+PASS Intl.DateTimeFormat('en').resolvedOptions().hourCycle is undefined
 PASS Intl.DateTimeFormat('en').resolvedOptions().hour12 is undefined
 PASS Intl.DateTimeFormat('en').resolvedOptions().minute is undefined
 PASS Intl.DateTimeFormat('en').resolvedOptions().second is undefined
@@ -242,6 +243,24 @@ PASS Intl.DateTimeFormat('en-u-nu-tamldec', { timeZone: 'America/Los_Angeles' })
 PASS Intl.DateTimeFormat('en-u-nu-telu', { timeZone: 'America/Los_Angeles' }).format(1451099872641) is '౧౨/౨౫/౨౦౧౫'
 PASS Intl.DateTimeFormat('en-u-nu-thai', { timeZone: 'America/Los_Angeles' }).format(1451099872641) is '๑๒/๒๕/๒๐๑๕'
 PASS Intl.DateTimeFormat('en-u-nu-tibt', { timeZone: 'America/Los_Angeles' }).format(1451099872641) is '༡༢/༢༥/༢༠༡༥'
+PASS Intl.DateTimeFormat('en-u-hc-h11').resolvedOptions().locale is 'en-u-hc-h11'
+PASS Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h12' }).resolvedOptions().locale is 'en'
+PASS Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h12' }).resolvedOptions().hourCycle is 'h12'
+PASS Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h11', hour12: true }).resolvedOptions().locale is 'en'
+PASS Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h11', hour12: true }).resolvedOptions().hourCycle is 'h12'
+PASS Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h11', hour12: false }).resolvedOptions().hourCycle is 'h23'
+PASS Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric' }).resolvedOptions().hourCycle is 'h11'
+PASS Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric' }).resolvedOptions().hour12 is true
+PASS Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', timeZone: 'UTC' }).format(12 * 60 * 60 * 1000).slice(0, 1) is '0'
+PASS Intl.DateTimeFormat('en-u-hc-h12', { hour: 'numeric' }).resolvedOptions().hourCycle is 'h12'
+PASS Intl.DateTimeFormat('en-u-hc-h12', { hour: 'numeric' }).resolvedOptions().hour12 is true
+PASS Intl.DateTimeFormat('en-u-hc-h12', { hour: 'numeric', timeZone: 'UTC' }).format(12 * 60 * 60 * 1000).slice(0, 2) is '12'
+PASS Intl.DateTimeFormat('en-u-hc-h23', { hour: 'numeric' }).resolvedOptions().hourCycle is 'h23'
+PASS Intl.DateTimeFormat('en-u-hc-h23', { hour: 'numeric' }).resolvedOptions().hour12 is false
+PASS Intl.DateTimeFormat('en-u-hc-h23', { hour: 'numeric', timeZone: 'UTC' }).format(0) is '00'
+PASS Intl.DateTimeFormat('en-u-hc-h24', { hour: 'numeric' }).resolvedOptions().hourCycle is 'h24'
+PASS Intl.DateTimeFormat('en-u-hc-h24', { hour: 'numeric' }).resolvedOptions().hour12 is false
+PASS Intl.DateTimeFormat('en-u-hc-h24', { hour: 'numeric', timeZone: 'UTC' }).format(0) is '24'
 PASS Intl.DateTimeFormat('en-u-ca-islamic-umalqura-nu-arab', { timeZone: 'America/Los_Angeles' }).format(1451099872641) is '٣/١٤/١٤٣٧'
 PASS Intl.DateTimeFormat('en', { weekday: { toString() { throw 'weekday' } } }) threw exception weekday.
 PASS Intl.DateTimeFormat('en', { weekday:'invalid' }) threw exception RangeError: weekday must be "narrow", "short", or "long".
@@ -296,8 +315,10 @@ PASS Intl.DateTimeFormat('en', { minute:'2-digit', hour:'2-digit', timeZone: 'UT
 PASS Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hour is 'numeric'
 PASS Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric', timeZone: 'UTC' }).format(0) is '12:00 AM'
 PASS Intl.DateTimeFormat('en').resolvedOptions().hour12 is undefined
+PASS Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hourCycle is 'h12'
 PASS Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hour12 is true
 PASS Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric', timeZone: 'UTC' }).format(0) is '12:00 AM'
+PASS Intl.DateTimeFormat('pt-BR', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hourCycle is 'h23'
 PASS Intl.DateTimeFormat('pt-BR', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hour12 is false
 PASS Intl.DateTimeFormat('pt-BR', { minute:'2-digit', hour:'numeric', timeZone: 'UTC' }).format(0) is '00:00'
 PASS Intl.DateTimeFormat('en', { minute: { toString() { throw 'minute' } } }) threw exception minute.
index 2040dcc..01e0331 100644 (file)
@@ -193,6 +193,7 @@ shouldBe("Intl.DateTimeFormat('en').resolvedOptions().month", "'numeric'");
 shouldBe("Intl.DateTimeFormat('en').resolvedOptions().day", "'numeric'");
 shouldBe("Intl.DateTimeFormat('en').resolvedOptions().year", "'numeric'");
 shouldBe("Intl.DateTimeFormat('en').resolvedOptions().hour", "undefined");
+shouldBe("Intl.DateTimeFormat('en').resolvedOptions().hourCycle", "undefined");
 shouldBe("Intl.DateTimeFormat('en').resolvedOptions().hour12", "undefined");
 shouldBe("Intl.DateTimeFormat('en').resolvedOptions().minute", "undefined");
 shouldBe("Intl.DateTimeFormat('en').resolvedOptions().second", "undefined");
@@ -346,6 +347,26 @@ shouldBe("Intl.DateTimeFormat('en-u-nu-telu', { timeZone: 'America/Los_Angeles'
 shouldBe("Intl.DateTimeFormat('en-u-nu-thai', { timeZone: 'America/Los_Angeles' }).format(1451099872641)", "'๑๒/๒๕/๒๐๑๕'");
 shouldBe("Intl.DateTimeFormat('en-u-nu-tibt', { timeZone: 'America/Los_Angeles' }).format(1451099872641)", "'༡༢/༢༥/༢༠༡༥'");
 
+// Hour cycle sensitive format().
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11').resolvedOptions().locale", "'en-u-hc-h11'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h12' }).resolvedOptions().locale", "'en'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h12' }).resolvedOptions().hourCycle", "'h12'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h11', hour12: true }).resolvedOptions().locale", "'en'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h11', hour12: true }).resolvedOptions().hourCycle", "'h12'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', hourCycle: 'h11', hour12: false }).resolvedOptions().hourCycle", "'h23'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric' }).resolvedOptions().hourCycle", "'h11'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric' }).resolvedOptions().hour12", "true")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h11', { hour: 'numeric', timeZone: 'UTC' }).format(12 * 60 * 60 * 1000).slice(0, 1)", "'0'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h12', { hour: 'numeric' }).resolvedOptions().hourCycle", "'h12'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h12', { hour: 'numeric' }).resolvedOptions().hour12", "true")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h12', { hour: 'numeric', timeZone: 'UTC' }).format(12 * 60 * 60 * 1000).slice(0, 2)", "'12'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h23', { hour: 'numeric' }).resolvedOptions().hourCycle", "'h23'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h23', { hour: 'numeric' }).resolvedOptions().hour12", "false")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h23', { hour: 'numeric', timeZone: 'UTC' }).format(0)", "'00'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h24', { hour: 'numeric' }).resolvedOptions().hourCycle", "'h24'")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h24', { hour: 'numeric' }).resolvedOptions().hour12", "false")
+shouldBe("Intl.DateTimeFormat('en-u-hc-h24', { hour: 'numeric', timeZone: 'UTC' }).format(0)", "'24'")
+
 // Tests multiple keys in extension.
 shouldBe("Intl.DateTimeFormat('en-u-ca-islamic-umalqura-nu-arab', { timeZone: 'America/Los_Angeles' }).format(1451099872641)", "'٣/١٤/١٤٣٧'");
 
@@ -408,8 +429,10 @@ shouldBe("Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric' }).resolv
 shouldBe("Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric', timeZone: 'UTC' }).format(0)", "'12:00 AM'");
 
 shouldBe("Intl.DateTimeFormat('en').resolvedOptions().hour12", "undefined");
+shouldBe("Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hourCycle", "'h12'");
 shouldBe("Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hour12", "true");
 shouldBe("Intl.DateTimeFormat('en', { minute:'2-digit', hour:'numeric', timeZone: 'UTC' }).format(0)", "'12:00 AM'");
+shouldBe("Intl.DateTimeFormat('pt-BR', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hourCycle", "'h23'");
 shouldBe("Intl.DateTimeFormat('pt-BR', { minute:'2-digit', hour:'numeric' }).resolvedOptions().hour12", "false");
 shouldBe("Intl.DateTimeFormat('pt-BR', { minute:'2-digit', hour:'numeric', timeZone: 'UTC' }).format(0)", "'00:00'");
 
index 886b849..e5c0af2 100644 (file)
@@ -1,3 +1,26 @@
+2018-08-01  Andy VanWagoner  <andy@vanwagoner.family>
+
+        [INTL] Implement hourCycle in DateTimeFormat
+        https://bugs.webkit.org/show_bug.cgi?id=188006
+
+        Reviewed by Darin Adler.
+
+        Implemented hourCycle, updating both the skeleton and the final pattern.
+        Changed resolveLocale to assume undefined options are not given and null
+        strings actually mean null, which removes the tag extension.
+
+        * runtime/CommonIdentifiers.h:
+        * runtime/IntlCollator.cpp:
+        (JSC::IntlCollator::initializeCollator):
+        * runtime/IntlDateTimeFormat.cpp:
+        (JSC::IntlDTFInternal::localeData):
+        (JSC::IntlDateTimeFormat::setFormatsFromPattern):
+        (JSC::IntlDateTimeFormat::initializeDateTimeFormat):
+        (JSC::IntlDateTimeFormat::resolvedOptions):
+        * runtime/IntlDateTimeFormat.h:
+        * runtime/IntlObject.cpp:
+        (JSC::resolveLocale):
+
 2018-08-01  Keith Miller  <keith_miller@apple.com>
 
         JSArrayBuffer should have its own JSType
index 0d27c4e..80227f7 100644 (file)
     macro(hash) \
     macro(header) \
     macro(hour) \
+    macro(hourCycle) \
     macro(hour12) \
     macro(href) \
     macro(id) \
index 144f761..eeba37f 100644 (file)
@@ -216,12 +216,14 @@ void IntlCollator::initializeCollator(ExecState& state, JSValue locales, JSValue
         RETURN_IF_EXCEPTION(scope, void());
         if (!usesFallback)
             numericString = numeric ? "true"_s : "false"_s;
-        opt.add("kn"_s, numericString);
+        if (!numericString.isNull())
+            opt.add("kn"_s, numericString);
     }
     {
         String caseFirst = intlStringOption(state, options, vm.propertyNames->caseFirst, { "upper", "lower", "false" }, "caseFirst must be either \"upper\", \"lower\", or \"false\"", nullptr);
         RETURN_IF_EXCEPTION(scope, void());
-        opt.add("kf"_s, caseFirst);
+        if (!caseFirst.isNull())
+            opt.add("kf"_s, caseFirst);
     }
 
     auto& availableLocales = state.jsCallee()->globalObject(vm)->intlCollatorAvailableLocales();
index f340301..ff50444 100644 (file)
@@ -52,11 +52,12 @@ static const double minECMAScriptTime = -8.64E15;
 const ClassInfo IntlDateTimeFormat::s_info = { "Object", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(IntlDateTimeFormat) };
 
 namespace IntlDTFInternal {
-static const char* const relevantExtensionKeys[2] = { "ca", "nu" };
+static const char* const relevantExtensionKeys[3] = { "ca", "nu", "hc" };
 }
 
 static const size_t indexOfExtensionKeyCa = 0;
 static const size_t indexOfExtensionKeyNu = 1;
+static const size_t indexOfExtensionKeyHc = 2;
 
 void IntlDateTimeFormat::UDateFormatDeleter::operator()(UDateFormat* dateFormat) const
 {
@@ -223,6 +224,14 @@ static Vector<String> localeData(const String& locale, size_t keyIndex)
     case indexOfExtensionKeyNu:
         keyLocaleData = numberingSystemsForLocale(locale);
         break;
+    case indexOfExtensionKeyHc:
+        // Null default so we know to use 'j' in pattern.
+        keyLocaleData.append(String());
+        keyLocaleData.append("h11"_s);
+        keyLocaleData.append("h12"_s);
+        keyLocaleData.append("h23"_s);
+        keyLocaleData.append("h24"_s);
+        break;
     default:
         ASSERT_NOT_REACHED();
     }
@@ -343,10 +352,17 @@ void IntlDateTimeFormat::setFormatsFromPattern(const StringView& pattern)
             ++i;
         }
 
-        if (currentCharacter == 'h' || currentCharacter == 'K')
-            m_hour12 = true;
-        else if (currentCharacter == 'H' || currentCharacter == 'k')
-            m_hour12 = false;
+        // If hourCycle was null, this sets it to the locale default.
+        if (m_hourCycle.isNull()) {
+            if (currentCharacter == 'h')
+                m_hourCycle = "h12"_s;
+            else if (currentCharacter == 'H')
+                m_hourCycle = "h23"_s;
+            else if (currentCharacter == 'k')
+                m_hourCycle = "h24"_s;
+            else if (currentCharacter == 'K')
+                m_hourCycle = "h11"_s;
+        }
 
         switch (currentCharacter) {
         case 'G':
@@ -430,92 +446,72 @@ void IntlDateTimeFormat::initializeDateTimeFormat(ExecState& exec, JSValue local
     VM& vm = exec.vm();
     auto scope = DECLARE_THROW_SCOPE(vm);
 
-    // 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.
+    // 12.1.1 InitializeDateTimeFormat (dateTimeFormat, locales, options) (ECMA-402)
+    // https://tc39.github.io/ecma402/#sec-initializedatetimeformat
 
-    // 3. Let requestedLocales be CanonicalizeLocaleList(locales).
     Vector<String> requestedLocales = canonicalizeLocaleList(exec, locales);
-    // 4. ReturnIfAbrupt(requestedLocales),
     RETURN_IF_EXCEPTION(scope, void());
 
-    // 5. Let options be ToDateTimeOptions(options, "any", "date").
     JSObject* options = IntlDTFInternal::toDateTimeOptionsAnyDate(exec, originalOptions);
-    // 6. ReturnIfAbrupt(options).
     RETURN_IF_EXCEPTION(scope, void());
 
-    // 7. Let opt be a new Record.
-    HashMap<String, String> localeOpt;
+    HashMap<String, String> opt;
 
-    // 8. Let matcher be GetOption(options, "localeMatcher", "string", «"lookup", "best fit"», "best fit").
     String localeMatcher = intlStringOption(exec, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit");
-    // 9. ReturnIfAbrupt(matcher).
     RETURN_IF_EXCEPTION(scope, void());
-    // 10. Set opt.[[localeMatcher]] to matcher.
-    localeOpt.add(vm.propertyNames->localeMatcher.string(), localeMatcher);
+    opt.add(vm.propertyNames->localeMatcher.string(), localeMatcher);
+
+    bool isHour12Undefined;
+    bool hour12 = intlBooleanOption(exec, options, vm.propertyNames->hour12, isHour12Undefined);
+    RETURN_IF_EXCEPTION(scope, void());
+
+    String hourCycle = intlStringOption(exec, options, vm.propertyNames->hourCycle, { "h11", "h12", "h23", "h24" }, "hourCycle must be \"h11\", \"h12\", \"h23\", or \"h24\"", nullptr);
+    RETURN_IF_EXCEPTION(scope, void());
+    if (isHour12Undefined) {
+        // Set hour12 here to simplify hour logic later.
+        hour12 = (hourCycle == "h11" || hourCycle == "h12");
+        if (!hourCycle.isNull())
+            opt.add("hc"_s, hourCycle);
+    } else
+        opt.add("hc"_s, String());
 
-    // 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.jsCallee()->globalObject(vm)->intlDateTimeFormatAvailableLocales();
-    HashMap<String, String> resolved = resolveLocale(exec, availableLocales, requestedLocales, localeOpt, IntlDTFInternal::relevantExtensionKeys, WTF_ARRAY_LENGTH(IntlDTFInternal::relevantExtensionKeys), IntlDTFInternal::localeData);
+    HashMap<String, String> resolved = resolveLocale(exec, availableLocales, requestedLocales, opt, IntlDTFInternal::relevantExtensionKeys, WTF_ARRAY_LENGTH(IntlDTFInternal::relevantExtensionKeys), IntlDTFInternal::localeData);
 
-    // 13. Set dateTimeFormat.[[locale]] to the value of r.[[locale]].
     m_locale = resolved.get(vm.propertyNames->locale.string());
     if (m_locale.isEmpty()) {
         throwTypeError(&exec, scope, "failed to initialize DateTimeFormat due to invalid locale"_s);
         return;
     }
-    // 14. Set dateTimeFormat.[[calendar]] to the value of r.[[ca]].
+
     m_calendar = resolved.get("ca"_s);
-    // Switch to preferred aliases.
     if (m_calendar == "gregorian")
         m_calendar = "gregory"_s;
     else if (m_calendar == "islamicc")
         m_calendar = "islamic-civil"_s;
     else if (m_calendar == "ethioaa")
         m_calendar = "ethiopic-amete-alem"_s;
-    // 15. Set dateTimeFormat.[[numberingSystem]] to the value of r.[[nu]].
+
+    m_hourCycle = resolved.get("hc"_s);
     m_numberingSystem = resolved.get("nu"_s);
-    // 16. Let dataLocale be the value of r.[[dataLocale]].
     String dataLocale = resolved.get("dataLocale"_s);
 
-    // 17. Let tz be Get(options, "timeZone").
     JSValue tzValue = options->get(&exec, vm.propertyNames->timeZone);
-    // 18. ReturnIfAbrupt(tz).
     RETURN_IF_EXCEPTION(scope, void());
-
-    // 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).
         RETURN_IF_EXCEPTION(scope, void());
-        // 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, scope, String::format("invalid time zone: %s", originalTz.utf8().data()));
             return;
         }
-    } else {
-        // 20. Else,
-        // a. Let tz be DefaultTimeZone().
+    } else
         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" };
@@ -578,32 +574,22 @@ void IntlDateTimeFormat::initializeDateTimeFormat(ExecState& exec, JSValue local
 
     String hour = intlStringOption(exec, options, vm.propertyNames->hour, twoDigitNumeric, "hour must be \"2-digit\" or \"numeric\"", nullptr);
     RETURN_IF_EXCEPTION(scope, void());
-
-    // 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;
-    bool hr12 = intlBooleanOption(exec, options, vm.propertyNames->hour12, isHour12Undefined);
-    // 33. ReturnIfAbrupt(hr12).
-    RETURN_IF_EXCEPTION(scope, void());
-
-    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');
-        }
-    }
+    if (hour == "2-digit") {
+        if (isHour12Undefined && m_hourCycle.isNull())
+            skeletonBuilder.appendLiteral("jj");
+        else if (hour12)
+            skeletonBuilder.appendLiteral("hh");
+        else
+            skeletonBuilder.appendLiteral("HH");
+    } else if (hour == "numeric") {
+        if (isHour12Undefined && m_hourCycle.isNull())
+            skeletonBuilder.append('j');
+        else if (hour12)
+            skeletonBuilder.append('h');
+        else
+            skeletonBuilder.append('H');
+    } else
+        m_hourCycle = String();
 
     String minute = intlStringOption(exec, options, vm.propertyNames->minute, twoDigitNumeric, "minute must be \"2-digit\" or \"numeric\"", nullptr);
     RETURN_IF_EXCEPTION(scope, void());
@@ -632,15 +618,10 @@ void IntlDateTimeFormat::initializeDateTimeFormat(ExecState& exec, JSValue local
             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).
     RETURN_IF_EXCEPTION(scope, void());
 
     // 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)) {
@@ -664,6 +645,31 @@ void IntlDateTimeFormat::initializeDateTimeFormat(ExecState& exec, JSValue local
         return;
     }
 
+    // Enforce our hourCycle, replacing hour characters in pattern.
+    if (!m_hourCycle.isNull()) {
+        UChar hour = 'H';
+        if (m_hourCycle == "h11")
+            hour = 'K';
+        else if (m_hourCycle == "h12")
+            hour = 'h';
+        else if (m_hourCycle == "h24")
+            hour = 'k';
+
+        bool isEscaped = false;
+        bool hasHour = false;
+        for (auto i = 0; i < patternLength; ++i) {
+            UChar c = patternBuffer[i];
+            if (c == '\'')
+                isEscaped = !isEscaped;
+            else if (!isEscaped && (c == 'h' || c == 'H' || c == 'k' || c == 'K')) {
+                patternBuffer[i] = hour;
+                hasHour = true;
+            }
+        }
+        if (!hasHour)
+            m_hourCycle = String();
+    }
+
     StringView pattern(patternBuffer.data(), patternLength);
     setFormatsFromPattern(pattern);
 
@@ -680,13 +686,7 @@ void IntlDateTimeFormat::initializeDateTimeFormat(ExecState& exec, JSValue local
     UCalendar* cal = const_cast<UCalendar*>(udat_getCalendar(m_dateFormat.get()));
     ucal_setGregorianChange(cal, minECMAScriptTime, &status);
 
-    // 37. Set dateTimeFormat.[[boundFormat]] to undefined.
-    // Already undefined.
-
-    // 38. Set dateTimeFormat.[[initializedDateTimeFormat]] to true.
     m_initializedDateTimeFormat = true;
-
-    // 39. Return dateTimeFormat.
 }
 
 ASCIILiteral IntlDateTimeFormat::weekdayString(Weekday weekday)
@@ -868,9 +868,12 @@ JSObject* IntlDateTimeFormat::resolvedOptions(ExecState& exec)
     if (m_day != Day::None)
         options->putDirect(vm, vm.propertyNames->day, jsNontrivialString(&exec, dayString(m_day)));
 
-    if (m_hour != Hour::None) {
+    if (m_hour != Hour::None)
         options->putDirect(vm, vm.propertyNames->hour, jsNontrivialString(&exec, hourString(m_hour)));
-        options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hour12));
+
+    if (!m_hourCycle.isNull()) {
+        options->putDirect(vm, vm.propertyNames->hourCycle, jsNontrivialString(&exec, m_hourCycle));
+        options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hourCycle == "h11" || m_hourCycle == "h12"));
     }
 
     if (m_minute != Minute::None)
index e2dcd0f..0762639 100644 (file)
@@ -97,7 +97,7 @@ private:
     String m_calendar;
     String m_numberingSystem;
     String m_timeZone;
-    bool m_hour12 { true };
+    String m_hourCycle;
     Weekday m_weekday { Weekday::None };
     Era m_era { Era::None };
     Year m_year { Year::None };
index be30d26..b19e95c 100644 (file)
@@ -779,11 +779,11 @@ HashMap<String, String> resolveLocale(ExecState& state, const HashSet<String>& a
         HashMap<String, String>::const_iterator iterator = options.find(key);
         if (iterator != options.end()) {
             const String& optionsValue = iterator->value;
-            if (!optionsValue.isNull() && keyLocaleData.contains(optionsValue)) {
-                if (optionsValue != value) {
-                    value = optionsValue;
-                    supportedExtensionAddition = String();
-                }
+            // Undefined should not get added to the options, it won't displace the extension.
+            // Null will remove the extension.
+            if ((optionsValue.isNull() || keyLocaleData.contains(optionsValue)) && optionsValue != value) {
+                value = optionsValue;
+                supportedExtensionAddition = String();
             }
         }
         result.add(key, value);