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
package/src/es-US.js CHANGED
@@ -20,7 +20,9 @@
20
20
  import { parseCardinalValue } from './utils/parse-cardinal.js'
21
21
  import { parseCurrencyValue } from './utils/parse-currency.js'
22
22
  import { parseOrdinalValue } from './utils/parse-ordinal.js'
23
- import { validateOptions } from './utils/validate-options.js'
23
+ import { checkMax } from './utils/check-max.js'
24
+ import { bounded, western } from './utils/scale.js'
25
+ import { resolveOptions } from './utils/resolve-options.js'
24
26
 
25
27
  // ============================================================================
26
28
  // Vocabulary (module-level constants)
@@ -45,6 +47,14 @@ const HUNDREDS_FEM = ['', 'cienta', 'doscientas', 'trescientas', 'cuatrocientas'
45
47
  const SCALES = ['mil', 'millón', 'billón', 'trillón', 'cuatrillón', 'quintillón']
46
48
  const SCALES_PLURAL = ['mil', 'millones', 'billones', 'trillones', 'cuatrillones', 'quintillones']
47
49
 
50
+ // Supported magnitude ceilings (checked at the public entry points). Short
51
+ // scale: SCALES covers 10^3..10^18, plus the units group, so cardinals span at
52
+ // most SCALES.length + 1 groups of 3 digits (below 10^21). Ordinals are bounded
53
+ // lower: the millions multiplier uses buildOrdinalSegment (0-999), so n < 10^9.
54
+ export const cardinalMax = western(SCALES.length)
55
+ export const ordinalMax = bounded(9)
56
+ export const currencyMax = western(SCALES.length)
57
+
48
58
  const ZERO = 'cero'
49
59
  const NEGATIVE = 'menos'
50
60
  const DECIMAL_SEP = 'punto'
@@ -78,7 +88,7 @@ const CURRENCY_CONNECTOR = 'con'
78
88
  * @param {boolean} feminine - Use feminine forms
79
89
  * @returns {string} Spanish word
80
90
  */
81
- function buildSegment (n, feminine) {
91
+ function buildSegment(n, feminine) {
82
92
  if (n === 0) return ''
83
93
 
84
94
  // Special case: exact 100 is "cien" (no gender)
@@ -100,22 +110,27 @@ function buildSegment (n, feminine) {
100
110
  // Tens and ones
101
111
  if (tensOnes === 0) {
102
112
  // Just hundreds
103
- } else if (tensOnes < 10) {
113
+ }
114
+ else if (tensOnes < 10) {
104
115
  // Single digit
105
116
  const onesArr = feminine ? ONES_FEM : ONES_MASC
106
117
  parts.push(onesArr[tensOnes])
107
- } else if (tensOnes < 20) {
118
+ }
119
+ else if (tensOnes < 20) {
108
120
  // 10-19: teens
109
121
  parts.push(TEENS[ones])
110
- } else if (tensOnes < 30) {
122
+ }
123
+ else if (tensOnes < 30) {
111
124
  // 20-29: special twenties
112
125
  const twentiesArr = feminine ? TWENTIES_FEM : TWENTIES_MASC
113
126
  parts.push(twentiesArr[ones])
114
- } else {
127
+ }
128
+ else {
115
129
  // 30-99: tens y ones
116
130
  if (ones === 0) {
117
131
  parts.push(TENS[tens])
118
- } else {
132
+ }
133
+ else {
119
134
  const onesArr = feminine ? ONES_FEM : ONES_MASC
120
135
  parts.push(TENS[tens] + ' y ' + onesArr[ones])
121
136
  }
@@ -130,12 +145,11 @@ function buildSegment (n, feminine) {
130
145
 
131
146
  /**
132
147
  * Converts a non-negative integer to Spanish words (short scale).
133
- *
134
148
  * @param {bigint} n - Non-negative integer to convert
135
149
  * @param {boolean} feminine - Use feminine forms
136
150
  * @returns {string} Spanish words
137
151
  */
138
- function integerToWords (n, feminine) {
152
+ function integerToWords(n, feminine) {
139
153
  if (n === 0n) return ZERO
140
154
 
141
155
  // Fast path: numbers < 1000
@@ -164,23 +178,25 @@ function integerToWords (n, feminine) {
164
178
  if (i === 0) {
165
179
  // Units segment - use requested gender
166
180
  result += buildSegment(Number(segment), feminine)
167
- } else if (i === 1) {
181
+ }
182
+ else if (i === 1) {
168
183
  // Thousands: "mil" not "uno mil"
169
184
  if (segment === 1n) {
170
185
  result += SCALES[0]
171
- } else {
186
+ }
187
+ else {
172
188
  result += buildSegment(Number(segment), false) + ' ' + SCALES[0]
173
189
  }
174
- } else {
190
+ }
191
+ else {
175
192
  // Millions and above: "un millón", "dos millones", etc.
193
+ // Callers guard the magnitude (cardinalMax) so scaleIndex stays in range.
176
194
  const scaleIndex = i - 1 // SCALES[1] = millón, SCALES[2] = billón, etc.
177
- if (scaleIndex >= SCALES.length) {
178
- // Beyond our scale vocabulary
179
- result += buildSegment(Number(segment), false)
180
- } else if (segment === 1n) {
195
+ if (segment === 1n) {
181
196
  // "un millón" not "uno millón"
182
197
  result += 'un ' + SCALES[scaleIndex]
183
- } else {
198
+ }
199
+ else {
184
200
  result += buildSegment(Number(segment), false) + ' ' + SCALES_PLURAL[scaleIndex]
185
201
  }
186
202
  }
@@ -191,12 +207,11 @@ function integerToWords (n, feminine) {
191
207
 
192
208
  /**
193
209
  * Converts decimal digits to Spanish words.
194
- *
195
210
  * @param {string} decimalPart - Decimal digits (without the point)
196
211
  * @param {boolean} feminine - Use feminine forms
197
212
  * @returns {string} Spanish words for decimal part
198
213
  */
199
- function decimalPartToWords (decimalPart, feminine) {
214
+ function decimalPartToWords(decimalPart, feminine) {
200
215
  let result = ''
201
216
 
202
217
  // Handle leading zeros
@@ -217,27 +232,37 @@ function decimalPartToWords (decimalPart, feminine) {
217
232
  return result
218
233
  }
219
234
 
235
+ /**
236
+ * @typedef {object} CardinalOptions
237
+ * @property {('masculine'|'feminine')} [gender] - Grammatical gender
238
+ */
239
+
240
+ /** @type {Required<CardinalOptions>} */
241
+ export const cardinalDefaults = { gender: 'masculine' }
242
+
243
+ /** @type {{ gender: ReadonlyArray<Required<CardinalOptions>['gender']> }} */
244
+ export const cardinalValues = { gender: ['masculine', 'feminine'] }
245
+
220
246
  /**
221
247
  * Converts a numeric value to Spanish words (US short scale).
222
- *
223
248
  * @param {number | string | bigint} value - The numeric value to convert
224
- * @param {Object} [options] - Optional configuration
225
- * @param {('masculine'|'feminine')} [options.gender='masculine'] - Grammatical gender
249
+ * @param {CardinalOptions} [options] - Optional configuration
226
250
  * @returns {string} The number in Spanish words
227
251
  * @throws {TypeError} If value is not a valid numeric type
228
252
  * @throws {Error} If value is not a valid number format
229
- *
230
253
  * @example
231
254
  * toCardinal(21) // 'veintiuno'
232
255
  * toCardinal(21, {gender: 'feminine'}) // 'veintiuna'
233
256
  * toCardinal(1000000000) // 'un billón'
234
257
  */
235
- function toCardinal (value, options) {
236
- options = validateOptions(options)
258
+ function toCardinal(value, options) {
237
259
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
260
+ // Both the integer part and the decimal's significant digits are spelled via
261
+ // the scale builder, so both must clear the ceiling.
262
+ checkMax(integerPart, cardinalMax, decimalPart)
238
263
 
239
264
  // Apply option defaults
240
- const { gender = 'masculine' } = options
265
+ const { gender } = resolveOptions(options, cardinalDefaults, cardinalValues)
241
266
  const feminine = gender === 'feminine'
242
267
 
243
268
  let result = ''
@@ -261,12 +286,11 @@ function toCardinal (value, options) {
261
286
 
262
287
  /**
263
288
  * Builds ordinal word for a 0-999 segment.
264
- *
265
289
  * @param {number} n - Segment value 0-999
266
290
  * @param {boolean} feminine - Use feminine forms
267
291
  * @returns {string} Spanish ordinal word
268
292
  */
269
- function buildOrdinalSegment (n, feminine) {
293
+ function buildOrdinalSegment(n, feminine) {
270
294
  if (n === 0) return ''
271
295
 
272
296
  const ones = n % 10
@@ -283,7 +307,8 @@ function buildOrdinalSegment (n, feminine) {
283
307
  if (hundreds > 0) {
284
308
  if (hundreds === 1) {
285
309
  parts.push(hundredWord)
286
- } else {
310
+ }
311
+ else {
287
312
  const prefixes = ['', '', 'du', 'tri', 'cuadri', 'quin', 'sex', 'septi', 'octi', 'noni']
288
313
  parts.push(prefixes[hundreds] + hundredWord)
289
314
  }
@@ -304,12 +329,11 @@ function buildOrdinalSegment (n, feminine) {
304
329
 
305
330
  /**
306
331
  * Converts a positive integer to Spanish ordinal words.
307
- *
308
332
  * @param {bigint} n - Positive integer to convert
309
333
  * @param {boolean} feminine - Use feminine forms
310
334
  * @returns {string} Spanish ordinal words
311
335
  */
312
- function integerToOrdinal (n, feminine) {
336
+ function integerToOrdinal(n, feminine) {
313
337
  const thousandWord = feminine ? ORDINAL_THOUSAND_FEM : ORDINAL_THOUSAND_MASC
314
338
  const millionWord = feminine ? ORDINAL_MILLION_FEM : ORDINAL_MILLION_MASC
315
339
 
@@ -323,11 +347,12 @@ function integerToOrdinal (n, feminine) {
323
347
  const thousands = Number(n / 1000n)
324
348
  const remainder = Number(n % 1000n)
325
349
 
326
- let result = ''
350
+ let result
327
351
 
328
352
  if (thousands === 1) {
329
353
  result = thousandWord
330
- } else {
354
+ }
355
+ else {
331
356
  result = buildOrdinalSegment(thousands, feminine) + ' ' + thousandWord
332
357
  }
333
358
 
@@ -342,11 +367,12 @@ function integerToOrdinal (n, feminine) {
342
367
  const millions = Number(n / 1_000_000n)
343
368
  const remainder = n % 1_000_000n
344
369
 
345
- let result = ''
370
+ let result
346
371
 
347
372
  if (millions === 1) {
348
373
  result = millionWord
349
- } else {
374
+ }
375
+ else {
350
376
  result = buildOrdinalSegment(millions, feminine) + ' ' + millionWord
351
377
  }
352
378
 
@@ -357,26 +383,34 @@ function integerToOrdinal (n, feminine) {
357
383
  return result
358
384
  }
359
385
 
386
+ /**
387
+ * @typedef {object} OrdinalOptions
388
+ * @property {('masculine'|'feminine')} [gender] - Grammatical gender
389
+ */
390
+
391
+ /** @type {Required<OrdinalOptions>} */
392
+ export const ordinalDefaults = { gender: 'masculine' }
393
+
394
+ /** @type {{ gender: ReadonlyArray<Required<OrdinalOptions>['gender']> }} */
395
+ export const ordinalValues = { gender: ['masculine', 'feminine'] }
396
+
360
397
  /**
361
398
  * Converts a numeric value to Spanish ordinal words.
362
- *
363
399
  * @param {number | string | bigint} value - The positive integer to convert
364
- * @param {Object} [options] - Optional configuration
365
- * @param {('masculine'|'feminine')} [options.gender='masculine'] - Grammatical gender
400
+ * @param {OrdinalOptions} [options] - Optional configuration
366
401
  * @returns {string} The number in Spanish ordinal words
367
402
  * @throws {TypeError} If value is not a valid numeric type
368
403
  * @throws {Error} If value is not a positive integer
369
- *
370
404
  * @example
371
405
  * toOrdinal(1) // 'primero'
372
406
  * toOrdinal(1, { gender: 'feminine' }) // 'primera'
373
407
  * toOrdinal(21) // 'vigésimo primero'
374
408
  */
375
- function toOrdinal (value, options) {
376
- options = validateOptions(options)
409
+ function toOrdinal(value, options) {
377
410
  const integerPart = parseOrdinalValue(value)
411
+ checkMax(integerPart, ordinalMax)
378
412
 
379
- const { gender = 'masculine' } = options
413
+ const { gender } = resolveOptions(options, ordinalDefaults, ordinalValues)
380
414
  const feminine = gender === 'feminine'
381
415
 
382
416
  return integerToOrdinal(integerPart, feminine)
@@ -386,29 +420,34 @@ function toOrdinal (value, options) {
386
420
  // CURRENCY: toCurrency(value, options?)
387
421
  // ============================================================================
388
422
 
423
+ /**
424
+ * @typedef {object} CurrencyOptions
425
+ * @property {boolean} [and] - Use "con" between dollars and cents
426
+ */
427
+
428
+ /** @type {Required<CurrencyOptions>} */
429
+ export const currencyDefaults = { and: true }
430
+
389
431
  /**
390
432
  * Converts a numeric value to US Dollar currency words in Spanish.
391
433
  *
392
434
  * US Dollar uses masculine gender for dólares (el dólar)
393
435
  * and masculine for centavos (el centavo).
394
- *
395
436
  * @param {number | string | bigint} value - The currency amount to convert
396
- * @param {Object} [options] - Optional configuration
397
- * @param {boolean} [options.and=true] - Use "con" between dollars and cents
437
+ * @param {CurrencyOptions} [options] - Optional configuration
398
438
  * @returns {string} The amount in Spanish US Dollar currency words
399
439
  * @throws {TypeError} If value is not a valid numeric type
400
440
  * @throws {Error} If value is not a valid number format
401
- *
402
441
  * @example
403
442
  * toCurrency(42.50) // 'cuarenta y dos dólares con cincuenta centavos'
404
443
  * toCurrency(1) // 'un dólar'
405
444
  * toCurrency(0.99) // 'noventa y nueve centavos'
406
445
  * toCurrency(42.50, { and: false }) // 'cuarenta y dos dólares cincuenta centavos'
407
446
  */
408
- function toCurrency (value, options) {
409
- options = validateOptions(options)
447
+ function toCurrency(value, options) {
410
448
  const { isNegative, dollars, cents: centavos } = parseCurrencyValue(value)
411
- const { and: useAnd = true } = options
449
+ checkMax(dollars, currencyMax)
450
+ const { and: useAnd } = resolveOptions(options, currencyDefaults)
412
451
 
413
452
  let result = ''
414
453
  if (isNegative) result = NEGATIVE + ' '
@@ -418,7 +457,8 @@ function toCurrency (value, options) {
418
457
  // Use masculine for dollars, but "un dólar" not "uno dólar"
419
458
  if (dollars === 1n) {
420
459
  result += 'un ' + DOLAR
421
- } else {
460
+ }
461
+ else {
422
462
  result += integerToWords(dollars, false) + ' ' + DOLARES
423
463
  }
424
464
  }
@@ -431,7 +471,8 @@ function toCurrency (value, options) {
431
471
  // Use masculine for centavos, but "un centavo" not "uno centavo"
432
472
  if (centavos === 1n) {
433
473
  result += 'un ' + CENTAVO
434
- } else {
474
+ }
475
+ else {
435
476
  result += integerToWords(centavos, false) + ' ' + CENTAVOS
436
477
  }
437
478
  }
package/src/fa-IR.d.ts CHANGED
@@ -1,18 +1,18 @@
1
+ export const cardinalMax: null;
2
+ export const ordinalMax: null;
3
+ export const currencyMax: null;
1
4
  /**
2
5
  * Converts a numeric value to Persian words.
3
- *
4
6
  * @param {number | string | bigint} value - The numeric value to convert
5
7
  * @returns {string} The number in Persian words
6
8
  */
7
9
  export function toCardinal(value: number | string | bigint): string;
8
10
  /**
9
11
  * Converts a numeric value to Persian ordinal words.
10
- *
11
12
  * @param {number | string | bigint} value - The numeric value to convert (positive integer)
12
13
  * @returns {string} The number as ordinal words
13
14
  * @throws {TypeError} If value is not a valid numeric type
14
15
  * @throws {RangeError} If value is negative, zero, or has a decimal part
15
- *
16
16
  * @example
17
17
  * toOrdinal(1) // 'اول'
18
18
  * toOrdinal(2) // 'دوم'
@@ -24,12 +24,10 @@ export function toOrdinal(value: number | string | bigint): string;
24
24
  *
25
25
  * Iranian Rial has no subunit in modern usage.
26
26
  * (Historically dinar was 1/100 rial, but not used today)
27
- *
28
27
  * @param {number | string | bigint} value - The currency amount to convert
29
28
  * @returns {string} The amount in Persian currency words
30
29
  * @throws {TypeError} If value is not a valid numeric type
31
30
  * @throws {Error} If value is not a valid number format
32
- *
33
31
  * @example
34
32
  * toCurrency(42) // 'چهل و دو ریال'
35
33
  * toCurrency(1000) // 'هزار ریال'
package/src/fa-IR.js CHANGED
@@ -12,14 +12,24 @@
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 { UNBOUNDED } from './utils/scale.js'
16
+
17
+ // No fixed scale ceiling — the speller composes every magnitude.
18
+ export const cardinalMax = UNBOUNDED
19
+ export const ordinalMax = UNBOUNDED
20
+ export const currencyMax = UNBOUNDED
15
21
 
16
22
  // ============================================================================
17
23
  // Vocabulary
18
24
  // ============================================================================
19
25
 
26
+ /** @type {Record<number, string>} */
20
27
  const ONES = { 1: 'یک', 2: 'دو', 3: 'سه', 4: 'چهار', 5: 'پنج', 6: 'شش', 7: 'هفت', 8: 'هشت', 9: 'نه' }
28
+ /** @type {Record<number, string>} */
21
29
  const TEENS = { 10: 'ده', 11: 'یازده', 12: 'دوازده', 13: 'سیزده', 14: 'چهارده', 15: 'پانزده', 16: 'شانزده', 17: 'هفده', 18: 'هجده', 19: 'نوزده' }
30
+ /** @type {Record<number, string>} */
22
31
  const TENS = { 20: 'بیست', 30: 'سی', 40: 'چهل', 50: 'پنجاه', 60: 'شصت', 70: 'هفتاد', 80: 'هشتاد', 90: 'نود' }
32
+ /** @type {Record<number, string>} */
23
33
  const HUNDREDS = { 100: 'صد', 200: 'دویست', 300: 'سيصد', 400: 'چهار صد', 500: 'پانصد', 600: 'ششصد', 700: 'هفتصد', 800: 'هشتصد', 900: 'نهصد' }
24
34
 
25
35
  const THOUSAND = 'هزار'
@@ -37,6 +47,7 @@ const DECIMAL_SEP = 'ممیّز'
37
47
  // Persian ordinals: add -ُم (-om) suffix to cardinal
38
48
  // Special forms for 1st, 2nd, 3rd
39
49
  const ORDINAL_SUFFIX = 'م' // ـُم (-om)
50
+ /** @type {Record<number, string>} */
40
51
  const ORDINAL_ONES = {
41
52
  1: 'اول', // avval (first)
42
53
  2: 'دوم', // dovvom (second)
@@ -46,7 +57,7 @@ const ORDINAL_ONES = {
46
57
  6: 'ششم',
47
58
  7: 'هفتم',
48
59
  8: 'هشتم',
49
- 9: 'نهم'
60
+ 9: 'نهم',
50
61
  }
51
62
 
52
63
  // ============================================================================
@@ -59,7 +70,12 @@ const RIAL = 'ریال'
59
70
  // Conversion Functions
60
71
  // ============================================================================
61
72
 
62
- function integerToWords (n) {
73
+ /**
74
+ * Converts a non-negative integer to Persian cardinal words.
75
+ * @param {bigint} n - Non-negative integer to convert
76
+ * @returns {string} Persian cardinal words
77
+ */
78
+ function integerToWords(n) {
63
79
  if (n === 0n) return ZERO
64
80
 
65
81
  // 1-9
@@ -121,7 +137,12 @@ function integerToWords (n) {
121
137
  return `${milliardPrefix}${suffix}`
122
138
  }
123
139
 
124
- function decimalPartToWords (decimalPart) {
140
+ /**
141
+ * Converts the fractional digits of a number to Persian words.
142
+ * @param {string} decimalPart - The fractional digits as a string
143
+ * @returns {string} Persian words for the decimal portion
144
+ */
145
+ function decimalPartToWords(decimalPart) {
125
146
  let result = ''
126
147
  let i = 0
127
148
 
@@ -142,11 +163,10 @@ function decimalPartToWords (decimalPart) {
142
163
 
143
164
  /**
144
165
  * Converts a numeric value to Persian words.
145
- *
146
166
  * @param {number | string | bigint} value - The numeric value to convert
147
167
  * @returns {string} The number in Persian words
148
168
  */
149
- function toCardinal (value) {
169
+ function toCardinal(value) {
150
170
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
151
171
 
152
172
  let result = ''
@@ -172,11 +192,10 @@ function toCardinal (value) {
172
192
  * Converts a non-negative integer to Persian ordinal words.
173
193
  *
174
194
  * Persian ordinals: اول (1st), دوم (2nd), سوم (3rd), then cardinal + م suffix.
175
- *
176
195
  * @param {bigint} n - Positive integer to convert
177
196
  * @returns {string} Persian ordinal words
178
197
  */
179
- function integerToOrdinal (n) {
198
+ function integerToOrdinal(n) {
180
199
  // Special forms for 1-9
181
200
  if (n >= 1n && n <= 9n) {
182
201
  return ORDINAL_ONES[Number(n)]
@@ -189,18 +208,16 @@ function integerToOrdinal (n) {
189
208
 
190
209
  /**
191
210
  * Converts a numeric value to Persian ordinal words.
192
- *
193
211
  * @param {number | string | bigint} value - The numeric value to convert (positive integer)
194
212
  * @returns {string} The number as ordinal words
195
213
  * @throws {TypeError} If value is not a valid numeric type
196
214
  * @throws {RangeError} If value is negative, zero, or has a decimal part
197
- *
198
215
  * @example
199
216
  * toOrdinal(1) // 'اول'
200
217
  * toOrdinal(2) // 'دوم'
201
218
  * toOrdinal(10) // 'دهم'
202
219
  */
203
- function toOrdinal (value) {
220
+ function toOrdinal(value) {
204
221
  const integerPart = parseOrdinalValue(value)
205
222
  return integerToOrdinal(integerPart)
206
223
  }
@@ -214,18 +231,16 @@ function toOrdinal (value) {
214
231
  *
215
232
  * Iranian Rial has no subunit in modern usage.
216
233
  * (Historically dinar was 1/100 rial, but not used today)
217
- *
218
234
  * @param {number | string | bigint} value - The currency amount to convert
219
235
  * @returns {string} The amount in Persian currency words
220
236
  * @throws {TypeError} If value is not a valid numeric type
221
237
  * @throws {Error} If value is not a valid number format
222
- *
223
238
  * @example
224
239
  * toCurrency(42) // 'چهل و دو ریال'
225
240
  * toCurrency(1000) // 'هزار ریال'
226
241
  * toCurrency(-5) // 'منفى پنج ریال'
227
242
  */
228
- function toCurrency (value) {
243
+ function toCurrency(value) {
229
244
  const { isNegative, dollars: rial } = parseCurrencyValue(value)
230
245
 
231
246
  let result = ''
package/src/fi-FI.d.ts CHANGED
@@ -1,11 +1,12 @@
1
+ export const cardinalMax: bigint;
2
+ export const ordinalMax: bigint;
3
+ export const currencyMax: bigint;
1
4
  /**
2
5
  * Converts a numeric value to Finnish words.
3
- *
4
6
  * @param {number | string | bigint} value - The numeric value to convert
5
7
  * @returns {string} The number in Finnish words
6
8
  * @throws {TypeError} If value is not a valid numeric type
7
9
  * @throws {Error} If value is not a valid number format
8
- *
9
10
  * @example
10
11
  * toCardinal(21) // 'kaksikymmentäyksi'
11
12
  * toCardinal(1000) // 'tuhat'
@@ -14,12 +15,10 @@
14
15
  export function toCardinal(value: number | string | bigint): string;
15
16
  /**
16
17
  * Converts a numeric value to Finnish ordinal words.
17
- *
18
18
  * @param {number | string | bigint} value - The numeric value to convert (positive integer)
19
19
  * @returns {string} The number as ordinal words
20
20
  * @throws {TypeError} If value is not a valid numeric type
21
21
  * @throws {RangeError} If value is negative, zero, or has a decimal part
22
- *
23
22
  * @example
24
23
  * toOrdinal(1) // 'ensimmäinen'
25
24
  * toOrdinal(2) // 'toinen'
@@ -32,12 +31,10 @@ export function toOrdinal(value: number | string | bigint): string;
32
31
  *
33
32
  * Euro uses sentti as subunit (100 senttiä = 1 euro).
34
33
  * Finnish has singular/plural: 1 euro vs 2 euroa, 1 sentti vs 2 senttiä.
35
- *
36
34
  * @param {number | string | bigint} value - The currency amount to convert
37
35
  * @returns {string} The amount in Finnish currency words
38
36
  * @throws {TypeError} If value is not a valid numeric type
39
37
  * @throws {Error} If value is not a valid number format
40
- *
41
38
  * @example
42
39
  * toCurrency(1) // 'yksi euro'
43
40
  * toCurrency(42) // 'neljäkymmentäkaksi euroa'