NumberFormat.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. //-------------------------------------------------------------------------------------------------------
  2. // Copyright (C) Microsoft. All rights reserved.
  3. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
  4. //-------------------------------------------------------------------------------------------------------
  5. WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
  6. let suppressFormatEqualityCheck = false;
  7. function format() {
  8. let locale = "en-US", options, n;
  9. assert.isTrue(arguments.length > 0);
  10. if (typeof arguments[0] === "number") {
  11. [n] = arguments;
  12. } else if (typeof arguments[0] === "object" && !(arguments[0] instanceof Array)) {
  13. [options, n] = arguments;
  14. } else {
  15. [locale, options, n] = arguments;
  16. }
  17. const nf = new Intl.NumberFormat(locale, options);
  18. const format = nf.format(n);
  19. const localeString = n.toLocaleString(locale, options);
  20. assert.isTrue(format === localeString, `[locale = ${JSON.stringify(locale)}, options = ${JSON.stringify(options)}] format does not match toLocaleString`);
  21. if (WScript.Platform.INTL_LIBRARY === "icu" && !suppressFormatEqualityCheck) {
  22. assert.isTrue(format === nf.formatToParts(n).map((part) => part.value).join(""), `[locale = ${JSON.stringify(locale)}, options = ${JSON.stringify(options)}] format does not match formatToParts`);
  23. }
  24. return format;
  25. }
  26. const tests = [
  27. {
  28. name: "Decimal style default options",
  29. body: function () {
  30. assert.areEqual("5", format(5));
  31. assert.areEqual("5,000", format(5000));
  32. assert.areEqual("50.474", format(50.474));
  33. }
  34. },
  35. {
  36. name: "Min/max fractional digits",
  37. body: function () {
  38. // min
  39. assert.areEqual("5.00", format({ minimumFractionDigits: 2 }, 5));
  40. assert.areEqual("5.0", format({ minimumFractionDigits: 1 }, 5));
  41. // min and max
  42. assert.areEqual("5.00", format({ minimumFractionDigits: 2, maximumFractionDigits: 2 }, 5));
  43. assert.areEqual("5.0", format({ minimumFractionDigits: 1, maximumFractionDigits: 2 }, 5));
  44. // max
  45. assert.areEqual("5.44", format({ maximumFractionDigits: 2 }, 5.444));
  46. assert.areEqual("5.444", format({ maximumFractionDigits: 4 }, 5.444));
  47. assert.areEqual("5.45", format({ maximumFractionDigits: 2 }, 5.445));
  48. assert.areEqual("5.445", format({ maximumFractionDigits: 4 }, 5.445));
  49. assert.areEqual("5.55", format({ maximumFractionDigits: 2 }, 5.554));
  50. assert.areEqual("5.554", format({ maximumFractionDigits: 4 }, 5.554));
  51. assert.areEqual("5", format({ maximumFractionDigits: 0 }, 5.45));
  52. assert.areEqual("6", format({ maximumFractionDigits: 0 }, 5.5));
  53. }
  54. },
  55. {
  56. name: "Min integer digits",
  57. body: function () {
  58. assert.areEqual("5", format({ minimumIntegerDigits: 1 }, 5));
  59. assert.areEqual("05", format({ minimumIntegerDigits: 2 }, 5));
  60. assert.areEqual("0,000,000,005", format({ minimumIntegerDigits: 10 }, 5));
  61. assert.areEqual("500", format({ minimumIntegerDigits: 1 }, 500));
  62. assert.areEqual("0,000,000,500", format({ minimumIntegerDigits: 10 }, 500));
  63. }
  64. },
  65. {
  66. name: "Min/max significant digits",
  67. body: function () {
  68. // min
  69. assert.areEqual("5.0", format({ minimumSignificantDigits: 2 }, 5));
  70. assert.areEqual("500", format({ minimumSignificantDigits: 2 }, 500));
  71. assert.areEqual("500.0", format({ minimumSignificantDigits: 4 }, 500));
  72. // min and max
  73. assert.areEqual("5.0", format({ minimumSignificantDigits: 2, maximumSignificantDigits: 2 }, 5));
  74. assert.areEqual("5", format({ minimumSignificantDigits: 1, maximumSignificantDigits: 2 }, 5));
  75. // max
  76. assert.areEqual("5.44", format({ maximumSignificantDigits: 3 }, 5.444));
  77. assert.areEqual("5.444", format({ maximumSignificantDigits: 4 }, 5.4444));
  78. assert.areEqual("5.45", format({ maximumSignificantDigits: 3 }, 5.445));
  79. assert.areEqual("5.445", format({ maximumSignificantDigits: 4 }, 5.4445));
  80. assert.areEqual("5.55", format({ maximumSignificantDigits: 3 }, 5.554));
  81. }
  82. },
  83. {
  84. name: "Grouping separator",
  85. body: function () {
  86. assert.areEqual("50,000", format({ useGrouping: true }, 50000));
  87. assert.areEqual("50000", format({ useGrouping: false }, 50000));
  88. assert.areEqual("0000000005", format({ minimumIntegerDigits: 10, useGrouping: false }, 5));
  89. assert.areEqual("0000005000", format({ minimumIntegerDigits: 10, useGrouping: false }, 5000));
  90. }
  91. },
  92. {
  93. name: "Default style option combinations",
  94. body: function () {
  95. assert.areEqual("123", format({ minimumSignificantDigits: 3, maximumSignificantDigits: 3, minimumIntegerDigits: 5, minimumFractionDigits: 5, maximumFractionDigits: 5 }, 123.1));
  96. assert.areEqual("00,123.10000", format({ minimumIntegerDigits: 5, minimumFractionDigits: 5, maximumFractionDigits: 5 }, 123.1))
  97. }
  98. },
  99. {
  100. name: "Currency style",
  101. body: function () {
  102. function formatCurrency() {
  103. let locale = "en-US", currency = "USD", options, n;
  104. assert.isTrue(arguments.length > 0);
  105. if (typeof arguments[0] === "number") {
  106. [n] = arguments;
  107. } else if (typeof arguments[0] === "object") {
  108. [options, n] = arguments;
  109. } else if (arguments.length === 3) {
  110. [currency, options, n] = arguments;
  111. } else {
  112. [locale, currency, options, n] = arguments;
  113. }
  114. options = options || {};
  115. options.style = "currency",
  116. options.currency = currency;
  117. return format(locale, options, n)
  118. }
  119. assert.areEqual("$1.00", formatCurrency(1));
  120. assert.areEqual("$1.50", formatCurrency(1.50));
  121. assert.areEqual("$1.50", formatCurrency(1.504));
  122. assert.areEqual("$1.51", formatCurrency(1.505));
  123. assert.matches(/USD[\x20\u00a0]?1.00/, formatCurrency({ currencyDisplay: "code" }, 1), "Currency display: code");
  124. assert.matches(/USD[\x20\u00a0]?1.50/, formatCurrency({ currencyDisplay: "code" }, 1.504), "Currency display: code");
  125. assert.matches(/USD[\x20\u00a0]?1.51/, formatCurrency({ currencyDisplay: "code" }, 1.505), "Currency display: code");
  126. assert.areEqual("$1.00", formatCurrency({ currencyDisplay: "symbol" }, 1), "Currency display: symbol");
  127. assert.areEqual("$1.50", formatCurrency({ currencyDisplay: "symbol" }, 1.504), "Currency display: symbol");
  128. assert.areEqual("$1.51", formatCurrency({ currencyDisplay: "symbol" }, 1.505), "Currency display: symbol");
  129. // ICU has a proper "name" currency display, while WinGlob falls back to "code"
  130. if (WScript.Platform.ICU_VERSION === 62) {
  131. // In ICU 62, there is a mismatch between "1.00 US dollar" and "1.00 US dollars"
  132. suppressFormatEqualityCheck = true;
  133. }
  134. assert.matches(/(?:USD[\x20\u00a0]?1.00|1.00 US dollars)/, formatCurrency({ currencyDisplay: "name" }, 1), "Currency display: name");
  135. suppressFormatEqualityCheck = false;
  136. assert.matches(/(?:USD[\x20\u00a0]?1.50|1.50 US dollars)/, formatCurrency({ currencyDisplay: "name" }, 1.504), "Currency display: name");
  137. assert.matches(/(?:USD[\x20\u00a0]?1.51|1.51 US dollars)/, formatCurrency({ currencyDisplay: "name" }, 1.505), "Currency display: name");
  138. }
  139. },
  140. {
  141. name: "Percent style",
  142. body: function () {
  143. assert.matches(/100[\x20\u00a0]?%/, format({ style: "percent" }, 1));
  144. assert.matches(/1[\x20\u00a0]?%/, format({ style: "percent" }, 0.01));
  145. assert.matches(/10,000[\x20\u00a0]?%/,format({ style: "percent" }, 100));
  146. }
  147. },
  148. {
  149. name: "Negative 0 (https://github.com/tc39/ecma402/issues/219)",
  150. body() {
  151. assert.areEqual(
  152. 0,
  153. new Intl.NumberFormat(undefined, { minimumFractionDigits: -0 }).resolvedOptions().minimumFractionDigits,
  154. "Passing -0 for minimumFractionDigits should get normalized to 0 by DefaultNumberOption"
  155. );
  156. assert.areEqual("-0", (-0).toLocaleString(), "-0 should be treated as a negative number (toLocaleString)");
  157. assert.areEqual("-0", new Intl.NumberFormat().format(-0), "-0 should be treated as a negative number (NumberFormat.prototype.format)");
  158. if (WScript.Platform.INTL_LIBRARY === "icu") {
  159. assert.areEqual("-0", new Intl.NumberFormat().formatToParts(-0).map(v => v.value).join(""), "-0 should be treated as a negative number (NumberFormat.prototype.formatToParts)");
  160. }
  161. }
  162. },
  163. {
  164. name: "formatToParts",
  165. body() {
  166. if (WScript.Platform.INTL_LIBRARY === "winglob") {
  167. return;
  168. }
  169. function assertParts(locale, options, n, expectedParts) {
  170. const nf = new Intl.NumberFormat(locale, options);
  171. const resolved = nf.resolvedOptions();
  172. assert.areEqual(locale, resolved.locale, `This test requires ${locale} support`);
  173. if (options) {
  174. for (const opt of Object.getOwnPropertyNames(options)) {
  175. assert.areEqual(options[opt], resolved[opt], `Bad option resolution for option ${opt}`)
  176. }
  177. }
  178. const actualParts = nf.formatToParts(n);
  179. assert.isTrue(Array.isArray(actualParts), `formatToParts(${n}) did not return an array`);
  180. if (WScript.Platform.ICU_VERSION < 61) {
  181. // real formatToParts support was only added with ICU 61
  182. assert.areEqual(1, actualParts.length, `formatToParts(${n}) stub implementation should return only one part`);
  183. const literal = actualParts[0];
  184. assert.areEqual("unknown", literal.type, `formatToParts(${n}) stub implementation should return an unknown part`);
  185. assert.areEqual(nf.format(n), literal.value, `formatToParts(${n}) stub implementation should return one part whose value matches the fully formatted number`);
  186. return;
  187. }
  188. assert.areEqual(expectedParts.length, actualParts.length, `formatToParts(${n}) returned wrong number of parts (actual: ${JSON.stringify(actualParts, null, 2)})`);
  189. expectedParts.forEach((part, i) => {
  190. assert.areEqual(expectedParts[i].type, actualParts[i].type, `formatToParts(${n}) returned bad type for part ${i}`);
  191. assert.areEqual(expectedParts[i].value, actualParts[i].value, `formatToParts(${n}) returned bad value for part ${i} (code points: ${actualParts[i].value.split("").map(char => char.charCodeAt(0)).toString()})`);
  192. })
  193. }
  194. assertParts("en-US", undefined, 1000, [
  195. { type: "integer" , value: "1" },
  196. { type: "group", value: "," },
  197. { type: "integer", value: "000" }
  198. ]);
  199. assertParts("en-US", undefined, -1000, [
  200. { type: "minusSign", value: "-" },
  201. { type: "integer" , value: "1" },
  202. { type: "group", value: "," },
  203. { type: "integer", value: "000" }
  204. ]);
  205. if (WScript.Platform.ICU_VERSION !== 62) {
  206. assertParts("en-US", undefined, NaN, [{ type: "nan", value: "NaN" }]);
  207. }
  208. assertParts("en-US", undefined, Infinity, [{ type: "infinity", value: "∞" }]);
  209. assertParts("en-US", undefined, 1000.3423, [
  210. { type: "integer", value: "1" },
  211. { type: "group", value: "," },
  212. { type: "integer", value: "000" },
  213. { type: "decimal", value: "." },
  214. { type: "fraction", value: "342" }
  215. ]);
  216. assertParts("en-US", { minimumFractionDigits: 5 }, 1000.3423, [
  217. { type: "integer", value: "1" },
  218. { type: "group", value: "," },
  219. { type: "integer", value: "000" },
  220. { type: "decimal", value: "." },
  221. { type: "fraction", value: "34230" }
  222. ]);
  223. assertParts("en-US", { style: "currency", currency: "CAD", currencyDisplay: "name" }, 1000.3423, [
  224. { type: "integer", value: "1" },
  225. { type: "group", value: "," },
  226. { type: "integer", value: "000" },
  227. { type: "decimal", value: "." },
  228. { type: "fraction", value: "34" },
  229. { type: "literal", value: " " },
  230. { type: "currency", value: "Canadian dollars" }
  231. ]);
  232. assertParts("en-US", { style: "percent", minimumSignificantDigits: 4 }, 0.3423, [
  233. { type: "integer", value: "34" },
  234. { type: "decimal", value: "." },
  235. { type: "fraction", value: "23" },
  236. { type: "percent", value: "%" }
  237. ]);
  238. assertParts("de-DE", { minimumFractionDigits: 5 }, 1000.3423, [
  239. { type: "integer", value: "1" },
  240. { type: "group", value: "." },
  241. { type: "integer", value: "000" },
  242. { type: "decimal", value: "," },
  243. { type: "fraction", value: "34230" }
  244. ]);
  245. assertParts("de-DE", { style: "currency", currency: "CAD", currencyDisplay: "name" }, 1000.3423, [
  246. { type: "integer", value: "1" },
  247. { type: "group", value: "." },
  248. { type: "integer", value: "000" },
  249. { type: "decimal", value: "," },
  250. { type: "fraction", value: "34" },
  251. { type: "literal", value: " " },
  252. { type: "currency", value: "Kanadische Dollar" }
  253. ]);
  254. assertParts("de-DE", { style: "percent", minimumSignificantDigits: 4 }, 0.3423, [
  255. { type: "integer", value: "34" },
  256. { type: "decimal", value: "," },
  257. { type: "fraction", value: "23" },
  258. { type: "literal", value: "\u00A0" }, // non-breaking space
  259. { type: "percent", value: "%" }
  260. ]);
  261. }
  262. }
  263. ];
  264. testRunner.runTests(tests, { verbose: false });