Bug 1386146 - Add support for hourCycle to Intl.DateTimeFormat. r=anba
authorZibi Braniecki <zbraniecki@mozilla.com>
Thu, 05 Oct 2017 17:16:03 -0700
changeset 390126 3b5994fa88ee988682325ff2cdbf9d767a5fbc4e
parent 390125 65d5d13b4ea02a1997861b5fa14aaf3c52337da2
child 390127 ed7bb5988eb13891a17cc8e2e5be42499cd1c4e5
push id32806
push userarchaeopteryx@coole-files.de
push dateSat, 04 Nov 2017 09:56:48 +0000
treeherdermozilla-central@52b2b0d65a90 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersanba
bugs1386146
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1386146 - Add support for hourCycle to Intl.DateTimeFormat. r=anba MozReview-Commit-ID: 8nwk3kyE3co
js/src/builtin/Intl.js
js/src/tests/Intl/DateTimeFormat/hourCycle.js
--- a/js/src/builtin/Intl.js
+++ b/js/src/builtin/Intl.js
@@ -2441,16 +2441,18 @@ function resolveDateTimeFormatInternals(
     //   {
     //     requestedLocales: List of locales,
     //
     //     localeOpt: // *first* opt computed in InitializeDateTimeFormat
     //       {
     //         localeMatcher: "lookup" / "best fit",
     //
     //         hour12: true / false,  // optional
+    //
+    //         hourCycle: "h11" / "h12" / "h23" / "h24", // optional
     //       }
     //
     //     timeZone: IANA time zone name,
     //
     //     formatOpt: // *second* opt computed in InitializeDateTimeFormat
     //       {
     //         // all the properties/values listed in Table 3
     //         // (weekday, era, year, month, day, &c.)
@@ -2501,16 +2503,22 @@ function resolveDateTimeFormatInternals(
     var dataLocale = r.dataLocale;
 
     // Steps 15-17.
     internalProps.timeZone = lazyDateTimeFormatData.timeZone;
 
     // Step 18.
     var formatOpt = lazyDateTimeFormatData.formatOpt;
 
+    // Copy the hourCycle setting, if present, to the format options. But
+    // only do this if no hour12 option is present, because the latter takes
+    // precedence over hourCycle.
+    if (r.hc !== null && formatOpt.hour12 === undefined)
+        formatOpt.hourCycle = r.hc;
+
     // Steps 27-28, more or less - see comment after this function.
     var pattern;
     if (lazyDateTimeFormatData.mozExtensions) {
         if (lazyDateTimeFormatData.patternOption !== undefined) {
             pattern = lazyDateTimeFormatData.patternOption;
 
             internalProps.patternOption = lazyDateTimeFormatData.patternOption;
         } else if (lazyDateTimeFormatData.dateStyle || lazyDateTimeFormatData.timeStyle) {
@@ -2523,29 +2531,75 @@ function resolveDateTimeFormatInternals(
         } else {
             pattern = toBestICUPattern(dataLocale, formatOpt);
         }
         internalProps.mozExtensions = true;
     } else {
       pattern = toBestICUPattern(dataLocale, formatOpt);
     }
 
+    // If the hourCycle option was set, adjust the resolved pattern to use the
+    // requested hour cycle representation.
+    if (formatOpt.hourCycle !== undefined)
+        pattern = replaceHourRepresentation(pattern, formatOpt.hourCycle);
+
     // Step 29.
     internalProps.pattern = pattern;
 
     // Step 30.
     internalProps.boundFormat = undefined;
 
     // The caller is responsible for associating |internalProps| with the right
     // object using |setInternalProperties|.
     return internalProps;
 }
 
 
 /**
+ * Replaces all hour pattern characters in |pattern| to use the matching hour
+ * representation for |hourCycle|.
+ */
+function replaceHourRepresentation(pattern, hourCycle) {
+    var hour;
+    switch (hourCycle) {
+      case "h11":
+        hour = "K";
+        break;
+      case "h12":
+        hour = "h";
+        break;
+      case "h23":
+        hour = "H";
+        break;
+      case "h24":
+        hour = "k";
+        break;
+    }
+    assert(hour !== undefined, "Unexpected hourCycle requested: " + hourCycle);
+
+    // Parse the pattern according to the format specified in
+    // https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
+    // and replace all hour symbols with |hour|.
+    var resultPattern = "";
+    var inQuote = false;
+    for (var i = 0; i < pattern.length; i++) {
+        var ch = pattern[i];
+        if (ch === "'") {
+            inQuote = !inQuote;
+        } else if (!inQuote && (ch === "h" || ch === "H" || ch === "k" || ch === "K")) {
+            ch = hour;
+        }
+        resultPattern += ch;
+    }
+
+    return resultPattern;
+}
+
+
+/**
  * Returns an object containing the DateTimeFormat internal properties of |obj|.
  */
 function getDateTimeFormatInternals(obj) {
     assert(IsObject(obj), "getDateTimeFormatInternals called with non-object");
     assert(IsDateTimeFormat(obj), "getDateTimeFormatInternals called with non-DateTimeFormat");
 
     var internals = getIntlObjectInternals(obj);
     assert(internals.type === "DateTimeFormat", "bad type escaped getIntlObjectInternals");
@@ -2612,46 +2666,51 @@ function InitializeDateTimeFormat(dateTi
     //
     //     timeZone: IANA time zone name,
     //
     //     formatOpt: // *second* opt computed in InitializeDateTimeFormat
     //       {
     //         // all the properties/values listed in Table 3
     //         // (weekday, era, year, month, day, &c.)
     //
-    //         hour12: true / false  // optional
+    //         hour12: true / false,  // optional
+    //         hourCycle: "h11" / "h12" / "h23" / "h24", // optional
     //       }
     //
     //     formatMatcher: "basic" / "best fit",
     //   }
     //
     // Note that lazy data is only installed as a final step of initialization,
     // so every DateTimeFormat lazy data object has *all* these properties,
     // never a subset of them.
     var lazyDateTimeFormatData = std_Object_create(null);
 
-    // Step 3.
+    // Step 1.
     var requestedLocales = CanonicalizeLocaleList(locales);
     lazyDateTimeFormatData.requestedLocales = requestedLocales;
 
-    // Step 4.
+    // Step 2.
     options = ToDateTimeOptions(options, "any", "date");
 
     // Compute options that impact interpretation of locale.
-    // Step 5.
+    // Step 3.
     var localeOpt = new Record();
     lazyDateTimeFormatData.localeOpt = localeOpt;
 
-    // Steps 6-7.
+    // Steps 4-5.
     var localeMatcher =
         GetOption(options, "localeMatcher", "string", ["lookup", "best fit"],
                   "best fit");
     localeOpt.localeMatcher = localeMatcher;
 
-    // Steps 15-17.
+    // Step 6.
+    var hc = GetOption(options, "hourCycle", "string", ["h11", "h12", "h23", "h24"], undefined);
+    localeOpt.hc = hc;
+
+    // Steps 15-18.
     var tz = options.timeZone;
     if (tz !== undefined) {
         // Step 15.a.
         tz = ToString(tz);
 
         // Step 15.b.
         var timeZone = intl_IsValidTimeZoneName(tz);
         if (timeZone === null)
@@ -2660,63 +2719,63 @@ function InitializeDateTimeFormat(dateTi
         // Step 15.c.
         tz = CanonicalizeTimeZoneName(timeZone);
     } else {
         // Step 16.
         tz = DefaultTimeZone();
     }
     lazyDateTimeFormatData.timeZone = tz;
 
-    // Step 18.
+    // Step 19.
     var formatOpt = new Record();
     lazyDateTimeFormatData.formatOpt = formatOpt;
 
     lazyDateTimeFormatData.mozExtensions = mozExtensions;
 
     if (mozExtensions) {
         let pattern = GetOption(options, "pattern", "string", undefined, undefined);
         lazyDateTimeFormatData.patternOption = pattern;
 
         let dateStyle = GetOption(options, "dateStyle", "string", ["full", "long", "medium", "short"], undefined);
         lazyDateTimeFormatData.dateStyle = dateStyle;
         let timeStyle = GetOption(options, "timeStyle", "string", ["full", "long", "medium", "short"], undefined);
         lazyDateTimeFormatData.timeStyle = timeStyle;
     }
 
-    // Step 19.
+    // Step 20.
     // 12.1, Table 4: Components of date and time formats.
     formatOpt.weekday = GetOption(options, "weekday", "string", ["narrow", "short", "long"],
                                   undefined);
     formatOpt.era = GetOption(options, "era", "string", ["narrow", "short", "long"], undefined);
     formatOpt.year = GetOption(options, "year", "string", ["2-digit", "numeric"], undefined);
     formatOpt.month = GetOption(options, "month", "string",
                                 ["2-digit", "numeric", "narrow", "short", "long"], undefined);
     formatOpt.day = GetOption(options, "day", "string", ["2-digit", "numeric"], undefined);
     formatOpt.hour = GetOption(options, "hour", "string", ["2-digit", "numeric"], undefined);
     formatOpt.minute = GetOption(options, "minute", "string", ["2-digit", "numeric"], undefined);
     formatOpt.second = GetOption(options, "second", "string", ["2-digit", "numeric"], undefined);
     formatOpt.timeZoneName = GetOption(options, "timeZoneName", "string", ["short", "long"],
                                        undefined);
 
-    // Steps 20-21 provided by ICU - see comment after this function.
-
-    // Step 22.
+    // Steps 21-22 provided by ICU - see comment after this function.
+
+    // Step 23.
     //
     // For some reason (ICU not exposing enough interface?) we drop the
     // requested format matcher on the floor after this.  In any case, even if
     // doing so is justified, we have to do this work here in case it triggers
     // getters or similar. (bug 852837)
     var formatMatcher =
         GetOption(options, "formatMatcher", "string", ["basic", "best fit"],
                   "best fit");
     void formatMatcher;
 
-    // Steps 23-25 provided by ICU, more or less - see comment after this function.
-
-    // Step 26.
+    // Steps 24-26 provided by ICU, more or less - see comment after this function.
+
+    // Step 27.
     var hr12  = GetOption(options, "hour12", "boolean", undefined, undefined);
 
     // Pass hr12 on to ICU.
     if (hr12 !== undefined)
         formatOpt.hour12 = hr12;
 
     // Step 31.
     //
@@ -2790,16 +2849,17 @@ function InitializeDateTimeFormat(dateTi
 // in the format method.
 //
 // An ICU pattern represents the information of the following DateTimeFormat
 // internal properties described in the specification, which therefore don't
 // exist separately in the implementation:
 // - [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]],
 //   [[second]], [[timeZoneName]]
 // - [[hour12]]
+// - [[hourCycle]]
 // - [[hourNo0]]
 // When needed for the resolvedOptions method, the resolveICUPattern function
 // maps the instance's ICU pattern back to the specified properties of the
 // object returned by resolvedOptions.
 //
 // ICU date-time skeletons and patterns aren't fully documented in the ICU
 // documentation (see http://bugs.icu-project.org/trac/ticket/9627). The best
 // documentation at this point is in UTR 35:
@@ -2863,22 +2923,34 @@ function toBestICUPattern(locale, option
     switch (options.day) {
     case "2-digit":
         skeleton += "dd";
         break;
     case "numeric":
         skeleton += "d";
         break;
     }
+    // If hour12 and hourCycle are both present, hour12 takes precedence.
     var hourSkeletonChar = "j";
     if (options.hour12 !== undefined) {
         if (options.hour12)
             hourSkeletonChar = "h";
         else
             hourSkeletonChar = "H";
+    } else {
+        switch (options.hourCycle) {
+        case "h11":
+        case "h12":
+            hourSkeletonChar = "h";
+            break;
+        case "h23":
+        case "h24":
+            hourSkeletonChar = "H";
+            break;
+        }
     }
     switch (options.hour) {
     case "2-digit":
         skeleton += hourSkeletonChar + hourSkeletonChar;
         break;
     case "numeric":
         skeleton += hourSkeletonChar;
         break;
@@ -3005,27 +3077,33 @@ var dateTimeFormatInternalProperties = {
         var locales = this._availableLocales;
         if (locales)
             return locales;
 
         locales = intl_DateTimeFormat_availableLocales();
         addSpecialMissingLanguageTags(locales);
         return (this._availableLocales = locales);
     },
-    relevantExtensionKeys: ["ca", "nu"]
+    relevantExtensionKeys: ["ca", "nu", "hc"]
 };
 
 
 function dateTimeFormatLocaleData() {
     return {
         ca: intl_availableCalendars,
         nu: getNumberingSystems,
+        hc: () => {
+            return [null, "h11", "h12", "h23", "h24"];
+        },
         default: {
             ca: intl_defaultCalendar,
             nu: intl_numberingSystem,
+            hc: () => {
+                return null;
+            }
         }
     };
 }
 
 
 /**
  * Function to be bound and returned by Intl.DateTimeFormat.prototype.format.
  *
@@ -3218,20 +3296,34 @@ function resolveICUPattern(pattern, resu
                 else
                     value = "narrow";
                 break;
             default:
                 // skip other pattern characters and literal text
             }
             if (hasOwn(c, icuPatternCharToComponent))
                 _DefineDataProperty(result, icuPatternCharToComponent[c], value);
-            if (c === "h" || c === "K")
+            switch (c) {
+            case "h":
+                _DefineDataProperty(result, "hourCycle", "h12");
+                _DefineDataProperty(result, "hour12", true);
+                break;
+            case "K":
+                _DefineDataProperty(result, "hourCycle", "h11");
                 _DefineDataProperty(result, "hour12", true);
-            else if (c === "H" || c === "k")
+                break;
+            case "H":
+                _DefineDataProperty(result, "hourCycle", "h23");
                 _DefineDataProperty(result, "hour12", false);
+                break;
+            case "k":
+                _DefineDataProperty(result, "hourCycle", "h24");
+                _DefineDataProperty(result, "hour12", false);
+                break;
+            }
         }
     }
 }
 
 
 /********** Intl.PluralRules **********/
 
 
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/DateTimeFormat/hourCycle.js
@@ -0,0 +1,145 @@
+// |reftest| skip-if(!this.hasOwnProperty("Intl"))
+
+const hourCycleToH12Map = {
+  "h11": true,
+  "h12": true,
+  "h23": false,
+  "h24": false,
+};
+
+for (const key of Object.keys(hourCycleToH12Map)) {
+  const langTag = "en-US";
+  const loc = `${langTag}-u-hc-${key}`;
+
+  const dtf = new Intl.DateTimeFormat(loc, {hour: "numeric"});
+  const dtf2 = new Intl.DateTimeFormat(langTag, {hour: "numeric", hourCycle: key});
+  assertEq(dtf.resolvedOptions().hourCycle, dtf2.resolvedOptions().hourCycle);
+}
+
+
+/* Legacy hour12 compatibility */
+
+// When constructed with hourCycle option, resolvedOptions' hour12 is correct.
+for (const key of Object.keys(hourCycleToH12Map)) {
+  const dtf = new Intl.DateTimeFormat("en-US", {hour: "numeric", hourCycle: key});
+  assertEq(dtf.resolvedOptions().hour12, hourCycleToH12Map[key]);
+}
+
+// When constructed with hour12 option, resolvedOptions' hourCycle is correct
+for (const [key, value] of Object.entries(hourCycleToH12Map)) {
+  const dtf = new Intl.DateTimeFormat("en-US", {hour: "numeric", hour12: value});
+  assertEq(hourCycleToH12Map[dtf.resolvedOptions().hourCycle], value);
+}
+
+// When constructed with both hour12 and hourCycle options that don't match
+// hour12 takes a precedence.
+for (const [key, value] of Object.entries(hourCycleToH12Map)) {
+  const dtf = new Intl.DateTimeFormat("en-US", {
+    hour: "numeric",
+    hourCycle: key,
+    hour12: !value
+  });
+  assertEq(hourCycleToH12Map[dtf.resolvedOptions().hourCycle], !value);
+  assertEq(dtf.resolvedOptions().hour12, !value);
+}
+
+// When constructed with hourCycle as extkey, resolvedOptions' hour12 is correct.
+for (const [key, value] of Object.entries(hourCycleToH12Map)) {
+  const langTag = "en-US";
+  const loc = `${langTag}-u-hc-${key}`;
+
+  const dtf = new Intl.DateTimeFormat(loc, {hour: "numeric"});
+  assertEq(dtf.resolvedOptions().hour12, value);
+}
+
+const expectedValuesENUS = {
+  h11: "0 AM",
+  h12: "12 AM",
+  h23: "00",
+  h24: "24"
+};
+
+const exampleDate = new Date(2017, 10-1, 10, 0);
+for (const [key, val] of Object.entries(expectedValuesENUS)) {
+  assertEq(
+    Intl.DateTimeFormat("en-US", {hour: "numeric", hourCycle: key}).format(exampleDate),
+    val
+  );
+}
+
+const invalidHourCycleValues = [
+  "h5",
+  "h0",
+  "h28",
+  "f28",
+  "23",
+];
+
+for (const key of invalidHourCycleValues) {
+  const langTag = "en-US";
+  const loc = `${langTag}-u-hc-${key}`;
+
+  const dtf = new Intl.DateTimeFormat(loc, {hour: "numeric"});
+  assertEq(dtf.resolvedOptions().hour12, true); // default value for en-US
+  assertEq(dtf.resolvedOptions().hourCycle, "h12"); //default value for en-US
+}
+
+{
+  // hourCycle is not present in resolvedOptions when the formatter has no hour field
+  const options = Intl.DateTimeFormat("en-US", {hourCycle:"h11"}).resolvedOptions();
+  assertEq("hourCycle" in options, false);
+  assertEq("hour12" in options, false);
+}
+
+{
+  // Make sure that hourCycle option overrides the unicode extension
+  let dtf = Intl.DateTimeFormat("en-US-u-hc-h23", {hourCycle: "h24", hour: "numeric"});
+  assertEq(
+    dtf.resolvedOptions().hourCycle,
+    "h24"
+  );
+}
+
+{
+  // Make sure that hour12 option overrides the unicode extension
+  let dtf = Intl.DateTimeFormat("en-US-u-hc-h23", {hour12: true, hour: "numeric"});
+  assertEq(
+    dtf.resolvedOptions().hourCycle,
+    "h12"
+  );
+}
+
+{
+  // Make sure that hour12 option overrides hourCycle options
+  let dtf = Intl.DateTimeFormat("en-US",
+    {hourCycle: "h12", hour12: false, hour: "numeric"});
+  assertEq(
+    dtf.resolvedOptions().hourCycle,
+    "h23"
+  );
+}
+
+{
+  // Make sure that hour12 option overrides hourCycle options
+  let dtf = Intl.DateTimeFormat("en-u-hc-h11", {hour: "numeric"});
+  assertEq(
+    dtf.resolvedOptions().locale,
+    "en-u-hc-h11"
+  );
+}
+
+{
+  // Make sure that hour12 option overrides unicode extension
+  let dtf = Intl.DateTimeFormat("en-u-hc-h11", {hour: "numeric", hourCycle: "h24"});
+  assertEq(
+    dtf.resolvedOptions().locale,
+    "en"
+  );
+  assertEq(
+    dtf.resolvedOptions().hourCycle,
+    "h24"
+  );
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(0, 0, "ok");