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/sr-Latn-RS.js CHANGED
@@ -14,7 +14,9 @@
14
14
  import { parseCardinalValue } from './utils/parse-cardinal.js'
15
15
  import { parseCurrencyValue } from './utils/parse-currency.js'
16
16
  import { parseOrdinalValue } from './utils/parse-ordinal.js'
17
- import { validateOptions } from './utils/validate-options.js'
17
+ import { checkMax } from './utils/check-max.js'
18
+ import { western } from './utils/scale.js'
19
+ import { resolveOptions } from './utils/resolve-options.js'
18
20
 
19
21
  // ============================================================================
20
22
  // Vocabulary
@@ -56,7 +58,7 @@ const ORDINAL_SCALES = [
56
58
  'trilioniti',
57
59
  'trilijarditi',
58
60
  'kvadrilioniti',
59
- 'kvadrilijarditi'
61
+ 'kvadrilijarditi',
60
62
  ]
61
63
 
62
64
  // ============================================================================
@@ -79,14 +81,27 @@ const SCALE_FORMS = [
79
81
  ['trilion', 'triliona', 'triliona'],
80
82
  ['trilijarda', 'trilijarde', 'trilijarda'],
81
83
  ['kvadrilion', 'kvadriliona', 'kvadriliona'],
82
- ['kvadrilijarda', 'kvadrilijarde', 'kvadrilijarda']
84
+ ['kvadrilijarda', 'kvadrilijarde', 'kvadrilijarda'],
83
85
  ]
84
86
 
87
+ // Supported magnitude ceilings (checked at the public entry points). Both the
88
+ // cardinal SCALE_FORMS and the ORDINAL_SCALES tables cover units + N scale
89
+ // groups, so values must stay below 10^((length + 1) * 3) = 10^30.
90
+ export const cardinalMax = western(SCALE_FORMS.length)
91
+ export const ordinalMax = western(ORDINAL_SCALES.length)
92
+ export const currencyMax = western(SCALE_FORMS.length)
93
+
85
94
  // ============================================================================
86
95
  // Segment Building
87
96
  // ============================================================================
88
97
 
89
- function pluralize (n, forms) {
98
+ /**
99
+ * Selects the correct plural form based on Serbian grammar rules.
100
+ * @param {number | bigint} n - The count
101
+ * @param {string[]} forms - Plural forms [one, few, many]
102
+ * @returns {string} The selected plural form
103
+ */
104
+ function pluralize(n, forms) {
90
105
  const num = typeof n === 'bigint' ? Number(n) : n
91
106
  const lastDigit = num % 10
92
107
  const lastTwoDigits = num % 100
@@ -100,7 +115,12 @@ function pluralize (n, forms) {
100
115
  return forms[2]
101
116
  }
102
117
 
103
- function buildSegmentMasc (n) {
118
+ /**
119
+ * Builds the masculine word form for a 0-999 segment.
120
+ * @param {number} n - Segment value 0-999
121
+ * @returns {string} The segment words
122
+ */
123
+ function buildSegmentMasc(n) {
104
124
  if (n === 0) return ''
105
125
 
106
126
  const onesDigit = n % 10
@@ -119,14 +139,20 @@ function buildSegmentMasc (n) {
119
139
 
120
140
  if (tensDigit === 1) {
121
141
  parts.push(TEENS[onesDigit])
122
- } else if (onesDigit > 0) {
142
+ }
143
+ else if (onesDigit > 0) {
123
144
  parts.push(ONES_MASC[onesDigit])
124
145
  }
125
146
 
126
147
  return parts.join(' ')
127
148
  }
128
149
 
129
- function buildSegmentFem (n) {
150
+ /**
151
+ * Builds the feminine word form for a 0-999 segment.
152
+ * @param {number} n - Segment value 0-999
153
+ * @returns {string} The segment words
154
+ */
155
+ function buildSegmentFem(n) {
130
156
  if (n === 0) return ''
131
157
 
132
158
  const onesDigit = n % 10
@@ -145,7 +171,8 @@ function buildSegmentFem (n) {
145
171
 
146
172
  if (tensDigit === 1) {
147
173
  parts.push(TEENS[onesDigit])
148
- } else if (onesDigit > 0) {
174
+ }
175
+ else if (onesDigit > 0) {
149
176
  parts.push(ONES_FEM[onesDigit])
150
177
  }
151
178
 
@@ -156,7 +183,13 @@ function buildSegmentFem (n) {
156
183
  // Conversion Functions
157
184
  // ============================================================================
158
185
 
159
- function integerToWords (n, gender) {
186
+ /**
187
+ * Converts a non-negative integer to Serbian words.
188
+ * @param {bigint} n - The integer to convert
189
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
190
+ * @returns {string} The integer in Serbian words
191
+ */
192
+ function integerToWords(n, gender) {
160
193
  if (n === 0n) return ZERO
161
194
 
162
195
  if (n < 1000n) {
@@ -166,7 +199,13 @@ function integerToWords (n, gender) {
166
199
  return buildLargeNumberWords(n, gender)
167
200
  }
168
201
 
169
- function buildLargeNumberWords (n, gender) {
202
+ /**
203
+ * Builds words for numbers >= 1000 using scale decomposition.
204
+ * @param {bigint} n - The integer to convert (>= 1000)
205
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
206
+ * @returns {string} The number in Serbian words
207
+ */
208
+ function buildLargeNumberWords(n, gender) {
170
209
  const numStr = n.toString()
171
210
  const len = numStr.length
172
211
 
@@ -193,7 +232,8 @@ function buildLargeNumberWords (n, gender) {
193
232
  if (segment !== 0) {
194
233
  if (scaleIndex === 0) {
195
234
  parts.push(gender === 'feminine' ? buildSegmentFem(segment) : buildSegmentMasc(segment))
196
- } else {
235
+ }
236
+ else {
197
237
  const scaleForms = SCALE_FORMS[scaleIndex - 1]
198
238
  const scaleWord = pluralize(segment, scaleForms)
199
239
  // Thousands (scaleIndex=1) are feminine, others masculine
@@ -209,7 +249,13 @@ function buildLargeNumberWords (n, gender) {
209
249
  return parts.join(' ')
210
250
  }
211
251
 
212
- function decimalPartToWords (decimalPart, gender) {
252
+ /**
253
+ * Converts the decimal-part digit string to Serbian words.
254
+ * @param {string} decimalPart - The fractional digits as a string
255
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
256
+ * @returns {string} The decimal part in Serbian words
257
+ */
258
+ function decimalPartToWords(decimalPart, gender) {
213
259
  let result = ''
214
260
  let i = 0
215
261
 
@@ -228,20 +274,31 @@ function decimalPartToWords (decimalPart, gender) {
228
274
  return result
229
275
  }
230
276
 
277
+ /**
278
+ * @typedef {object} CardinalOptions
279
+ * @property {('masculine'|'feminine')} [gender] - Grammatical gender
280
+ */
281
+
282
+ /** @type {Required<CardinalOptions>} */
283
+ export const cardinalDefaults = { gender: 'masculine' }
284
+
285
+ /** @type {{ gender: ReadonlyArray<Required<CardinalOptions>['gender']> }} */
286
+ export const cardinalValues = { gender: ['masculine', 'feminine'] }
287
+
231
288
  /**
232
289
  * Converts a numeric value to Serbian (Latin) words.
233
- *
234
290
  * @param {number | string | bigint} value - The numeric value to convert
235
- * @param {Object} [options] - Optional configuration
236
- * @param {('masculine'|'feminine')} [options.gender='masculine'] - Grammatical gender
291
+ * @param {CardinalOptions} [options] - Optional configuration
237
292
  * @returns {string} The number in Serbian Latin words
238
293
  */
239
- function toCardinal (value, options) {
240
- options = validateOptions(options)
294
+ function toCardinal(value, options) {
241
295
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
296
+ // Both the integer part and the decimal's significant digits are spelled via
297
+ // the scale builder, so both must clear the ceiling.
298
+ checkMax(integerPart, cardinalMax, decimalPart)
242
299
 
243
300
  // Apply option defaults
244
- const { gender = 'masculine' } = options
301
+ const { gender } = resolveOptions(options, cardinalDefaults, cardinalValues)
245
302
 
246
303
  let result = ''
247
304
 
@@ -265,11 +322,10 @@ function toCardinal (value, options) {
265
322
  /**
266
323
  * Builds ordinal for a 0-99 segment when it's the final (ordinal) part.
267
324
  * Returns ordinal form: "prvi", "dvadeset prvi", etc.
268
- *
269
325
  * @param {number} n - Number 0-99
270
326
  * @returns {string} Ordinal words
271
327
  */
272
- function buildOrdinalTensOnes (n) {
328
+ function buildOrdinalTensOnes(n) {
273
329
  if (n === 0) return ''
274
330
 
275
331
  const onesDigit = n % 10
@@ -300,11 +356,10 @@ function buildOrdinalTensOnes (n) {
300
356
  *
301
357
  * In Serbian ordinals, only the LAST component becomes ordinal.
302
358
  * E.g., 121 = "sto dvadeset prvi" (one hundred twenty first)
303
- *
304
359
  * @param {bigint} n - Positive integer to convert
305
360
  * @returns {string} Ordinal Serbian words
306
361
  */
307
- function integerToOrdinal (n) {
362
+ function integerToOrdinal(n) {
308
363
  // Fast path: numbers < 100
309
364
  if (n < 100n) {
310
365
  return buildOrdinalTensOnes(Number(n))
@@ -352,11 +407,10 @@ function integerToOrdinal (n) {
352
407
  /**
353
408
  * Builds ordinal words for numbers >= 1,000,000.
354
409
  * All segments except the final one are cardinal; final segment is ordinal.
355
- *
356
410
  * @param {bigint} n - Number >= 1,000,000
357
411
  * @returns {string} Ordinal Serbian words
358
412
  */
359
- function buildLargeOrdinal (n) {
413
+ function buildLargeOrdinal(n) {
360
414
  const numStr = n.toString()
361
415
  const len = numStr.length
362
416
 
@@ -394,22 +448,26 @@ function buildLargeOrdinal (n) {
394
448
  // Units position (no scale)
395
449
  if (isLastNonZero) {
396
450
  parts.push(integerToOrdinal(BigInt(segment)))
397
- } else {
451
+ }
452
+ else {
398
453
  parts.push(buildSegmentMasc(segment))
399
454
  }
400
- } else {
455
+ }
456
+ else {
401
457
  // Has scale word
402
458
  if (isLastNonZero) {
403
459
  // This scale position is the final ordinal
404
460
  if (segment === 1) {
405
461
  parts.push(ORDINAL_SCALES[scaleIndex - 1])
406
- } else {
462
+ }
463
+ else {
407
464
  // Use cardinal segment + ordinal scale
408
465
  const isFeminine = scaleIndex === 1 // thousands are feminine
409
466
  const segmentWord = isFeminine ? buildSegmentFem(segment) : buildSegmentMasc(segment)
410
467
  parts.push(segmentWord + ' ' + ORDINAL_SCALES[scaleIndex - 1])
411
468
  }
412
- } else {
469
+ }
470
+ else {
413
471
  // Not the final segment: use cardinal
414
472
  const scaleForms = SCALE_FORMS[scaleIndex - 1]
415
473
  const scaleWord = pluralize(segment, scaleForms)
@@ -428,12 +486,10 @@ function buildLargeOrdinal (n) {
428
486
 
429
487
  /**
430
488
  * Converts a numeric value to Serbian ordinal words (masculine nominative).
431
- *
432
489
  * @param {number | string | bigint} value - The numeric value to convert (must be a positive integer)
433
490
  * @returns {string} The number as ordinal words (e.g., "prvi", "četrdeset drugi")
434
491
  * @throws {TypeError} If value is not a valid numeric type
435
492
  * @throws {RangeError} If value is negative, zero, or has a decimal part
436
- *
437
493
  * @example
438
494
  * toOrdinal(1) // 'prvi'
439
495
  * toOrdinal(2) // 'drugi'
@@ -443,8 +499,9 @@ function buildLargeOrdinal (n) {
443
499
  * toOrdinal(100) // 'stoti'
444
500
  * toOrdinal(1000) // 'hiljaditi'
445
501
  */
446
- function toOrdinal (value) {
502
+ function toOrdinal(value) {
447
503
  const integerPart = parseOrdinalValue(value)
504
+ checkMax(integerPart, ordinalMax)
448
505
  return integerToOrdinal(integerPart)
449
506
  }
450
507
 
@@ -452,16 +509,21 @@ function toOrdinal (value) {
452
509
  // CURRENCY: toCurrency(value, options?)
453
510
  // ============================================================================
454
511
 
512
+ /**
513
+ * @typedef {object} CurrencyOptions
514
+ * @property {boolean} [and] - Use "i" between dinars and para
515
+ */
516
+
517
+ /** @type {Required<CurrencyOptions>} */
518
+ export const currencyDefaults = { and: true }
519
+
455
520
  /**
456
521
  * Converts a numeric value to Serbian currency words (Serbian Dinar).
457
- *
458
522
  * @param {number | string | bigint} value - The currency amount to convert
459
- * @param {Object} [options] - Optional configuration
460
- * @param {boolean} [options.and=true] - Use "i" between dinars and para
523
+ * @param {CurrencyOptions} [options] - Optional configuration
461
524
  * @returns {string} The amount in Serbian currency words
462
525
  * @throws {TypeError} If value is not a valid numeric type
463
526
  * @throws {Error} If value is not a valid number format
464
- *
465
527
  * @example
466
528
  * toCurrency(42.50) // 'četrdeset dva dinara i pedeset para'
467
529
  * toCurrency(1) // 'jedan dinar'
@@ -469,10 +531,10 @@ function toOrdinal (value) {
469
531
  * toCurrency(0.01) // 'jedna para'
470
532
  * toCurrency(42.50, { and: false }) // 'četrdeset dva dinara pedeset para'
471
533
  */
472
- function toCurrency (value, options) {
473
- options = validateOptions(options)
534
+ function toCurrency(value, options) {
474
535
  const { isNegative, dollars: dinars, cents: para } = parseCurrencyValue(value)
475
- const { and: useAnd = true } = options
536
+ checkMax(dinars, currencyMax)
537
+ const { and: useAnd } = resolveOptions(options, currencyDefaults)
476
538
 
477
539
  // Build result
478
540
  let result = ''
package/src/sv-SE.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 Swedish words.
3
- *
4
6
  * @param {number | string | bigint} value - The numeric value to convert
5
7
  * @returns {string} The number in Swedish 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(42) // 'fyrtio-två'
11
12
  * toCardinal(101) // 'hundra och ett'
@@ -14,12 +15,10 @@
14
15
  export function toCardinal(value: number | string | bigint): string;
15
16
  /**
16
17
  * Converts a numeric value to Swedish 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) // 'första'
25
24
  * toOrdinal(2) // 'andra'
@@ -30,12 +29,10 @@ export function toOrdinal(value: number | string | bigint): string;
30
29
  * Converts a numeric value to Swedish currency words (Swedish Krona).
31
30
  *
32
31
  * Uses krona/kronor and öre (100 öre = 1 krona).
33
- *
34
32
  * @param {number | string | bigint} value - The currency amount to convert
35
33
  * @returns {string} The amount in Swedish currency words
36
34
  * @throws {TypeError} If value is not a valid numeric type
37
35
  * @throws {Error} If value is not a valid number format
38
- *
39
36
  * @example
40
37
  * toCurrency(1) // 'en krona'
41
38
  * toCurrency(42) // 'fyrtio-två kronor'
package/src/sv-SE.js CHANGED
@@ -14,6 +14,8 @@
14
14
  import { parseCardinalValue } from './utils/parse-cardinal.js'
15
15
  import { parseCurrencyValue } from './utils/parse-currency.js'
16
16
  import { parseOrdinalValue } from './utils/parse-ordinal.js'
17
+ import { checkMax } from './utils/check-max.js'
18
+ import { western } from './utils/scale.js'
17
19
 
18
20
  // ============================================================================
19
21
  // Vocabulary (module-level constants)
@@ -33,12 +35,22 @@ const DECIMAL_SEP = 'komma'
33
35
  // Scale words (long scale with -ard forms)
34
36
  const SCALES = ['tusen', 'miljon', 'miljard', 'biljon', 'biljard', 'triljon', 'triljard', 'kvadriljon']
35
37
 
38
+ // Supported magnitude ceiling (checked at the public entry points). SCALES is
39
+ // indexed [scaleIndex - 1] from 'tusen' (10^3), so segments are [units, then
40
+ // one per SCALES entry] -> ceiling 10^((SCALES.length + 1) * 3) = 10^27.
41
+ // Ordinal (cardinal + suffix) and currency build on the cardinal, and the
42
+ // decimal is spelled via integerToWords, so all share it.
43
+ export const cardinalMax = western(SCALES.length)
44
+ export const ordinalMax = western(SCALES.length)
45
+ export const currencyMax = western(SCALES.length)
46
+
36
47
  // ============================================================================
37
48
  // Ordinal Vocabulary
38
49
  // ============================================================================
39
50
 
40
51
  // Swedish ordinals: 1st-2nd use -a (första, andra), then -de suffix
41
52
  // Numbers ending in 1, 2 (except 11, 12) use special forms
53
+ /** @type {Record<number, string>} */
42
54
  const ORDINAL_ONES = {
43
55
  1: 'första',
44
56
  2: 'andra',
@@ -51,7 +63,7 @@ const ORDINAL_ONES = {
51
63
  9: 'nionde',
52
64
  10: 'tionde',
53
65
  11: 'elfte',
54
- 12: 'tolfte'
66
+ 12: 'tolfte',
55
67
  }
56
68
 
57
69
  // ============================================================================
@@ -69,8 +81,10 @@ const ORE = 'öre' // same singular and plural
69
81
  /**
70
82
  * Builds segment word for 0-999.
71
83
  * Returns object with word and metadata for "och" logic.
84
+ * @param {number} n - Integer in range 0-999
85
+ * @returns {{word: string, hasHundred: boolean, lessThan100: boolean}} Segment word with "och" metadata
72
86
  */
73
- function buildSegment (n) {
87
+ function buildSegment(n) {
74
88
  if (n === 0) return { word: '', hasHundred: false, lessThan100: false }
75
89
 
76
90
  const ones = n % 10
@@ -85,7 +99,8 @@ function buildSegment (n) {
85
99
  hasHundred = true
86
100
  if (hundreds === 1) {
87
101
  parts.push(HUNDRED)
88
- } else {
102
+ }
103
+ else {
89
104
  parts.push(ONES[hundreds] + ' ' + HUNDRED)
90
105
  }
91
106
  }
@@ -94,22 +109,27 @@ function buildSegment (n) {
94
109
  let tensOnesWord = ''
95
110
  if (tens === 1) {
96
111
  tensOnesWord = TEENS[ones]
97
- } else if (tens >= 2) {
112
+ }
113
+ else if (tens >= 2) {
98
114
  if (ones > 0) {
99
115
  tensOnesWord = TENS[tens] + '-' + ONES[ones]
100
- } else {
116
+ }
117
+ else {
101
118
  tensOnesWord = TENS[tens]
102
119
  }
103
- } else if (ones > 0) {
120
+ }
121
+ else if (ones > 0) {
104
122
  tensOnesWord = ONES[ones]
105
123
  }
106
124
 
107
125
  // Combine with "och" after hundreds if there's a remainder
108
126
  if (hasHundred && tensOnesWord) {
109
127
  return { word: parts[0] + ' och ' + tensOnesWord, hasHundred: true, lessThan100: false }
110
- } else if (hasHundred) {
128
+ }
129
+ else if (hasHundred) {
111
130
  return { word: parts[0], hasHundred: true, lessThan100: false }
112
- } else {
131
+ }
132
+ else {
113
133
  return { word: tensOnesWord, hasHundred: false, lessThan100: true }
114
134
  }
115
135
  }
@@ -120,11 +140,10 @@ function buildSegment (n) {
120
140
 
121
141
  /**
122
142
  * Converts a non-negative integer to Swedish words.
123
- *
124
143
  * @param {bigint} n - Non-negative integer to convert
125
144
  * @returns {string} Swedish words
126
145
  */
127
- function integerToWords (n) {
146
+ function integerToWords(n) {
128
147
  if (n === 0n) return ZERO
129
148
 
130
149
  // Fast path: numbers < 1000
@@ -145,7 +164,8 @@ function integerToWords (n) {
145
164
  // Insert "och" if remainder < 100 (doesn't have hundred)
146
165
  if (remainderResult.lessThan100) {
147
166
  result += ' och ' + remainderResult.word
148
- } else {
167
+ }
168
+ else {
149
169
  result += ' ' + remainderResult.word
150
170
  }
151
171
  }
@@ -159,11 +179,10 @@ function integerToWords (n) {
159
179
 
160
180
  /**
161
181
  * Builds words for numbers >= 1,000,000.
162
- *
163
182
  * @param {bigint} n - Number >= 1,000,000
164
183
  * @returns {string} Swedish words
165
184
  */
166
- function buildLargeNumberWords (n) {
185
+ function buildLargeNumberWords(n) {
167
186
  const numStr = n.toString()
168
187
  const len = numStr.length
169
188
 
@@ -197,9 +216,10 @@ function buildLargeNumberWords (n) {
197
216
  parts.push({
198
217
  word: segmentResult.word,
199
218
  hasHundred: segmentResult.hasHundred,
200
- isScale: false
219
+ isScale: false,
201
220
  })
202
- } else {
221
+ }
222
+ else {
203
223
  // Segment with scale word
204
224
  const scaleWord = SCALES[scaleIndex - 1]
205
225
 
@@ -208,10 +228,12 @@ function buildLargeNumberWords (n) {
208
228
  // Omit "ett" before tusen, use "en" before million+
209
229
  if (scaleIndex === 1) {
210
230
  segmentWord = '' // Just "tusen"
211
- } else {
231
+ }
232
+ else {
212
233
  segmentWord = 'en' // "en miljon"
213
234
  }
214
- } else {
235
+ }
236
+ else {
215
237
  segmentWord = segmentResult.word
216
238
  }
217
239
 
@@ -232,11 +254,10 @@ function buildLargeNumberWords (n) {
232
254
  /**
233
255
  * Joins parts with Swedish "och" rules.
234
256
  * Insert "och" before final segment if it follows a scale word and doesn't have "hundra".
235
- *
236
- * @param {Array} parts - Parts with metadata
257
+ * @param {Array<{word: string, hasHundred: boolean, isScale: boolean}>} parts - Parts with metadata
237
258
  * @returns {string} Joined string
238
259
  */
239
- function joinSwedishParts (parts) {
260
+ function joinSwedishParts(parts) {
240
261
  if (parts.length === 0) return ZERO
241
262
  if (parts.length === 1) return parts[0].word
242
263
 
@@ -262,11 +283,10 @@ function joinSwedishParts (parts) {
262
283
 
263
284
  /**
264
285
  * Converts decimal digits to Swedish words.
265
- *
266
286
  * @param {string} decimalPart - Decimal digits (without the point)
267
287
  * @returns {string} Swedish words for decimal part
268
288
  */
269
- function decimalPartToWords (decimalPart) {
289
+ function decimalPartToWords(decimalPart) {
270
290
  let result = ''
271
291
 
272
292
  // Handle leading zeros
@@ -289,19 +309,20 @@ function decimalPartToWords (decimalPart) {
289
309
 
290
310
  /**
291
311
  * Converts a numeric value to Swedish words.
292
- *
293
312
  * @param {number | string | bigint} value - The numeric value to convert
294
313
  * @returns {string} The number in Swedish words
295
314
  * @throws {TypeError} If value is not a valid numeric type
296
315
  * @throws {Error} If value is not a valid number format
297
- *
298
316
  * @example
299
317
  * toCardinal(42) // 'fyrtio-två'
300
318
  * toCardinal(101) // 'hundra och ett'
301
319
  * toCardinal(1000000) // 'en miljon'
302
320
  */
303
- function toCardinal (value) {
321
+ function toCardinal(value) {
304
322
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
323
+ // Both the integer part and the decimal's significant digits are spelled via
324
+ // the scale builder, so both must clear the ceiling.
325
+ checkMax(integerPart, cardinalMax, decimalPart)
305
326
 
306
327
  let result = ''
307
328
 
@@ -327,11 +348,10 @@ function toCardinal (value) {
327
348
  *
328
349
  * Swedish ordinals: första (1st), andra (2nd), tredje (3rd), etc.
329
350
  * Most use cardinal + de suffix, with special forms 1-12.
330
- *
331
351
  * @param {bigint} n - Positive integer to convert
332
352
  * @returns {string} Swedish ordinal words
333
353
  */
334
- function integerToOrdinal (n) {
354
+ function integerToOrdinal(n) {
335
355
  // Special forms for 1-12
336
356
  if (n >= 1n && n <= 12n) {
337
357
  return ORDINAL_ONES[Number(n)]
@@ -348,19 +368,18 @@ function integerToOrdinal (n) {
348
368
 
349
369
  /**
350
370
  * Converts a numeric value to Swedish ordinal words.
351
- *
352
371
  * @param {number | string | bigint} value - The numeric value to convert (positive integer)
353
372
  * @returns {string} The number as ordinal words
354
373
  * @throws {TypeError} If value is not a valid numeric type
355
374
  * @throws {RangeError} If value is negative, zero, or has a decimal part
356
- *
357
375
  * @example
358
376
  * toOrdinal(1) // 'första'
359
377
  * toOrdinal(2) // 'andra'
360
378
  * toOrdinal(21) // 'tjugo-ettde'
361
379
  */
362
- function toOrdinal (value) {
380
+ function toOrdinal(value) {
363
381
  const integerPart = parseOrdinalValue(value)
382
+ checkMax(integerPart, ordinalMax)
364
383
  return integerToOrdinal(integerPart)
365
384
  }
366
385
 
@@ -372,19 +391,18 @@ function toOrdinal (value) {
372
391
  * Converts a numeric value to Swedish currency words (Swedish Krona).
373
392
  *
374
393
  * Uses krona/kronor and öre (100 öre = 1 krona).
375
- *
376
394
  * @param {number | string | bigint} value - The currency amount to convert
377
395
  * @returns {string} The amount in Swedish currency words
378
396
  * @throws {TypeError} If value is not a valid numeric type
379
397
  * @throws {Error} If value is not a valid number format
380
- *
381
398
  * @example
382
399
  * toCurrency(1) // 'en krona'
383
400
  * toCurrency(42) // 'fyrtio-två kronor'
384
401
  * toCurrency(1.50) // 'en krona och femtio öre'
385
402
  */
386
- function toCurrency (value) {
403
+ function toCurrency(value) {
387
404
  const { isNegative, dollars: kronor, cents: ore } = parseCurrencyValue(value)
405
+ checkMax(kronor, currencyMax)
388
406
 
389
407
  let result = ''
390
408
  if (isNegative) {
@@ -396,7 +414,8 @@ function toCurrency (value) {
396
414
  // Use "en" for 1 krona (not "ett")
397
415
  if (kronor === 1n) {
398
416
  result += 'en ' + KRONA
399
- } else {
417
+ }
418
+ else {
400
419
  result += integerToWords(kronor) + ' ' + KRONOR
401
420
  }
402
421
  }
package/src/sw-KE.d.ts CHANGED
@@ -1,18 +1,18 @@
1
+ export const cardinalMax: bigint;
2
+ export const ordinalMax: bigint;
3
+ export const currencyMax: bigint;
1
4
  /**
2
5
  * Converts a numeric value to Swahili words.
3
- *
4
6
  * @param {number | string | bigint} value - The numeric value to convert
5
7
  * @returns {string} The number in Swahili words
6
8
  */
7
9
  export function toCardinal(value: number | string | bigint): string;
8
10
  /**
9
11
  * Converts a numeric value to Swahili 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) // 'wa kwanza'
18
18
  * toOrdinal(2) // 'wa pili'
@@ -23,12 +23,10 @@ export function toOrdinal(value: number | string | bigint): string;
23
23
  * Converts a numeric value to Swahili currency words (Kenyan Shilling).
24
24
  *
25
25
  * Uses shilingi and senti (100 senti = 1 shilingi).
26
- *
27
26
  * @param {number | string | bigint} value - The currency amount to convert
28
27
  * @returns {string} The amount in Swahili currency words
29
28
  * @throws {TypeError} If value is not a valid numeric type
30
29
  * @throws {Error} If value is not a valid number format
31
- *
32
30
  * @example
33
31
  * toCurrency(42) // 'shilingi arobaini na mbili'
34
32
  * toCurrency(1.50) // 'shilingi moja na senti hamsini'