get-formatter.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { getNumberFormatLocales } from './numberformat/locales.js';
  2. import { getNumberFormatModifier, getNumberFormatModifierSource } from './numberformat/modifier.js';
  3. import { getNumberFormatOptions } from './numberformat/options.js';
  4. import { parseNumberPattern } from './parse-pattern.js';
  5. import { parseNumberSkeleton } from './parse-skeleton.js';
  6. /**
  7. * Returns a number formatter function for the given locales and number skeleton
  8. *
  9. * @remarks
  10. * Uses `Intl.NumberFormat` (ES2020) internally.
  11. *
  12. * @public
  13. * @param locales - One or more valid BCP 47 language tags, e.g. `fr` or `en-CA`
  14. * @param skeleton - An ICU NumberFormatter pattern or `::`-prefixed skeleton
  15. * string, or a parsed `Skeleton` structure
  16. * @param currency - If `skeleton` is a pattern string that includes ¤ tokens,
  17. * their skeleton representation requires a three-letter currency code.
  18. * @param onError - If defined, will be called separately for each encountered
  19. * parsing error and unsupported feature.
  20. * @example
  21. * ```js
  22. * import { getNumberFormatter } from '@messageformat/number-skeleton'
  23. *
  24. * let src = ':: currency/CAD unit-width-narrow'
  25. * let fmt = getNumberFormatter('en-CA', src, console.error)
  26. * fmt(42) // '$42.00'
  27. *
  28. * src = '::percent scale/100'
  29. * fmt = getNumberFormatter('en', src, console.error)
  30. * fmt(0.3) // '30%'
  31. * ```
  32. */
  33. export function getNumberFormatter(locales, skeleton, currency, onError) {
  34. if (typeof skeleton === 'string') {
  35. skeleton =
  36. skeleton.indexOf('::') === 0
  37. ? parseNumberSkeleton(skeleton.slice(2), onError)
  38. : parseNumberPattern(skeleton, currency, onError);
  39. }
  40. const lc = getNumberFormatLocales(locales, skeleton);
  41. const opt = getNumberFormatOptions(skeleton, onError);
  42. const mod = getNumberFormatModifier(skeleton);
  43. const nf = new Intl.NumberFormat(lc, opt);
  44. if (skeleton.affix) {
  45. const [p0, p1] = skeleton.affix.pos;
  46. const [n0, n1] = skeleton.affix.neg || ['', ''];
  47. return (value) => {
  48. const n = nf.format(mod(value));
  49. return value < 0 ? `${n0}${n}${n1}` : `${p0}${n}${p1}`;
  50. };
  51. }
  52. return (value) => nf.format(mod(value));
  53. }
  54. /**
  55. * Returns a string of JavaScript source that evaluates to a number formatter
  56. * function with the same `(value: number) => string` signature as the function
  57. * returned by {@link getNumberFormatter}.
  58. *
  59. * @remarks
  60. * The returned function will memoize an `Intl.NumberFormat` instance.
  61. *
  62. * @public
  63. * @param locales - One or more valid BCP 47 language tags, e.g. `fr` or `en-CA`
  64. * @param skeleton - An ICU NumberFormatter pattern or `::`-prefixed skeleton
  65. * string, or a parsed `Skeleton` structure
  66. * @param currency - If `skeleton` is a pattern string that includes ¤ tokens,
  67. * their skeleton representation requires a three-letter currency code.
  68. * @param onError - If defined, will be called separately for each encountered
  69. * parsing error and unsupported feature.
  70. * @example
  71. * ```js
  72. * import { getNumberFormatterSource } from '@messageformat/number-skeleton'
  73. *
  74. * getNumberFormatterSource('en', '::percent', console.error)
  75. * // '(function() {\n' +
  76. * // ' var opt = {"style":"percent"};\n' +
  77. * // ' var nf = new Intl.NumberFormat(["en"], opt);\n' +
  78. * // ' var mod = function(n) { return n * 0.01; };\n' +
  79. * // ' return function(value) { return nf.format(mod(value)); }\n' +
  80. * // '})()'
  81. *
  82. * const src = getNumberFormatterSource('en-CA', ':: currency/CAD unit-width-narrow', console.error)
  83. * // '(function() {\n' +
  84. * // ' var opt = {"style":"currency","currency":"CAD","currencyDisplay":"narrowSymbol","unitDisplay":"narrow"};\n' +
  85. * // ' var nf = new Intl.NumberFormat(["en-CA"], opt);\n'
  86. * // ' return function(value) { return nf.format(value); }\n' +
  87. * // '})()'
  88. * const fmt = new Function(`return ${src}`)()
  89. * fmt(42) // '$42.00'
  90. * ```
  91. */
  92. export function getNumberFormatterSource(locales, skeleton, currency, onError) {
  93. if (typeof skeleton === 'string') {
  94. skeleton =
  95. skeleton.indexOf('::') === 0
  96. ? parseNumberSkeleton(skeleton.slice(2), onError)
  97. : parseNumberPattern(skeleton, currency, onError);
  98. }
  99. const lc = getNumberFormatLocales(locales, skeleton);
  100. const opt = getNumberFormatOptions(skeleton, onError);
  101. const modSrc = getNumberFormatModifierSource(skeleton);
  102. const lines = [
  103. `(function() {`,
  104. `var opt = ${JSON.stringify(opt)};`,
  105. `var nf = new Intl.NumberFormat(${JSON.stringify(lc)}, opt);`
  106. ];
  107. let res = 'nf.format(value)';
  108. if (modSrc) {
  109. lines.push(`var mod = ${modSrc};`);
  110. res = 'nf.format(mod(value))';
  111. }
  112. if (skeleton.affix) {
  113. const [p0, p1] = skeleton.affix.pos.map(s => JSON.stringify(s));
  114. if (skeleton.affix.neg) {
  115. const [n0, n1] = skeleton.affix.neg.map(s => JSON.stringify(s));
  116. res = `value < 0 ? ${n0} + ${res} + ${n1} : ${p0} + ${res} + ${p1}`;
  117. }
  118. else {
  119. res = `${p0} + ${res} + ${p1}`;
  120. }
  121. }
  122. lines.push(`return function(value) { return ${res}; }`);
  123. return lines.join('\n ') + '\n})()';
  124. }