@automattic/number-formatters 1.1.8 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
 
7
+ ## [1.1.10] - 2026-05-21
8
+ ### Changed
9
+ - Update package dependencies. [#49012]
10
+
11
+ ## [1.1.9] - 2026-05-19
12
+ ### Fixed
13
+ - Currency formatting: use ISO 4217 minor-unit exponent for smallest-unit conversion to correctly format IDR and other currencies where browser ICU disagrees with ISO 4217. [#48967]
14
+
7
15
  ## [1.1.8] - 2026-05-19
8
16
  ### Changed
9
17
  - Internal updates.
@@ -144,6 +152,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
144
152
  - Initial release
145
153
  - Basic number formatting functionality
146
154
 
155
+ [1.1.10]: https://github.com/Automattic/number-formatters/compare/1.1.9...1.1.10
156
+ [1.1.9]: https://github.com/Automattic/number-formatters/compare/1.1.8...1.1.9
147
157
  [1.1.8]: https://github.com/Automattic/number-formatters/compare/1.1.7...1.1.8
148
158
  [1.1.7]: https://github.com/Automattic/number-formatters/compare/1.1.6...1.1.7
149
159
  [1.1.6]: https://github.com/Automattic/number-formatters/compare/1.1.5...1.1.6
@@ -84,6 +84,35 @@ function getCurrencyFormatter({ number, currency, browserSafeLocale, forceLatin
84
84
  options: numberFormatOptions,
85
85
  });
86
86
  }
87
+ /**
88
+ * Smallest-unit exponent overrides for currencies where browser ICU's
89
+ * `maximumFractionDigits` disagrees with the API's smallest-unit encoding.
90
+ *
91
+ * Keep this list minimal — the backend is the source of truth for the API's
92
+ * smallest-unit encoding, so adding speculative entries here risks silent
93
+ * drift. Only add a currency once we've verified that browsers report a
94
+ * value the API does not use.
95
+ *
96
+ * - IDR: modern Chrome / Node 24+ ICU reports 0; the API encodes with exponent 2.
97
+ * - HUF: same browser/API divergence as IDR.
98
+ */
99
+ const SMALLEST_UNIT_EXPONENT_OVERRIDES = {
100
+ IDR: 2,
101
+ HUF: 2,
102
+ };
103
+ /**
104
+ * Returns the smallest unit exponent for a currency.
105
+ *
106
+ * Falls back to the browser-derived display precision for any currency not in
107
+ * the override map — i.e. existing behavior is preserved for everything except
108
+ * the explicitly listed currencies.
109
+ * @param currency - The currency code (ISO 4217)
110
+ * @param fallback - The browser-derived precision to use when no override applies
111
+ * @return number - The smallest unit exponent
112
+ */
113
+ function getSmallestUnitExponent(currency, fallback) {
114
+ return SMALLEST_UNIT_EXPONENT_OVERRIDES[currency] ?? fallback;
115
+ }
87
116
  /**
88
117
  * Returns the precision for a given locale and currency.
89
118
  * @param browserSafeLocale - The browser safe locale.
@@ -119,14 +148,12 @@ function scaleNumberForPrecision(number, currencyPrecision) {
119
148
  /**
120
149
  * Prepares a number for formatting.
121
150
  * @param number - The number to prepare.
122
- * @param currencyPrecision - The precision to prepare the number for.
151
+ * @param currencyPrecision - The display precision (from the browser) to round the result to.
152
+ * @param currency - The currency code, used to look up any smallest-unit exponent override.
123
153
  * @param isSmallestUnit - Whether the number is the smallest unit of a currency.
124
154
  * @return {number} The prepared number.
125
155
  */
126
- function prepareNumberForFormatting(number,
127
- // currencyPrecision here must be the precision of the currency, regardless
128
- // of what precision is requested for display!
129
- currencyPrecision, isSmallestUnit) {
156
+ function prepareNumberForFormatting(number, currencyPrecision, currency, isSmallestUnit) {
130
157
  if (isNaN(number)) {
131
158
  debug('formatCurrency was called with NaN');
132
159
  return 0;
@@ -135,7 +162,7 @@ currencyPrecision, isSmallestUnit) {
135
162
  if (!Number.isInteger(number)) {
136
163
  debug('formatCurrency was called with isSmallestUnit and a float which will be rounded', number);
137
164
  }
138
- const smallestUnitDivisor = 10 ** currencyPrecision;
165
+ const smallestUnitDivisor = 10 ** getSmallestUnitExponent(currency, currencyPrecision);
139
166
  return scaleNumberForPrecision(Math.round(number) / smallestUnitDivisor, currencyPrecision);
140
167
  }
141
168
  return scaleNumberForPrecision(number, currencyPrecision);
@@ -186,7 +213,7 @@ const numberFormatCurrency = ({ number, browserSafeLocale, currency, stripZeros,
186
213
  if (isSmallestUnit && typeof currencyPrecision === 'undefined') {
187
214
  throw new Error(`Could not determine currency precision for ${validCurrency} in ${browserSafeLocale}`);
188
215
  }
189
- const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, isSmallestUnit);
216
+ const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit);
190
217
  const formatter = getCurrencyFormatter({
191
218
  number: numberAsFloat,
192
219
  currency: validCurrency,
@@ -259,7 +286,7 @@ const getCurrencyObject = ({ number, browserSafeLocale, currency, stripZeros, is
259
286
  const validCurrency = getValidCurrency(currency, geoLocation);
260
287
  const currencyOverride = getCurrencyOverride(validCurrency, geoLocation);
261
288
  const currencyPrecision = getPrecisionForLocaleAndCurrency(browserSafeLocale, validCurrency, forceLatin);
262
- const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, isSmallestUnit);
289
+ const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit);
263
290
  const formatter = getCurrencyFormatter({
264
291
  number: numberAsFloat,
265
292
  currency: validCurrency,
@@ -80,6 +80,35 @@ function getCurrencyFormatter({ number, currency, browserSafeLocale, forceLatin
80
80
  options: numberFormatOptions,
81
81
  });
82
82
  }
83
+ /**
84
+ * Smallest-unit exponent overrides for currencies where browser ICU's
85
+ * `maximumFractionDigits` disagrees with the API's smallest-unit encoding.
86
+ *
87
+ * Keep this list minimal — the backend is the source of truth for the API's
88
+ * smallest-unit encoding, so adding speculative entries here risks silent
89
+ * drift. Only add a currency once we've verified that browsers report a
90
+ * value the API does not use.
91
+ *
92
+ * - IDR: modern Chrome / Node 24+ ICU reports 0; the API encodes with exponent 2.
93
+ * - HUF: same browser/API divergence as IDR.
94
+ */
95
+ const SMALLEST_UNIT_EXPONENT_OVERRIDES = {
96
+ IDR: 2,
97
+ HUF: 2,
98
+ };
99
+ /**
100
+ * Returns the smallest unit exponent for a currency.
101
+ *
102
+ * Falls back to the browser-derived display precision for any currency not in
103
+ * the override map — i.e. existing behavior is preserved for everything except
104
+ * the explicitly listed currencies.
105
+ * @param currency - The currency code (ISO 4217)
106
+ * @param fallback - The browser-derived precision to use when no override applies
107
+ * @return number - The smallest unit exponent
108
+ */
109
+ function getSmallestUnitExponent(currency, fallback) {
110
+ return SMALLEST_UNIT_EXPONENT_OVERRIDES[currency] ?? fallback;
111
+ }
83
112
  /**
84
113
  * Returns the precision for a given locale and currency.
85
114
  * @param browserSafeLocale - The browser safe locale.
@@ -115,14 +144,12 @@ function scaleNumberForPrecision(number, currencyPrecision) {
115
144
  /**
116
145
  * Prepares a number for formatting.
117
146
  * @param number - The number to prepare.
118
- * @param currencyPrecision - The precision to prepare the number for.
147
+ * @param currencyPrecision - The display precision (from the browser) to round the result to.
148
+ * @param currency - The currency code, used to look up any smallest-unit exponent override.
119
149
  * @param isSmallestUnit - Whether the number is the smallest unit of a currency.
120
150
  * @return {number} The prepared number.
121
151
  */
122
- function prepareNumberForFormatting(number,
123
- // currencyPrecision here must be the precision of the currency, regardless
124
- // of what precision is requested for display!
125
- currencyPrecision, isSmallestUnit) {
152
+ function prepareNumberForFormatting(number, currencyPrecision, currency, isSmallestUnit) {
126
153
  if (isNaN(number)) {
127
154
  debug('formatCurrency was called with NaN');
128
155
  return 0;
@@ -131,7 +158,7 @@ currencyPrecision, isSmallestUnit) {
131
158
  if (!Number.isInteger(number)) {
132
159
  debug('formatCurrency was called with isSmallestUnit and a float which will be rounded', number);
133
160
  }
134
- const smallestUnitDivisor = 10 ** currencyPrecision;
161
+ const smallestUnitDivisor = 10 ** getSmallestUnitExponent(currency, currencyPrecision);
135
162
  return scaleNumberForPrecision(Math.round(number) / smallestUnitDivisor, currencyPrecision);
136
163
  }
137
164
  return scaleNumberForPrecision(number, currencyPrecision);
@@ -182,7 +209,7 @@ const numberFormatCurrency = ({ number, browserSafeLocale, currency, stripZeros,
182
209
  if (isSmallestUnit && typeof currencyPrecision === 'undefined') {
183
210
  throw new Error(`Could not determine currency precision for ${validCurrency} in ${browserSafeLocale}`);
184
211
  }
185
- const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, isSmallestUnit);
212
+ const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit);
186
213
  const formatter = getCurrencyFormatter({
187
214
  number: numberAsFloat,
188
215
  currency: validCurrency,
@@ -254,7 +281,7 @@ const getCurrencyObject = ({ number, browserSafeLocale, currency, stripZeros, is
254
281
  const validCurrency = getValidCurrency(currency, geoLocation);
255
282
  const currencyOverride = getCurrencyOverride(validCurrency, geoLocation);
256
283
  const currencyPrecision = getPrecisionForLocaleAndCurrency(browserSafeLocale, validCurrency, forceLatin);
257
- const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, isSmallestUnit);
284
+ const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit);
258
285
  const formatter = getCurrencyFormatter({
259
286
  number: numberAsFloat,
260
287
  currency: validCurrency,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/number-formatters",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "Number formatting utilities",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/number-formatters/#readme",
6
6
  "bugs": {
@@ -39,11 +39,11 @@
39
39
  "devDependencies": {
40
40
  "@babel/core": "7.29.0",
41
41
  "@babel/preset-react": "7.28.5",
42
- "@jest/globals": "30.3.0",
42
+ "@jest/globals": "30.4.1",
43
43
  "@knighted/duel": "4.0.2",
44
44
  "@types/jest": "30.0.0",
45
45
  "@typescript/native-preview": "7.0.0-dev.20260225.1",
46
- "jest": "30.3.0",
46
+ "jest": "30.4.2",
47
47
  "jetpack-js-tools": "workspace:*",
48
48
  "typescript": "5.9.3"
49
49
  }
@@ -98,6 +98,37 @@ function getCurrencyFormatter( {
98
98
  } );
99
99
  }
100
100
 
101
+ /**
102
+ * Smallest-unit exponent overrides for currencies where browser ICU's
103
+ * `maximumFractionDigits` disagrees with the API's smallest-unit encoding.
104
+ *
105
+ * Keep this list minimal — the backend is the source of truth for the API's
106
+ * smallest-unit encoding, so adding speculative entries here risks silent
107
+ * drift. Only add a currency once we've verified that browsers report a
108
+ * value the API does not use.
109
+ *
110
+ * - IDR: modern Chrome / Node 24+ ICU reports 0; the API encodes with exponent 2.
111
+ * - HUF: same browser/API divergence as IDR.
112
+ */
113
+ const SMALLEST_UNIT_EXPONENT_OVERRIDES: Record< string, number > = {
114
+ IDR: 2,
115
+ HUF: 2,
116
+ };
117
+
118
+ /**
119
+ * Returns the smallest unit exponent for a currency.
120
+ *
121
+ * Falls back to the browser-derived display precision for any currency not in
122
+ * the override map — i.e. existing behavior is preserved for everything except
123
+ * the explicitly listed currencies.
124
+ * @param currency - The currency code (ISO 4217)
125
+ * @param fallback - The browser-derived precision to use when no override applies
126
+ * @return number - The smallest unit exponent
127
+ */
128
+ function getSmallestUnitExponent( currency: string, fallback: number ): number {
129
+ return SMALLEST_UNIT_EXPONENT_OVERRIDES[ currency ] ?? fallback;
130
+ }
131
+
101
132
  /**
102
133
  * Returns the precision for a given locale and currency.
103
134
  * @param browserSafeLocale - The browser safe locale.
@@ -139,15 +170,15 @@ function scaleNumberForPrecision( number: number, currencyPrecision: number ): n
139
170
  /**
140
171
  * Prepares a number for formatting.
141
172
  * @param number - The number to prepare.
142
- * @param currencyPrecision - The precision to prepare the number for.
173
+ * @param currencyPrecision - The display precision (from the browser) to round the result to.
174
+ * @param currency - The currency code, used to look up any smallest-unit exponent override.
143
175
  * @param isSmallestUnit - Whether the number is the smallest unit of a currency.
144
176
  * @return {number} The prepared number.
145
177
  */
146
178
  function prepareNumberForFormatting(
147
179
  number: number,
148
- // currencyPrecision here must be the precision of the currency, regardless
149
- // of what precision is requested for display!
150
180
  currencyPrecision: number,
181
+ currency: string,
151
182
  isSmallestUnit?: boolean
152
183
  ): number {
153
184
  if ( isNaN( number ) ) {
@@ -162,7 +193,7 @@ function prepareNumberForFormatting(
162
193
  number
163
194
  );
164
195
  }
165
- const smallestUnitDivisor = 10 ** currencyPrecision;
196
+ const smallestUnitDivisor = 10 ** getSmallestUnitExponent( currency, currencyPrecision );
166
197
  return scaleNumberForPrecision( Math.round( number ) / smallestUnitDivisor, currencyPrecision );
167
198
  }
168
199
 
@@ -235,6 +266,7 @@ const numberFormatCurrency = ( {
235
266
  const numberAsFloat = prepareNumberForFormatting(
236
267
  number,
237
268
  currencyPrecision ?? 0,
269
+ validCurrency,
238
270
  isSmallestUnit
239
271
  );
240
272
  const formatter = getCurrencyFormatter( {
@@ -326,6 +358,7 @@ const getCurrencyObject = ( {
326
358
  const numberAsFloat = prepareNumberForFormatting(
327
359
  number,
328
360
  currencyPrecision ?? 0,
361
+ validCurrency,
329
362
  isSmallestUnit
330
363
  );
331
364
  const formatter = getCurrencyFormatter( {