n2words 4.0.0 → 5.1.0

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.
Files changed (309) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +14 -12
  3. package/dist/am-ET.js +2 -2
  4. package/dist/am-ET.umd.js +2 -2
  5. package/dist/am-Latn-ET.js +2 -2
  6. package/dist/am-Latn-ET.umd.js +2 -2
  7. package/dist/ar-SA.js +2 -2
  8. package/dist/ar-SA.umd.js +2 -2
  9. package/dist/az-AZ.js +2 -2
  10. package/dist/az-AZ.umd.js +2 -2
  11. package/dist/bn-BD.js +2 -2
  12. package/dist/bn-BD.umd.js +2 -2
  13. package/dist/cs-CZ.js +2 -2
  14. package/dist/cs-CZ.umd.js +2 -2
  15. package/dist/da-DK.js +2 -2
  16. package/dist/da-DK.umd.js +2 -2
  17. package/dist/de-DE.js +2 -2
  18. package/dist/de-DE.umd.js +2 -2
  19. package/dist/el-GR.js +2 -2
  20. package/dist/el-GR.umd.js +2 -2
  21. package/dist/en-AU.js +2 -2
  22. package/dist/en-AU.umd.js +2 -2
  23. package/dist/en-BD.js +2 -2
  24. package/dist/en-BD.umd.js +2 -2
  25. package/dist/en-CA.js +2 -2
  26. package/dist/en-CA.umd.js +2 -2
  27. package/dist/en-GB.js +2 -2
  28. package/dist/en-GB.umd.js +2 -2
  29. package/dist/en-GH.js +2 -2
  30. package/dist/en-GH.umd.js +2 -2
  31. package/dist/en-IE.js +2 -2
  32. package/dist/en-IE.umd.js +2 -2
  33. package/dist/en-IN.js +2 -2
  34. package/dist/en-IN.umd.js +2 -2
  35. package/dist/en-KE.js +2 -2
  36. package/dist/en-KE.umd.js +2 -2
  37. package/dist/en-MY.js +2 -2
  38. package/dist/en-MY.umd.js +2 -2
  39. package/dist/en-NG.js +2 -2
  40. package/dist/en-NG.umd.js +2 -2
  41. package/dist/en-NZ.js +2 -2
  42. package/dist/en-NZ.umd.js +2 -2
  43. package/dist/en-PH.js +2 -2
  44. package/dist/en-PH.umd.js +2 -2
  45. package/dist/en-PK.js +2 -2
  46. package/dist/en-PK.umd.js +2 -2
  47. package/dist/en-SG.js +2 -2
  48. package/dist/en-SG.umd.js +2 -2
  49. package/dist/en-US.js +2 -2
  50. package/dist/en-US.umd.js +2 -2
  51. package/dist/en-ZA.js +2 -2
  52. package/dist/en-ZA.umd.js +2 -2
  53. package/dist/es-ES.js +2 -2
  54. package/dist/es-ES.umd.js +2 -2
  55. package/dist/es-MX.js +2 -2
  56. package/dist/es-MX.umd.js +2 -2
  57. package/dist/es-US.js +2 -2
  58. package/dist/es-US.umd.js +2 -2
  59. package/dist/fa-IR.js +2 -2
  60. package/dist/fa-IR.umd.js +2 -2
  61. package/dist/fi-FI.js +2 -2
  62. package/dist/fi-FI.umd.js +2 -2
  63. package/dist/fil-PH.js +2 -2
  64. package/dist/fil-PH.umd.js +2 -2
  65. package/dist/fr-BE.js +2 -2
  66. package/dist/fr-BE.umd.js +2 -2
  67. package/dist/fr-FR.js +2 -2
  68. package/dist/fr-FR.umd.js +2 -2
  69. package/dist/gu-IN.js +2 -2
  70. package/dist/gu-IN.umd.js +2 -2
  71. package/dist/ha-NG.js +2 -2
  72. package/dist/ha-NG.umd.js +2 -2
  73. package/dist/hbo-IL.js +2 -2
  74. package/dist/hbo-IL.umd.js +2 -2
  75. package/dist/he-IL.js +2 -2
  76. package/dist/he-IL.umd.js +2 -2
  77. package/dist/hi-IN.js +2 -2
  78. package/dist/hi-IN.umd.js +2 -2
  79. package/dist/hr-HR.js +2 -2
  80. package/dist/hr-HR.umd.js +2 -2
  81. package/dist/hu-HU.js +2 -2
  82. package/dist/hu-HU.umd.js +2 -2
  83. package/dist/id-ID.js +2 -2
  84. package/dist/id-ID.umd.js +2 -2
  85. package/dist/it-IT.js +2 -2
  86. package/dist/it-IT.umd.js +2 -2
  87. package/dist/ja-JP.js +2 -2
  88. package/dist/ja-JP.umd.js +2 -2
  89. package/dist/ka-GE.js +2 -2
  90. package/dist/ka-GE.umd.js +2 -2
  91. package/dist/kn-IN.js +2 -2
  92. package/dist/kn-IN.umd.js +2 -2
  93. package/dist/ko-KR.js +2 -2
  94. package/dist/ko-KR.umd.js +2 -2
  95. package/dist/lt-LT.js +2 -2
  96. package/dist/lt-LT.umd.js +2 -2
  97. package/dist/lv-LV.js +2 -2
  98. package/dist/lv-LV.umd.js +2 -2
  99. package/dist/mr-IN.js +2 -2
  100. package/dist/mr-IN.umd.js +2 -2
  101. package/dist/ms-MY.js +2 -2
  102. package/dist/ms-MY.umd.js +2 -2
  103. package/dist/nb-NO.js +2 -2
  104. package/dist/nb-NO.umd.js +2 -2
  105. package/dist/nl-NL.js +2 -2
  106. package/dist/nl-NL.umd.js +2 -2
  107. package/dist/pa-IN.js +2 -2
  108. package/dist/pa-IN.umd.js +2 -2
  109. package/dist/pl-PL.js +2 -2
  110. package/dist/pl-PL.umd.js +2 -2
  111. package/dist/pt-BR.js +2 -0
  112. package/dist/pt-BR.umd.js +2 -0
  113. package/dist/pt-PT.js +2 -2
  114. package/dist/pt-PT.umd.js +2 -2
  115. package/dist/ro-RO.js +2 -2
  116. package/dist/ro-RO.umd.js +2 -2
  117. package/dist/ru-RU.js +2 -2
  118. package/dist/ru-RU.umd.js +2 -2
  119. package/dist/sr-Cyrl-RS.js +2 -2
  120. package/dist/sr-Cyrl-RS.umd.js +2 -2
  121. package/dist/sr-Latn-RS.js +2 -2
  122. package/dist/sr-Latn-RS.umd.js +2 -2
  123. package/dist/sv-SE.js +2 -2
  124. package/dist/sv-SE.umd.js +2 -2
  125. package/dist/sw-KE.js +2 -2
  126. package/dist/sw-KE.umd.js +2 -2
  127. package/dist/ta-IN.js +2 -2
  128. package/dist/ta-IN.umd.js +2 -2
  129. package/dist/te-IN.js +2 -2
  130. package/dist/te-IN.umd.js +2 -2
  131. package/dist/th-TH.js +2 -2
  132. package/dist/th-TH.umd.js +2 -2
  133. package/dist/tr-TR.js +2 -2
  134. package/dist/tr-TR.umd.js +2 -2
  135. package/dist/uk-UA.js +2 -2
  136. package/dist/uk-UA.umd.js +2 -2
  137. package/dist/ur-PK.js +2 -2
  138. package/dist/ur-PK.umd.js +2 -2
  139. package/dist/vi-VN.js +2 -2
  140. package/dist/vi-VN.umd.js +2 -2
  141. package/dist/yo-NG.js +2 -2
  142. package/dist/yo-NG.umd.js +2 -2
  143. package/dist/zh-Hans-CN.js +2 -2
  144. package/dist/zh-Hans-CN.umd.js +2 -2
  145. package/dist/zh-Hant-TW.js +2 -2
  146. package/dist/zh-Hant-TW.umd.js +2 -2
  147. package/package.json +53 -36
  148. package/src/am-ET.d.ts +3 -5
  149. package/src/am-ET.js +41 -16
  150. package/src/am-Latn-ET.d.ts +3 -5
  151. package/src/am-Latn-ET.js +45 -16
  152. package/src/ar-SA.d.ts +44 -18
  153. package/src/ar-SA.js +93 -40
  154. package/src/az-AZ.d.ts +3 -5
  155. package/src/az-AZ.js +58 -20
  156. package/src/bn-BD.d.ts +3 -5
  157. package/src/bn-BD.js +32 -16
  158. package/src/cs-CZ.d.ts +3 -6
  159. package/src/cs-CZ.js +66 -42
  160. package/src/da-DK.d.ts +3 -6
  161. package/src/da-DK.js +53 -48
  162. package/src/de-DE.d.ts +17 -11
  163. package/src/de-DE.js +88 -57
  164. package/src/el-GR.d.ts +3 -6
  165. package/src/el-GR.js +45 -32
  166. package/src/en-AU.d.ts +17 -11
  167. package/src/en-AU.js +56 -41
  168. package/src/en-BD.d.ts +17 -11
  169. package/src/en-BD.js +60 -41
  170. package/src/en-CA.d.ts +36 -18
  171. package/src/en-CA.js +67 -46
  172. package/src/en-GB.d.ts +17 -11
  173. package/src/en-GB.js +56 -41
  174. package/src/en-GH.d.ts +32 -3
  175. package/src/en-GH.js +104 -26
  176. package/src/en-IE.d.ts +17 -11
  177. package/src/en-IE.js +56 -41
  178. package/src/en-IN.d.ts +17 -11
  179. package/src/en-IN.js +60 -41
  180. package/src/en-KE.d.ts +28 -3
  181. package/src/en-KE.js +93 -26
  182. package/src/en-MY.d.ts +26 -3
  183. package/src/en-MY.js +91 -26
  184. package/src/en-NG.d.ts +17 -11
  185. package/src/en-NG.js +56 -41
  186. package/src/en-NZ.d.ts +32 -3
  187. package/src/en-NZ.js +85 -31
  188. package/src/en-PH.d.ts +32 -3
  189. package/src/en-PH.js +97 -26
  190. package/src/en-PK.d.ts +17 -11
  191. package/src/en-PK.js +60 -41
  192. package/src/en-SG.d.ts +28 -3
  193. package/src/en-SG.js +93 -26
  194. package/src/en-US.d.ts +36 -18
  195. package/src/en-US.js +70 -47
  196. package/src/en-ZA.d.ts +17 -11
  197. package/src/en-ZA.js +56 -41
  198. package/src/es-ES.d.ts +53 -21
  199. package/src/es-ES.js +104 -56
  200. package/src/es-MX.d.ts +53 -21
  201. package/src/es-MX.js +104 -56
  202. package/src/es-US.d.ts +53 -21
  203. package/src/es-US.js +92 -51
  204. package/src/fa-IR.d.ts +3 -5
  205. package/src/fa-IR.js +28 -13
  206. package/src/fi-FI.d.ts +3 -6
  207. package/src/fi-FI.js +47 -29
  208. package/src/fil-PH.d.ts +3 -5
  209. package/src/fil-PH.js +61 -28
  210. package/src/fr-BE.d.ts +31 -15
  211. package/src/fr-BE.js +128 -57
  212. package/src/fr-FR.d.ts +31 -16
  213. package/src/fr-FR.js +97 -60
  214. package/src/gu-IN.d.ts +3 -5
  215. package/src/gu-IN.js +31 -16
  216. package/src/ha-NG.d.ts +3 -5
  217. package/src/ha-NG.js +55 -27
  218. package/src/hbo-IL.d.ts +26 -12
  219. package/src/hbo-IL.js +92 -51
  220. package/src/he-IL.d.ts +17 -10
  221. package/src/he-IL.js +92 -50
  222. package/src/hi-IN.d.ts +3 -5
  223. package/src/hi-IN.js +30 -17
  224. package/src/hr-HR.d.ts +21 -10
  225. package/src/hr-HR.js +89 -33
  226. package/src/hu-HU.d.ts +3 -5
  227. package/src/hu-HU.js +57 -23
  228. package/src/id-ID.d.ts +3 -5
  229. package/src/id-ID.js +56 -23
  230. package/src/it-IT.d.ts +17 -11
  231. package/src/it-IT.js +74 -43
  232. package/src/ja-JP.d.ts +3 -6
  233. package/src/ja-JP.js +39 -26
  234. package/src/ka-GE.d.ts +3 -6
  235. package/src/ka-GE.js +38 -26
  236. package/src/kn-IN.d.ts +3 -5
  237. package/src/kn-IN.js +31 -16
  238. package/src/ko-KR.d.ts +3 -6
  239. package/src/ko-KR.js +34 -26
  240. package/src/lt-LT.d.ts +21 -11
  241. package/src/lt-LT.js +64 -42
  242. package/src/lv-LV.d.ts +21 -11
  243. package/src/lv-LV.js +79 -51
  244. package/src/mr-IN.d.ts +3 -5
  245. package/src/mr-IN.js +31 -16
  246. package/src/ms-MY.d.ts +3 -5
  247. package/src/ms-MY.js +58 -24
  248. package/src/nb-NO.d.ts +3 -6
  249. package/src/nb-NO.js +54 -34
  250. package/src/nl-NL.d.ts +41 -20
  251. package/src/nl-NL.js +111 -69
  252. package/src/pa-IN.d.ts +3 -5
  253. package/src/pa-IN.js +32 -16
  254. package/src/pl-PL.d.ts +21 -11
  255. package/src/pl-PL.js +69 -45
  256. package/src/pt-BR.d.ts +42 -0
  257. package/src/pt-BR.js +574 -0
  258. package/src/pt-PT.d.ts +17 -11
  259. package/src/pt-PT.js +80 -48
  260. package/src/ro-RO.d.ts +21 -11
  261. package/src/ro-RO.js +77 -39
  262. package/src/ru-RU.d.ts +35 -15
  263. package/src/ru-RU.js +100 -38
  264. package/src/sr-Cyrl-RS.d.ts +35 -15
  265. package/src/sr-Cyrl-RS.js +100 -38
  266. package/src/sr-Latn-RS.d.ts +35 -15
  267. package/src/sr-Latn-RS.js +100 -38
  268. package/src/sv-SE.d.ts +3 -6
  269. package/src/sv-SE.js +53 -34
  270. package/src/sw-KE.d.ts +3 -5
  271. package/src/sw-KE.js +50 -20
  272. package/src/ta-IN.d.ts +3 -5
  273. package/src/ta-IN.js +29 -17
  274. package/src/te-IN.d.ts +3 -5
  275. package/src/te-IN.js +31 -16
  276. package/src/th-TH.d.ts +3 -5
  277. package/src/th-TH.js +42 -19
  278. package/src/tr-TR.d.ts +17 -11
  279. package/src/tr-TR.js +63 -37
  280. package/src/uk-UA.d.ts +21 -10
  281. package/src/uk-UA.js +89 -33
  282. package/src/ur-PK.d.ts +3 -5
  283. package/src/ur-PK.js +32 -16
  284. package/src/utils/check-max.d.ts +26 -0
  285. package/src/utils/check-max.js +33 -0
  286. package/src/utils/expand-scientific.d.ts +0 -4
  287. package/src/utils/expand-scientific.js +7 -9
  288. package/src/utils/is-plain-object.d.ts +3 -4
  289. package/src/utils/is-plain-object.js +3 -4
  290. package/src/utils/parse-cardinal.d.ts +1 -2
  291. package/src/utils/parse-cardinal.js +12 -9
  292. package/src/utils/parse-currency.d.ts +1 -2
  293. package/src/utils/parse-currency.js +9 -11
  294. package/src/utils/parse-ordinal.d.ts +0 -1
  295. package/src/utils/parse-ordinal.js +9 -10
  296. package/src/utils/resolve-options.d.ts +17 -0
  297. package/src/utils/resolve-options.js +56 -0
  298. package/src/utils/scale.d.ts +49 -0
  299. package/src/utils/scale.js +65 -0
  300. package/src/vi-VN.d.ts +3 -6
  301. package/src/vi-VN.js +41 -28
  302. package/src/yo-NG.d.ts +3 -5
  303. package/src/yo-NG.js +49 -33
  304. package/src/zh-Hans-CN.d.ts +45 -20
  305. package/src/zh-Hans-CN.js +84 -31
  306. package/src/zh-Hant-TW.d.ts +45 -20
  307. package/src/zh-Hant-TW.js +85 -34
  308. package/src/utils/validate-options.d.ts +0 -8
  309. package/src/utils/validate-options.js +0 -16
@@ -10,24 +10,23 @@ import { expandScientificNotation, hasScientificNotation, numberToString } from
10
10
  /**
11
11
  * Parses a value for cardinal conversion.
12
12
  * Cardinals accept any numeric value: integers, decimals, negatives.
13
- *
14
13
  * @param {number|string|bigint} value - The value to parse
15
- * @returns {{isNegative: boolean, integerPart: bigint, decimalPart?: string}}
14
+ * @returns {{isNegative: boolean, integerPart: bigint, decimalPart?: string}} The parsed cardinal components
16
15
  * @throws {TypeError} If value is not number, string, or bigint
17
16
  * @throws {RangeError} If value is not finite
18
17
  */
19
- export function parseCardinalValue (value) {
18
+ export function parseCardinalValue(value) {
20
19
  const type = typeof value
21
20
 
22
21
  // BigInt: simplest case
23
- if (type === 'bigint') {
22
+ if (typeof value === 'bigint') {
24
23
  return value < 0n
25
24
  ? { isNegative: true, integerPart: -value }
26
25
  : { isNegative: false, integerPart: value }
27
26
  }
28
27
 
29
28
  // Number: fast path for safe integers
30
- if (type === 'number') {
29
+ if (typeof value === 'number') {
31
30
  if (!Number.isFinite(value)) {
32
31
  throw new RangeError('Number must be finite (NaN and Infinity are not supported)')
33
32
  }
@@ -40,19 +39,21 @@ export function parseCardinalValue (value) {
40
39
  }
41
40
 
42
41
  // String input
43
- if (type === 'string') {
42
+ if (typeof value === 'string') {
44
43
  return parseNumericString(normalizeString(value))
45
44
  }
46
45
 
47
46
  throw new TypeError(
48
- `Invalid value type: expected number, string, or bigint, received ${type}`
47
+ `Invalid value type: expected number, string, or bigint, received ${type}`,
49
48
  )
50
49
  }
51
50
 
52
51
  /**
53
52
  * Validates and normalizes a string numeric input.
53
+ * @param {string} value - The string to normalize
54
+ * @returns {string} The normalized numeric string
54
55
  */
55
- function normalizeString (value) {
56
+ function normalizeString(value) {
56
57
  const trimmed = value.trim()
57
58
  if (trimmed.length === 0 || Number.isNaN(Number(trimmed))) {
58
59
  throw new RangeError(`Invalid number format: "${value}"`)
@@ -62,8 +63,10 @@ function normalizeString (value) {
62
63
 
63
64
  /**
64
65
  * Parses a normalized numeric string into components.
66
+ * @param {string} str - The normalized numeric string
67
+ * @returns {{isNegative: boolean, integerPart: bigint, decimalPart?: string}} The parsed numeric components
65
68
  */
66
- function parseNumericString (str) {
69
+ function parseNumericString(str) {
67
70
  const isNegative = str[0] === '-'
68
71
  if (isNegative) str = str.slice(1)
69
72
 
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Parses a value for currency conversion.
3
3
  * Returns dollars and cents as separate bigints, plus negative flag.
4
- *
5
4
  * @param {number|string|bigint} value - The value to parse
6
- * @returns {{isNegative: boolean, dollars: bigint, cents: bigint}}
5
+ * @returns {{isNegative: boolean, dollars: bigint, cents: bigint}} The parsed dollars and cents with a negative flag.
7
6
  * @throws {TypeError} If value is not number, string, or bigint
8
7
  * @throws {RangeError} If value is not finite
9
8
  */
@@ -9,24 +9,23 @@ import { expandScientificNotation, hasScientificNotation, numberToString } from
9
9
  /**
10
10
  * Parses a value for currency conversion.
11
11
  * Returns dollars and cents as separate bigints, plus negative flag.
12
- *
13
12
  * @param {number|string|bigint} value - The value to parse
14
- * @returns {{isNegative: boolean, dollars: bigint, cents: bigint}}
13
+ * @returns {{isNegative: boolean, dollars: bigint, cents: bigint}} The parsed dollars and cents with a negative flag.
15
14
  * @throws {TypeError} If value is not number, string, or bigint
16
15
  * @throws {RangeError} If value is not finite
17
16
  */
18
- export function parseCurrencyValue (value) {
17
+ export function parseCurrencyValue(value) {
19
18
  const type = typeof value
20
19
 
21
20
  // BigInt: whole dollars only
22
- if (type === 'bigint') {
21
+ if (typeof value === 'bigint') {
23
22
  return value < 0n
24
23
  ? { isNegative: true, dollars: -value, cents: 0n }
25
24
  : { isNegative: false, dollars: value, cents: 0n }
26
25
  }
27
26
 
28
27
  // Number: fast path for safe integers
29
- if (type === 'number') {
28
+ if (typeof value === 'number') {
30
29
  if (!Number.isFinite(value)) {
31
30
  throw new RangeError('Currency must be a finite number')
32
31
  }
@@ -40,22 +39,21 @@ export function parseCurrencyValue (value) {
40
39
  }
41
40
 
42
41
  // String input
43
- if (type === 'string') {
42
+ if (typeof value === 'string') {
44
43
  return parseCurrencyString(value)
45
44
  }
46
45
 
47
46
  throw new TypeError(
48
- `Invalid value type: expected number, string, or bigint, received ${type}`
47
+ `Invalid value type: expected number, string, or bigint, received ${type}`,
49
48
  )
50
49
  }
51
50
 
52
51
  /**
53
52
  * Parses a string for currency conversion.
54
- *
55
53
  * @param {string} value - The string to parse
56
- * @returns {{isNegative: boolean, dollars: bigint, cents: bigint}}
54
+ * @returns {{isNegative: boolean, dollars: bigint, cents: bigint}} The parsed dollars and cents with a negative flag.
57
55
  */
58
- function parseCurrencyString (value) {
56
+ function parseCurrencyString(value) {
59
57
  let str = value.trim()
60
58
 
61
59
  if (str.length === 0 || Number.isNaN(Number(str))) {
@@ -86,6 +84,6 @@ function parseCurrencyString (value) {
86
84
  return {
87
85
  isNegative,
88
86
  dollars: BigInt(dollarStr),
89
- cents: BigInt(centStr)
87
+ cents: BigInt(centStr),
90
88
  }
91
89
  }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Parses a value for ordinal conversion.
3
3
  * Ordinals require positive integers only (no zero, negatives, or decimals).
4
- *
5
4
  * @param {number|string|bigint} value - The value to parse
6
5
  * @returns {bigint} The positive integer value
7
6
  * @throws {TypeError} If value is not number, string, or bigint
@@ -9,17 +9,16 @@ import { expandScientificNotation, hasScientificNotation } from './expand-scient
9
9
  /**
10
10
  * Parses a value for ordinal conversion.
11
11
  * Ordinals require positive integers only (no zero, negatives, or decimals).
12
- *
13
12
  * @param {number|string|bigint} value - The value to parse
14
13
  * @returns {bigint} The positive integer value
15
14
  * @throws {TypeError} If value is not number, string, or bigint
16
15
  * @throws {RangeError} If value is zero, negative, or has a decimal part
17
16
  */
18
- export function parseOrdinalValue (value) {
17
+ export function parseOrdinalValue(value) {
19
18
  const type = typeof value
20
19
 
21
20
  // BigInt: simplest case
22
- if (type === 'bigint') {
21
+ if (typeof value === 'bigint') {
23
22
  if (value <= 0n) {
24
23
  throw new RangeError('Ordinals must be positive integers')
25
24
  }
@@ -27,7 +26,7 @@ export function parseOrdinalValue (value) {
27
26
  }
28
27
 
29
28
  // Number: fast path for safe integers
30
- if (type === 'number') {
29
+ if (typeof value === 'number') {
31
30
  if (!Number.isFinite(value)) {
32
31
  throw new RangeError('Ordinals must be finite numbers')
33
32
  }
@@ -41,23 +40,22 @@ export function parseOrdinalValue (value) {
41
40
  }
42
41
 
43
42
  // String input
44
- if (type === 'string') {
43
+ if (typeof value === 'string') {
45
44
  return parseOrdinalString(value)
46
45
  }
47
46
 
48
47
  throw new TypeError(
49
- `Invalid value type: expected number, string, or bigint, received ${type}`
48
+ `Invalid value type: expected number, string, or bigint, received ${type}`,
50
49
  )
51
50
  }
52
51
 
53
52
  /**
54
53
  * Parses a string for ordinal conversion.
55
- *
56
54
  * @param {string} value - The string to parse
57
55
  * @returns {bigint} The positive integer value
58
56
  * @throws {RangeError} If string is not a valid positive integer
59
57
  */
60
- function parseOrdinalString (value) {
58
+ function parseOrdinalString(value) {
61
59
  const trimmed = value.trim()
62
60
 
63
61
  if (trimmed.length === 0) {
@@ -96,8 +94,9 @@ function parseOrdinalString (value) {
96
94
  throw new RangeError('Ordinals must be positive integers')
97
95
  }
98
96
  return result
99
- } catch (e) {
97
+ }
98
+ catch (e) {
100
99
  if (e instanceof RangeError) throw e
101
- throw new RangeError(`Invalid ordinal format: "${value}"`)
100
+ throw new RangeError(`Invalid ordinal format: "${value}"`, { cause: e })
102
101
  }
103
102
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Apply a form's option defaults and reject anything it doesn't declare.
3
+ *
4
+ * The exported defaults map (e.g. `cardinalDefaults`) is the single source of
5
+ * truth for each option's default value — the form never restates it, and the
6
+ * docs generator imports it rather than scraping the body. A malformed options
7
+ * argument (unknown key, wrong-typed value, inherited key) throws `TypeError`;
8
+ * a known option with a value outside its declared allowed set (an exported
9
+ * `<form>Values` map, e.g. gender) throws `RangeError` — right kind of value,
10
+ * out of range — instead of silently falling back to the default.
11
+ * @template {object} T
12
+ * @param {T | undefined} options - Caller-provided options, or undefined
13
+ * @param {Required<T>} defaults - The form's default for every option
14
+ * @param {Partial<Record<keyof T & string, readonly unknown[]>>} [values] - Allowed set per enum-valued option
15
+ * @returns {Required<T>} The options with every default applied
16
+ */
17
+ export function resolveOptions<T extends object>(options: T | undefined, defaults: Required<T>, values?: Partial<Record<keyof T & string, readonly unknown[]>>): Required<T>;
@@ -0,0 +1,56 @@
1
+ import { isPlainObject } from './is-plain-object.js'
2
+
3
+ /**
4
+ * Apply a form's option defaults and reject anything it doesn't declare.
5
+ *
6
+ * The exported defaults map (e.g. `cardinalDefaults`) is the single source of
7
+ * truth for each option's default value — the form never restates it, and the
8
+ * docs generator imports it rather than scraping the body. A malformed options
9
+ * argument (unknown key, wrong-typed value, inherited key) throws `TypeError`;
10
+ * a known option with a value outside its declared allowed set (an exported
11
+ * `<form>Values` map, e.g. gender) throws `RangeError` — right kind of value,
12
+ * out of range — instead of silently falling back to the default.
13
+ * @template {object} T
14
+ * @param {T | undefined} options - Caller-provided options, or undefined
15
+ * @param {Required<T>} defaults - The form's default for every option
16
+ * @param {Partial<Record<keyof T & string, readonly unknown[]>>} [values] - Allowed set per enum-valued option
17
+ * @returns {Required<T>} The options with every default applied
18
+ */
19
+ export function resolveOptions(options, defaults, values) {
20
+ /** @type {Record<string, unknown>} */
21
+ const resolved = { ...defaults }
22
+ if (options === undefined) return /** @type {Required<T>} */ (resolved)
23
+
24
+ if (!isPlainObject(options)) {
25
+ throw new TypeError(`Invalid options: expected plain object or undefined, got ${typeof options}`)
26
+ }
27
+ const allowed = Object.keys(defaults)
28
+ for (const [key, value] of Object.entries(options)) {
29
+ // Own-property check: inherited keys (__proto__, constructor, …) are not
30
+ // options. Using `key in defaults` here would let them past the guard and
31
+ // into `resolved[key] = value` — a prototype-pollution vector. An unknown
32
+ // key is a malformed-argument (shape) error, hence TypeError — RangeError is
33
+ // reserved for a value outside an allowed range/set (see checkMax).
34
+ if (!Object.hasOwn(defaults, key)) {
35
+ throw new TypeError(`Unknown option "${key}" — expected one of: ${allowed.join(', ')}`)
36
+ }
37
+ // `{ key: undefined }` means "use the default": `key?: T` is `T | undefined`
38
+ // without exactOptionalPropertyTypes, and the old destructuring defaults
39
+ // treated undefined the same way. Omit it rather than reject it as a type.
40
+ if (value === undefined) continue
41
+ if (typeof value !== typeof resolved[key]) {
42
+ throw new TypeError(`Option "${key}" must be a ${typeof resolved[key]}, got ${typeof value}`)
43
+ }
44
+ if (values !== undefined && Object.hasOwn(values, key)) {
45
+ const set = /** @type {Record<string, readonly unknown[]>} */ (values)[key]
46
+ if (!set.includes(value)) {
47
+ // The received value is caller-supplied and arbitrary — stringify it so
48
+ // quotes/newlines can't garble the message. The allowed set is our own
49
+ // gate-verified declaration and reads cleaner unquoted.
50
+ throw new RangeError(`Option "${key}" must be one of: ${set.join(', ')} — got ${JSON.stringify(value)}`)
51
+ }
52
+ }
53
+ resolved[key] = value
54
+ }
55
+ return /** @type {Required<T>} */ (resolved)
56
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 3-digit grouping, scale array starting at "thousand" (en-US, de-DE, ru-RU, …).
3
+ * @param {number} scaleCount Number of scale words in the table
4
+ * @returns {bigint} The first unsupported value (10^((scaleCount + 1) * 3))
5
+ */
6
+ export function western(scaleCount: number): bigint;
7
+ /**
8
+ * Myriad (4-digit) grouping — one scale word per power of 10,000 (ja-JP, ko-KR).
9
+ * @param {number} scaleCount Number of scale words in the table
10
+ * @returns {bigint} The first unsupported value
11
+ */
12
+ export function myriad(scaleCount: number): bigint;
13
+ /**
14
+ * South Asian 3-2-2 grouping: a 3-digit base segment then 2 digits per scale
15
+ * word, with the table's index 0 reserved for the (empty) units slot.
16
+ * @param {number} wordCount Length of the scale-word table (including the units slot)
17
+ * @returns {bigint} The first unsupported value
18
+ */
19
+ export function indian(wordCount: number): bigint;
20
+ /**
21
+ * Long scale — each unit (a base scale word, or a prefix) yields two levels
22
+ * (-illion / -illiard), spanning 6 powers of ten. es-*, fr-*, it-IT.
23
+ * @param {number} unitCount Number of base scale words or prefixes
24
+ * @returns {bigint} The first unsupported value (10^(6 * (unitCount + 1)))
25
+ */
26
+ export function longScale(unitCount: number): bigint;
27
+ /**
28
+ * Escape hatch: a structural bound given as its base-10 exponent (zh: `bounded(16)`).
29
+ * @param {number} exponent The base-10 exponent of the ceiling
30
+ * @returns {bigint} 10^exponent
31
+ */
32
+ export function bounded(exponent: number): bigint;
33
+ /**
34
+ * Scale-range helpers — pure.
35
+ *
36
+ * Each form declares its maximum supported value as a bigint export
37
+ * (`cardinalMax`, `ordinalMax`, `currencyMax`): the smallest value the form
38
+ * refuses, so the largest it converts is that minus one. `UNBOUNDED` (null)
39
+ * means no fixed limit.
40
+ *
41
+ * These helpers derive that bigint from a language's own scale-table size, so
42
+ * the ceiling tracks the vocabulary and can't drift. A language whose shape
43
+ * fits none of them declares the value directly (`bounded(n)` or a literal
44
+ * bigint). They only *produce* a max — the range check that consumes one
45
+ * (`checkMax`) lives in check-max.js, and the verification checks (boundary,
46
+ * gaps, injectivity) live in the gate. Nothing here throws.
47
+ */
48
+ /** No fixed ceiling — recursive/compounding spellers (th-TH, fa-IR, …). */
49
+ export const UNBOUNDED: null;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Scale-range helpers — pure.
3
+ *
4
+ * Each form declares its maximum supported value as a bigint export
5
+ * (`cardinalMax`, `ordinalMax`, `currencyMax`): the smallest value the form
6
+ * refuses, so the largest it converts is that minus one. `UNBOUNDED` (null)
7
+ * means no fixed limit.
8
+ *
9
+ * These helpers derive that bigint from a language's own scale-table size, so
10
+ * the ceiling tracks the vocabulary and can't drift. A language whose shape
11
+ * fits none of them declares the value directly (`bounded(n)` or a literal
12
+ * bigint). They only *produce* a max — the range check that consumes one
13
+ * (`checkMax`) lives in check-max.js, and the verification checks (boundary,
14
+ * gaps, injectivity) live in the gate. Nothing here throws.
15
+ */
16
+
17
+ /** No fixed ceiling — recursive/compounding spellers (th-TH, fa-IR, …). */
18
+ export const UNBOUNDED = null
19
+
20
+ /**
21
+ * 3-digit grouping, scale array starting at "thousand" (en-US, de-DE, ru-RU, …).
22
+ * @param {number} scaleCount Number of scale words in the table
23
+ * @returns {bigint} The first unsupported value (10^((scaleCount + 1) * 3))
24
+ */
25
+ export function western(scaleCount) {
26
+ return 10n ** BigInt((scaleCount + 1) * 3)
27
+ }
28
+
29
+ /**
30
+ * Myriad (4-digit) grouping — one scale word per power of 10,000 (ja-JP, ko-KR).
31
+ * @param {number} scaleCount Number of scale words in the table
32
+ * @returns {bigint} The first unsupported value
33
+ */
34
+ export function myriad(scaleCount) {
35
+ return 10n ** BigInt((scaleCount + 1) * 4)
36
+ }
37
+
38
+ /**
39
+ * South Asian 3-2-2 grouping: a 3-digit base segment then 2 digits per scale
40
+ * word, with the table's index 0 reserved for the (empty) units slot.
41
+ * @param {number} wordCount Length of the scale-word table (including the units slot)
42
+ * @returns {bigint} The first unsupported value
43
+ */
44
+ export function indian(wordCount) {
45
+ return 10n ** BigInt(3 + 2 * (wordCount - 1))
46
+ }
47
+
48
+ /**
49
+ * Long scale — each unit (a base scale word, or a prefix) yields two levels
50
+ * (-illion / -illiard), spanning 6 powers of ten. es-*, fr-*, it-IT.
51
+ * @param {number} unitCount Number of base scale words or prefixes
52
+ * @returns {bigint} The first unsupported value (10^(6 * (unitCount + 1)))
53
+ */
54
+ export function longScale(unitCount) {
55
+ return 10n ** BigInt(6 * (unitCount + 1))
56
+ }
57
+
58
+ /**
59
+ * Escape hatch: a structural bound given as its base-10 exponent (zh: `bounded(16)`).
60
+ * @param {number} exponent The base-10 exponent of the ceiling
61
+ * @returns {bigint} 10^exponent
62
+ */
63
+ export function bounded(exponent) {
64
+ return 10n ** BigInt(exponent)
65
+ }
package/src/vi-VN.d.ts CHANGED
@@ -1,14 +1,15 @@
1
+ export const cardinalMax: bigint;
2
+ export const ordinalMax: bigint;
3
+ export const currencyMax: bigint;
1
4
  /**
2
5
  * Converts a numeric value to Vietnamese words.
3
6
  *
4
7
  * This is the main public API. It accepts any valid numeric input
5
8
  * (number, string, or bigint) and handles parsing internally.
6
- *
7
9
  * @param {number | string | bigint} value - The numeric value to convert
8
10
  * @returns {string} The number in Vietnamese words
9
11
  * @throws {TypeError} If value is not a valid numeric type
10
12
  * @throws {Error} If value is not a valid number format
11
- *
12
13
  * @example
13
14
  * toCardinal(42) // 'bốn mươi hai'
14
15
  * toCardinal(101) // 'một trăm lẻ một'
@@ -17,12 +18,10 @@
17
18
  export function toCardinal(value: number | string | bigint): string;
18
19
  /**
19
20
  * Converts a numeric value to Vietnamese ordinal words.
20
- *
21
21
  * @param {number | string | bigint} value - The numeric value to convert (positive integer)
22
22
  * @returns {string} The number as ordinal words
23
23
  * @throws {TypeError} If value is not a valid numeric type
24
24
  * @throws {RangeError} If value is negative, zero, or has a decimal part
25
- *
26
25
  * @example
27
26
  * toOrdinal(1) // 'thứ nhất'
28
27
  * toOrdinal(2) // 'thứ hai'
@@ -34,12 +33,10 @@ export function toOrdinal(value: number | string | bigint): string;
34
33
  *
35
34
  * Vietnamese Dong has no subunit in modern usage (xu are historical).
36
35
  * Amounts are rounded to whole đồng.
37
- *
38
36
  * @param {number | string | bigint} value - The currency amount to convert
39
37
  * @returns {string} The amount in Vietnamese currency words
40
38
  * @throws {TypeError} If value is not a valid numeric type
41
39
  * @throws {Error} If value is not a valid number format
42
- *
43
40
  * @example
44
41
  * toCurrency(42) // 'bốn mươi hai đồng'
45
42
  * toCurrency(1000) // 'một nghìn đồng'
package/src/vi-VN.js CHANGED
@@ -12,6 +12,8 @@
12
12
  import { parseCardinalValue } from './utils/parse-cardinal.js'
13
13
  import { parseCurrencyValue } from './utils/parse-currency.js'
14
14
  import { parseOrdinalValue } from './utils/parse-ordinal.js'
15
+ import { checkMax } from './utils/check-max.js'
16
+ import { western } from './utils/scale.js'
15
17
 
16
18
  // ============================================================================
17
19
  // Vocabulary (module-level constants)
@@ -21,14 +23,20 @@ import { parseOrdinalValue } from './utils/parse-ordinal.js'
21
23
  const ONES = ['không', 'một', 'hai', 'ba', 'bốn', 'năm', 'sáu', 'bảy', 'tám', 'chín']
22
24
 
23
25
  // Scale words indexed by scale level (0 = units, 1 = thousands, etc.)
26
+ // Vietnamese composes large numbers by cycling nghìn/triệu/tỷ and appending
27
+ // another tỷ every three groups. The recursion past tỷ tỷ (10^18) is rarely used
28
+ // and not firmly fixed, so the table stops at that scale word rather than invent
29
+ // more; the ceiling then falls where the next one would be needed (see below).
24
30
  const SCALES = [
25
- '', 'nghìn', 'triệu', 'tỷ', 'nghìn tỷ', 'trăm nghìn tỷ',
26
- 'Quintillion', 'Sextillion', 'Septillion', 'Octillion',
27
- 'Nonillion', 'Decillion', 'Undecillion', 'Duodecillion',
28
- 'Tredecillion', 'Quattuordecillion', 'Sexdecillion',
29
- 'Septendecillion', 'Octodecillion', 'Novemdecillion', 'Vigintillion'
31
+ '', 'nghìn', 'triệu', 'tỷ', 'nghìn tỷ', 'triệu tỷ', 'tỷ tỷ',
30
32
  ]
31
33
 
34
+ // 3-digit grouping with SCALES[0] = '' (units); the highest scale word is
35
+ // tỷ tỷ at index 6 (10^18), so values from 10^(3 * SCALES.length) up have no scale word.
36
+ export const cardinalMax = western(SCALES.length - 1)
37
+ export const ordinalMax = western(SCALES.length - 1)
38
+ export const currencyMax = western(SCALES.length - 1)
39
+
32
40
  const HUNDRED = 'trăm'
33
41
  const ZERO = 'không'
34
42
  const NEGATIVE = 'âm'
@@ -59,8 +67,10 @@ const LAM = 'lăm' // 5 in tens position (25, 35, etc.)
59
67
 
60
68
  /**
61
69
  * Builds word for 0-99 with special forms (mốt, lăm).
70
+ * @param {number} n - Integer in range 0-99
71
+ * @returns {string} Vietnamese words
62
72
  */
63
- function buildBelowHundred (n) {
73
+ function buildBelowHundred(n) {
64
74
  if (n === 0) return ONES[0]
65
75
  if (n < 10) return ONES[n]
66
76
 
@@ -85,8 +95,10 @@ function buildBelowHundred (n) {
85
95
 
86
96
  /**
87
97
  * Builds segment word for 0-999.
98
+ * @param {number} n - Integer in range 0-999
99
+ * @returns {string} Vietnamese words
88
100
  */
89
- function buildSegment (n) {
101
+ function buildSegment(n) {
90
102
  if (n === 0) return ''
91
103
 
92
104
  const hundreds = Math.trunc(n / 100)
@@ -105,10 +117,12 @@ function buildSegment (n) {
105
117
  result += ' ' + LE + ' '
106
118
  // Use "năm" not "lăm" after lẻ
107
119
  result += remainder === 5 ? 'năm' : ONES[remainder]
108
- } else {
120
+ }
121
+ else {
109
122
  result = ONES[remainder]
110
123
  }
111
- } else {
124
+ }
125
+ else {
112
126
  // 10-99 after hundreds
113
127
  if (result) result += ' '
114
128
  result += buildBelowHundred(remainder)
@@ -120,8 +134,10 @@ function buildSegment (n) {
120
134
 
121
135
  /**
122
136
  * Builds "lẻ" prefixed word for small remainders (1-99) after scale words.
137
+ * @param {number} n - Integer in range 0-99
138
+ * @returns {string} Vietnamese words
123
139
  */
124
- function buildLeSegment (n) {
140
+ function buildLeSegment(n) {
125
141
  if (n === 0) return ''
126
142
  if (n < 10) {
127
143
  // Use "năm" not "lăm" after lẻ
@@ -136,11 +152,10 @@ function buildLeSegment (n) {
136
152
 
137
153
  /**
138
154
  * Converts a non-negative integer to Vietnamese words.
139
- *
140
155
  * @param {bigint} n - Non-negative integer to convert
141
156
  * @returns {string} Vietnamese words
142
157
  */
143
- function integerToWords (n) {
158
+ function integerToWords(n) {
144
159
  if (n === 0n) return ZERO
145
160
 
146
161
  // Fast path: numbers < 100
@@ -178,11 +193,10 @@ function integerToWords (n) {
178
193
 
179
194
  /**
180
195
  * Builds words for numbers >= 1,000,000.
181
- *
182
196
  * @param {bigint} n - Number >= 1,000,000
183
197
  * @returns {string} Vietnamese words
184
198
  */
185
- function buildLargeNumberWords (n) {
199
+ function buildLargeNumberWords(n) {
186
200
  const numStr = n.toString()
187
201
  const len = numStr.length
188
202
 
@@ -212,7 +226,8 @@ function buildLargeNumberWords (n) {
212
226
  if (words) {
213
227
  if (scaleIndex > 0) {
214
228
  parts.push(words + ' ' + SCALES[scaleIndex])
215
- } else {
229
+ }
230
+ else {
216
231
  parts.push(words)
217
232
  }
218
233
  }
@@ -246,11 +261,10 @@ function buildLargeNumberWords (n) {
246
261
 
247
262
  /**
248
263
  * Converts decimal digits to Vietnamese words.
249
- *
250
264
  * @param {string} decimalPart - Decimal digits (without the point)
251
265
  * @returns {string} Vietnamese words for decimal part
252
266
  */
253
- function decimalPartToWords (decimalPart) {
267
+ function decimalPartToWords(decimalPart) {
254
268
  let result = ''
255
269
 
256
270
  // Handle leading zeros
@@ -276,19 +290,20 @@ function decimalPartToWords (decimalPart) {
276
290
  *
277
291
  * This is the main public API. It accepts any valid numeric input
278
292
  * (number, string, or bigint) and handles parsing internally.
279
- *
280
293
  * @param {number | string | bigint} value - The numeric value to convert
281
294
  * @returns {string} The number in Vietnamese words
282
295
  * @throws {TypeError} If value is not a valid numeric type
283
296
  * @throws {Error} If value is not a valid number format
284
- *
285
297
  * @example
286
298
  * toCardinal(42) // 'bốn mươi hai'
287
299
  * toCardinal(101) // 'một trăm lẻ một'
288
300
  * toCardinal(1000000) // 'một triệu'
289
301
  */
290
- function toCardinal (value) {
302
+ function toCardinal(value) {
291
303
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
304
+ // Both the integer part and the decimal's significant digits are spelled via
305
+ // the scale builder, so both must clear the ceiling.
306
+ checkMax(integerPart, cardinalMax, decimalPart)
292
307
 
293
308
  let result = ''
294
309
 
@@ -314,11 +329,10 @@ function toCardinal (value) {
314
329
  *
315
330
  * Vietnamese ordinals use "thứ" prefix + cardinal number.
316
331
  * Special case: "thứ nhất" for 1st (not "thứ một").
317
- *
318
332
  * @param {bigint} n - Positive integer to convert
319
333
  * @returns {string} Vietnamese ordinal words
320
334
  */
321
- function integerToOrdinal (n) {
335
+ function integerToOrdinal(n) {
322
336
  // Special case: 1st is "thứ nhất"
323
337
  if (n === 1n) {
324
338
  return ORDINAL_PREFIX + ' ' + ORDINAL_ONE
@@ -330,19 +344,19 @@ function integerToOrdinal (n) {
330
344
 
331
345
  /**
332
346
  * Converts a numeric value to Vietnamese ordinal words.
333
- *
334
347
  * @param {number | string | bigint} value - The numeric value to convert (positive integer)
335
348
  * @returns {string} The number as ordinal words
336
349
  * @throws {TypeError} If value is not a valid numeric type
337
350
  * @throws {RangeError} If value is negative, zero, or has a decimal part
338
- *
339
351
  * @example
340
352
  * toOrdinal(1) // 'thứ nhất'
341
353
  * toOrdinal(2) // 'thứ hai'
342
354
  * toOrdinal(10) // 'thứ mười'
343
355
  */
344
- function toOrdinal (value) {
356
+ function toOrdinal(value) {
345
357
  const integerPart = parseOrdinalValue(value)
358
+ // Ordinals are built from the cardinal speller, so they share its ceiling.
359
+ checkMax(integerPart, ordinalMax)
346
360
  return integerToOrdinal(integerPart)
347
361
  }
348
362
 
@@ -355,19 +369,18 @@ function toOrdinal (value) {
355
369
  *
356
370
  * Vietnamese Dong has no subunit in modern usage (xu are historical).
357
371
  * Amounts are rounded to whole đồng.
358
- *
359
372
  * @param {number | string | bigint} value - The currency amount to convert
360
373
  * @returns {string} The amount in Vietnamese currency words
361
374
  * @throws {TypeError} If value is not a valid numeric type
362
375
  * @throws {Error} If value is not a valid number format
363
- *
364
376
  * @example
365
377
  * toCurrency(42) // 'bốn mươi hai đồng'
366
378
  * toCurrency(1000) // 'một nghìn đồng'
367
379
  * toCurrency(-5) // 'âm năm đồng'
368
380
  */
369
- function toCurrency (value) {
381
+ function toCurrency(value) {
370
382
  const { isNegative, dollars: dong } = parseCurrencyValue(value)
383
+ checkMax(dong, currencyMax)
371
384
 
372
385
  let result = ''
373
386
  if (isNegative) {