n2words 1.24.0 → 3.0.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 (280) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +183 -156
  3. package/dist/languages/am-Latn.js +3 -0
  4. package/dist/languages/am-Latn.js.map +1 -0
  5. package/dist/languages/am.js +3 -0
  6. package/dist/languages/am.js.map +1 -0
  7. package/dist/languages/ar.js +3 -2
  8. package/dist/languages/ar.js.map +1 -1
  9. package/dist/languages/az.js +3 -2
  10. package/dist/languages/az.js.map +1 -1
  11. package/dist/languages/bn.js +3 -2
  12. package/dist/languages/bn.js.map +1 -1
  13. package/dist/languages/cs.js +3 -2
  14. package/dist/languages/cs.js.map +1 -1
  15. package/dist/languages/da.js +3 -2
  16. package/dist/languages/da.js.map +1 -1
  17. package/dist/languages/de.js +3 -2
  18. package/dist/languages/de.js.map +1 -1
  19. package/dist/languages/el.js +3 -2
  20. package/dist/languages/el.js.map +1 -1
  21. package/dist/languages/en.js +3 -2
  22. package/dist/languages/en.js.map +1 -1
  23. package/dist/languages/es.js +3 -2
  24. package/dist/languages/es.js.map +1 -1
  25. package/dist/languages/fa.js +3 -2
  26. package/dist/languages/fa.js.map +1 -1
  27. package/dist/languages/fi.js +3 -0
  28. package/dist/languages/fi.js.map +1 -0
  29. package/dist/languages/fil.js +3 -2
  30. package/dist/languages/fil.js.map +1 -1
  31. package/dist/languages/fr-BE.js +3 -2
  32. package/dist/languages/fr-BE.js.map +1 -1
  33. package/dist/languages/fr.js +3 -2
  34. package/dist/languages/fr.js.map +1 -1
  35. package/dist/languages/gu.js +3 -2
  36. package/dist/languages/gu.js.map +1 -1
  37. package/dist/languages/ha.js +3 -0
  38. package/dist/languages/ha.js.map +1 -0
  39. package/dist/languages/hbo.js +3 -0
  40. package/dist/languages/hbo.js.map +1 -0
  41. package/dist/languages/he.js +3 -2
  42. package/dist/languages/he.js.map +1 -1
  43. package/dist/languages/hi.js +3 -2
  44. package/dist/languages/hi.js.map +1 -1
  45. package/dist/languages/hr.js +3 -2
  46. package/dist/languages/hr.js.map +1 -1
  47. package/dist/languages/hu.js +3 -2
  48. package/dist/languages/hu.js.map +1 -1
  49. package/dist/languages/id.js +3 -2
  50. package/dist/languages/id.js.map +1 -1
  51. package/dist/languages/it.js +3 -2
  52. package/dist/languages/it.js.map +1 -1
  53. package/dist/languages/ja.js +3 -2
  54. package/dist/languages/ja.js.map +1 -1
  55. package/dist/languages/kn.js +3 -2
  56. package/dist/languages/kn.js.map +1 -1
  57. package/dist/languages/ko.js +3 -2
  58. package/dist/languages/ko.js.map +1 -1
  59. package/dist/languages/lt.js +3 -2
  60. package/dist/languages/lt.js.map +1 -1
  61. package/dist/languages/lv.js +3 -2
  62. package/dist/languages/lv.js.map +1 -1
  63. package/dist/languages/mr.js +3 -2
  64. package/dist/languages/mr.js.map +1 -1
  65. package/dist/languages/ms.js +3 -2
  66. package/dist/languages/ms.js.map +1 -1
  67. package/dist/languages/nb.js +3 -2
  68. package/dist/languages/nb.js.map +1 -1
  69. package/dist/languages/nl.js +3 -2
  70. package/dist/languages/nl.js.map +1 -1
  71. package/dist/languages/pa.js +3 -0
  72. package/dist/languages/pa.js.map +1 -0
  73. package/dist/languages/pl.js +3 -2
  74. package/dist/languages/pl.js.map +1 -1
  75. package/dist/languages/pt.js +3 -2
  76. package/dist/languages/pt.js.map +1 -1
  77. package/dist/languages/ro.js +3 -2
  78. package/dist/languages/ro.js.map +1 -1
  79. package/dist/languages/ru.js +3 -2
  80. package/dist/languages/ru.js.map +1 -1
  81. package/dist/languages/sr-Cyrl.js +3 -0
  82. package/dist/languages/sr-Cyrl.js.map +1 -0
  83. package/dist/languages/sr-Latn.js +3 -2
  84. package/dist/languages/sr-Latn.js.map +1 -1
  85. package/dist/languages/sv.js +3 -2
  86. package/dist/languages/sv.js.map +1 -1
  87. package/dist/languages/sw.js +3 -2
  88. package/dist/languages/sw.js.map +1 -1
  89. package/dist/languages/ta.js +3 -2
  90. package/dist/languages/ta.js.map +1 -1
  91. package/dist/languages/te.js +3 -2
  92. package/dist/languages/te.js.map +1 -1
  93. package/dist/languages/th.js +3 -2
  94. package/dist/languages/th.js.map +1 -1
  95. package/dist/languages/tr.js +3 -2
  96. package/dist/languages/tr.js.map +1 -1
  97. package/dist/languages/uk.js +3 -2
  98. package/dist/languages/uk.js.map +1 -1
  99. package/dist/languages/ur.js +3 -2
  100. package/dist/languages/ur.js.map +1 -1
  101. package/dist/languages/vi.js +3 -2
  102. package/dist/languages/vi.js.map +1 -1
  103. package/dist/languages/zh-Hans.js +3 -2
  104. package/dist/languages/zh-Hans.js.map +1 -1
  105. package/dist/languages/zh-Hant.js +3 -0
  106. package/dist/languages/zh-Hant.js.map +1 -0
  107. package/dist/n2words.js +3 -2
  108. package/dist/n2words.js.map +1 -1
  109. package/lib/languages/am-Latn.d.ts +7 -0
  110. package/lib/languages/am-Latn.js +164 -0
  111. package/lib/languages/am.d.ts +7 -0
  112. package/lib/languages/am.js +164 -0
  113. package/lib/languages/ar.d.ts +17 -0
  114. package/lib/languages/ar.js +171 -209
  115. package/lib/languages/az.d.ts +7 -0
  116. package/lib/languages/az.js +167 -49
  117. package/lib/languages/bn.d.ts +7 -0
  118. package/lib/languages/bn.js +142 -123
  119. package/lib/languages/cs.d.ts +18 -0
  120. package/lib/languages/cs.js +303 -176
  121. package/lib/languages/da.d.ts +14 -0
  122. package/lib/languages/da.js +267 -139
  123. package/lib/languages/de.d.ts +17 -0
  124. package/lib/languages/de.js +310 -113
  125. package/lib/languages/el.d.ts +14 -0
  126. package/lib/languages/el.js +225 -98
  127. package/lib/languages/en.d.ts +17 -0
  128. package/lib/languages/en.js +235 -102
  129. package/lib/languages/es.d.ts +21 -0
  130. package/lib/languages/es.js +307 -125
  131. package/lib/languages/fa.d.ts +7 -0
  132. package/lib/languages/fa.js +115 -108
  133. package/lib/languages/fi.d.ts +14 -0
  134. package/lib/languages/fi.js +245 -0
  135. package/lib/languages/fil.d.ts +7 -0
  136. package/lib/languages/fil.js +199 -139
  137. package/lib/languages/fr-BE.d.ts +11 -0
  138. package/lib/languages/fr-BE.js +287 -48
  139. package/lib/languages/fr.d.ts +21 -0
  140. package/lib/languages/fr.js +343 -119
  141. package/lib/languages/gu.d.ts +7 -0
  142. package/lib/languages/gu.js +125 -144
  143. package/lib/languages/ha.d.ts +7 -0
  144. package/lib/languages/ha.js +230 -0
  145. package/lib/languages/hbo.d.ts +13 -0
  146. package/lib/languages/hbo.js +300 -0
  147. package/lib/languages/he.d.ts +13 -0
  148. package/lib/languages/he.js +230 -283
  149. package/lib/languages/hi.d.ts +7 -0
  150. package/lib/languages/hi.js +142 -123
  151. package/lib/languages/hr.d.ts +11 -0
  152. package/lib/languages/hr.js +190 -129
  153. package/lib/languages/hu.d.ts +7 -0
  154. package/lib/languages/hu.js +194 -133
  155. package/lib/languages/id.d.ts +7 -0
  156. package/lib/languages/id.js +167 -140
  157. package/lib/languages/it.d.ts +19 -0
  158. package/lib/languages/it.js +337 -108
  159. package/lib/languages/ja.d.ts +17 -0
  160. package/lib/languages/ja.js +224 -155
  161. package/lib/languages/kn.d.ts +7 -0
  162. package/lib/languages/kn.js +128 -62
  163. package/lib/languages/ko.d.ts +14 -0
  164. package/lib/languages/ko.js +250 -70
  165. package/lib/languages/lt.d.ts +18 -0
  166. package/lib/languages/lt.js +287 -148
  167. package/lib/languages/lv.d.ts +18 -0
  168. package/lib/languages/lv.js +291 -123
  169. package/lib/languages/mr.d.ts +7 -0
  170. package/lib/languages/mr.js +125 -144
  171. package/lib/languages/ms.d.ts +7 -0
  172. package/lib/languages/ms.js +171 -112
  173. package/lib/languages/nb.d.ts +14 -0
  174. package/lib/languages/nb.js +275 -100
  175. package/lib/languages/nl.d.ts +26 -0
  176. package/lib/languages/nl.js +307 -174
  177. package/lib/languages/pa.d.ts +7 -0
  178. package/lib/languages/pa.js +163 -0
  179. package/lib/languages/pl.d.ts +22 -0
  180. package/lib/languages/pl.js +299 -158
  181. package/lib/languages/pt.d.ts +17 -0
  182. package/lib/languages/pt.js +279 -120
  183. package/lib/languages/ro.d.ts +18 -0
  184. package/lib/languages/ro.js +214 -337
  185. package/lib/languages/ru.d.ts +11 -0
  186. package/lib/languages/ru.js +219 -95
  187. package/lib/languages/sr-Cyrl.d.ts +11 -0
  188. package/lib/languages/sr-Cyrl.js +215 -0
  189. package/lib/languages/sr-Latn.d.ts +11 -0
  190. package/lib/languages/sr-Latn.js +190 -132
  191. package/lib/languages/sv.d.ts +14 -0
  192. package/lib/languages/sv.js +280 -103
  193. package/lib/languages/sw.d.ts +7 -0
  194. package/lib/languages/sw.js +135 -103
  195. package/lib/languages/ta.d.ts +7 -0
  196. package/lib/languages/ta.js +133 -205
  197. package/lib/languages/te.d.ts +7 -0
  198. package/lib/languages/te.js +148 -213
  199. package/lib/languages/th.d.ts +7 -0
  200. package/lib/languages/th.js +139 -101
  201. package/lib/languages/tr.d.ts +18 -0
  202. package/lib/languages/tr.js +246 -66
  203. package/lib/languages/uk.d.ts +11 -0
  204. package/lib/languages/uk.js +197 -101
  205. package/lib/languages/ur.d.ts +7 -0
  206. package/lib/languages/ur.js +160 -123
  207. package/lib/languages/vi.d.ts +17 -0
  208. package/lib/languages/vi.js +287 -164
  209. package/lib/languages/zh-Hans.d.ts +11 -0
  210. package/lib/languages/zh-Hans.js +159 -142
  211. package/lib/languages/zh-Hant.d.ts +11 -0
  212. package/lib/languages/zh-Hant.js +202 -0
  213. package/lib/n2words.d.ts +53 -0
  214. package/lib/n2words.js +91 -227
  215. package/lib/utils/is-plain-object.d.ts +13 -0
  216. package/lib/utils/is-plain-object.js +17 -0
  217. package/lib/utils/parse-numeric.d.ts +17 -0
  218. package/lib/utils/parse-numeric.js +108 -0
  219. package/lib/utils/validate-options.d.ts +8 -0
  220. package/lib/utils/validate-options.js +16 -0
  221. package/package.json +118 -67
  222. package/dist/languages/pa-Guru.js +0 -2
  223. package/dist/languages/pa-Guru.js.map +0 -1
  224. package/lib/classes/abstract-language.js +0 -261
  225. package/lib/classes/greedy-scale-language.js +0 -195
  226. package/lib/classes/slavic-language.js +0 -251
  227. package/lib/classes/south-asian-language.js +0 -161
  228. package/lib/classes/turkic-language.js +0 -63
  229. package/lib/languages/pa-Guru.js +0 -126
  230. package/typings/classes/abstract-language.d.ts +0 -144
  231. package/typings/classes/greedy-scale-language.d.ts +0 -148
  232. package/typings/classes/slavic-language.d.ts +0 -145
  233. package/typings/classes/south-asian-language.d.ts +0 -101
  234. package/typings/classes/turkic-language.d.ts +0 -42
  235. package/typings/languages/ar.d.ts +0 -93
  236. package/typings/languages/az.d.ts +0 -25
  237. package/typings/languages/bn.d.ts +0 -1
  238. package/typings/languages/cs.d.ts +0 -120
  239. package/typings/languages/da.d.ts +0 -53
  240. package/typings/languages/de.d.ts +0 -26
  241. package/typings/languages/el.d.ts +0 -11
  242. package/typings/languages/en.d.ts +0 -30
  243. package/typings/languages/es.d.ts +0 -43
  244. package/typings/languages/fa.d.ts +0 -81
  245. package/typings/languages/fil.d.ts +0 -12
  246. package/typings/languages/fr-BE.d.ts +0 -41
  247. package/typings/languages/fr.d.ts +0 -43
  248. package/typings/languages/gu.d.ts +0 -12
  249. package/typings/languages/he.d.ts +0 -197
  250. package/typings/languages/hi.d.ts +0 -1
  251. package/typings/languages/hr.d.ts +0 -110
  252. package/typings/languages/hu.d.ts +0 -37
  253. package/typings/languages/id.d.ts +0 -69
  254. package/typings/languages/it.d.ts +0 -51
  255. package/typings/languages/ja.d.ts +0 -58
  256. package/typings/languages/kn.d.ts +0 -11
  257. package/typings/languages/ko.d.ts +0 -25
  258. package/typings/languages/lt.d.ts +0 -110
  259. package/typings/languages/lv.d.ts +0 -99
  260. package/typings/languages/mr.d.ts +0 -12
  261. package/typings/languages/ms.d.ts +0 -37
  262. package/typings/languages/nb.d.ts +0 -27
  263. package/typings/languages/nl.d.ts +0 -65
  264. package/typings/languages/pa-Guru.d.ts +0 -1
  265. package/typings/languages/pl.d.ts +0 -116
  266. package/typings/languages/pt.d.ts +0 -39
  267. package/typings/languages/ro.d.ts +0 -229
  268. package/typings/languages/ru.d.ts +0 -108
  269. package/typings/languages/sr-Latn.d.ts +0 -98
  270. package/typings/languages/sv.d.ts +0 -30
  271. package/typings/languages/sw.d.ts +0 -1
  272. package/typings/languages/ta.d.ts +0 -1
  273. package/typings/languages/te.d.ts +0 -1
  274. package/typings/languages/th.d.ts +0 -1
  275. package/typings/languages/tr.d.ts +0 -46
  276. package/typings/languages/uk.d.ts +0 -117
  277. package/typings/languages/ur.d.ts +0 -1
  278. package/typings/languages/vi.d.ts +0 -116
  279. package/typings/languages/zh-Hans.d.ts +0 -57
  280. package/typings/n2words.d.ts +0 -177
@@ -1,380 +1,257 @@
1
- import AbstractLanguage from '../classes/abstract-language.js'
2
-
3
- /**
4
- * @typedef {Object} RomanianOptions
5
- * @property {boolean} [feminine=false] Use feminine forms for numbers.
6
- */
7
-
8
1
  /**
9
- * Romanian language converter.
2
+ * Romanian language converter - Functional Implementation
10
3
  *
11
- * Converts numbers to Romanian words with full grammatical support:
12
- * - Gender agreement (masculine/feminine forms)
13
- * - Complex pluralization (singular/plural forms)
14
- * - "De" preposition insertion for groups >= 20
15
- * - Special feminine handling for thousands
16
- * - Proper case and agreement patterns
17
- *
18
- * Key Features:
19
- * - Gender-aware number forms (unu/una, doi/două, doisprezece/douăsprezece)
20
- * - Group-based algorithm:
21
- * 1. Split number into groups of 3 digits
22
- * 2. Convert each group using gender rules and special forms
23
- * 3. Insert "de" preposition for groups >= 20 (e.g., "douăzeci de mii")
24
- * 4. Append magnitude word with proper singular/plural form (mie/mii, milion/milioane)
25
- * 5. Join with spaces
26
- * - Special feminine units for thousands group
27
- * - Automatic "de" insertion rules (nouăsprezece mii vs douăzeci de mii)
28
- * - Proper singular/plural forms (mie/mii, milion/milioane)
29
- * - Support for very large numbers (up to decillions)
4
+ * A performance-optimized number-to-words converter using precomputed lookup tables.
30
5
  *
31
- * Features:
32
- * - Feminine units for thousands group
33
- * - Support for very large numbers (up to decillions)
6
+ * Key features:
7
+ * - Gender agreement (unu/una, doi/două)
8
+ * - "De" preposition insertion for groups >= 20
9
+ * - Complex scale word handling (mie/mii, milion/milioane)
10
+ * - Feminine units for thousands
34
11
  */
35
- export class Romanian extends AbstractLanguage {
36
- negativeWord = 'minus'
37
- decimalSeparatorWord = 'virgulă'
38
- zeroWord = 'zero'
39
-
40
- ones = {
41
- 1: 'unu',
42
- 2: 'doi',
43
- 3: 'trei',
44
- 4: 'patru',
45
- 5: 'cinci',
46
- 6: 'șase',
47
- 7: 'șapte',
48
- 8: 'opt',
49
- 9: 'nouă'
50
- }
51
12
 
52
- onesFeminine = {
53
- 1: 'una',
54
- 2: 'două',
55
- 3: 'trei',
56
- 4: 'patru',
57
- 5: 'cinci',
58
- 6: 'șase',
59
- 7: 'șapte',
60
- 8: 'opt',
61
- 9: 'nouă'
62
- }
13
+ import { parseNumericValue } from '../utils/parse-numeric.js'
14
+ import { validateOptions } from '../utils/validate-options.js'
15
+
16
+ // ============================================================================
17
+ // Vocabulary (module-level constants)
18
+ // ============================================================================
19
+
20
+ const ONES_MASC = ['', 'unu', 'doi', 'trei', 'patru', 'cinci', 'șase', 'șapte', 'opt', 'nouă']
21
+ const ONES_FEM = ['', 'una', 'două', 'trei', 'patru', 'cinci', 'șase', 'șapte', 'opt', 'nouă']
22
+
23
+ const TEENS = ['zece', 'unsprezece', 'douăsprezece', 'treisprezece', 'paisprezece', 'cincisprezece', 'șaisprezece', 'șaptesprezece', 'optsprezece', 'nouăsprezece']
24
+ const TEENS_MASC = ['zece', 'unsprezece', 'doisprezece', 'treisprezece', 'paisprezece', 'cincisprezece', 'șaisprezece', 'șaptesprezece', 'optsprezece', 'nouăsprezece']
25
+
26
+ const TWENTIES = ['', '', 'douăzeci', 'treizeci', 'patruzeci', 'cincizeci', 'șaizeci', 'șaptezeci', 'optzeci', 'nouăzeci']
27
+
28
+ const HUNDREDS = ['', 'o sută', 'două sute', 'trei sute', 'patru sute', 'cinci sute', 'șase sute', 'șapte sute', 'opt sute', 'nouă sute']
29
+
30
+ const ZERO = 'zero'
31
+ const NEGATIVE = 'minus'
32
+ const DECIMAL_SEP = 'virgulă'
33
+
34
+ // Scale metadata: [singular, plural, article, feminine, needsDe]
35
+ // - singular: form for 1
36
+ // - plural: form for 2+
37
+ // - article: 'o' for feminine, 'un' for masculine
38
+ // - feminine: whether units should be feminine
39
+ // - needsDe: whether "de" is inserted for segment >= 20
40
+ const SCALE_META = [
41
+ { singular: 'mie', plural: 'mii', article: 'o', feminine: true, needsDe: true },
42
+ { singular: 'milion', plural: 'milioane', article: 'un', feminine: false, needsDe: true },
43
+ { singular: 'miliard', plural: 'miliarde', article: 'un', feminine: false, needsDe: true },
44
+ { singular: 'trilion', plural: 'trilioane', article: 'un', feminine: false, needsDe: true },
45
+ { singular: 'cvadrilion', plural: 'cvadrilioane', article: 'un', feminine: false, needsDe: true },
46
+ { singular: 'cvintilion', plural: 'cvintilioane', article: 'un', feminine: false, needsDe: true },
47
+ { singular: 'sextilion', plural: 'sextilioane', article: 'un', feminine: false, needsDe: true },
48
+ { singular: 'septilion', plural: 'septilioane', article: 'un', feminine: false, needsDe: true },
49
+ { singular: 'octilion', plural: 'octilioane', article: 'un', feminine: false, needsDe: true }
50
+ ]
51
+
52
+ // ============================================================================
53
+ // Helper Functions
54
+ // ============================================================================
63
55
 
64
- tens = {
65
- 0: 'zece',
66
- 1: 'unsprezece',
67
- 2: 'douăsprezece',
68
- 3: 'treisprezece',
69
- 4: 'paisprezece',
70
- 5: 'cincisprezece',
71
- 6: 'șaisprezece',
72
- 7: 'șaptesprezece',
73
- 8: 'optsprezece',
74
- 9: 'nouăsprezece'
56
+ /**
57
+ * Spells number under 100.
58
+ */
59
+ function spellUnder100 (n, feminine = false, masculineTeens = false) {
60
+ if (n === 0) return ''
61
+ if (n < 10) {
62
+ return feminine ? ONES_FEM[n] : ONES_MASC[n]
75
63
  }
76
-
77
- tensMasculine = {
78
- 0: 'zece',
79
- 1: 'unsprezece',
80
- 2: 'doisprezece',
81
- 3: 'treisprezece',
82
- 4: 'paisprezece',
83
- 5: 'cincisprezece',
84
- 6: 'șaisprezece',
85
- 7: 'șaptesprezece',
86
- 8: 'optsprezece',
87
- 9: 'nouăsprezece'
64
+ if (n < 20) {
65
+ return masculineTeens ? TEENS_MASC[n - 10] : TEENS[n - 10]
88
66
  }
89
-
90
- twenties = {
91
- 2: 'douăzeci',
92
- 3: 'treizeci',
93
- 4: 'patruzeci',
94
- 5: 'cincizeci',
95
- 6: 'șaizeci',
96
- 7: 'șaptezeci',
97
- 8: 'optzeci',
98
- 9: 'nouăzeci'
67
+ const t = Math.floor(n / 10)
68
+ const u = n % 10
69
+ if (u === 0) {
70
+ return TWENTIES[t]
99
71
  }
72
+ const onesWord = feminine ? ONES_FEM[u] : ONES_MASC[u]
73
+ return TWENTIES[t] + ' și ' + onesWord
74
+ }
100
75
 
101
- hundreds = {
102
- 1: 'o sută',
103
- 2: 'două sute',
104
- 3: 'trei sute',
105
- 4: 'patru sute',
106
- 5: 'cinci sute',
107
- 6: 'șase sute',
108
- 7: 'șapte sute',
109
- 8: 'opt sute',
110
- 9: 'nouă sute'
111
- }
76
+ /**
77
+ * Spells number under 1000.
78
+ */
79
+ function spellUnder1000 (n, feminine = false, masculineTeens = false) {
80
+ if (n === 0) return ''
81
+ if (n < 100) return spellUnder100(n, feminine, masculineTeens)
112
82
 
113
- /**
114
- * Romanian big units.
115
- * For each power group we keep: singular, plural, feminineUnits?, needsDe?
116
- * - 10^3: mie/mii (feminine units in chunk; "de" for chunk >= 20)
117
- * - 10^6: milion/milioane ("de" for chunk >= 20)
118
- * - 10^9: miliard/miliarde ("de" for chunk >= 20)
119
- */
120
- thousands = {
121
- 1: { singular: 'mie', plural: 'mii', feminine: true, needsDe: true }, // 10^3
122
- 2: { singular: 'milion', plural: 'milioane', feminine: false, needsDe: true }, // 10^6
123
- 3: { singular: 'miliard', plural: 'miliarde', feminine: false, needsDe: true }, // 10^9
124
- 4: { singular: 'trilion', plural: 'trilioane', feminine: false, needsDe: true }, // 10^12
125
- 5: { singular: 'cvadrilion', plural: 'cvadrilioane', feminine: false, needsDe: true }, // 10^15
126
- 6: { singular: 'cvintilion', plural: 'cvintilioane', feminine: false, needsDe: true }, // 10^18
127
- 7: { singular: 'sextilion', plural: 'sextilioane', feminine: false, needsDe: true }, // 10^21
128
- 8: { singular: 'septilion', plural: 'septilioane', feminine: false, needsDe: true }, // 10^24
129
- 9: { singular: 'octilion', plural: 'octilioane', feminine: false, needsDe: true }, // 10^27
130
- 10: { singular: 'decilion', plural: 'decilioane', feminine: false, needsDe: true } // 10^33
131
- }
83
+ const h = Math.floor(n / 100)
84
+ const r = n % 100
85
+ const hundredWord = HUNDREDS[h]
132
86
 
133
- /**
134
- * Initializes the Romanian converter with language-specific options.
135
- *
136
- * @param {RomanianOptions} [options={}] Configuration options.
137
- */
138
- constructor ({ feminine = false } = {}) {
139
- super()
87
+ if (r === 0) return hundredWord
88
+ return hundredWord + ' ' + spellUnder100(r, feminine, masculineTeens)
89
+ }
140
90
 
141
- this.feminine = feminine
142
- }
91
+ /**
92
+ * Builds scale word with proper pluralization and "de" insertion.
93
+ * Romanian always uses feminine forms (două, not doi) when counting scale words.
94
+ */
95
+ function buildScalePhrase (segment, scaleIndex) {
96
+ const meta = SCALE_META[scaleIndex - 1]
97
+ if (!meta) return spellUnder1000(segment, true)
143
98
 
144
- /**
145
- * Split numeric string into BigInt groups of size x from left to right.
146
- * @param {string} n - The numeric string to split
147
- * @param {number} x - The size of each group
148
- * @returns {bigint[]} Array of BigInt groups
149
- */
150
- splitByX (n, x) {
151
- const results = []
152
- const l = n.length
153
- let result
154
-
155
- if (l > x) {
156
- const start = l % x
157
- if (start > 0) {
158
- result = n.slice(0, start)
159
- results.push(BigInt(result))
160
- }
161
- for (let index = start; index < l; index += x) {
162
- result = n.slice(index, index + x)
163
- results.push(BigInt(result))
164
- }
165
- } else {
166
- results.push(BigInt(n))
167
- }
168
- return results
99
+ if (segment === 1) {
100
+ return meta.article + ' ' + meta.singular
169
101
  }
170
102
 
171
- getDigits (value) {
172
- const stringValue = value.toString().padStart(3, '0').slice(-3)
173
- const a = [...stringValue].toReversed()
174
- return a.map(BigInt)
103
+ // Special case: 21 with scale words uses feminine "una"
104
+ if (segment === 21 && meta.needsDe) {
105
+ return 'douăzeci și una de ' + meta.plural
175
106
  }
176
107
 
177
- /**
178
- * Romanian pluralization & "de" rule for big units.
179
- * - 1 → singular with article ("o mie", "un milion", "un miliard", …)
180
- * - otherwise → spell chunk + (optional "de") + plural
181
- * "de" is inserted when chunk >= 20 (e.g., "douăzeci de mii/milioane/miliarde").
182
- * @param {bigint} chunk - The chunk value
183
- * @param {object} form - The form object with singular, plural, feminine, needsDe properties
184
- * @returns {string} The pluralized form
185
- */
186
- romanianPluralize (chunk, form) {
187
- const n = Number(chunk)
188
-
189
- if (n === 1) {
190
- // article differs for feminine "mie" (o mie) vs the rest (un milion/miliard…)
191
- const article = form.feminine ? 'o' : 'un'
192
- return `${article} ${form.singular}`
193
- }
108
+ // Romanian always uses feminine when counting scale words (două milioane, not doi milioane)
109
+ const words = spellUnder1000(segment, true)
194
110
 
195
- // For 21 with big units, use feminine "una" with plural nouns
196
- if (n === 21 && form.needsDe) {
197
- return `douăzeci și una de ${form.plural}`
198
- }
111
+ // "de" after >= 20
112
+ const needsDe = meta.needsDe && segment >= 20
113
+ const separator = needsDe ? ' de ' : ' '
199
114
 
200
- // spell the chunk itself (use feminine units for big numbers)
201
- const words = this.spellUnder1000(n, true)
115
+ return words + separator + meta.plural
116
+ }
202
117
 
203
- // "de" after >= 20 (covers 20, 21, 100, 101, 999, etc.)
204
- const needsDe = form.needsDe && n >= 20
205
- const de = needsDe ? ' de ' : ' '
118
+ // ============================================================================
119
+ // Conversion Functions
120
+ // ============================================================================
206
121
 
207
- return `${words}${de}${form.plural}`
208
- }
122
+ /**
123
+ * Converts a non-negative integer to Romanian words.
124
+ *
125
+ * @param {bigint} n - Non-negative integer to convert
126
+ * @param {Object} options - Conversion options
127
+ * @returns {string} Romanian words
128
+ */
129
+ function integerToWords (n, options = {}) {
130
+ if (n === 0n) return ZERO
209
131
 
210
- spellUnder100 (n, feminineUnits = false) {
211
- if (n < 10) {
212
- return (feminineUnits ? this.onesFeminine : this.ones)[n]
213
- }
214
- if (n < 20) {
215
- return this.tens[n - 10]
216
- }
217
- const t = Math.floor(n / 10)
218
- const u = n % 10
219
- return u
220
- ? `${this.twenties[t]} și ${(feminineUnits ? this.onesFeminine : this.ones)[u]}`
221
- : this.twenties[t]
132
+ // Fast path: numbers < 1000
133
+ if (n < 1000n) {
134
+ const feminine = options.gender === 'feminine'
135
+ return spellUnder1000(Number(n), feminine)
222
136
  }
223
137
 
224
- spellUnder1000 (n, feminineUnits = false) {
225
- if (n < 100) return this.spellUnder100(n, feminineUnits)
226
- const h = Math.floor(n / 100)
227
- const r = n % 100
228
- const hundredWords = this.hundreds[h]
229
- if (!r) return hundredWords
230
- // Standard readable form: "o sută unu" (for units) or "o sută cincizeci" (for tens)
231
- const separator = ' '
232
- return `${hundredWords}${separator}${this.spellUnder100(r, feminineUnits)}`
233
- }
138
+ return buildLargeNumberWords(n, options)
139
+ }
234
140
 
235
- /**
236
- * Override decimalDigitsToWords to use masculine forms for decimal places
237
- * @param {string} decimal Decimal string to convert
238
- * @returns {string} Value in written format
239
- */
240
- decimalDigitsToWords (decimal) {
241
- const words = []
242
-
243
- const chars = [...decimal]
244
-
245
- // Loop through characters adding leading zeros to words array
246
- let index = 0
247
- while (index < chars.length && chars[index] === '0') {
248
- words.push(this.zeroWord)
249
- index++
250
- }
141
+ /**
142
+ * Builds words for numbers >= 1000.
143
+ * Uses BigInt division for faster segment extraction.
144
+ *
145
+ * @param {bigint} n - Number >= 1000
146
+ * @param {Object} options - Conversion options
147
+ * @returns {string} Romanian words
148
+ */
149
+ function buildLargeNumberWords (n, options) {
150
+ // Extract segments using BigInt division (faster than string slicing)
151
+ // Segments stored least-significant first (index 0 = ones, 1 = thousands, etc.)
152
+ const segmentValues = []
153
+ let temp = n
154
+ while (temp > 0n) {
155
+ segmentValues.push(Number(temp % 1000n))
156
+ temp = temp / 1000n
157
+ }
251
158
 
252
- // Prevent further processing if entire string was zeros
253
- if (index === chars.length) {
254
- return words
255
- }
159
+ // Build result string directly (avoid regex cleanup)
160
+ let result = ''
256
161
 
257
- // Convert and add remaining using masculine forms for decimal places
258
- const decimalNumber = BigInt(decimal)
259
- const masculineWords = this.toCardinalWithMasculine(decimalNumber)
260
- return [...words, masculineWords]
261
- }
162
+ for (let i = segmentValues.length - 1; i >= 0; i--) {
163
+ const segment = segmentValues[i]
164
+ if (segment === 0) continue
262
165
 
263
- /**
264
- * Convert number to cardinal form using masculine units
265
- * @param {bigint} number Number to convert
266
- * @returns {string} Value in written format
267
- */
268
- toCardinalWithMasculine (number) {
269
- if (number === 0n) {
270
- return this.zeroWord
166
+ let segmentWords
167
+ if (i === 0) {
168
+ // Units segment - use gender from options
169
+ const feminine = options.gender === 'feminine'
170
+ segmentWords = spellUnder1000(segment, feminine)
171
+ } else {
172
+ // Scale segment
173
+ segmentWords = buildScalePhrase(segment, i)
271
174
  }
272
175
 
273
- const words = []
274
- const chunks = this.splitByX(number.toString(), 3)
275
- let index = chunks.length
276
-
277
- for (const x of chunks) {
278
- let onesMap = []
279
- index = index - 1
280
-
281
- if (x === 0n) continue
282
-
283
- const [n1, n2, n3] = this.getDigits(x) // units, tens, hundreds (as BigInt)
284
-
285
- // hundreds
286
- if (n3 > 0n) {
287
- words.push(this.hundreds[Number(n3)])
288
- }
289
-
290
- // tens & teens
291
- if (n2 > 1n) {
292
- words.push(this.twenties[Number(n2)])
293
- }
294
-
295
- if (n2 === 1n) {
296
- words.push(this.tensMasculine[Number(n1)])
297
- } else if (n1 > 0n) {
298
- // Always use masculine units for decimal places
299
- onesMap = this.ones
300
-
301
- // if there is a twenty/treizeci/etc AND ones > 0 → add "și"
302
- if (n2 > 1n) words.push('și')
303
- words.push(onesMap[Number(n1)])
304
- }
305
-
306
- // big unit name (mie/mii, milion/milioane, …)
307
- if (index > 0) {
308
- const form = this.thousands[index]
309
- if (form) {
310
- words.push(this.romanianPluralize(x, form))
311
- } else {
312
- // For very large numbers beyond our defined units, just spell out the number
313
- words.push(this.spellUnder1000(Number(x), false))
314
- }
315
- }
176
+ if (result && segmentWords) {
177
+ result += ' ' + segmentWords
178
+ } else if (segmentWords) {
179
+ result = segmentWords
316
180
  }
181
+ }
182
+
183
+ return result
184
+ }
317
185
 
318
- return words.join(' ').replaceAll(/\s+/g, ' ').trim()
186
+ /**
187
+ * Converts decimal digits to Romanian words.
188
+ * Decimals always use masculine forms.
189
+ *
190
+ * @param {string} decimalPart - Decimal digits (without the point)
191
+ * @returns {string} Romanian words for decimal part
192
+ */
193
+ function decimalPartToWords (decimalPart) {
194
+ let result = ''
195
+ let i = 0
196
+
197
+ // Handle leading zeros
198
+ while (i < decimalPart.length && decimalPart[i] === '0') {
199
+ if (result) result += ' '
200
+ result += ZERO
201
+ i++
319
202
  }
320
203
 
321
- convertWholePart (number) {
322
- if (number === 0n) {
323
- return this.zeroWord
324
- }
325
- const words = []
326
- const chunks = this.splitByX(number.toString(), 3)
327
- let index = chunks.length
328
- for (const x of chunks) {
329
- let onesMap = []
330
- index = index - 1
331
- if (x === 0n) continue
332
- const [n1, n2, n3] = this.getDigits(x) // units, tens, hundreds (as BigInt)
333
- // hundreds (only for the last group, not for thousands)
334
- if (n3 > 0n && index === 0) {
335
- words.push(this.hundreds[Number(n3)])
336
- }
337
- // tens & teens (only for the last group, not for thousands)
338
- if (index === 0) {
339
- if (n2 > 1n) {
340
- words.push(this.twenties[Number(n2)])
341
- }
342
- if (n2 === 1n) {
343
- words.push(this.tens[Number(n1)])
344
- } else if (n1 > 0n) {
345
- // pick masculine/feminine units (only for the last group, not for thousands)
346
- const feminineUnits = this.feminine && index === 0
347
- onesMap = feminineUnits ? this.onesFeminine : this.ones
348
- // if there is a twenty/treizeci/etc AND ones > 0 → add "și"
349
- if (n2 > 1n) words.push('și')
350
- words.push(onesMap[Number(n1)])
351
- }
352
- }
353
- // big unit name (mie/mii, milion/milioane, …)
354
- if (index > 0) {
355
- const form = this.thousands[index]
356
- if (form) {
357
- words.push(this.romanianPluralize(x, form))
358
- } else {
359
- // For very large numbers beyond our defined units, just spell out the number
360
- words.push(this.spellUnder1000(Number(x), true))
361
- }
362
- }
204
+ // Convert remainder as a single number (masculine, with masculine teens)
205
+ const remainder = decimalPart.slice(i)
206
+ if (remainder) {
207
+ if (result) result += ' '
208
+ const n = BigInt(remainder)
209
+ if (n < 1000n) {
210
+ result += spellUnder1000(Number(n), false, true)
211
+ } else {
212
+ result += integerToWords(n, { gender: 'masculine' })
363
213
  }
364
- return words.join(' ').replaceAll(/\s+/g, ' ').trim()
365
214
  }
215
+
216
+ return result
366
217
  }
367
218
 
368
219
  /**
369
- * Converts a number to Romanian cardinal (written) form.
220
+ * Converts a numeric value to Romanian words.
221
+ *
222
+ * @param {number | string | bigint} value - The numeric value to convert
223
+ * @param {Object} [options] - Conversion options
224
+ * @param {string} [options.gender='masculine'] - Gender for numbers
225
+ * @returns {string} The number in Romanian words
226
+ * @throws {TypeError} If value is not a valid numeric type
227
+ * @throws {Error} If value is not a valid number format
370
228
  *
371
- * @param {number|string|bigint} value The number to convert.
372
- * @param {Object} [options={}] Configuration options.
373
- * @param {boolean} [options.feminine=false] Use feminine forms for numbers.
374
- * @returns {string} The number expressed in Romanian words.
375
- * @throws {TypeError} If value is NaN or invalid type.
376
- * @throws {Error} If value is an invalid number string.
229
+ * @example
230
+ * toWords(21) // 'douăzeci și unu'
231
+ * toWords(1, { gender: 'feminine' }) // 'una'
232
+ * toWords(1000) // 'o mie'
377
233
  */
378
- export default function convertToWords (value, options = {}) {
379
- return new Romanian(options).convertToWords(value)
234
+ function toWords (value, options) {
235
+ options = validateOptions(options)
236
+ const { isNegative, integerPart, decimalPart } = parseNumericValue(value)
237
+
238
+ let result = ''
239
+
240
+ if (isNegative) {
241
+ result = NEGATIVE + ' '
242
+ }
243
+
244
+ result += integerToWords(integerPart, options)
245
+
246
+ if (decimalPart) {
247
+ result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
248
+ }
249
+
250
+ return result
380
251
  }
252
+
253
+ // ============================================================================
254
+ // Public API
255
+ // ============================================================================
256
+
257
+ export { toWords }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Converts a numeric value to Russian words.
3
+ *
4
+ * @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
7
+ * @returns {string} The number in Russian words
8
+ */
9
+ export function toWords(value: number | string | bigint, options?: {
10
+ gender?: "masculine" | "feminine" | undefined;
11
+ }): string;