options.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import { UnsupportedError } from '../errors.js';
  2. /**
  3. * Given an input ICU NumberFormatter skeleton, does its best to construct a
  4. * corresponding `Intl.NumberFormat` options structure.
  5. *
  6. * @remarks
  7. * Some features depend on `Intl.NumberFormat` features defined in ES2020.
  8. *
  9. * @internal
  10. * @param onUnsupported - If defined, called when encountering unsupported (but
  11. * valid) tokens, such as `decimal-always` or `permille`. The error `source`
  12. * may specify the source of an unsupported option.
  13. *
  14. * @example
  15. * ```js
  16. * import {
  17. * getNumberFormatOptions,
  18. * parseNumberSkeleton
  19. * } from '@messageformat/number-skeleton'
  20. *
  21. * const src = 'currency/CAD unit-width-narrow'
  22. * const skeleton = parseNumberSkeleton(src, console.error)
  23. * // {
  24. * // unit: { style: 'currency', currency: 'CAD' },
  25. * // unitWidth: 'unit-width-narrow'
  26. * // }
  27. *
  28. * getNumberFormatOptions(skeleton, console.error)
  29. * // {
  30. * // style: 'currency',
  31. * // currency: 'CAD',
  32. * // currencyDisplay: 'narrowSymbol',
  33. * // unitDisplay: 'narrow'
  34. * // }
  35. *
  36. * const sk2 = parseNumberSkeleton('group-min2')
  37. * // { group: 'group-min2' }
  38. *
  39. * getNumberFormatOptions(sk2, console.error)
  40. * // Error: The stem group-min2 is not supported
  41. * // at UnsupportedError.NumberFormatError ... {
  42. * // code: 'UNSUPPORTED',
  43. * // stem: 'group-min2'
  44. * // }
  45. * // {}
  46. * ```
  47. */
  48. export function getNumberFormatOptions(skeleton, onUnsupported) {
  49. const { decimal, group, integerWidth, notation, precision, roundingMode, sign, unit, unitPer, unitWidth } = skeleton;
  50. const fail = (stem, source) => {
  51. if (onUnsupported)
  52. onUnsupported(new UnsupportedError(stem, source));
  53. };
  54. const opt = {};
  55. if (unit) {
  56. switch (unit.style) {
  57. case 'base-unit':
  58. opt.style = 'decimal';
  59. break;
  60. case 'currency':
  61. opt.style = 'currency';
  62. opt.currency = unit.currency;
  63. break;
  64. case 'measure-unit':
  65. opt.style = 'unit';
  66. opt.unit = unit.unit.replace(/.*-/, '');
  67. if (unitPer)
  68. opt.unit += '-per-' + unitPer.replace(/.*-/, '');
  69. break;
  70. case 'percent':
  71. opt.style = 'percent';
  72. break;
  73. case 'permille':
  74. fail('permille');
  75. break;
  76. }
  77. }
  78. switch (unitWidth) {
  79. case 'unit-width-full-name':
  80. opt.currencyDisplay = 'name';
  81. opt.unitDisplay = 'long';
  82. break;
  83. case 'unit-width-hidden':
  84. fail(unitWidth);
  85. break;
  86. case 'unit-width-iso-code':
  87. opt.currencyDisplay = 'code';
  88. break;
  89. case 'unit-width-narrow':
  90. opt.currencyDisplay = 'narrowSymbol';
  91. opt.unitDisplay = 'narrow';
  92. break;
  93. case 'unit-width-short':
  94. opt.currencyDisplay = 'symbol';
  95. opt.unitDisplay = 'short';
  96. break;
  97. }
  98. switch (group) {
  99. case 'group-off':
  100. opt.useGrouping = false;
  101. break;
  102. case 'group-auto':
  103. opt.useGrouping = true;
  104. break;
  105. case 'group-min2':
  106. case 'group-on-aligned':
  107. case 'group-thousands':
  108. fail(group);
  109. opt.useGrouping = true;
  110. break;
  111. }
  112. if (precision) {
  113. switch (precision.style) {
  114. case 'precision-fraction': {
  115. const { minFraction: minF, maxFraction: maxF, minSignificant: minS, maxSignificant: maxS, source } = precision;
  116. if (typeof minF === 'number') {
  117. opt.minimumFractionDigits = minF;
  118. if (typeof minS === 'number')
  119. fail('precision-fraction', source);
  120. }
  121. if (typeof maxF === 'number')
  122. opt.maximumFractionDigits = maxF;
  123. if (typeof minS === 'number')
  124. opt.minimumSignificantDigits = minS;
  125. if (typeof maxS === 'number')
  126. opt.maximumSignificantDigits = maxS;
  127. break;
  128. }
  129. case 'precision-integer':
  130. opt.maximumFractionDigits = 0;
  131. break;
  132. case 'precision-unlimited':
  133. opt.maximumFractionDigits = 20;
  134. break;
  135. case 'precision-increment':
  136. break;
  137. case 'precision-currency-standard':
  138. opt.trailingZeroDisplay = precision.trailingZero;
  139. break;
  140. case 'precision-currency-cash':
  141. fail(precision.style);
  142. break;
  143. }
  144. }
  145. if (notation) {
  146. switch (notation.style) {
  147. case 'compact-short':
  148. opt.notation = 'compact';
  149. opt.compactDisplay = 'short';
  150. break;
  151. case 'compact-long':
  152. opt.notation = 'compact';
  153. opt.compactDisplay = 'long';
  154. break;
  155. case 'notation-simple':
  156. opt.notation = 'standard';
  157. break;
  158. case 'scientific':
  159. case 'engineering': {
  160. const { expDigits, expSign, source, style } = notation;
  161. opt.notation = style;
  162. if ((expDigits && expDigits > 1) ||
  163. (expSign && expSign !== 'sign-auto'))
  164. fail(style, source);
  165. break;
  166. }
  167. }
  168. }
  169. if (integerWidth) {
  170. const { min, max, source } = integerWidth;
  171. if (min > 0)
  172. opt.minimumIntegerDigits = min;
  173. if (Number(max) > 0) {
  174. const hasExp = opt.notation === 'engineering' || opt.notation === 'scientific';
  175. if (max === 3 && hasExp)
  176. opt.notation = 'engineering';
  177. else
  178. fail('integer-width', source);
  179. }
  180. }
  181. switch (sign) {
  182. case 'sign-auto':
  183. opt.signDisplay = 'auto';
  184. break;
  185. case 'sign-always':
  186. opt.signDisplay = 'always';
  187. break;
  188. case 'sign-except-zero':
  189. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  190. // @ts-ignore https://github.com/microsoft/TypeScript/issues/46712
  191. opt.signDisplay = 'exceptZero';
  192. break;
  193. case 'sign-never':
  194. opt.signDisplay = 'never';
  195. break;
  196. case 'sign-accounting':
  197. opt.currencySign = 'accounting';
  198. break;
  199. case 'sign-accounting-always':
  200. opt.currencySign = 'accounting';
  201. opt.signDisplay = 'always';
  202. break;
  203. case 'sign-accounting-except-zero':
  204. opt.currencySign = 'accounting';
  205. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  206. // @ts-ignore https://github.com/microsoft/TypeScript/issues/46712
  207. opt.signDisplay = 'exceptZero';
  208. break;
  209. }
  210. if (decimal === 'decimal-always')
  211. fail(decimal);
  212. if (roundingMode)
  213. fail(roundingMode);
  214. return opt;
  215. }