n2words 5.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 +128 -42
  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 +31 -22
  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 +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/uk-UA.js CHANGED
@@ -13,7 +13,9 @@
13
13
  import { parseCardinalValue } from './utils/parse-cardinal.js'
14
14
  import { parseCurrencyValue } from './utils/parse-currency.js'
15
15
  import { parseOrdinalValue } from './utils/parse-ordinal.js'
16
- import { validateOptions } from './utils/validate-options.js'
16
+ import { checkMax } from './utils/check-max.js'
17
+ import { western } from './utils/scale.js'
18
+ import { resolveOptions } from './utils/resolve-options.js'
17
19
 
18
20
  // ============================================================================
19
21
  // Vocabulary
@@ -71,14 +73,20 @@ const SCALE_FORMS = [
71
73
  ['квiнтильйон', 'квiнтильйони', 'квiнтильйонiв'],
72
74
  ['секстильйон', 'секстильйони', 'секстильйонiв'],
73
75
  ['септильйон', 'септильйони', 'септильйонiв'],
74
- ['октильйон', 'октильйони', 'октильйонiв']
76
+ ['октильйон', 'октильйони', 'октильйонiв'],
75
77
  ]
76
78
 
77
79
  // ============================================================================
78
80
  // Segment Building
79
81
  // ============================================================================
80
82
 
81
- function pluralize (n, forms) {
83
+ /**
84
+ * Selects the correct plural form for a count.
85
+ * @param {number | bigint} n - The count
86
+ * @param {string[]} forms - Plural forms [one, few, many]
87
+ * @returns {string} The selected plural form
88
+ */
89
+ function pluralize(n, forms) {
82
90
  const num = typeof n === 'bigint' ? Number(n) : n
83
91
  const lastDigit = num % 10
84
92
  const lastTwoDigits = num % 100
@@ -92,7 +100,12 @@ function pluralize (n, forms) {
92
100
  return forms[2]
93
101
  }
94
102
 
95
- function buildSegmentMasc (n) {
103
+ /**
104
+ * Builds the masculine words for a 0-999 segment.
105
+ * @param {number} n - Number 0-999
106
+ * @returns {string} Segment words
107
+ */
108
+ function buildSegmentMasc(n) {
96
109
  if (n === 0) return ''
97
110
 
98
111
  const onesDigit = n % 10
@@ -111,14 +124,20 @@ function buildSegmentMasc (n) {
111
124
 
112
125
  if (tensDigit === 1) {
113
126
  parts.push(TEENS[onesDigit])
114
- } else if (onesDigit > 0) {
127
+ }
128
+ else if (onesDigit > 0) {
115
129
  parts.push(ONES_MASC[onesDigit])
116
130
  }
117
131
 
118
132
  return parts.join(' ')
119
133
  }
120
134
 
121
- function buildSegmentFem (n) {
135
+ /**
136
+ * Builds the feminine words for a 0-999 segment.
137
+ * @param {number} n - Number 0-999
138
+ * @returns {string} Segment words
139
+ */
140
+ function buildSegmentFem(n) {
122
141
  if (n === 0) return ''
123
142
 
124
143
  const onesDigit = n % 10
@@ -137,7 +156,8 @@ function buildSegmentFem (n) {
137
156
 
138
157
  if (tensDigit === 1) {
139
158
  parts.push(TEENS[onesDigit])
140
- } else if (onesDigit > 0) {
159
+ }
160
+ else if (onesDigit > 0) {
141
161
  parts.push(ONES_FEM[onesDigit])
142
162
  }
143
163
 
@@ -148,7 +168,13 @@ function buildSegmentFem (n) {
148
168
  // Conversion Functions
149
169
  // ============================================================================
150
170
 
151
- function integerToWords (n, gender) {
171
+ /**
172
+ * Converts a non-negative integer to Ukrainian words.
173
+ * @param {bigint} n - The integer to convert
174
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
175
+ * @returns {string} The integer in Ukrainian words
176
+ */
177
+ function integerToWords(n, gender) {
152
178
  if (n === 0n) return ZERO
153
179
 
154
180
  if (n < 1000n) {
@@ -158,7 +184,13 @@ function integerToWords (n, gender) {
158
184
  return buildLargeNumberWords(n, gender)
159
185
  }
160
186
 
161
- function buildLargeNumberWords (n, gender) {
187
+ /**
188
+ * Builds Ukrainian words for numbers >= 1000.
189
+ * @param {bigint} n - The integer to convert
190
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
191
+ * @returns {string} The integer in Ukrainian words
192
+ */
193
+ function buildLargeNumberWords(n, gender) {
162
194
  const numStr = n.toString()
163
195
  const len = numStr.length
164
196
 
@@ -185,7 +217,8 @@ function buildLargeNumberWords (n, gender) {
185
217
  if (segment !== 0) {
186
218
  if (scaleIndex === 0) {
187
219
  parts.push(gender === 'feminine' ? buildSegmentFem(segment) : buildSegmentMasc(segment))
188
- } else {
220
+ }
221
+ else {
189
222
  const scaleForms = SCALE_FORMS[scaleIndex - 1]
190
223
  const scaleWord = pluralize(segment, scaleForms)
191
224
  // Thousands (scaleIndex=1) are feminine, others masculine
@@ -201,7 +234,13 @@ function buildLargeNumberWords (n, gender) {
201
234
  return parts.join(' ')
202
235
  }
203
236
 
204
- function decimalPartToWords (decimalPart, gender) {
237
+ /**
238
+ * Converts a decimal fractional string to Ukrainian words.
239
+ * @param {string} decimalPart - The fractional digits
240
+ * @param {('masculine'|'feminine')} gender - Grammatical gender
241
+ * @returns {string} The fractional part in Ukrainian words
242
+ */
243
+ function decimalPartToWords(decimalPart, gender) {
205
244
  let result = ''
206
245
  let i = 0
207
246
 
@@ -220,20 +259,38 @@ function decimalPartToWords (decimalPart, gender) {
220
259
  return result
221
260
  }
222
261
 
262
+ // Supported magnitude ceilings (checked at the public entry points). Both
263
+ // tables are indexed [scaleIndex - 1] (units separate), so the ceiling is
264
+ // 10^((length + 1) * 3): cardinal/currency 10^30, ordinal 10^15.
265
+ export const cardinalMax = western(SCALE_FORMS.length)
266
+ export const ordinalMax = western(ORDINAL_SCALES.length)
267
+ export const currencyMax = western(SCALE_FORMS.length)
268
+
269
+ /**
270
+ * @typedef {object} CardinalOptions
271
+ * @property {('masculine'|'feminine')} [gender] - Grammatical gender
272
+ */
273
+
274
+ /** @type {Required<CardinalOptions>} */
275
+ export const cardinalDefaults = { gender: 'masculine' }
276
+
277
+ /** @type {{ gender: ReadonlyArray<Required<CardinalOptions>['gender']> }} */
278
+ export const cardinalValues = { gender: ['masculine', 'feminine'] }
279
+
223
280
  /**
224
281
  * Converts a numeric value to Ukrainian words.
225
- *
226
282
  * @param {number | string | bigint} value - The numeric value to convert
227
- * @param {Object} [options] - Optional configuration
228
- * @param {('masculine'|'feminine')} [options.gender='masculine'] - Grammatical gender
283
+ * @param {CardinalOptions} [options] - Optional configuration
229
284
  * @returns {string} The number in Ukrainian words
230
285
  */
231
- function toCardinal (value, options) {
232
- options = validateOptions(options)
286
+ function toCardinal(value, options) {
233
287
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
288
+ // Both the integer part and the decimal's significant digits are spelled via
289
+ // the scale builder, so both must clear the ceiling.
290
+ checkMax(integerPart, cardinalMax, decimalPart)
234
291
 
235
292
  // Apply option defaults
236
- const { gender = 'masculine' } = options
293
+ const { gender } = resolveOptions(options, cardinalDefaults, cardinalValues)
237
294
 
238
295
  let result = ''
239
296
 
@@ -256,11 +313,10 @@ function toCardinal (value, options) {
256
313
 
257
314
  /**
258
315
  * Builds ordinal for a 0-99 segment when it's the final (ordinal) part.
259
- *
260
316
  * @param {number} n - Number 0-99
261
317
  * @returns {string} Ordinal words
262
318
  */
263
- function buildOrdinalTensOnes (n) {
319
+ function buildOrdinalTensOnes(n) {
264
320
  if (n === 0) return ''
265
321
 
266
322
  const onesDigit = n % 10
@@ -283,11 +339,10 @@ function buildOrdinalTensOnes (n) {
283
339
 
284
340
  /**
285
341
  * Converts a positive integer to Ukrainian ordinal words (masculine nominative).
286
- *
287
342
  * @param {bigint} n - Positive integer to convert
288
343
  * @returns {string} Ordinal Ukrainian words
289
344
  */
290
- function integerToOrdinal (n) {
345
+ function integerToOrdinal(n) {
291
346
  if (n < 100n) {
292
347
  return buildOrdinalTensOnes(Number(n))
293
348
  }
@@ -325,11 +380,10 @@ function integerToOrdinal (n) {
325
380
 
326
381
  /**
327
382
  * Builds ordinal words for numbers >= 1,000,000.
328
- *
329
383
  * @param {bigint} n - Number >= 1,000,000
330
384
  * @returns {string} Ordinal Ukrainian words
331
385
  */
332
- function buildLargeOrdinal (n) {
386
+ function buildLargeOrdinal(n) {
333
387
  const numStr = n.toString()
334
388
  const len = numStr.length
335
389
 
@@ -364,19 +418,23 @@ function buildLargeOrdinal (n) {
364
418
  if (scaleIndex === 0) {
365
419
  if (isLastNonZero) {
366
420
  parts.push(integerToOrdinal(BigInt(segment)))
367
- } else {
421
+ }
422
+ else {
368
423
  parts.push(buildSegmentMasc(segment))
369
424
  }
370
- } else {
425
+ }
426
+ else {
371
427
  if (isLastNonZero) {
372
428
  if (segment === 1) {
373
429
  parts.push(ORDINAL_SCALES[scaleIndex - 1])
374
- } else {
430
+ }
431
+ else {
375
432
  const isFeminine = scaleIndex === 1
376
433
  const segmentWord = isFeminine ? buildSegmentFem(segment) : buildSegmentMasc(segment)
377
434
  parts.push(segmentWord + ' ' + ORDINAL_SCALES[scaleIndex - 1])
378
435
  }
379
- } else {
436
+ }
437
+ else {
380
438
  const scaleForms = SCALE_FORMS[scaleIndex - 1]
381
439
  const scaleWord = pluralize(segment, scaleForms)
382
440
  const isFeminine = scaleIndex === 1
@@ -394,12 +452,10 @@ function buildLargeOrdinal (n) {
394
452
 
395
453
  /**
396
454
  * Converts a numeric value to Ukrainian ordinal words (masculine nominative).
397
- *
398
455
  * @param {number | string | bigint} value - The numeric value to convert (must be a positive integer)
399
456
  * @returns {string} The number as ordinal words
400
457
  * @throws {TypeError} If value is not a valid numeric type
401
458
  * @throws {RangeError} If value is negative, zero, or has a decimal part
402
- *
403
459
  * @example
404
460
  * toOrdinal(1) // 'перший'
405
461
  * toOrdinal(2) // 'другий'
@@ -407,8 +463,9 @@ function buildLargeOrdinal (n) {
407
463
  * toOrdinal(100) // 'сотий'
408
464
  * toOrdinal(1000) // 'тисячний'
409
465
  */
410
- function toOrdinal (value) {
466
+ function toOrdinal(value) {
411
467
  const integerPart = parseOrdinalValue(value)
468
+ checkMax(integerPart, ordinalMax)
412
469
  return integerToOrdinal(integerPart)
413
470
  }
414
471
 
@@ -418,20 +475,19 @@ function toOrdinal (value) {
418
475
 
419
476
  /**
420
477
  * Converts a numeric value to Ukrainian currency words (Hryvnia).
421
- *
422
478
  * @param {number | string | bigint} value - The currency amount to convert
423
479
  * @returns {string} The amount in Ukrainian currency words
424
480
  * @throws {TypeError} If value is not a valid numeric type
425
481
  * @throws {Error} If value is not a valid number format
426
- *
427
482
  * @example
428
483
  * toCurrency(42) // 'сорок двi гривнi'
429
484
  * toCurrency(1) // 'одна гривня'
430
485
  * toCurrency(1.50) // 'одна гривня п\'ятдесят копiйок'
431
486
  * toCurrency(-5) // 'мiнус п\'ять гривень'
432
487
  */
433
- function toCurrency (value) {
488
+ function toCurrency(value) {
434
489
  const { isNegative, dollars: hryvnia, cents: kopiyky } = parseCurrencyValue(value)
490
+ checkMax(hryvnia, currencyMax)
435
491
 
436
492
  let result = ''
437
493
  if (isNegative) {
package/src/ur-PK.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 Urdu words.
3
- *
4
6
  * @param {number | string | bigint} value - The numeric value to convert
5
7
  * @returns {string} The number in Urdu words
6
8
  */
7
9
  export function toCardinal(value: number | string | bigint): string;
8
10
  /**
9
11
  * Converts a numeric value to Urdu 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) // 'دوسرا'
@@ -22,12 +22,10 @@ export function toCardinal(value: number | string | bigint): string;
22
22
  export function toOrdinal(value: number | string | bigint): string;
23
23
  /**
24
24
  * Converts a numeric value to Urdu currency words (Pakistani Rupee).
25
- *
26
25
  * @param {number | string | bigint} value - The currency amount to convert
27
26
  * @returns {string} The amount in Urdu currency words
28
27
  * @throws {TypeError} If value is not a valid numeric type
29
28
  * @throws {Error} If value is not a valid number format
30
- *
31
29
  * @example
32
30
  * toCurrency(42.50) // 'بیالیس روپے پچاس پیسے'
33
31
  * toCurrency(1) // 'ایک روپیہ'
package/src/ur-PK.js CHANGED
@@ -13,6 +13,8 @@
13
13
  import { parseCardinalValue } from './utils/parse-cardinal.js'
14
14
  import { parseCurrencyValue } from './utils/parse-currency.js'
15
15
  import { parseOrdinalValue } from './utils/parse-ordinal.js'
16
+ import { checkMax } from './utils/check-max.js'
17
+ import { indian } from './utils/scale.js'
16
18
 
17
19
  // ============================================================================
18
20
  // Vocabulary
@@ -55,20 +57,29 @@ const BELOW_HUNDRED = [
55
57
  'ساٹھ', 'اکسٹھ', 'باسٹھ', 'ترسٹھ', 'چونسٹھ', 'پینسٹھ', 'چھیاسٹھ', 'سڑسٹھ', 'اڑسٹھ', 'انہتر',
56
58
  'ستر', 'اکہتر', 'بہتر', 'تہتر', 'چوہتر', 'پچھتر', 'چھہتر', 'ستتر', 'اٹھہتر', 'اناسی',
57
59
  'اسی', 'اکیاسی', 'بیاسی', 'تریاسی', 'چوراسی', 'پچاسی', 'چھیاسی', 'ستاسی', 'اٹھاسی', 'نواسی',
58
- 'نوے', 'اکانوے', 'بانوے', 'ترانوے', 'چورانوے', 'پچانوے', 'چھیانوے', 'ستانوے', 'اٹھانوے', 'ننانوے'
60
+ 'نوے', 'اکانوے', 'بانوے', 'ترانوے', 'چورانوے', 'پچانوے', 'چھیانوے', 'ستانوے', 'اٹھانوے', 'ننانوے',
59
61
  ]
60
62
 
61
63
  // Scale words: index 0 = units (empty), 1 = thousand, 2 = lakh, 3 = crore, etc.
62
64
  const SCALE_WORDS = ['', 'ہزار', 'لاکھ', 'کروڑ', 'ارب', 'کھرب', 'نیل', 'پدم', 'شنکھ']
63
65
 
66
+ // 3-2-2 Indian grouping: a 3-digit base segment, then 2 digits per scale word
67
+ // (SCALE_WORDS[0] = '' is the units slot). Past the table the scale word is
68
+ // dropped, which collapses the magnitude — so cap there.
69
+ export const cardinalMax = indian(SCALE_WORDS.length)
70
+ export const ordinalMax = indian(SCALE_WORDS.length)
71
+ export const currencyMax = indian(SCALE_WORDS.length)
72
+
64
73
  // ============================================================================
65
74
  // Segment Building
66
75
  // ============================================================================
67
76
 
68
77
  /**
69
78
  * Builds words for a 0-999 segment.
79
+ * @param {number} n - Segment value (0-999)
80
+ * @returns {string} Urdu words for the segment
70
81
  */
71
- function buildSegment (n) {
82
+ function buildSegment(n) {
72
83
  if (n === 0) return ''
73
84
  if (n < 100) return BELOW_HUNDRED[n]
74
85
 
@@ -90,11 +101,10 @@ function buildSegment (n) {
90
101
  *
91
102
  * Uses BigInt modulo for segment extraction (faster than string slicing).
92
103
  * South Asian 3-2-2 grouping: first 3 digits, then groups of 2.
93
- *
94
104
  * @param {bigint} n - Non-negative integer to convert
95
105
  * @returns {string} Urdu words
96
106
  */
97
- function integerToWords (n) {
107
+ function integerToWords(n) {
98
108
  if (n === 0n) return ZERO
99
109
 
100
110
  // Fast path: numbers < 1000 (direct lookup)
@@ -120,7 +130,8 @@ function integerToWords (n) {
120
130
 
121
131
  if (i === 0) {
122
132
  words.push(buildSegment(segment))
123
- } else {
133
+ }
134
+ else {
124
135
  words.push(BELOW_HUNDRED[segment])
125
136
  }
126
137
 
@@ -132,7 +143,12 @@ function integerToWords (n) {
132
143
  return words.join(' ')
133
144
  }
134
145
 
135
- function decimalPartToWords (decimalPart) {
146
+ /**
147
+ * Converts the fractional digit string to Urdu words.
148
+ * @param {string} decimalPart - Digits after the decimal separator
149
+ * @returns {string} Urdu words for the decimal part
150
+ */
151
+ function decimalPartToWords(decimalPart) {
136
152
  let result = ''
137
153
  let i = 0
138
154
 
@@ -153,12 +169,14 @@ function decimalPartToWords (decimalPart) {
153
169
 
154
170
  /**
155
171
  * Converts a numeric value to Urdu words.
156
- *
157
172
  * @param {number | string | bigint} value - The numeric value to convert
158
173
  * @returns {string} The number in Urdu words
159
174
  */
160
- function toCardinal (value) {
175
+ function toCardinal(value) {
161
176
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
177
+ // Both the integer part and the decimal's significant digits are spelled via
178
+ // the scale builder, so both must clear the ceiling.
179
+ checkMax(integerPart, cardinalMax, decimalPart)
162
180
 
163
181
  let result = ''
164
182
 
@@ -183,11 +201,10 @@ function toCardinal (value) {
183
201
  * Converts a positive integer to Urdu ordinal words.
184
202
  *
185
203
  * Urdu ordinals: First 6 are irregular, then add -واں suffix.
186
- *
187
204
  * @param {bigint} n - Positive integer to convert
188
205
  * @returns {string} Urdu ordinal words
189
206
  */
190
- function integerToOrdinal (n) {
207
+ function integerToOrdinal(n) {
191
208
  // Special ordinals for 1-6
192
209
  if (n >= 1n && n <= 6n) {
193
210
  return ORDINAL_SPECIAL[Number(n)]
@@ -200,20 +217,20 @@ function integerToOrdinal (n) {
200
217
 
201
218
  /**
202
219
  * Converts a numeric value to Urdu ordinal words.
203
- *
204
220
  * @param {number | string | bigint} value - The numeric value to convert (positive integer)
205
221
  * @returns {string} The number as ordinal words
206
222
  * @throws {TypeError} If value is not a valid numeric type
207
223
  * @throws {RangeError} If value is negative, zero, or has a decimal part
208
- *
209
224
  * @example
210
225
  * toOrdinal(1) // 'پہلا'
211
226
  * toOrdinal(2) // 'دوسرا'
212
227
  * toOrdinal(3) // 'تیسرا'
213
228
  * toOrdinal(10) // 'دسواں'
214
229
  */
215
- function toOrdinal (value) {
230
+ function toOrdinal(value) {
216
231
  const integerPart = parseOrdinalValue(value)
232
+ // Ordinals build on the cardinal speller, so they share its ceiling.
233
+ checkMax(integerPart, ordinalMax)
217
234
  return integerToOrdinal(integerPart)
218
235
  }
219
236
 
@@ -223,19 +240,18 @@ function toOrdinal (value) {
223
240
 
224
241
  /**
225
242
  * Converts a numeric value to Urdu currency words (Pakistani Rupee).
226
- *
227
243
  * @param {number | string | bigint} value - The currency amount to convert
228
244
  * @returns {string} The amount in Urdu currency words
229
245
  * @throws {TypeError} If value is not a valid numeric type
230
246
  * @throws {Error} If value is not a valid number format
231
- *
232
247
  * @example
233
248
  * toCurrency(42.50) // 'بیالیس روپے پچاس پیسے'
234
249
  * toCurrency(1) // 'ایک روپیہ'
235
250
  * toCurrency(0.01) // 'ایک پیسہ'
236
251
  */
237
- function toCurrency (value) {
252
+ function toCurrency(value) {
238
253
  const { isNegative, dollars: rupees, cents: paise } = parseCurrencyValue(value)
254
+ checkMax(rupees, currencyMax)
239
255
 
240
256
  // Build result
241
257
  let result = ''
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Range guard — the throwing counterpart of the scale-range producers.
3
+ *
4
+ * A precondition validator in the same family as `parseCardinalValue` /
5
+ * `resolveOptions`: the language calls it and it throws on a value past the
6
+ * form's ceiling, e.g. `checkMax(integerPart, cardinalMax, decimalPart)`. The
7
+ * `*Max` it consumes is produced by the pure helpers in `scale.js`.
8
+ * @module check-max
9
+ */
10
+ /**
11
+ * Throws a `RangeError` when `value` — or its integer-spelled fraction —
12
+ * reaches a form's ceiling (`max`, the smallest value the form refuses).
13
+ *
14
+ * Runs at the entry point: an O(1) short-circuit before any spelling is built —
15
+ * the in-range path is just the bigint comparison, plus a single
16
+ * `BigInt(fraction)` parse when an integer-spelled fraction is supplied. The
17
+ * message renders `10^N - 1` when the ceiling is an exact power of ten,
18
+ * otherwise the raw maximum.
19
+ * @param {bigint} value The integer magnitude to test (integer part, dollars, …)
20
+ * @param {bigint | null} max The form's ceiling, or `null` for no limit (never throws)
21
+ * @param {string} [fraction] The decimal digit string — pass it **only** when the
22
+ * fraction is spelled via the scale builder; digit-by-digit and ordinal /
23
+ * currency forms omit it so long fractions stay valid.
24
+ * @throws {RangeError} when the value is out of range
25
+ */
26
+ export function checkMax(value: bigint, max: bigint | null, fraction?: string): void;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Range guard — the throwing counterpart of the scale-range producers.
3
+ *
4
+ * A precondition validator in the same family as `parseCardinalValue` /
5
+ * `resolveOptions`: the language calls it and it throws on a value past the
6
+ * form's ceiling, e.g. `checkMax(integerPart, cardinalMax, decimalPart)`. The
7
+ * `*Max` it consumes is produced by the pure helpers in `scale.js`.
8
+ * @module check-max
9
+ */
10
+
11
+ /**
12
+ * Throws a `RangeError` when `value` — or its integer-spelled fraction —
13
+ * reaches a form's ceiling (`max`, the smallest value the form refuses).
14
+ *
15
+ * Runs at the entry point: an O(1) short-circuit before any spelling is built —
16
+ * the in-range path is just the bigint comparison, plus a single
17
+ * `BigInt(fraction)` parse when an integer-spelled fraction is supplied. The
18
+ * message renders `10^N - 1` when the ceiling is an exact power of ten,
19
+ * otherwise the raw maximum.
20
+ * @param {bigint} value The integer magnitude to test (integer part, dollars, …)
21
+ * @param {bigint | null} max The form's ceiling, or `null` for no limit (never throws)
22
+ * @param {string} [fraction] The decimal digit string — pass it **only** when the
23
+ * fraction is spelled via the scale builder; digit-by-digit and ordinal /
24
+ * currency forms omit it so long fractions stay valid.
25
+ * @throws {RangeError} when the value is out of range
26
+ */
27
+ export function checkMax(value, max, fraction) {
28
+ if (max === null) return
29
+ if (value < max && !(fraction && BigInt(fraction) >= max)) return
30
+ const exponent = max.toString().length - 1
31
+ const largest = max === 10n ** BigInt(exponent) ? `10^${exponent} - 1` : `${max - 1n}`
32
+ throw new RangeError(`Number too large to convert: the largest supported value is ${largest}`)
33
+ }
@@ -6,10 +6,8 @@
6
6
  /**
7
7
  * Expands scientific notation to full decimal form.
8
8
  * Handles arbitrarily large exponents without precision loss.
9
- *
10
9
  * @param {string} str - String in scientific notation (e.g., "1e21", "1.5e-3")
11
10
  * @returns {string} Full decimal representation (e.g., "1000000000000000000000", "0.0015")
12
- *
13
11
  * @example
14
12
  * expandScientificNotation("1e21") // "1000000000000000000000"
15
13
  * expandScientificNotation("1.5e3") // "1500"
@@ -18,14 +16,12 @@
18
16
  export function expandScientificNotation(str: string): string;
19
17
  /**
20
18
  * Converts a number to decimal string, expanding scientific notation if needed.
21
- *
22
19
  * @param {number} value - The number to convert
23
20
  * @returns {string} Decimal string representation
24
21
  */
25
22
  export function numberToString(value: number): string;
26
23
  /**
27
24
  * Checks if a string contains scientific notation.
28
- *
29
25
  * @param {string} str - String to check
30
26
  * @returns {boolean} True if string contains 'e' or 'E'
31
27
  */
@@ -7,16 +7,14 @@
7
7
  /**
8
8
  * Expands scientific notation to full decimal form.
9
9
  * Handles arbitrarily large exponents without precision loss.
10
- *
11
10
  * @param {string} str - String in scientific notation (e.g., "1e21", "1.5e-3")
12
11
  * @returns {string} Full decimal representation (e.g., "1000000000000000000000", "0.0015")
13
- *
14
12
  * @example
15
13
  * expandScientificNotation("1e21") // "1000000000000000000000"
16
14
  * expandScientificNotation("1.5e3") // "1500"
17
15
  * expandScientificNotation("1e-3") // "0.001"
18
16
  */
19
- export function expandScientificNotation (str) {
17
+ export function expandScientificNotation(str) {
20
18
  let [mantissa, expStr] = str.toLowerCase().split('e')
21
19
  const exp = parseInt(expStr, 10)
22
20
 
@@ -36,30 +34,30 @@ export function expandScientificNotation (str) {
36
34
 
37
35
  if (newDotPosition >= digits.length) {
38
36
  return sign + digits + '0'.repeat(newDotPosition - digits.length)
39
- } else if (newDotPosition <= 0) {
37
+ }
38
+ else if (newDotPosition <= 0) {
40
39
  return sign + '0.' + '0'.repeat(-newDotPosition) + digits
41
- } else {
40
+ }
41
+ else {
42
42
  return sign + digits.slice(0, newDotPosition) + '.' + digits.slice(newDotPosition)
43
43
  }
44
44
  }
45
45
 
46
46
  /**
47
47
  * Converts a number to decimal string, expanding scientific notation if needed.
48
- *
49
48
  * @param {number} value - The number to convert
50
49
  * @returns {string} Decimal string representation
51
50
  */
52
- export function numberToString (value) {
51
+ export function numberToString(value) {
53
52
  const str = value.toString()
54
53
  return hasScientificNotation(str) ? expandScientificNotation(str) : str
55
54
  }
56
55
 
57
56
  /**
58
57
  * Checks if a string contains scientific notation.
59
- *
60
58
  * @param {string} str - String to check
61
59
  * @returns {boolean} True if string contains 'e' or 'E'
62
60
  */
63
- export function hasScientificNotation (str) {
61
+ export function hasScientificNotation(str) {
64
62
  return str.includes('e') || str.includes('E')
65
63
  }
@@ -6,8 +6,7 @@
6
6
  * - Object.create(null): null-prototype object
7
7
  *
8
8
  * This excludes arrays, class instances, Map, Set, and other object types.
9
- *
10
- * @param {*} value Value to check
11
- * @returns {boolean} True if value is a plain object
9
+ * @param {unknown} value Value to check
10
+ * @returns {value is object} True if value is a plain object
12
11
  */
13
- export function isPlainObject(value: any): boolean;
12
+ export function isPlainObject(value: unknown): value is object;
@@ -6,11 +6,10 @@
6
6
  * - Object.create(null): null-prototype object
7
7
  *
8
8
  * This excludes arrays, class instances, Map, Set, and other object types.
9
- *
10
- * @param {*} value Value to check
11
- * @returns {boolean} True if value is a plain object
9
+ * @param {unknown} value Value to check
10
+ * @returns {value is object} True if value is a plain object
12
11
  */
13
- export function isPlainObject (value) {
12
+ export function isPlainObject(value) {
14
13
  if (value === null || typeof value !== 'object') return false
15
14
  const proto = Object.getPrototypeOf(value)
16
15
  return proto === null || proto === Object.prototype
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Parses a value for cardinal conversion.
3
3
  * Cardinals accept any numeric value: integers, decimals, negatives.
4
- *
5
4
  * @param {number|string|bigint} value - The value to parse
6
- * @returns {{isNegative: boolean, integerPart: bigint, decimalPart?: string}}
5
+ * @returns {{isNegative: boolean, integerPart: bigint, decimalPart?: string}} The parsed cardinal components
7
6
  * @throws {TypeError} If value is not number, string, or bigint
8
7
  * @throws {RangeError} If value is not finite
9
8
  */