blob: 5baa2b31dfbf0c329d1a7fdabff00aa22b840e2a [file] [log] [blame]
//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
// remove non-ascii characters from strings, mostly for stripping Bi-Di markers
const nonAsciiRegex = /[^\x00-\x7F]/g;
function ascii (str) {
return str.replace(nonAsciiRegex, "");
}
const isICU = WScript.Platform.INTL_LIBRARY === "icu";
const isWinGlob = WScript.Platform.INTL_LIBRARY === "winglob";
function assertFormat(expected, fmt, date, msg = "assertFormat") {
assert.areEqual(ascii(expected), ascii(fmt.format(date)), `${msg}: fmt.format(date) did not match expected value`);
if (isICU) {
const parts = fmt.formatToParts(date);
assert.areEqual(expected, parts.map((part) => part.value).join(""), `${msg}: fmt.formatToParts(date) did not match expected value`);
const types = parts.filter((part) => part.type != "literal").map((part) => part.type);
assert.areEqual(new Set(types).size, types.length, `Duplicate non-literal parts detected in ${JSON.stringify(parts)}`)
}
}
const tests = [
{
name: "Basic functionality",
body() {
const date = new Date(2000, 1, 1, 1, 1, 1);
function test(options, expected) {
assertFormat(expected, new Intl.DateTimeFormat("en-US", options), date);
assert.areEqual(
expected,
ascii(date.toLocaleString("en-US", options)),
`date.toLocaleString("en-US", ${JSON.stringify(options)})`
);
}
function testPlatformSpecific(options, expectedWinGlob, expectedICU) {
if (isICU) {
test(options, expectedICU);
} else {
assert.isTrue(isWinGlob);
test(options, expectedWinGlob);
}
}
test({ year: "numeric" }, "2000");
test({ year: "2-digit" }, "00");
test({ month: "numeric" }, "2");
test({ month: "2-digit" }, "02");
test({ month: "long" }, "February");
test({ month: "short" }, "Feb");
// WinGlob narrow is Feb, ICU narrow is F
testPlatformSpecific({ month: "narrow" }, "Feb", "F");
test({ day: "2-digit" }, "01");
test({ day: "numeric" }, "1");
test({ hour: "2-digit" }, "01 AM");
test({ hour: "numeric" }, "1 AM");
test({ hour: "numeric", minute: "2-digit" }, "1:01 AM");
test({ hour: "numeric", minute: "numeric" }, "1:01 AM");
test({ hour: "numeric", minute: "2-digit", second: "2-digit" }, "1:01:01 AM");
// WinGlob doesn't have non-2-digit seconds
testPlatformSpecific({ hour: "numeric", minute: "2-digit", second: "numeric" }, "1:01:01 AM", "1:01:1 AM");
test({ hour: "numeric", hour12: true }, "1 AM");
testPlatformSpecific({ hour: "numeric", hour12: false }, "1:00", "01");
const epochYear = parseInt(ascii(new Intl.DateTimeFormat("en-US", { year: "numeric", timeZone: "UTC" }).format(0)), 10);
assert.isTrue(epochYear === 1970, "Allow falsy argument to format() to mean the epoch");
// By default, DateTimeFormat formats year, month, and day. Technically, Date.now() in the second call will be slightly after
// the defaulted Date.now() in the first call, but this should only produce a different string if the first call runs before
// midnight on day X and the second call runs after midnight on day X+1. That seems unlikely enough that it will only cause
// flakiness in the rarest of circumstances
assert.areEqual(new Intl.DateTimeFormat().format(), new Intl.DateTimeFormat().format(Date.now()), "The default argument for DateTimeFormat.format should be Date.now()");
}
},
{
name: "Options resolution",
body() {
function test(locale, options, expected, message) {
expected = Object.assign({}, {
locale: /en/,
numberingSystem: "latn",
calendar: "gregory",
timeZone: /.+/
}, expected);
const actual = new Intl.DateTimeFormat(locale, options).resolvedOptions();
for (const key in expected) {
if (expected[key] !== null) {
expected[key] instanceof RegExp
? assert.matches(expected[key], actual[key], message)
: assert.areEqual(expected[key], actual[key], message);
} else {
assert.isFalse(actual.hasOwnProperty(key), `${message} - ${key} should not be present in ${JSON.stringify(actual, null, 2)}`);
}
}
}
test("en-US", undefined, { year: "numeric", month: "numeric", day: "numeric" }, "Default options do not match");
test("en-US", { year: "numeric" }, { year: "numeric", month: null, day: null }, "Requesting year should not fill in other date or time options");
test("en-US", { hour: "numeric" }, { hour: "numeric", minute: null, month: null }, "Requesting hour should not fill in other date or time options");
test("en-US", { hour12: false }, { hour12: null }, "Requesting hour12 without hour shouldn't do anything");
test("en-US", { hour: "numeric", hour12: "non-falsy value" }, { hour: "numeric", hour12: true });
}
},
{
name: "Invalid options",
body() {
function test(key, validOptions, kind = RangeError) {
for (const option of validOptions) {
assert.doesNotThrow(
() => new Intl.DateTimeFormat(undefined, { [key]: option }).format(),
`Valid option { ${key}: "${option}" } threw`
);
}
assert.throws(
() => new Intl.DateTimeFormat(undefined, { [key]: "invalid" }).format(),
kind,
`Invalid option value for ${key} did not throw`
);
}
const twoDigitNumeric = ["2-digit", "numeric"];
const narrowShortLong = ["narrow", "short", "long"];
const allOptionValues = twoDigitNumeric.concat(narrowShortLong);
const options = {
weekday: narrowShortLong,
era: narrowShortLong,
year: twoDigitNumeric,
month: allOptionValues,
day: twoDigitNumeric,
hour: twoDigitNumeric,
minute: twoDigitNumeric,
second: twoDigitNumeric,
localeMatcher: ["lookup", "best fit"],
formatMatcher: ["basic", "best fit"],
};
// see https://github.com/Microsoft/ChakraCore/issues/3096
if (isICU) {
options.timeZoneName = narrowShortLong.slice(1);
}
for (const option of Object.keys(options)) {
test(option, options[option]);
}
}
},
{
name: "Intl.DateTimeFormat.prototype.formatToParts",
body() {
// WinGlob does not implement formatToParts
if (isWinGlob) {
return;
}
assert.isTrue(isICU, "This test requires an ICU implementation of Intl");
const date = new Date(2000, 0, 1, 0, 0, 0);
function test(options, key, value, message = "Error") {
const fmt = new Intl.DateTimeFormat("en-US", options);
const toString = fmt.format(date);
const toParts = fmt.formatToParts(date);
assertFormat(toString, fmt, date);
if (typeof key === "string") {
const part = toParts.find((p) => p.type === key);
assert.isTrue(part && typeof part.value === "string", `${message} - ${JSON.stringify(toParts)} expected to have part with type "${key}"`);
assert.areEqual(value, part.value, `${message} - expected ${key} to be ${value}, but was actually ${part.value}`);
} else {
assert.areEqual(key.length, value.length, "test called with invalid arguments");
key.forEach(function (k, i) {
const v = value[i];
const part = toParts.find((p) => p.type === k);
assert.isTrue(part && typeof part.value === "string", `${message} - ${JSON.stringify(toParts)} expected to have part with type "${k}"`);
assert.areEqual(v, part.value, `${message} - expected ${k} to be ${v}, but was actually ${part.value}`);
});
}
}
test(undefined, ["year", "month", "day"], ["2000", "1", "1"]);
test({ year: "2-digit", month: "2-digit", day: "2-digit" }, ["year", "month", "day"], ["00", "01", "01"]);
test({ hour: "numeric", minute: "numeric", second: "numeric" }, ["hour", "minute", "second"], ["12", "00", "00"]);
test({ month: "long" }, "month", "January");
// the "literal" tested here is the first of two literals, the second of which is a space between "12" and "AM"
test({ hour: "numeric", weekday: "long" }, ["weekday", "literal", "hour", "dayPeriod"], ["Saturday", ", ", "12", "AM"]);
}
},
{
name: "Intl.DateTimeFormat hourCycle option",
body() {
// WinGlob does not implement hourCycle
if (isWinGlob) {
return;
}
assert.isTrue(isICU, "This test requires an ICU implementation of Intl");
const midnight = new Date(2000, 0, 1, 0, 0, 0);
const noon = new Date(2000, 0, 1, 12, 0, 0);
function test(locale, useHourField, options, expectedHC, expectedHour12) {
options = useHourField === false ? options : Object.assign({}, { hour: "2-digit" }, options);
const fmt = new Intl.DateTimeFormat(locale, options);
const message = `locale: "${locale}", options: ${JSON.stringify(options)}`;
assert.areEqual(expectedHC, fmt.resolvedOptions().hourCycle, `${message} - hourCycle is not correct`);
assert.areEqual(expectedHour12, fmt.resolvedOptions().hour12, `${message} - hour12 is not correct`);
if (useHourField === true) {
const expectedNoon = {
h11: "00",
h12: "12",
h23: "12",
h24: "12",
}[expectedHC];
const expectedMidnight = {
h11: "00",
h12: "12",
h23: "00",
h24: "24",
}[expectedHC];
assert.areEqual(expectedMidnight, fmt.formatToParts(midnight).find((p) => p.type === "hour").value, `${message} - midnight value was incorrect`);
assert.areEqual(expectedNoon, fmt.formatToParts(noon).find((p) => p.type === "hour").value, `${message} - noon value was incorrect`);
} else {
assert.isUndefined(fmt.formatToParts(midnight).find((p) => p.type === "hour"), `${message} - unexpected hour field`);
}
}
function withoutHour(locale, options) {
test(locale, false, options, undefined, undefined);
}
function withHour(locale, options, expectedHC, expectedHour12) {
test(locale, true, options, expectedHC, expectedHour12);
}
// ensure hourCycle and hour properties are not there when we don't ask for them
withoutHour("en-US", undefined);
withoutHour("en-US", { hourCycle: "h11" });
withoutHour("en-US", { hour12: true });
withoutHour("en-US", { hourCycle: "h11", hour12: false });
withoutHour("en-US-u-hc-h24", undefined);
withoutHour("en-US-u-hc-h24", { hourCycle: "h11" });
withoutHour("en-US-u-hc-h24", { hour12: true });
withoutHour("en-US-u-hc-h24", { hourCycle: "h11", hour12: false });
// ensure hourCycle and hour12 properties along with hour values are correct when we do ask for hour
withHour("en-US", undefined, "h12", true);
withHour("en-US", { hour12: false }, "h23", false);
withHour("en-US", { hourCycle: "h24" }, "h24", false);
withHour("en-US", { hourCycle: "h24", hour12: true }, "h12", true);
withHour("en-US", { hourCycle: "h11", hour12: false }, "h23", false);
withHour("en-US-u-hc-h24", undefined, "h24", false);
withHour("en-US-u-hc-h24", { hourCycle: "h23" }, "h23", false);
withHour("en-US-u-hc-h24", { hourCycle: "h11" }, "h11", true);
withHour("en-US-u-hc-h24", { hour12: false }, "h23", false);
withHour("en-US-u-hc-h24", { hour12: true }, "h12", true);
if (new Intl.DateTimeFormat("en-GB").resolvedOptions().locale === "en-GB") {
withoutHour("en-GB", undefined);
withHour("en-GB", undefined, "h23", false);
withHour("en-GB", { hour12: true }, "h12", true);
withHour("en-GB-u-hc-h24", undefined, "h24", false);
}
if (new Intl.DateTimeFormat("ja-JP").resolvedOptions().locale === "ja-JP") {
withoutHour("ja-JP", undefined);
withHour("ja-JP", undefined, "h23", false);
withHour("ja-JP", { hour12: true }, "h11", true);
withHour("ja-JP-u-hc-h12", undefined, "h12", true);
}
}
},
{
name: "options.timeZone + options.timeZoneName",
body: function () {
// WinGlob does not implement formatToParts, which is used for more easily testing
// Also, bizarrely, WinGlob code throws an exception *only in ch* when using timeZoneName
// Bug: https://github.com/Microsoft/ChakraCore/issues/3096
if (isWinGlob) {
return;
}
assert.isTrue(isICU, "This test requires an ICU implementation of Intl");
function innerTest(date, timeZone, timeZoneName, expectedPart, expectedTimeZone) {
const options = {
hour: "numeric",
timeZone,
timeZoneName
};
const fmt = new Intl.DateTimeFormat("en-US", options);
const actualTimeZone = fmt.resolvedOptions().timeZone;
assert.areEqual(expectedTimeZone, actualTimeZone, `resolvedOptions().timeZone was incorrect`);
const parts = fmt.formatToParts(date);
assert.isTrue(parts.length > 2, `There must at least be a time and timeZone part of ${JSON.stringify(parts)}`);
const actualPart = parts.filter((part) => part.type === "timeZoneName")[0];
assert.isNotUndefined(actualPart, `No timeZone part in ${JSON.stringify(parts)}`);
assert.areEqual(expectedPart, actualPart.value, `Incorrect timeZoneName for ${date.toString()} with options ${JSON.stringify(options)}`);
}
function test(date, timeZone, expectedShortPart, expectedLongPart, expectedTimeZone) {
innerTest(date, timeZone, "short", expectedShortPart, expectedTimeZone);
innerTest(date, timeZone, "long", expectedLongPart, expectedTimeZone);
}
const newYears = new Date(Date.parse("2018-01-01T00:00:00.000Z"));
const juneFirst = new Date(Date.parse("2018-06-01T00:00:00.000Z"));
// see https://github.com/tc39/ecma402/issues/121 for edge cases here
// ICU ~55 formats GMT-like time zones as GMT, but ICU ~60 formats them as UTC
const UTCshort = WScript.Platform.ICU_VERSION >= 59 ? "UTC" : "GMT";
const UTClong = WScript.Platform.ICU_VERSION >= 59 ? "Coordinated Universal Time" : "GMT";
test(newYears, "GMT", UTCshort, UTClong, "UTC");
test(newYears, "Etc/GMT", UTCshort, UTClong, "UTC");
test(newYears, "Etc/UTC", UTCshort, UTClong, "UTC");
test(newYears, "Etc/UCT", UTCshort, UTClong, "UTC");
test(newYears, "US/Pacific", "PST", "Pacific Standard Time", "America/Los_Angeles");
test(newYears, "Etc/GMT-2", "GMT+2", "GMT+02:00", "Etc/GMT-2");
test(newYears, "America/New_York", "EST", "Eastern Standard Time", "America/New_York");
test(newYears, "America/Los_Angeles", "PST", "Pacific Standard Time","America/Los_Angeles");
test(juneFirst, "America/New_York", "EDT", "Eastern Daylight Time", "America/New_York");
test(juneFirst, "America/Los_Angeles", "PDT", "Pacific Daylight Time", "America/Los_Angeles");
}
},
{
name: "ca and nu extensions",
body() {
if (isWinGlob) {
return;
}
// This test raised Microsoft/ChakraCore#4885 and tc39/ecma402#225 - In the original ICU implementation
// of Intl.DateTimeFormat, we would generate the date pattern using the fully resolved locale, including
// any unicode extension keys to specify the calendar and numbering system.
// This caused ICU to generate more accurate patterns in the given calendar system, but is not spec
// compliant by #sec-initializedatetimeformat as of Intl 2018.
// Revisit the values for chinese and dangi calendars in particular in the future if pattern generation
// switches to using the full locale instead of just the basename, as those calendar systems prefer to
// to be represented in ICU by a year name and a related gregorian year.
const d = new Date(Date.UTC(2018, 2, 27, 12, 0, 0));
// lists of calendars and aliases taken from https://unicode.org/repos/cldr/trunk/common/bcp47/calendar.xml
// as of March 27th, 2018
const yearForCalendar = {
buddhist: {
latn: "2561",
thai: "๒๕๖๑",
},
// TODO(jahorto): investigate chinese and dangi calendars - Microsoft/ChakraCore#4885
chinese: {
latn: "35",
thai: "๓๕",
},
coptic: {
latn: "1734",
thai: "๑๗๓๔",
},
dangi: {
latn: "35",
thai: "๓๕",
},
ethioaa: {
latn: "7510",
thai: "๗๕๑๐",
},
ethiopic: {
latn: "2010",
thai: "๒๐๑๐",
},
gregory: {
latn: "2018",
thai: "๒๐๑๘",
},
hebrew: {
latn: "5778",
thai: "๕๗๗๘"
},
indian: {
latn: "1940",
thai: "๑๙๔๐",
},
islamic: {
latn: "1439",
thai: "๑๔๓๙",
},
"islamic-umalqura": {
latn: "1439",
thai: "๑๔๓๙",
},
"islamic-tbla": {
latn: "1439",
thai: "๑๔๓๙",
},
"islamic-civil": {
latn: "1439",
thai: "๑๔๓๙",
},
"islamic-rgsa": {
latn: "1439",
thai: "๑๔๓๙",
},
iso8601: {
latn: "2018",
thai: "๒๐๑๘",
},
japanese: {
latn: "30",
thai: "๓๐",
},
persian: {
latn: "1397",
thai: "๑๓๙๗",
},
roc: {
latn: "107",
thai: "๑๐๗",
},
};
const calendarAliases = {
ethioaa: ["ethiopic-amete-alem"],
// ICU does not recognize "gregorian" as a valid alias
// gregory: ["gregorian"],
"islamic-civil": ["islamicc"],
};
function test(expected, base, calendar, numberingSystem) {
let langtag = `${base}-u-ca-${calendar}`;
if (numberingSystem) {
langtag += `-nu-${numberingSystem}`;
}
// Extract just the year out of the string to ensure we don't get confused by added information, like eras.
const fmt = new Intl.DateTimeFormat(langtag, { year: "numeric" });
assertFormat(fmt.format(d), fmt, d);
assert.areEqual(
expected,
fmt.formatToParts(d).filter((part) => part.type === "year")[0].value,
`${langtag} did not produce the correct year`
);
}
function testEachCalendar(numberingSystem) {
for (const calendar of Object.getOwnPropertyNames(yearForCalendar)) {
test(yearForCalendar[calendar][numberingSystem || "latn"], "en", calendar, numberingSystem);
if (calendar in calendarAliases) {
const aliases = calendarAliases[calendar];
for (const alias of aliases) {
test(yearForCalendar[calendar][numberingSystem || "latn"], "en", alias, numberingSystem);
}
}
}
}
for (const numberingSystem of [undefined, "latn", "thai"]) {
testEachCalendar(numberingSystem);
}
}
},
{
name: "Supplied times should be clipped using TimeClip",
body() {
if (isWinGlob) {
return;
}
const dtf = new Intl.DateTimeFormat("en", { hour: "numeric", minute: "numeric", second: "numeric" });
for (const nearZero of [-0.9, -0.1, -Number.MIN_VALUE, -0, +0, Number.MIN_VALUE, 0.1, 0.9]) {
assert.areEqual(dtf.format(0), dtf.format(nearZero), `Formatting 0 and ${nearZero} should produce the same result`);
}
assert.throws(() => dtf.format(-8.64e15 - 1), RangeError, "Formatting a time before the beginning of ES time");
assert.doesNotThrow(() => dtf.format(-8.64e15), "Formatting the beginning of ES time");
assert.doesNotThrow(() => dtf.format(8.64e15), "Formatting the end of ES time");
assert.throws(() => dtf.format(8.64e15 + 1), RangeError, "Formatting a time after the end of ES time");
}
},
];
testRunner.runTests(tests, { verbose: !WScript.Arguments.includes("summary") });