n2words 5.0.0 → 5.1.1

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 +133 -40
  2. package/README.md +6 -4
  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 -2
  112. package/dist/pt-BR.umd.js +2 -2
  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 +33 -24
  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 +22 -11
  257. package/src/pt-BR.js +93 -53
  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 +106 -43
  266. package/src/sr-Latn-RS.d.ts +35 -15
  267. package/src/sr-Latn-RS.js +106 -43
  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-Cyrl-RS.js CHANGED
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Key features:
7
7
  * - Three-form pluralization (one/few/many)
8
- * - Gender: thousands are feminine, millions+ are masculine
8
+ * - Gender by scale word: the -arda forms (hiljada, milijarda, bilijarda, ...)
9
+ * are feminine; the -ion forms (milion, bilion, ...) are masculine
9
10
  * - Irregular hundreds
10
11
  * - Long scale naming with -ard forms
11
12
  * - Cyrillic script
@@ -14,7 +15,9 @@
14
15
  import { parseCardinalValue } from './utils/parse-cardinal.js'
15
16
  import { parseCurrencyValue } from './utils/parse-currency.js'
16
17
  import { parseOrdinalValue } from './utils/parse-ordinal.js'
17
- import { validateOptions } from './utils/validate-options.js'
18
+ import { checkMax } from './utils/check-max.js'
19
+ import { western } from './utils/scale.js'
20
+ import { resolveOptions } from './utils/resolve-options.js'
18
21
 
19
22
  // ============================================================================
20
23
  // Vocabulary
@@ -56,7 +59,7 @@ const ORDINAL_SCALES = [
56
59
  'трилионити',
57
60
  'трилијардити',
58
61
  'квадрилионити',
59
- 'квадрилијардити'
62
+ 'квадрилијардити',
60
63
  ]
61
64
 
62
65
  // ============================================================================
@@ -79,14 +82,27 @@ const SCALE_FORMS = [
79
82
  ['трилион', 'трилиона', 'трилиона'],
80
83
  ['трилијарда', 'трилијарде', 'трилијарда'],
81
84
  ['квадрилион', 'квадрилиона', 'квадрилиона'],
82
- ['квадрилијарда', 'квадрилијарде', 'квадрилијарда']
85
+ ['квадрилијарда', 'квадрилијарде', 'квадрилијарда'],
83
86
  ]
84
87
 
88
+ // Supported magnitude ceilings (checked at the public entry points). Both the
89
+ // cardinal SCALE_FORMS and the ORDINAL_SCALES tables cover units + N scale
90
+ // groups, so values must stay below 10^((length + 1) * 3) = 10^30.
91
+ export const cardinalMax = western(SCALE_FORMS.length)
92
+ export const ordinalMax = western(ORDINAL_SCALES.length)
93
+ export const currencyMax = western(SCALE_FORMS.length)
94
+
85
95
  // ============================================================================
86
96
  // Segment Building
87
97
  // ============================================================================
88
98
 
89
- function pluralize (n, forms) {
99
+ /**
100
+ * Selects the correct plural form for a count.
101
+ * @param {number | bigint} n - The count
102
+ * @param {string[]} forms - Plural forms [one, few, many]
103
+ * @returns {string} The selected plural form
104
+ */
105
+ function pluralize(n, forms) {
90
106
  const num = typeof n === 'bigint' ? Number(n) : n
91
107
  const lastDigit = num % 10
92
108
  const lastTwoDigits = num % 100
@@ -100,7 +116,12 @@ function pluralize (n, forms) {
100
116
  return forms[2]
101
117
  }
102
118
 
103
- function buildSegmentMasc (n) {
119
+ /**
120
+ * Builds the masculine word form for a 0-999 segment.
121
+ * @param {number} n - Number 0-999
122
+ * @returns {string} The segment words
123
+ */
124
+ function buildSegmentMasc(n) {
104
125
  if (n === 0) return ''
105
126
 
106
127
  const onesDigit = n % 10
@@ -119,14 +140,20 @@ function buildSegmentMasc (n) {
119
140
 
120
141
  if (tensDigit === 1) {
121
142
  parts.push(TEENS[onesDigit])
122
- } else if (onesDigit > 0) {
143
+ }
144
+ else if (onesDigit > 0) {
123
145
  parts.push(ONES_MASC[onesDigit])
124
146
  }
125
147
 
126
148
  return parts.join(' ')
127
149
  }
128
150
 
129
- function buildSegmentFem (n) {
151
+ /**
152
+ * Builds the feminine word form for a 0-999 segment.
153
+ * @param {number} n - Number 0-999
154
+ * @returns {string} The segment words
155
+ */
156
+ function buildSegmentFem(n) {
130
157
  if (n === 0) return ''
131
158
 
132
159
  const onesDigit = n % 10
@@ -145,7 +172,8 @@ function buildSegmentFem (n) {
145
172
 
146
173
  if (tensDigit === 1) {
147
174
  parts.push(TEENS[onesDigit])
148
- } else if (onesDigit > 0) {
175
+ }
176
+ else if (onesDigit > 0) {
149
177
  parts.push(ONES_FEM[onesDigit])
150
178
  }
151
179
 
@@ -156,7 +184,13 @@ function buildSegmentFem (n) {
156
184
  // Conversion Functions
157
185
  // ============================================================================
158
186
 
159
- function integerToWords (n, gender) {
187
+ /**
188
+ * Converts a non-negative integer to Serbian words.
189
+ * @param {bigint} n - The integer to convert
190
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
191
+ * @returns {string} The integer in words
192
+ */
193
+ function integerToWords(n, gender) {
160
194
  if (n === 0n) return ZERO
161
195
 
162
196
  if (n < 1000n) {
@@ -166,7 +200,13 @@ function integerToWords (n, gender) {
166
200
  return buildLargeNumberWords(n, gender)
167
201
  }
168
202
 
169
- function buildLargeNumberWords (n, gender) {
203
+ /**
204
+ * Builds words for integers >= 1000 using scale decomposition.
205
+ * @param {bigint} n - The integer to convert (>= 1000)
206
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
207
+ * @returns {string} The integer in words
208
+ */
209
+ function buildLargeNumberWords(n, gender) {
170
210
  const numStr = n.toString()
171
211
  const len = numStr.length
172
212
 
@@ -193,11 +233,12 @@ function buildLargeNumberWords (n, gender) {
193
233
  if (segment !== 0) {
194
234
  if (scaleIndex === 0) {
195
235
  parts.push(gender === 'feminine' ? buildSegmentFem(segment) : buildSegmentMasc(segment))
196
- } else {
236
+ }
237
+ else {
197
238
  const scaleForms = SCALE_FORMS[scaleIndex - 1]
198
239
  const scaleWord = pluralize(segment, scaleForms)
199
- // Thousands (scaleIndex=1) are feminine, others masculine
200
- const isFeminine = scaleIndex === 1
240
+ // -arda scale words (hiljada, milijarda, ...) are feminine; -ion words masculine
241
+ const isFeminine = scaleIndex % 2 === 1
201
242
  const segmentWord = isFeminine ? buildSegmentFem(segment) : buildSegmentMasc(segment)
202
243
  parts.push(segmentWord + ' ' + scaleWord)
203
244
  }
@@ -209,7 +250,13 @@ function buildLargeNumberWords (n, gender) {
209
250
  return parts.join(' ')
210
251
  }
211
252
 
212
- function decimalPartToWords (decimalPart, gender) {
253
+ /**
254
+ * Converts the decimal-part digit string to Serbian words.
255
+ * @param {string} decimalPart - The decimal digits as a string
256
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
257
+ * @returns {string} The decimal part in words
258
+ */
259
+ function decimalPartToWords(decimalPart, gender) {
213
260
  let result = ''
214
261
  let i = 0
215
262
 
@@ -228,20 +275,31 @@ function decimalPartToWords (decimalPart, gender) {
228
275
  return result
229
276
  }
230
277
 
278
+ /**
279
+ * @typedef {object} CardinalOptions
280
+ * @property {('masculine'|'feminine')} [gender] - Grammatical gender
281
+ */
282
+
283
+ /** @type {Required<CardinalOptions>} */
284
+ export const cardinalDefaults = { gender: 'masculine' }
285
+
286
+ /** @type {{ gender: ReadonlyArray<Required<CardinalOptions>['gender']> }} */
287
+ export const cardinalValues = { gender: ['masculine', 'feminine'] }
288
+
231
289
  /**
232
290
  * Converts a numeric value to Serbian (Cyrillic) words.
233
- *
234
291
  * @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
292
+ * @param {CardinalOptions} [options] - Optional configuration
237
293
  * @returns {string} The number in Serbian Cyrillic words
238
294
  */
239
- function toCardinal (value, options) {
240
- options = validateOptions(options)
295
+ function toCardinal(value, options) {
241
296
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
297
+ // Both the integer part and the decimal's significant digits are spelled via
298
+ // the scale builder, so both must clear the ceiling.
299
+ checkMax(integerPart, cardinalMax, decimalPart)
242
300
 
243
301
  // Apply option defaults
244
- const { gender = 'masculine' } = options
302
+ const { gender } = resolveOptions(options, cardinalDefaults, cardinalValues)
245
303
 
246
304
  let result = ''
247
305
 
@@ -265,11 +323,10 @@ function toCardinal (value, options) {
265
323
  /**
266
324
  * Builds ordinal for a 0-99 segment when it's the final (ordinal) part.
267
325
  * Returns ordinal form: "први", "двадесет први", etc.
268
- *
269
326
  * @param {number} n - Number 0-99
270
327
  * @returns {string} Ordinal words
271
328
  */
272
- function buildOrdinalTensOnes (n) {
329
+ function buildOrdinalTensOnes(n) {
273
330
  if (n === 0) return ''
274
331
 
275
332
  const onesDigit = n % 10
@@ -300,11 +357,10 @@ function buildOrdinalTensOnes (n) {
300
357
  *
301
358
  * In Serbian ordinals, only the LAST component becomes ordinal.
302
359
  * E.g., 121 = "сто двадесет први" (one hundred twenty first)
303
- *
304
360
  * @param {bigint} n - Positive integer to convert
305
361
  * @returns {string} Ordinal Serbian words
306
362
  */
307
- function integerToOrdinal (n) {
363
+ function integerToOrdinal(n) {
308
364
  // Fast path: numbers < 100
309
365
  if (n < 100n) {
310
366
  return buildOrdinalTensOnes(Number(n))
@@ -352,11 +408,10 @@ function integerToOrdinal (n) {
352
408
  /**
353
409
  * Builds ordinal words for numbers >= 1,000,000.
354
410
  * All segments except the final one are cardinal; final segment is ordinal.
355
- *
356
411
  * @param {bigint} n - Number >= 1,000,000
357
412
  * @returns {string} Ordinal Serbian words
358
413
  */
359
- function buildLargeOrdinal (n) {
414
+ function buildLargeOrdinal(n) {
360
415
  const numStr = n.toString()
361
416
  const len = numStr.length
362
417
 
@@ -394,26 +449,30 @@ function buildLargeOrdinal (n) {
394
449
  // Units position (no scale)
395
450
  if (isLastNonZero) {
396
451
  parts.push(integerToOrdinal(BigInt(segment)))
397
- } else {
452
+ }
453
+ else {
398
454
  parts.push(buildSegmentMasc(segment))
399
455
  }
400
- } else {
456
+ }
457
+ else {
401
458
  // Has scale word
402
459
  if (isLastNonZero) {
403
460
  // This scale position is the final ordinal
404
461
  if (segment === 1) {
405
462
  parts.push(ORDINAL_SCALES[scaleIndex - 1])
406
- } else {
463
+ }
464
+ else {
407
465
  // Use cardinal segment + ordinal scale
408
- const isFeminine = scaleIndex === 1 // thousands are feminine
466
+ const isFeminine = scaleIndex % 2 === 1 // feminine at -arda scales (hiljada, milijarda, ...)
409
467
  const segmentWord = isFeminine ? buildSegmentFem(segment) : buildSegmentMasc(segment)
410
468
  parts.push(segmentWord + ' ' + ORDINAL_SCALES[scaleIndex - 1])
411
469
  }
412
- } else {
470
+ }
471
+ else {
413
472
  // Not the final segment: use cardinal
414
473
  const scaleForms = SCALE_FORMS[scaleIndex - 1]
415
474
  const scaleWord = pluralize(segment, scaleForms)
416
- const isFeminine = scaleIndex === 1
475
+ const isFeminine = scaleIndex % 2 === 1
417
476
  const segmentWord = isFeminine ? buildSegmentFem(segment) : buildSegmentMasc(segment)
418
477
  parts.push(segmentWord + ' ' + scaleWord)
419
478
  }
@@ -428,12 +487,10 @@ function buildLargeOrdinal (n) {
428
487
 
429
488
  /**
430
489
  * Converts a numeric value to Serbian ordinal words (masculine nominative).
431
- *
432
490
  * @param {number | string | bigint} value - The numeric value to convert (must be a positive integer)
433
491
  * @returns {string} The number as ordinal words (e.g., "први", "четрдесет други")
434
492
  * @throws {TypeError} If value is not a valid numeric type
435
493
  * @throws {RangeError} If value is negative, zero, or has a decimal part
436
- *
437
494
  * @example
438
495
  * toOrdinal(1) // 'први'
439
496
  * toOrdinal(2) // 'други'
@@ -443,8 +500,9 @@ function buildLargeOrdinal (n) {
443
500
  * toOrdinal(100) // 'стоти'
444
501
  * toOrdinal(1000) // 'хиљадити'
445
502
  */
446
- function toOrdinal (value) {
503
+ function toOrdinal(value) {
447
504
  const integerPart = parseOrdinalValue(value)
505
+ checkMax(integerPart, ordinalMax)
448
506
  return integerToOrdinal(integerPart)
449
507
  }
450
508
 
@@ -452,16 +510,21 @@ function toOrdinal (value) {
452
510
  // CURRENCY: toCurrency(value, options?)
453
511
  // ============================================================================
454
512
 
513
+ /**
514
+ * @typedef {object} CurrencyOptions
515
+ * @property {boolean} [and] - Use "и" between dinars and para
516
+ */
517
+
518
+ /** @type {Required<CurrencyOptions>} */
519
+ export const currencyDefaults = { and: true }
520
+
455
521
  /**
456
522
  * Converts a numeric value to Serbian currency words (Serbian Dinar).
457
- *
458
523
  * @param {number | string | bigint} value - The currency amount to convert
459
- * @param {Object} [options] - Optional configuration
460
- * @param {boolean} [options.and=true] - Use "и" between dinars and para
524
+ * @param {CurrencyOptions} [options] - Optional configuration
461
525
  * @returns {string} The amount in Serbian currency words
462
526
  * @throws {TypeError} If value is not a valid numeric type
463
527
  * @throws {Error} If value is not a valid number format
464
- *
465
528
  * @example
466
529
  * toCurrency(42.50) // 'четрдесет два динара и педесет пара'
467
530
  * toCurrency(1) // 'један динар'
@@ -469,10 +532,10 @@ function toOrdinal (value) {
469
532
  * toCurrency(0.01) // 'једна пара'
470
533
  * toCurrency(42.50, { and: false }) // 'четрдесет два динара педесет пара'
471
534
  */
472
- function toCurrency (value, options) {
473
- options = validateOptions(options)
535
+ function toCurrency(value, options) {
474
536
  const { isNegative, dollars: dinars, cents: para } = parseCurrencyValue(value)
475
- const { and: useAnd = true } = options
537
+ checkMax(dinars, currencyMax)
538
+ const { and: useAnd } = resolveOptions(options, currencyDefaults)
476
539
 
477
540
  // Build result
478
541
  let result = ''
@@ -1,22 +1,47 @@
1
+ export const cardinalMax: bigint;
2
+ export const ordinalMax: bigint;
3
+ export const currencyMax: bigint;
4
+ /**
5
+ * @typedef {object} CardinalOptions
6
+ * @property {('masculine'|'feminine')} [gender] - Grammatical gender
7
+ */
8
+ /** @type {Required<CardinalOptions>} */
9
+ export const cardinalDefaults: Required<CardinalOptions>;
10
+ /** @type {{ gender: ReadonlyArray<Required<CardinalOptions>['gender']> }} */
11
+ export const cardinalValues: {
12
+ gender: ReadonlyArray<Required<CardinalOptions>["gender"]>;
13
+ };
14
+ /**
15
+ * @typedef {object} CurrencyOptions
16
+ * @property {boolean} [and] - Use "i" between dinars and para
17
+ */
18
+ /** @type {Required<CurrencyOptions>} */
19
+ export const currencyDefaults: Required<CurrencyOptions>;
20
+ export type CardinalOptions = {
21
+ /**
22
+ * - Grammatical gender
23
+ */
24
+ gender?: "feminine" | "masculine" | undefined;
25
+ };
26
+ export type CurrencyOptions = {
27
+ /**
28
+ * - Use "i" between dinars and para
29
+ */
30
+ and?: boolean | undefined;
31
+ };
1
32
  /**
2
33
  * Converts a numeric value to Serbian (Latin) words.
3
- *
4
34
  * @param {number | string | bigint} value - The numeric value to convert
5
- * @param {Object} [options] - Optional configuration
6
- * @param {('masculine'|'feminine')} [options.gender='masculine'] - Grammatical gender
35
+ * @param {CardinalOptions} [options] - Optional configuration
7
36
  * @returns {string} The number in Serbian Latin words
8
37
  */
9
- export function toCardinal(value: number | string | bigint, options?: {
10
- gender?: "masculine" | "feminine" | undefined;
11
- }): string;
38
+ export function toCardinal(value: number | string | bigint, options?: CardinalOptions): string;
12
39
  /**
13
40
  * Converts a numeric value to Serbian ordinal words (masculine nominative).
14
- *
15
41
  * @param {number | string | bigint} value - The numeric value to convert (must be a positive integer)
16
42
  * @returns {string} The number as ordinal words (e.g., "prvi", "četrdeset drugi")
17
43
  * @throws {TypeError} If value is not a valid numeric type
18
44
  * @throws {RangeError} If value is negative, zero, or has a decimal part
19
- *
20
45
  * @example
21
46
  * toOrdinal(1) // 'prvi'
22
47
  * toOrdinal(2) // 'drugi'
@@ -29,14 +54,11 @@ export function toCardinal(value: number | string | bigint, options?: {
29
54
  export function toOrdinal(value: number | string | bigint): string;
30
55
  /**
31
56
  * Converts a numeric value to Serbian currency words (Serbian Dinar).
32
- *
33
57
  * @param {number | string | bigint} value - The currency amount to convert
34
- * @param {Object} [options] - Optional configuration
35
- * @param {boolean} [options.and=true] - Use "i" between dinars and para
58
+ * @param {CurrencyOptions} [options] - Optional configuration
36
59
  * @returns {string} The amount in Serbian currency words
37
60
  * @throws {TypeError} If value is not a valid numeric type
38
61
  * @throws {Error} If value is not a valid number format
39
- *
40
62
  * @example
41
63
  * toCurrency(42.50) // 'četrdeset dva dinara i pedeset para'
42
64
  * toCurrency(1) // 'jedan dinar'
@@ -44,6 +66,4 @@ export function toOrdinal(value: number | string | bigint): string;
44
66
  * toCurrency(0.01) // 'jedna para'
45
67
  * toCurrency(42.50, { and: false }) // 'četrdeset dva dinara pedeset para'
46
68
  */
47
- export function toCurrency(value: number | string | bigint, options?: {
48
- and?: boolean | undefined;
49
- }): string;
69
+ export function toCurrency(value: number | string | bigint, options?: CurrencyOptions): string;