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/ro-RO.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 { bounded, western } from './utils/scale.js'
18
+ import { resolveOptions } from './utils/resolve-options.js'
17
19
 
18
20
  // ============================================================================
19
21
  // Vocabulary (module-level constants)
@@ -62,17 +64,28 @@ const SCALE_META = [
62
64
  { singular: 'cvintilion', plural: 'cvintilioane', article: 'un', feminine: false, needsDe: true },
63
65
  { singular: 'sextilion', plural: 'sextilioane', article: 'un', feminine: false, needsDe: true },
64
66
  { singular: 'septilion', plural: 'septilioane', article: 'un', feminine: false, needsDe: true },
65
- { singular: 'octilion', plural: 'octilioane', article: 'un', feminine: false, needsDe: true }
67
+ { singular: 'octilion', plural: 'octilioane', article: 'un', feminine: false, needsDe: true },
66
68
  ]
67
69
 
70
+ // Cardinal scale words run thousand..octilion; past SCALE_META the builder drops
71
+ // the scale word. The ordinal spells its millions multiplier via spellUnder1000
72
+ // (0-999), so it overflows once that multiplier reaches 1000 — n must stay below 10^9.
73
+ export const cardinalMax = western(SCALE_META.length)
74
+ export const ordinalMax = bounded(9)
75
+ export const currencyMax = western(SCALE_META.length)
76
+
68
77
  // ============================================================================
69
78
  // Helper Functions
70
79
  // ============================================================================
71
80
 
72
81
  /**
73
82
  * Spells number under 100.
83
+ * @param {number} n - Number 0-99
84
+ * @param {boolean} [feminine] - Use feminine forms
85
+ * @param {boolean} [masculineTeens] - Use masculine teen forms
86
+ * @returns {string} The number 0-99 in words
74
87
  */
75
- function spellUnder100 (n, feminine = false, masculineTeens = false) {
88
+ function spellUnder100(n, feminine = false, masculineTeens = false) {
76
89
  if (n === 0) return ''
77
90
  if (n < 10) {
78
91
  return feminine ? ONES_FEM[n] : ONES_MASC[n]
@@ -91,8 +104,12 @@ function spellUnder100 (n, feminine = false, masculineTeens = false) {
91
104
 
92
105
  /**
93
106
  * Spells number under 1000.
107
+ * @param {number} n - Number 0-999
108
+ * @param {boolean} [feminine] - Use feminine forms
109
+ * @param {boolean} [masculineTeens] - Use masculine teen forms
110
+ * @returns {string} The number 0-999 in words
94
111
  */
95
- function spellUnder1000 (n, feminine = false, masculineTeens = false) {
112
+ function spellUnder1000(n, feminine = false, masculineTeens = false) {
96
113
  if (n === 0) return ''
97
114
  if (n < 100) return spellUnder100(n, feminine, masculineTeens)
98
115
 
@@ -107,8 +124,11 @@ function spellUnder1000 (n, feminine = false, masculineTeens = false) {
107
124
  /**
108
125
  * Builds scale word with proper pluralization and "de" insertion.
109
126
  * Romanian always uses feminine forms (două, not doi) when counting scale words.
127
+ * @param {number} segment - Three-digit segment value (1-999)
128
+ * @param {number} scaleIndex - Scale position (1 = thousands, 2 = millions, ...)
129
+ * @returns {string} The scale phrase in words
110
130
  */
111
- function buildScalePhrase (segment, scaleIndex) {
131
+ function buildScalePhrase(segment, scaleIndex) {
112
132
  const meta = SCALE_META[scaleIndex - 1]
113
133
  if (!meta) return spellUnder1000(segment, true)
114
134
 
@@ -137,12 +157,11 @@ function buildScalePhrase (segment, scaleIndex) {
137
157
 
138
158
  /**
139
159
  * Converts a non-negative integer to Romanian words.
140
- *
141
160
  * @param {bigint} n - Non-negative integer to convert
142
- * @param {Object} options - Conversion options
161
+ * @param {string | {gender?: string}} gender - Gender for numbers ('feminine'/'masculine')
143
162
  * @returns {string} Romanian words
144
163
  */
145
- function integerToWords (n, gender) {
164
+ function integerToWords(n, gender) {
146
165
  if (n === 0n) return ZERO
147
166
 
148
167
  // Fast path: numbers < 1000
@@ -157,12 +176,11 @@ function integerToWords (n, gender) {
157
176
  /**
158
177
  * Builds words for numbers >= 1000.
159
178
  * Uses BigInt division for faster segment extraction.
160
- *
161
179
  * @param {bigint} n - Number >= 1000
162
- * @param {Object} options - Conversion options
180
+ * @param {string | {gender?: string}} gender - Gender for numbers ('feminine'/'masculine')
163
181
  * @returns {string} Romanian words
164
182
  */
165
- function buildLargeNumberWords (n, gender) {
183
+ function buildLargeNumberWords(n, gender) {
166
184
  // Extract segments using BigInt division (faster than string slicing)
167
185
  // Segments stored least-significant first (index 0 = ones, 1 = thousands, etc.)
168
186
  const segmentValues = []
@@ -184,14 +202,16 @@ function buildLargeNumberWords (n, gender) {
184
202
  // Units segment - use gender from options
185
203
  const feminine = gender === 'feminine'
186
204
  segmentWords = spellUnder1000(segment, feminine)
187
- } else {
205
+ }
206
+ else {
188
207
  // Scale segment
189
208
  segmentWords = buildScalePhrase(segment, i)
190
209
  }
191
210
 
192
211
  if (result && segmentWords) {
193
212
  result += ' ' + segmentWords
194
- } else if (segmentWords) {
213
+ }
214
+ else if (segmentWords) {
195
215
  result = segmentWords
196
216
  }
197
217
  }
@@ -202,11 +222,10 @@ function buildLargeNumberWords (n, gender) {
202
222
  /**
203
223
  * Converts decimal digits to Romanian words.
204
224
  * Decimals always use masculine forms.
205
- *
206
225
  * @param {string} decimalPart - Decimal digits (without the point)
207
226
  * @returns {string} Romanian words for decimal part
208
227
  */
209
- function decimalPartToWords (decimalPart) {
228
+ function decimalPartToWords(decimalPart) {
210
229
  let result = ''
211
230
  let i = 0
212
231
 
@@ -224,7 +243,8 @@ function decimalPartToWords (decimalPart) {
224
243
  const n = BigInt(remainder)
225
244
  if (n < 1000n) {
226
245
  result += spellUnder1000(Number(n), false, true)
227
- } else {
246
+ }
247
+ else {
228
248
  result += integerToWords(n, { gender: 'masculine' })
229
249
  }
230
250
  }
@@ -232,27 +252,37 @@ function decimalPartToWords (decimalPart) {
232
252
  return result
233
253
  }
234
254
 
255
+ /**
256
+ * @typedef {object} CardinalOptions
257
+ * @property {('masculine'|'feminine')} [gender] - Gender for numbers
258
+ */
259
+
260
+ /** @type {Required<CardinalOptions>} */
261
+ export const cardinalDefaults = { gender: 'masculine' }
262
+
263
+ /** @type {{ gender: ReadonlyArray<Required<CardinalOptions>['gender']> }} */
264
+ export const cardinalValues = { gender: ['masculine', 'feminine'] }
265
+
235
266
  /**
236
267
  * Converts a numeric value to Romanian words.
237
- *
238
268
  * @param {number | string | bigint} value - The numeric value to convert
239
- * @param {Object} [options] - Conversion options
240
- * @param {string} [options.gender='masculine'] - Gender for numbers
269
+ * @param {CardinalOptions} [options] - Conversion options
241
270
  * @returns {string} The number in Romanian words
242
271
  * @throws {TypeError} If value is not a valid numeric type
243
272
  * @throws {Error} If value is not a valid number format
244
- *
245
273
  * @example
246
274
  * toCardinal(21) // 'douăzeci și unu'
247
275
  * toCardinal(1, { gender: 'feminine' }) // 'una'
248
276
  * toCardinal(1000) // 'o mie'
249
277
  */
250
- function toCardinal (value, options) {
251
- options = validateOptions(options)
278
+ function toCardinal(value, options) {
252
279
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
280
+ // Both the integer part and the decimal's significant digits are spelled via
281
+ // the scale builder, so both must clear the ceiling.
282
+ checkMax(integerPart, cardinalMax, decimalPart)
253
283
 
254
284
  // Apply option defaults
255
- const { gender = 'masculine' } = options
285
+ const { gender } = resolveOptions(options, cardinalDefaults, cardinalValues)
256
286
 
257
287
  let result = ''
258
288
 
@@ -275,11 +305,10 @@ function toCardinal (value, options) {
275
305
 
276
306
  /**
277
307
  * Builds ordinal for tens and ones (0-99).
278
- *
279
308
  * @param {number} n - Number 0-99
280
309
  * @returns {string} Ordinal word
281
310
  */
282
- function buildOrdinalTensOnes (n) {
311
+ function buildOrdinalTensOnes(n) {
283
312
  if (n === 0) return ''
284
313
  if (n < 10) return ORDINAL_ONES[n]
285
314
  if (n < 20) return ORDINAL_TEENS[n - 10]
@@ -296,11 +325,10 @@ function buildOrdinalTensOnes (n) {
296
325
 
297
326
  /**
298
327
  * Converts a non-negative integer to Romanian ordinal words.
299
- *
300
328
  * @param {bigint} n - Non-negative integer to convert
301
329
  * @returns {string} Romanian ordinal words
302
330
  */
303
- function integerToOrdinal (n) {
331
+ function integerToOrdinal(n) {
304
332
  if (n === 0n) return ''
305
333
  if (n === 1n) return ORDINAL_ONES[1]
306
334
 
@@ -336,7 +364,8 @@ function integerToOrdinal (n) {
336
364
  let result
337
365
  if (thousands === 1) {
338
366
  result = 'o mie'
339
- } else {
367
+ }
368
+ else {
340
369
  result = buildScalePhrase(thousands, 1)
341
370
  }
342
371
 
@@ -368,7 +397,8 @@ function integerToOrdinal (n) {
368
397
  let result
369
398
  if (millions === 1) {
370
399
  result = 'un milion'
371
- } else {
400
+ }
401
+ else {
372
402
  result = buildScalePhrase(millions, 2)
373
403
  }
374
404
 
@@ -377,18 +407,17 @@ function integerToOrdinal (n) {
377
407
 
378
408
  /**
379
409
  * Converts a numeric value to Romanian ordinal words.
380
- *
381
410
  * @param {number | string | bigint} value - The numeric value to convert
382
411
  * @returns {string} The ordinal in Romanian words
383
412
  * @throws {TypeError} If value is not a valid numeric type
384
413
  * @throws {Error} If value is not a positive integer
385
- *
386
414
  * @example
387
415
  * toOrdinal(1) // 'primul'
388
416
  * toOrdinal(21) // 'douăzeci și primul'
389
417
  */
390
- function toOrdinal (value) {
418
+ function toOrdinal(value) {
391
419
  const n = parseOrdinalValue(value)
420
+ checkMax(n, ordinalMax)
392
421
  return integerToOrdinal(n)
393
422
  }
394
423
 
@@ -398,18 +427,17 @@ function toOrdinal (value) {
398
427
 
399
428
  /**
400
429
  * Converts a numeric value to Romanian Leu currency words.
401
- *
402
430
  * @param {number | string | bigint} value - The numeric value to convert
403
431
  * @returns {string} The currency in Romanian words
404
432
  * @throws {TypeError} If value is not a valid numeric type
405
433
  * @throws {Error} If value is not a valid number format
406
- *
407
434
  * @example
408
435
  * toCurrency(1) // 'un leu'
409
436
  * toCurrency(2.50) // 'doi lei cincizeci de bani'
410
437
  */
411
- function toCurrency (value) {
438
+ function toCurrency(value) {
412
439
  const { isNegative, dollars, cents } = parseCurrencyValue(value)
440
+ checkMax(dollars, currencyMax)
413
441
 
414
442
  const parts = []
415
443
 
@@ -421,9 +449,17 @@ function toCurrency (value) {
421
449
  if (dollars > 0n || cents === 0n) {
422
450
  if (dollars === 1n) {
423
451
  parts.push('un ' + LEU_SINGULAR)
424
- } else {
452
+ }
453
+ else {
425
454
  const leuWord = integerToWords(dollars, 'masculine')
426
- parts.push(leuWord + ' ' + LEU_PLURAL)
455
+ // Romanian inserts "de" before the noun for the CLDR `other` category:
456
+ // count >= 20 whose last two digits are 00 or 20-99 — "douăzeci de lei",
457
+ // "o sută de lei", but "o sută unu lei" (101) and no "de" below 20. The
458
+ // bani path below applies the same rule; buildScalePhrase uses a related
459
+ // per-segment predicate for scale words.
460
+ const m = dollars % 100n
461
+ const needsDe = dollars >= 20n && (m === 0n || m >= 20n)
462
+ parts.push(leuWord + (needsDe ? ' de ' : ' ') + LEU_PLURAL)
427
463
  }
428
464
  }
429
465
 
@@ -432,10 +468,12 @@ function toCurrency (value) {
432
468
  const centNum = Number(cents)
433
469
  if (centNum === 1) {
434
470
  parts.push('un ' + BAN_SINGULAR)
435
- } else if (centNum >= 20) {
471
+ }
472
+ else if (centNum >= 20) {
436
473
  const banWord = spellUnder100(centNum, false)
437
474
  parts.push(banWord + ' de ' + BAN_PLURAL)
438
- } else {
475
+ }
476
+ else {
439
477
  const banWord = spellUnder100(centNum, false)
440
478
  parts.push(banWord + ' ' + BAN_PLURAL)
441
479
  }
package/src/ru-RU.d.ts CHANGED
@@ -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 "и" between rubles and kopecks
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 "и" between rubles and kopecks
29
+ */
30
+ and?: boolean | undefined;
31
+ };
1
32
  /**
2
33
  * Converts a numeric value to Russian 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 Russian 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 Russian 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., "первый", "сорок второй")
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) // 'первый'
22
47
  * toOrdinal(2) // 'второй'
@@ -30,14 +55,11 @@ export function toCardinal(value: number | string | bigint, options?: {
30
55
  export function toOrdinal(value: number | string | bigint): string;
31
56
  /**
32
57
  * Converts a numeric value to Russian currency words (Russian Ruble).
33
- *
34
58
  * @param {number | string | bigint} value - The currency amount to convert
35
- * @param {Object} [options] - Optional configuration
36
- * @param {boolean} [options.and=true] - Use "и" between rubles and kopecks
59
+ * @param {CurrencyOptions} [options] - Optional configuration
37
60
  * @returns {string} The amount in Russian currency words
38
61
  * @throws {TypeError} If value is not a valid numeric type
39
62
  * @throws {Error} If value is not a valid number format
40
- *
41
63
  * @example
42
64
  * toCurrency(42.50) // 'сорок два рубля и пятьдесят копеек'
43
65
  * toCurrency(1) // 'один рубль'
@@ -45,6 +67,4 @@ export function toOrdinal(value: number | string | bigint): string;
45
67
  * toCurrency(0.01) // 'одна копейка'
46
68
  * toCurrency(42.50, { and: false }) // 'сорок два рубля пятьдесят копеек'
47
69
  */
48
- export function toCurrency(value: number | string | bigint, options?: {
49
- and?: boolean | undefined;
50
- }): string;
70
+ export function toCurrency(value: number | string | bigint, options?: CurrencyOptions): string;