DateTimeFormat.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. //-------------------------------------------------------------------------------------------------------
  2. // Copyright (C) Microsoft. All rights reserved.
  3. // Copyright (c) ChakraCore Project Contributors. All rights reserved.
  4. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
  5. //-------------------------------------------------------------------------------------------------------
  6. WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
  7. // remove non-ascii characters from strings, mostly for stripping Bi-Di markers
  8. const nonAsciiRegex = /[^\x00-\x7F]/g;
  9. function ascii (str) {
  10. return str.replace(nonAsciiRegex, "");
  11. }
  12. const isICU = WScript.Platform.INTL_LIBRARY === "icu";
  13. const isWinGlob = WScript.Platform.INTL_LIBRARY === "winglob";
  14. function assertFormat(expected, fmt, date, msg = "assertFormat") {
  15. assert.areEqual(ascii(expected), ascii(fmt.format(date)), `${msg}: fmt.format(date) did not match expected value`);
  16. if (isICU) {
  17. const parts = fmt.formatToParts(date);
  18. assert.areEqual(expected, parts.map((part) => part.value).join(""), `${msg}: fmt.formatToParts(date) did not match expected value`);
  19. const types = parts.filter((part) => part.type != "literal").map((part) => part.type);
  20. assert.areEqual(new Set(types).size, types.length, `Duplicate non-literal parts detected in ${JSON.stringify(parts)}`)
  21. }
  22. }
  23. const tests = [
  24. {
  25. name: "Basic functionality",
  26. body() {
  27. const date = new Date(2000, 1, 1, 1, 1, 1);
  28. function test(options, expected) {
  29. assertFormat(expected, new Intl.DateTimeFormat("en-US", options), date);
  30. assert.areEqual(
  31. expected,
  32. ascii(date.toLocaleString("en-US", options)),
  33. `date.toLocaleString("en-US", ${JSON.stringify(options)})`
  34. );
  35. }
  36. function testPlatformSpecific(options, expectedWinGlob, expectedICU) {
  37. if (isICU) {
  38. test(options, expectedICU);
  39. } else {
  40. assert.isTrue(isWinGlob);
  41. test(options, expectedWinGlob);
  42. }
  43. }
  44. test({ year: "numeric" }, "2000");
  45. test({ year: "2-digit" }, "00");
  46. test({ month: "numeric" }, "2");
  47. test({ month: "2-digit" }, "02");
  48. test({ month: "long" }, "February");
  49. test({ month: "short" }, "Feb");
  50. // WinGlob narrow is Feb, ICU narrow is F
  51. testPlatformSpecific({ month: "narrow" }, "Feb", "F");
  52. test({ day: "2-digit" }, "01");
  53. test({ day: "numeric" }, "1");
  54. if(!isICU || WScript.Platform.ICU_VERSION < 72)
  55. {
  56. test({ hour: "2-digit" }, "01 AM");
  57. test({ hour: "numeric" }, "1 AM");
  58. test({ hour: "numeric", minute: "2-digit" }, "1:01 AM");
  59. test({ hour: "numeric", minute: "numeric" }, "1:01 AM");
  60. test({ hour: "numeric", minute: "2-digit", second: "2-digit" }, "1:01:01 AM");
  61. // WinGlob doesn't have non-2-digit seconds
  62. testPlatformSpecific({ hour: "numeric", minute: "2-digit", second: "numeric" }, "1:01:01 AM", "1:01:1 AM");
  63. test({ hour: "numeric", hour12: true }, "1 AM");
  64. }
  65. testPlatformSpecific({ hour: "numeric", hour12: false }, "1:00", "01");
  66. const epochYear = parseInt(ascii(new Intl.DateTimeFormat("en-US", { year: "numeric", timeZone: "UTC" }).format(0)), 10);
  67. assert.isTrue(epochYear === 1970, "Allow falsy argument to format() to mean the epoch");
  68. // By default, DateTimeFormat formats year, month, and day. Technically, Date.now() in the second call will be slightly after
  69. // the defaulted Date.now() in the first call, but this should only produce a different string if the first call runs before
  70. // midnight on day X and the second call runs after midnight on day X+1. That seems unlikely enough that it will only cause
  71. // flakiness in the rarest of circumstances
  72. assert.areEqual(new Intl.DateTimeFormat().format(), new Intl.DateTimeFormat().format(Date.now()), "The default argument for DateTimeFormat.format should be Date.now()");
  73. }
  74. },
  75. {
  76. name: "Options resolution",
  77. body() {
  78. function test(locale, options, expected, message) {
  79. expected = Object.assign({}, {
  80. locale: /en/,
  81. numberingSystem: "latn",
  82. calendar: "gregory",
  83. timeZone: /.+/
  84. }, expected);
  85. const actual = new Intl.DateTimeFormat(locale, options).resolvedOptions();
  86. for (const key in expected) {
  87. if (expected[key] !== null) {
  88. expected[key] instanceof RegExp
  89. ? assert.matches(expected[key], actual[key], message)
  90. : assert.areEqual(expected[key], actual[key], message);
  91. } else {
  92. assert.isFalse(actual.hasOwnProperty(key), `${message} - ${key} should not be present in ${JSON.stringify(actual, null, 2)}`);
  93. }
  94. }
  95. }
  96. test("en-US", undefined, { year: "numeric", month: "numeric", day: "numeric" }, "Default options do not match");
  97. test("en-US", { year: "numeric" }, { year: "numeric", month: null, day: null }, "Requesting year should not fill in other date or time options");
  98. test("en-US", { hour: "numeric" }, { hour: "numeric", minute: null, month: null }, "Requesting hour should not fill in other date or time options");
  99. test("en-US", { hour12: false }, { hour12: null }, "Requesting hour12 without hour shouldn't do anything");
  100. test("en-US", { hour: "numeric", hour12: "non-falsy value" }, { hour: "numeric", hour12: true });
  101. }
  102. },
  103. {
  104. name: "Invalid options",
  105. body() {
  106. function test(key, validOptions, kind = RangeError) {
  107. for (const option of validOptions) {
  108. assert.doesNotThrow(
  109. () => new Intl.DateTimeFormat(undefined, { [key]: option }).format(),
  110. `Valid option { ${key}: "${option}" } threw`
  111. );
  112. }
  113. assert.throws(
  114. () => new Intl.DateTimeFormat(undefined, { [key]: "invalid" }).format(),
  115. kind,
  116. `Invalid option value for ${key} did not throw`
  117. );
  118. }
  119. const twoDigitNumeric = ["2-digit", "numeric"];
  120. const narrowShortLong = ["narrow", "short", "long"];
  121. const allOptionValues = twoDigitNumeric.concat(narrowShortLong);
  122. const options = {
  123. weekday: narrowShortLong,
  124. era: narrowShortLong,
  125. year: twoDigitNumeric,
  126. month: allOptionValues,
  127. day: twoDigitNumeric,
  128. hour: twoDigitNumeric,
  129. minute: twoDigitNumeric,
  130. second: twoDigitNumeric,
  131. localeMatcher: ["lookup", "best fit"],
  132. formatMatcher: ["basic", "best fit"],
  133. };
  134. // see https://github.com/Microsoft/ChakraCore/issues/3096
  135. if (isICU) {
  136. options.timeZoneName = narrowShortLong.slice(1);
  137. }
  138. for (const option of Object.keys(options)) {
  139. test(option, options[option]);
  140. }
  141. }
  142. },
  143. {
  144. name: "Intl.DateTimeFormat.prototype.formatToParts",
  145. body() {
  146. // WinGlob does not implement formatToParts
  147. if (isWinGlob) {
  148. return;
  149. }
  150. assert.isTrue(isICU, "This test requires an ICU implementation of Intl");
  151. const date = new Date(2000, 0, 1, 0, 0, 0);
  152. function test(options, key, value, message = "Error") {
  153. const fmt = new Intl.DateTimeFormat("en-US", options);
  154. const toString = fmt.format(date);
  155. const toParts = fmt.formatToParts(date);
  156. assertFormat(toString, fmt, date);
  157. if (typeof key === "string") {
  158. const part = toParts.find((p) => p.type === key);
  159. assert.isTrue(part && typeof part.value === "string", `${message} - ${JSON.stringify(toParts)} expected to have part with type "${key}"`);
  160. assert.areEqual(value, part.value, `${message} - expected ${key} to be ${value}, but was actually ${part.value}`);
  161. } else {
  162. assert.areEqual(key.length, value.length, "test called with invalid arguments");
  163. key.forEach(function (k, i) {
  164. const v = value[i];
  165. const part = toParts.find((p) => p.type === k);
  166. assert.isTrue(part && typeof part.value === "string", `${message} - ${JSON.stringify(toParts)} expected to have part with type "${k}"`);
  167. assert.areEqual(v, part.value, `${message} - expected ${k} to be ${v}, but was actually ${part.value}`);
  168. });
  169. }
  170. }
  171. test(undefined, ["year", "month", "day"], ["2000", "1", "1"]);
  172. test({ year: "2-digit", month: "2-digit", day: "2-digit" }, ["year", "month", "day"], ["00", "01", "01"]);
  173. test({ hour: "numeric", minute: "numeric", second: "numeric" }, ["hour", "minute", "second"], ["12", "00", "00"]);
  174. test({ month: "long" }, "month", "January");
  175. // See https://github.com/chakra-core/ChakraCore/issues/7047
  176. if (!isICU || WScript.Platform.ICU_VERSION != 78) {
  177. // the "literal" tested here is the first of two literals, the second of which is a space between "12" and "AM"
  178. test({ hour: "numeric", weekday: "long" }, ["weekday", "literal", "hour", "dayPeriod"], ["Saturday", ", ", "12", "AM"]);
  179. }
  180. }
  181. },
  182. {
  183. name: "Intl.DateTimeFormat hourCycle option",
  184. body() {
  185. // WinGlob does not implement hourCycle
  186. if (isWinGlob) {
  187. return;
  188. }
  189. assert.isTrue(isICU, "This test requires an ICU implementation of Intl");
  190. const midnight = new Date(2000, 0, 1, 0, 0, 0);
  191. const noon = new Date(2000, 0, 1, 12, 0, 0);
  192. function test(locale, useHourField, options, expectedHC, expectedHour12) {
  193. options = useHourField === false ? options : Object.assign({}, { hour: "2-digit" }, options);
  194. const fmt = new Intl.DateTimeFormat(locale, options);
  195. const message = `locale: "${locale}", options: ${JSON.stringify(options)}`;
  196. assert.areEqual(expectedHC, fmt.resolvedOptions().hourCycle, `${message} - hourCycle is not correct`);
  197. assert.areEqual(expectedHour12, fmt.resolvedOptions().hour12, `${message} - hour12 is not correct`);
  198. if (useHourField === true) {
  199. const expectedNoon = {
  200. h11: "00",
  201. h12: "12",
  202. h23: "12",
  203. h24: "12",
  204. }[expectedHC];
  205. const expectedMidnight = {
  206. h11: "00",
  207. h12: "12",
  208. h23: "00",
  209. h24: "24",
  210. }[expectedHC];
  211. assert.areEqual(expectedMidnight, fmt.formatToParts(midnight).find((p) => p.type === "hour").value, `${message} - midnight value was incorrect`);
  212. assert.areEqual(expectedNoon, fmt.formatToParts(noon).find((p) => p.type === "hour").value, `${message} - noon value was incorrect`);
  213. } else {
  214. assert.isUndefined(fmt.formatToParts(midnight).find((p) => p.type === "hour"), `${message} - unexpected hour field`);
  215. }
  216. }
  217. function withoutHour(locale, options) {
  218. test(locale, false, options, undefined, undefined);
  219. }
  220. function withHour(locale, options, expectedHC, expectedHour12) {
  221. test(locale, true, options, expectedHC, expectedHour12);
  222. }
  223. // ensure hourCycle and hour properties are not there when we don't ask for them
  224. withoutHour("en-US", undefined);
  225. withoutHour("en-US", { hourCycle: "h11" });
  226. withoutHour("en-US", { hour12: true });
  227. withoutHour("en-US", { hourCycle: "h11", hour12: false });
  228. withoutHour("en-US-u-hc-h24", undefined);
  229. withoutHour("en-US-u-hc-h24", { hourCycle: "h11" });
  230. withoutHour("en-US-u-hc-h24", { hour12: true });
  231. withoutHour("en-US-u-hc-h24", { hourCycle: "h11", hour12: false });
  232. // ensure hourCycle and hour12 properties along with hour values are correct when we do ask for hour
  233. withHour("en-US", undefined, "h12", true);
  234. withHour("en-US", { hour12: false }, "h23", false);
  235. withHour("en-US", { hourCycle: "h24" }, "h24", false);
  236. withHour("en-US", { hourCycle: "h24", hour12: true }, "h12", true);
  237. withHour("en-US", { hourCycle: "h11", hour12: false }, "h23", false);
  238. withHour("en-US-u-hc-h24", undefined, "h24", false);
  239. withHour("en-US-u-hc-h24", { hourCycle: "h23" }, "h23", false);
  240. withHour("en-US-u-hc-h24", { hourCycle: "h11" }, "h11", true);
  241. withHour("en-US-u-hc-h24", { hour12: false }, "h23", false);
  242. withHour("en-US-u-hc-h24", { hour12: true }, "h12", true);
  243. if (new Intl.DateTimeFormat("en-GB").resolvedOptions().locale === "en-GB") {
  244. withoutHour("en-GB", undefined);
  245. withHour("en-GB", undefined, "h23", false);
  246. withHour("en-GB", { hour12: true }, "h12", true);
  247. withHour("en-GB-u-hc-h24", undefined, "h24", false);
  248. }
  249. if (new Intl.DateTimeFormat("ja-JP").resolvedOptions().locale === "ja-JP") {
  250. withoutHour("ja-JP", undefined);
  251. withHour("ja-JP", undefined, "h23", false);
  252. withHour("ja-JP", { hour12: true }, "h11", true);
  253. withHour("ja-JP-u-hc-h12", undefined, "h12", true);
  254. }
  255. }
  256. },
  257. {
  258. name: "options.timeZone + options.timeZoneName",
  259. body: function () {
  260. // WinGlob does not implement formatToParts, which is used for more easily testing
  261. // Also, bizarrely, WinGlob code throws an exception *only in ch* when using timeZoneName
  262. // Bug: https://github.com/Microsoft/ChakraCore/issues/3096
  263. if (isWinGlob) {
  264. return;
  265. }
  266. assert.isTrue(isICU, "This test requires an ICU implementation of Intl");
  267. function innerTest(date, timeZone, timeZoneName, expectedPart, expectedTimeZone) {
  268. const options = {
  269. hour: "numeric",
  270. timeZone,
  271. timeZoneName
  272. };
  273. const fmt = new Intl.DateTimeFormat("en-US", options);
  274. const actualTimeZone = fmt.resolvedOptions().timeZone;
  275. assert.areEqual(expectedTimeZone, actualTimeZone, `resolvedOptions().timeZone was incorrect`);
  276. const parts = fmt.formatToParts(date);
  277. assert.isTrue(parts.length > 2, `There must at least be a time and timeZone part of ${JSON.stringify(parts)}`);
  278. const actualPart = parts.filter((part) => part.type === "timeZoneName")[0];
  279. assert.isNotUndefined(actualPart, `No timeZone part in ${JSON.stringify(parts)}`);
  280. assert.areEqual(expectedPart, actualPart.value, `Incorrect timeZoneName for ${date.toString()} with options ${JSON.stringify(options)}`);
  281. }
  282. function test(date, timeZone, expectedShortPart, expectedLongPart, expectedTimeZone) {
  283. innerTest(date, timeZone, "short", expectedShortPart, expectedTimeZone);
  284. innerTest(date, timeZone, "long", expectedLongPart, expectedTimeZone);
  285. }
  286. const newYears = new Date(Date.parse("2018-01-01T00:00:00.000Z"));
  287. const juneFirst = new Date(Date.parse("2018-06-01T00:00:00.000Z"));
  288. // see https://github.com/tc39/ecma402/issues/121 for edge cases here
  289. // ICU ~55 formats GMT-like time zones as GMT, but ICU ~60 formats them as UTC
  290. const UTCshort = WScript.Platform.ICU_VERSION >= 59 ? "UTC" : "GMT";
  291. const UTClong = WScript.Platform.ICU_VERSION >= 59 ? "Coordinated Universal Time" : "GMT";
  292. test(newYears, "GMT", UTCshort, UTClong, "UTC");
  293. test(newYears, "Etc/GMT", UTCshort, UTClong, "UTC");
  294. test(newYears, "Etc/UTC", UTCshort, UTClong, "UTC");
  295. test(newYears, "Etc/UCT", UTCshort, UTClong, "UTC");
  296. test(newYears, "US/Pacific", "PST", "Pacific Standard Time", "America/Los_Angeles");
  297. test(newYears, "Etc/GMT-2", "GMT+2", "GMT+02:00", "Etc/GMT-2");
  298. test(newYears, "America/New_York", "EST", "Eastern Standard Time", "America/New_York");
  299. test(newYears, "America/Los_Angeles", "PST", "Pacific Standard Time","America/Los_Angeles");
  300. test(juneFirst, "America/New_York", "EDT", "Eastern Daylight Time", "America/New_York");
  301. test(juneFirst, "America/Los_Angeles", "PDT", "Pacific Daylight Time", "America/Los_Angeles");
  302. }
  303. },
  304. {
  305. name: "ca and nu extensions",
  306. body() {
  307. if (isWinGlob) {
  308. return;
  309. }
  310. // This test raised Microsoft/ChakraCore#4885 and tc39/ecma402#225 - In the original ICU implementation
  311. // of Intl.DateTimeFormat, we would generate the date pattern using the fully resolved locale, including
  312. // any unicode extension keys to specify the calendar and numbering system.
  313. // This caused ICU to generate more accurate patterns in the given calendar system, but is not spec
  314. // compliant by #sec-initializedatetimeformat as of Intl 2018.
  315. // Revisit the values for chinese and dangi calendars in particular in the future if pattern generation
  316. // switches to using the full locale instead of just the basename, as those calendar systems prefer to
  317. // to be represented in ICU by a year name and a related gregorian year.
  318. const d = new Date(Date.UTC(2018, 2, 27, 12, 0, 0));
  319. // lists of calendars and aliases taken from https://unicode.org/repos/cldr/trunk/common/bcp47/calendar.xml
  320. // as of March 27th, 2018
  321. const yearForCalendar = {
  322. buddhist: {
  323. latn: "2561",
  324. thai: "๒๕๖๑",
  325. },
  326. // TODO(jahorto): investigate chinese and dangi calendars - Microsoft/ChakraCore#4885
  327. chinese: {
  328. latn: "35",
  329. thai: "๓๕",
  330. },
  331. coptic: {
  332. latn: "1734",
  333. thai: "๑๗๓๔",
  334. },
  335. dangi: {
  336. latn: "35",
  337. thai: "๓๕",
  338. },
  339. ethioaa: {
  340. latn: "7510",
  341. thai: "๗๕๑๐",
  342. },
  343. ethiopic: {
  344. latn: "2010",
  345. thai: "๒๐๑๐",
  346. },
  347. gregory: {
  348. latn: "2018",
  349. thai: "๒๐๑๘",
  350. },
  351. hebrew: {
  352. latn: "5778",
  353. thai: "๕๗๗๘"
  354. },
  355. indian: {
  356. latn: "1940",
  357. thai: "๑๙๔๐",
  358. },
  359. islamic: {
  360. latn: "1439",
  361. thai: "๑๔๓๙",
  362. },
  363. "islamic-umalqura": {
  364. latn: "1439",
  365. thai: "๑๔๓๙",
  366. },
  367. "islamic-tbla": {
  368. latn: "1439",
  369. thai: "๑๔๓๙",
  370. },
  371. "islamic-civil": {
  372. latn: "1439",
  373. thai: "๑๔๓๙",
  374. },
  375. "islamic-rgsa": {
  376. latn: "1439",
  377. thai: "๑๔๓๙",
  378. },
  379. iso8601: {
  380. latn: "2018",
  381. thai: "๒๐๑๘",
  382. },
  383. japanese: {
  384. latn: "30",
  385. thai: "๓๐",
  386. },
  387. persian: {
  388. latn: "1397",
  389. thai: "๑๓๙๗",
  390. },
  391. roc: {
  392. latn: "107",
  393. thai: "๑๐๗",
  394. },
  395. };
  396. const calendarAliases = {
  397. ethioaa: ["ethiopic-amete-alem"],
  398. // ICU does not recognize "gregorian" as a valid alias
  399. // gregory: ["gregorian"],
  400. "islamic-civil": ["islamicc"],
  401. };
  402. function test(expected, base, calendar, numberingSystem) {
  403. let langtag = `${base}-u-ca-${calendar}`;
  404. if (numberingSystem) {
  405. langtag += `-nu-${numberingSystem}`;
  406. }
  407. // Extract just the year out of the string to ensure we don't get confused by added information, like eras.
  408. const fmt = new Intl.DateTimeFormat(langtag, { year: "numeric" });
  409. assertFormat(fmt.format(d), fmt, d);
  410. assert.areEqual(
  411. expected,
  412. fmt.formatToParts(d).filter((part) => part.type === "year")[0].value,
  413. `${langtag} did not produce the correct year`
  414. );
  415. }
  416. function testEachCalendar(numberingSystem) {
  417. for (const calendar of Object.getOwnPropertyNames(yearForCalendar)) {
  418. test(yearForCalendar[calendar][numberingSystem || "latn"], "en", calendar, numberingSystem);
  419. if (calendar in calendarAliases) {
  420. const aliases = calendarAliases[calendar];
  421. for (const alias of aliases) {
  422. test(yearForCalendar[calendar][numberingSystem || "latn"], "en", alias, numberingSystem);
  423. }
  424. }
  425. }
  426. }
  427. for (const numberingSystem of [undefined, "latn", "thai"]) {
  428. testEachCalendar(numberingSystem);
  429. }
  430. }
  431. },
  432. {
  433. name: "Supplied times should be clipped using TimeClip",
  434. body() {
  435. if (isWinGlob) {
  436. return;
  437. }
  438. const dtf = new Intl.DateTimeFormat("en", { hour: "numeric", minute: "numeric", second: "numeric" });
  439. for (const nearZero of [-0.9, -0.1, -Number.MIN_VALUE, -0, +0, Number.MIN_VALUE, 0.1, 0.9]) {
  440. assert.areEqual(dtf.format(0), dtf.format(nearZero), `Formatting 0 and ${nearZero} should produce the same result`);
  441. }
  442. assert.throws(() => dtf.format(-8.64e15 - 1), RangeError, "Formatting a time before the beginning of ES time");
  443. assert.doesNotThrow(() => dtf.format(-8.64e15), "Formatting the beginning of ES time");
  444. assert.doesNotThrow(() => dtf.format(8.64e15), "Formatting the end of ES time");
  445. assert.throws(() => dtf.format(8.64e15 + 1), RangeError, "Formatting a time after the end of ES time");
  446. }
  447. },
  448. ];
  449. testRunner.runTests(tests, { verbose: !WScript.Arguments.includes("summary") });