--- 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");