n2words 1.23.0 → 1.24.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 (317) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +182 -53
  3. package/dist/languages/ar.js +2 -0
  4. package/dist/languages/ar.js.map +1 -0
  5. package/dist/languages/az.js +2 -0
  6. package/dist/languages/az.js.map +1 -0
  7. package/dist/languages/bn.js +2 -0
  8. package/dist/languages/bn.js.map +1 -0
  9. package/dist/languages/cs.js +2 -0
  10. package/dist/languages/cs.js.map +1 -0
  11. package/dist/languages/da.js +2 -0
  12. package/dist/languages/da.js.map +1 -0
  13. package/dist/languages/de.js +2 -0
  14. package/dist/languages/de.js.map +1 -0
  15. package/dist/languages/el.js +2 -0
  16. package/dist/languages/el.js.map +1 -0
  17. package/dist/languages/en.js +2 -0
  18. package/dist/languages/en.js.map +1 -0
  19. package/dist/languages/es.js +2 -0
  20. package/dist/languages/es.js.map +1 -0
  21. package/dist/languages/fa.js +2 -0
  22. package/dist/languages/fa.js.map +1 -0
  23. package/dist/languages/fil.js +2 -0
  24. package/dist/languages/fil.js.map +1 -0
  25. package/dist/languages/fr-BE.js +2 -0
  26. package/dist/languages/fr-BE.js.map +1 -0
  27. package/dist/languages/fr.js +2 -0
  28. package/dist/languages/fr.js.map +1 -0
  29. package/dist/languages/gu.js +2 -0
  30. package/dist/languages/gu.js.map +1 -0
  31. package/dist/languages/he.js +2 -0
  32. package/dist/languages/he.js.map +1 -0
  33. package/dist/languages/hi.js +2 -0
  34. package/dist/languages/hi.js.map +1 -0
  35. package/dist/languages/hr.js +2 -0
  36. package/dist/languages/hr.js.map +1 -0
  37. package/dist/languages/hu.js +2 -0
  38. package/dist/languages/hu.js.map +1 -0
  39. package/dist/languages/id.js +2 -0
  40. package/dist/languages/id.js.map +1 -0
  41. package/dist/languages/it.js +2 -0
  42. package/dist/languages/it.js.map +1 -0
  43. package/dist/languages/ja.js +2 -0
  44. package/dist/languages/ja.js.map +1 -0
  45. package/dist/languages/kn.js +2 -0
  46. package/dist/languages/kn.js.map +1 -0
  47. package/dist/languages/ko.js +2 -0
  48. package/dist/languages/ko.js.map +1 -0
  49. package/dist/languages/lt.js +2 -0
  50. package/dist/languages/lt.js.map +1 -0
  51. package/dist/languages/lv.js +2 -0
  52. package/dist/languages/lv.js.map +1 -0
  53. package/dist/languages/mr.js +2 -0
  54. package/dist/languages/mr.js.map +1 -0
  55. package/dist/languages/ms.js +2 -0
  56. package/dist/languages/ms.js.map +1 -0
  57. package/dist/languages/nb.js +2 -0
  58. package/dist/languages/nb.js.map +1 -0
  59. package/dist/languages/nl.js +2 -0
  60. package/dist/languages/nl.js.map +1 -0
  61. package/dist/languages/pa-Guru.js +2 -0
  62. package/dist/languages/pa-Guru.js.map +1 -0
  63. package/dist/languages/pl.js +2 -0
  64. package/dist/languages/pl.js.map +1 -0
  65. package/dist/languages/pt.js +2 -0
  66. package/dist/languages/pt.js.map +1 -0
  67. package/dist/languages/ro.js +2 -0
  68. package/dist/languages/ro.js.map +1 -0
  69. package/dist/languages/ru.js +2 -0
  70. package/dist/languages/ru.js.map +1 -0
  71. package/dist/languages/sr-Latn.js +2 -0
  72. package/dist/languages/sr-Latn.js.map +1 -0
  73. package/dist/languages/sv.js +2 -0
  74. package/dist/languages/sv.js.map +1 -0
  75. package/dist/languages/sw.js +2 -0
  76. package/dist/languages/sw.js.map +1 -0
  77. package/dist/languages/ta.js +2 -0
  78. package/dist/languages/ta.js.map +1 -0
  79. package/dist/languages/te.js +2 -0
  80. package/dist/languages/te.js.map +1 -0
  81. package/dist/languages/th.js +2 -0
  82. package/dist/languages/th.js.map +1 -0
  83. package/dist/languages/tr.js +2 -0
  84. package/dist/languages/tr.js.map +1 -0
  85. package/dist/languages/uk.js +2 -0
  86. package/dist/languages/uk.js.map +1 -0
  87. package/dist/languages/ur.js +2 -0
  88. package/dist/languages/ur.js.map +1 -0
  89. package/dist/languages/vi.js +2 -0
  90. package/dist/languages/vi.js.map +1 -0
  91. package/dist/languages/zh-Hans.js +2 -0
  92. package/dist/languages/zh-Hans.js.map +1 -0
  93. package/dist/n2words.js +1 -1
  94. package/dist/n2words.js.map +1 -1
  95. package/lib/classes/abstract-language.js +211 -110
  96. package/lib/classes/greedy-scale-language.js +195 -0
  97. package/lib/classes/slavic-language.js +251 -0
  98. package/lib/classes/south-asian-language.js +161 -0
  99. package/lib/classes/turkic-language.js +63 -0
  100. package/lib/languages/ar.js +243 -0
  101. package/lib/languages/az.js +58 -0
  102. package/lib/languages/bn.js +126 -0
  103. package/lib/languages/cs.js +212 -0
  104. package/lib/languages/da.js +167 -0
  105. package/lib/languages/de.js +135 -0
  106. package/lib/languages/el.js +116 -0
  107. package/lib/languages/en.js +123 -0
  108. package/lib/languages/es.js +153 -0
  109. package/lib/languages/fa.js +127 -0
  110. package/lib/languages/fil.js +162 -0
  111. package/lib/languages/fr-BE.js +61 -0
  112. package/lib/languages/fr.js +145 -0
  113. package/lib/languages/gu.js +156 -0
  114. package/lib/languages/he.js +329 -0
  115. package/lib/languages/hi.js +126 -0
  116. package/lib/languages/hr.js +157 -0
  117. package/lib/languages/hu.js +155 -0
  118. package/lib/languages/id.js +174 -0
  119. package/lib/languages/it.js +148 -0
  120. package/lib/languages/ja.js +190 -0
  121. package/lib/languages/kn.js +71 -0
  122. package/lib/languages/ko.js +83 -0
  123. package/lib/languages/lt.js +171 -0
  124. package/lib/languages/lv.js +153 -0
  125. package/lib/languages/mr.js +156 -0
  126. package/lib/languages/ms.js +146 -0
  127. package/lib/languages/nb.js +120 -0
  128. package/lib/languages/nl.js +206 -0
  129. package/lib/languages/pa-Guru.js +126 -0
  130. package/lib/languages/pl.js +189 -0
  131. package/lib/languages/pt.js +147 -0
  132. package/lib/languages/ro.js +380 -0
  133. package/lib/languages/ru.js +116 -0
  134. package/lib/languages/sr-Latn.js +157 -0
  135. package/lib/languages/sv.js +127 -0
  136. package/lib/languages/sw.js +121 -0
  137. package/lib/languages/ta.js +226 -0
  138. package/lib/languages/te.js +229 -0
  139. package/lib/languages/th.js +123 -0
  140. package/lib/languages/tr.js +83 -0
  141. package/lib/{i18n → languages}/uk.js +50 -23
  142. package/lib/languages/ur.js +126 -0
  143. package/lib/languages/vi.js +193 -0
  144. package/lib/languages/zh-Hans.js +165 -0
  145. package/lib/n2words.js +246 -75
  146. package/package.json +80 -72
  147. package/typings/classes/abstract-language.d.ts +144 -0
  148. package/typings/classes/greedy-scale-language.d.ts +148 -0
  149. package/typings/classes/slavic-language.d.ts +145 -0
  150. package/typings/classes/south-asian-language.d.ts +101 -0
  151. package/typings/classes/turkic-language.d.ts +42 -0
  152. package/typings/languages/ar.d.ts +93 -0
  153. package/typings/languages/az.d.ts +25 -0
  154. package/typings/languages/bn.d.ts +1 -0
  155. package/typings/languages/cs.d.ts +120 -0
  156. package/typings/languages/da.d.ts +53 -0
  157. package/typings/languages/de.d.ts +26 -0
  158. package/typings/languages/el.d.ts +11 -0
  159. package/typings/languages/en.d.ts +30 -0
  160. package/typings/languages/es.d.ts +43 -0
  161. package/typings/languages/fa.d.ts +81 -0
  162. package/typings/languages/fil.d.ts +12 -0
  163. package/typings/languages/fr-BE.d.ts +41 -0
  164. package/typings/languages/fr.d.ts +43 -0
  165. package/typings/languages/gu.d.ts +12 -0
  166. package/typings/languages/he.d.ts +197 -0
  167. package/typings/languages/hi.d.ts +1 -0
  168. package/typings/languages/hr.d.ts +110 -0
  169. package/typings/languages/hu.d.ts +37 -0
  170. package/typings/languages/id.d.ts +69 -0
  171. package/typings/languages/it.d.ts +51 -0
  172. package/typings/languages/ja.d.ts +58 -0
  173. package/typings/languages/kn.d.ts +11 -0
  174. package/typings/languages/ko.d.ts +25 -0
  175. package/typings/languages/lt.d.ts +110 -0
  176. package/typings/languages/lv.d.ts +99 -0
  177. package/typings/languages/mr.d.ts +12 -0
  178. package/typings/languages/ms.d.ts +37 -0
  179. package/typings/languages/nb.d.ts +27 -0
  180. package/typings/languages/nl.d.ts +65 -0
  181. package/typings/languages/pa-Guru.d.ts +1 -0
  182. package/typings/languages/pl.d.ts +116 -0
  183. package/typings/languages/pt.d.ts +39 -0
  184. package/typings/languages/ro.d.ts +229 -0
  185. package/typings/languages/ru.d.ts +108 -0
  186. package/typings/languages/sr-Latn.d.ts +98 -0
  187. package/typings/languages/sv.d.ts +30 -0
  188. package/typings/languages/sw.d.ts +1 -0
  189. package/typings/languages/ta.d.ts +1 -0
  190. package/typings/languages/te.d.ts +1 -0
  191. package/typings/languages/th.d.ts +1 -0
  192. package/typings/languages/tr.d.ts +46 -0
  193. package/typings/languages/uk.d.ts +117 -0
  194. package/typings/languages/ur.d.ts +1 -0
  195. package/typings/languages/vi.d.ts +116 -0
  196. package/typings/languages/zh-Hans.d.ts +57 -0
  197. package/typings/n2words.d.ts +177 -0
  198. package/dist/ar.js +0 -2
  199. package/dist/ar.js.map +0 -1
  200. package/dist/az.js +0 -2
  201. package/dist/az.js.map +0 -1
  202. package/dist/cz.js +0 -2
  203. package/dist/cz.js.map +0 -1
  204. package/dist/de.js +0 -2
  205. package/dist/de.js.map +0 -1
  206. package/dist/dk.js +0 -2
  207. package/dist/dk.js.map +0 -1
  208. package/dist/en.js +0 -2
  209. package/dist/en.js.map +0 -1
  210. package/dist/es.js +0 -2
  211. package/dist/es.js.map +0 -1
  212. package/dist/fa.js +0 -2
  213. package/dist/fa.js.map +0 -1
  214. package/dist/fr-BE.js +0 -2
  215. package/dist/fr-BE.js.map +0 -1
  216. package/dist/fr.js +0 -2
  217. package/dist/fr.js.map +0 -1
  218. package/dist/he.js +0 -2
  219. package/dist/he.js.map +0 -1
  220. package/dist/hr.js +0 -2
  221. package/dist/hr.js.map +0 -1
  222. package/dist/hu.js +0 -2
  223. package/dist/hu.js.map +0 -1
  224. package/dist/id.js +0 -2
  225. package/dist/id.js.map +0 -1
  226. package/dist/it.js +0 -2
  227. package/dist/it.js.map +0 -1
  228. package/dist/ko.js +0 -2
  229. package/dist/ko.js.map +0 -1
  230. package/dist/lt.js +0 -2
  231. package/dist/lt.js.map +0 -1
  232. package/dist/lv.js +0 -2
  233. package/dist/lv.js.map +0 -1
  234. package/dist/n2words.d.ts +0 -2
  235. package/dist/nl.js +0 -2
  236. package/dist/nl.js.map +0 -1
  237. package/dist/no.js +0 -2
  238. package/dist/no.js.map +0 -1
  239. package/dist/pl.js +0 -2
  240. package/dist/pl.js.map +0 -1
  241. package/dist/pt.js +0 -2
  242. package/dist/pt.js.map +0 -1
  243. package/dist/ro.js +0 -2
  244. package/dist/ro.js.map +0 -1
  245. package/dist/ru.js +0 -2
  246. package/dist/ru.js.map +0 -1
  247. package/dist/sr.js +0 -2
  248. package/dist/sr.js.map +0 -1
  249. package/dist/tr.js +0 -2
  250. package/dist/tr.js.map +0 -1
  251. package/dist/uk.js +0 -2
  252. package/dist/uk.js.map +0 -1
  253. package/dist/vi.js +0 -2
  254. package/dist/vi.js.map +0 -1
  255. package/dist/zh.js +0 -2
  256. package/dist/zh.js.map +0 -1
  257. package/lib/classes/abstract-language.d.ts +0 -54
  258. package/lib/classes/base-language.d.ts +0 -58
  259. package/lib/classes/base-language.js +0 -172
  260. package/lib/i18n/ar.d.ts +0 -41
  261. package/lib/i18n/ar.js +0 -209
  262. package/lib/i18n/az.d.ts +0 -15
  263. package/lib/i18n/az.js +0 -66
  264. package/lib/i18n/cz.d.ts +0 -68
  265. package/lib/i18n/cz.js +0 -135
  266. package/lib/i18n/de.d.ts +0 -17
  267. package/lib/i18n/de.js +0 -103
  268. package/lib/i18n/dk.d.ts +0 -14
  269. package/lib/i18n/dk.js +0 -110
  270. package/lib/i18n/en.d.ts +0 -22
  271. package/lib/i18n/en.js +0 -86
  272. package/lib/i18n/es.d.ts +0 -16
  273. package/lib/i18n/es.js +0 -110
  274. package/lib/i18n/fa.d.ts +0 -54
  275. package/lib/i18n/fa.js +0 -106
  276. package/lib/i18n/fr-BE.d.ts +0 -11
  277. package/lib/i18n/fr-BE.js +0 -20
  278. package/lib/i18n/fr.d.ts +0 -15
  279. package/lib/i18n/fr.js +0 -99
  280. package/lib/i18n/he.d.ts +0 -61
  281. package/lib/i18n/he.js +0 -132
  282. package/lib/i18n/hr.d.ts +0 -68
  283. package/lib/i18n/hr.js +0 -129
  284. package/lib/i18n/hu.d.ts +0 -17
  285. package/lib/i18n/hu.js +0 -135
  286. package/lib/i18n/id.d.ts +0 -43
  287. package/lib/i18n/id.js +0 -156
  288. package/lib/i18n/it.d.ts +0 -29
  289. package/lib/i18n/it.js +0 -137
  290. package/lib/i18n/ko.d.ts +0 -15
  291. package/lib/i18n/ko.js +0 -56
  292. package/lib/i18n/lt.d.ts +0 -68
  293. package/lib/i18n/lt.js +0 -138
  294. package/lib/i18n/lv.d.ts +0 -57
  295. package/lib/i18n/lv.js +0 -120
  296. package/lib/i18n/nl.d.ts +0 -20
  297. package/lib/i18n/nl.js +0 -125
  298. package/lib/i18n/no.d.ts +0 -15
  299. package/lib/i18n/no.js +0 -77
  300. package/lib/i18n/pl.d.ts +0 -67
  301. package/lib/i18n/pl.js +0 -126
  302. package/lib/i18n/pt.d.ts +0 -26
  303. package/lib/i18n/pt.js +0 -118
  304. package/lib/i18n/ro.d.ts +0 -109
  305. package/lib/i18n/ro.js +0 -360
  306. package/lib/i18n/ru.d.ts +0 -30
  307. package/lib/i18n/ru.js +0 -198
  308. package/lib/i18n/sr.d.ts +0 -56
  309. package/lib/i18n/sr.js +0 -127
  310. package/lib/i18n/tr.d.ts +0 -15
  311. package/lib/i18n/tr.js +0 -64
  312. package/lib/i18n/uk.d.ts +0 -78
  313. package/lib/i18n/vi.d.ts +0 -70
  314. package/lib/i18n/vi.js +0 -151
  315. package/lib/i18n/zh.d.ts +0 -18
  316. package/lib/i18n/zh.js +0 -78
  317. package/lib/n2words.d.ts +0 -9
@@ -0,0 +1,380 @@
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
+ /**
9
+ * Romanian language converter.
10
+ *
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)
30
+ *
31
+ * Features:
32
+ * - Feminine units for thousands group
33
+ * - Support for very large numbers (up to decillions)
34
+ */
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
+
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
+ }
63
+
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'
75
+ }
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'
88
+ }
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'
99
+ }
100
+
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
+ }
112
+
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
+ }
132
+
133
+ /**
134
+ * Initializes the Romanian converter with language-specific options.
135
+ *
136
+ * @param {RomanianOptions} [options={}] Configuration options.
137
+ */
138
+ constructor ({ feminine = false } = {}) {
139
+ super()
140
+
141
+ this.feminine = feminine
142
+ }
143
+
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
169
+ }
170
+
171
+ getDigits (value) {
172
+ const stringValue = value.toString().padStart(3, '0').slice(-3)
173
+ const a = [...stringValue].toReversed()
174
+ return a.map(BigInt)
175
+ }
176
+
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
+ }
194
+
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
+ }
199
+
200
+ // spell the chunk itself (use feminine units for big numbers)
201
+ const words = this.spellUnder1000(n, true)
202
+
203
+ // "de" after >= 20 (covers 20, 21, 100, 101, 999, etc.)
204
+ const needsDe = form.needsDe && n >= 20
205
+ const de = needsDe ? ' de ' : ' '
206
+
207
+ return `${words}${de}${form.plural}`
208
+ }
209
+
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]
222
+ }
223
+
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
+ }
234
+
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
+ }
251
+
252
+ // Prevent further processing if entire string was zeros
253
+ if (index === chars.length) {
254
+ return words
255
+ }
256
+
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
+ }
262
+
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
271
+ }
272
+
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
+ }
316
+ }
317
+
318
+ return words.join(' ').replaceAll(/\s+/g, ' ').trim()
319
+ }
320
+
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
+ }
363
+ }
364
+ return words.join(' ').replaceAll(/\s+/g, ' ').trim()
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Converts a number to Romanian cardinal (written) form.
370
+ *
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.
377
+ */
378
+ export default function convertToWords (value, options = {}) {
379
+ return new Romanian(options).convertToWords(value)
380
+ }
@@ -0,0 +1,116 @@
1
+ import SlavicLanguage from '../classes/slavic-language.js'
2
+
3
+ /**
4
+ * @typedef {Object} SlavicOptions
5
+ * @property {boolean} [feminine=false] Use feminine forms for numbers.
6
+ */
7
+
8
+ /**
9
+ * Russian language converter.
10
+ *
11
+ * Converts numbers to Russian words with full grammatical support:
12
+ * - Gender agreement (masculine/feminine forms)
13
+ * - Complex pluralization rules (one/few/many forms)
14
+ * - Declension patterns for thousands, millions, billions, etc.
15
+ * - Proper case endings for number words
16
+ *
17
+ * Features:
18
+ * - Gender-aware forms (один/одна, два/две)
19
+ * - Three-form pluralization (тысяча/тысячи/тысяч)
20
+ * - Support for very large numbers (up to nonillions)
21
+ * - Proper spacing and conjunction rules
22
+ *
23
+ * This class extends SlavicLanguage, which provides the conversion algorithm
24
+ * shared by Czech, Polish, Ukrainian, Serbian, Croatian, Hebrew, Lithuanian, Latvian.
25
+ */
26
+ export class Russian extends SlavicLanguage {
27
+ negativeWord = 'минус'
28
+ decimalSeparatorWord = 'запятая'
29
+ zeroWord = 'ноль'
30
+
31
+ ones = {
32
+ 1: 'один',
33
+ 2: 'два',
34
+ 3: 'три',
35
+ 4: 'четыре',
36
+ 5: 'пять',
37
+ 6: 'шесть',
38
+ 7: 'семь',
39
+ 8: 'восемь',
40
+ 9: 'девять'
41
+ }
42
+
43
+ onesFeminine = {
44
+ 1: 'одна',
45
+ 2: 'две',
46
+ 3: 'три',
47
+ 4: 'четыре',
48
+ 5: 'пять',
49
+ 6: 'шесть',
50
+ 7: 'семь',
51
+ 8: 'восемь',
52
+ 9: 'девять'
53
+ }
54
+
55
+ tens = {
56
+ 0: 'десять',
57
+ 1: 'одиннадцать',
58
+ 2: 'двенадцать',
59
+ 3: 'тринадцать',
60
+ 4: 'четырнадцать',
61
+ 5: 'пятнадцать',
62
+ 6: 'шестнадцать',
63
+ 7: 'семнадцать',
64
+ 8: 'восемнадцать',
65
+ 9: 'девятнадцать'
66
+ }
67
+
68
+ twenties = {
69
+ 2: 'двадцать',
70
+ 3: 'тридцать',
71
+ 4: 'сорок',
72
+ 5: 'пятьдесят',
73
+ 6: 'шестьдесят',
74
+ 7: 'семьдесят',
75
+ 8: 'восемьдесят',
76
+ 9: 'девяносто'
77
+ }
78
+
79
+ hundreds = {
80
+ 1: 'сто',
81
+ 2: 'двести',
82
+ 3: 'триста',
83
+ 4: 'четыреста',
84
+ 5: 'пятьсот',
85
+ 6: 'шестьсот',
86
+ 7: 'семьсот',
87
+ 8: 'восемьсот',
88
+ 9: 'девятьсот'
89
+ }
90
+
91
+ thousands = {
92
+ 1: ['тысяча', 'тысячи', 'тысяч'], // 10^ 3
93
+ 2: ['миллион', 'миллиона', 'миллионов'], // 10^ 6
94
+ 3: ['миллиард', 'миллиарда', 'миллиардов'], // 10^ 9
95
+ 4: ['триллион', 'триллиона', 'триллионов'], // 10^ 12
96
+ 5: ['квадриллион', 'квадриллиона', 'квадриллионов'], // 10^ 15
97
+ 6: ['квинтиллион', 'квинтиллиона', 'квинтиллионов'], // 10^ 18
98
+ 7: ['секстиллион', 'секстиллиона', 'секстиллионов'], // 10^ 21
99
+ 8: ['септиллион', 'септиллиона', 'септиллионов'], // 10^ 24
100
+ 9: ['октиллион', 'октиллиона', 'октиллионов'], // 10^ 27
101
+ 10: ['нониллион', 'нониллиона', 'нониллионов'] // 10^ 30
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Converts a number to Russian cardinal (written) form.
107
+ *
108
+ * @param {number|string|bigint} value The number to convert.
109
+ * @param {SlavicOptions} [options={}] Configuration options.
110
+ * @returns {string} The number expressed in Russian words.
111
+ * @throws {TypeError} If value is NaN or invalid type.
112
+ * @throws {Error} If value is an invalid number string.
113
+ */
114
+ export default function convertToWords (value, options = {}) {
115
+ return new Russian(options).convertToWords(value)
116
+ }
@@ -0,0 +1,157 @@
1
+ import SlavicLanguage from '../classes/slavic-language.js'
2
+
3
+ /**
4
+ * @typedef {Object} SlavicOptions
5
+ * @property {boolean} [feminine=false] Use feminine forms for numbers.
6
+ */
7
+
8
+ /**
9
+ * Serbian language converter.
10
+ *
11
+ * Implements Serbian number words using the Slavic language pattern:
12
+ * - Serbian number words (jedan/jedna, dva/dve, tri, četiri...)
13
+ * - Gender-aware forms (masculine/feminine)
14
+ * - Slavic three-form pluralization (hiljada/hiljade/hiljada)
15
+ * - Latin script representation
16
+ *
17
+ * Key Features:
18
+ * - Three-form pluralization system shared across Slavic languages
19
+ * * Form 1 (singular): 1 (e.g., "hiljada")
20
+ * * Form 2 (few): 2-4, 22-24, 32-34... excluding teens (e.g., "hiljade")
21
+ * * Form 3 (many): all other numbers (e.g., "hiljada")
22
+ * - Chunk-based decomposition (splits into groups of 3 digits: ones, thousands, millions, etc.)
23
+ * - Large number handling via SCALE[] array with indexed [singular, few, many] forms
24
+ * - Gender-specific number forms for 1 and 2 (masculine/feminine dual forms)
25
+ *
26
+ * Features:
27
+ * - Dual gender forms for 1 and 2 (jedan/jedna, dva/dve)
28
+ * - Similar structure to Croatian with different orthography
29
+ * - Latin script (Serbian can use both Latin and Cyrillic)
30
+ *
31
+ * Inherits from SlavicLanguage for complex pluralization algorithms.
32
+ */
33
+ export class Serbian extends SlavicLanguage {
34
+ negativeWord = 'minus'
35
+ decimalSeparatorWord = 'zapeta'
36
+ zeroWord = 'nula'
37
+ ones = {
38
+ 1: ['jedan', 'jedna'],
39
+ 2: ['dva', 'dve'],
40
+ 3: ['tri', 'tri'],
41
+ 4: ['četiri', 'četiri'],
42
+ 5: ['pet', 'pet'],
43
+ 6: ['šest', 'šest'],
44
+ 7: ['sedam', 'sedam'],
45
+ 8: ['osam', 'osam'],
46
+ 9: ['devet', 'devet']
47
+ }
48
+
49
+ tens = {
50
+ 0: 'deset',
51
+ 1: 'jedanaest',
52
+ 2: 'dvanaest',
53
+ 3: 'trinaest',
54
+ 4: 'četrnaest',
55
+ 5: 'petnaest',
56
+ 6: 'šesnaest',
57
+ 7: 'sedamnaest',
58
+ 8: 'osamnaest',
59
+ 9: 'devetnaest'
60
+ }
61
+
62
+ twenties = {
63
+ 2: 'dvadeset',
64
+ 3: 'trideset',
65
+ 4: 'četrdeset',
66
+ 5: 'pedeset',
67
+ 6: 'šezdeset',
68
+ 7: 'sedamdeset',
69
+ 8: 'osamdeset',
70
+ 9: 'devedeset'
71
+ }
72
+
73
+ hundreds = {
74
+ 1: 'sto',
75
+ 2: 'dvesta',
76
+ 3: 'trista',
77
+ 4: 'četiristo',
78
+ 5: 'petsto',
79
+ 6: 'šesto',
80
+ 7: 'sedamsto',
81
+ 8: 'osamsto',
82
+ 9: 'devetsto'
83
+ }
84
+
85
+ SCALE = [
86
+ ['', '', '', false],
87
+ ['hiljada', 'hiljade', 'hiljada', true], // 10 ^ 3
88
+ ['milion', 'miliona', 'miliona', false], // 10 ^ 6
89
+ ['milijarda', 'milijarde', 'milijarda', false], // 10 ^ 9
90
+ ['bilion', 'biliona', 'biliona', false], // 10 ^ 12
91
+ ['bilijarda', 'bilijarde', 'bilijarda', false], // 10 ^ 15
92
+ ['trilion', 'triliona', 'triliona', false], // 10 ^ 18
93
+ ['trilijarda', 'trilijarde', 'trilijarda', false], // 10 ^ 21
94
+ ['kvadrilion', 'kvadriliona', 'kvadriliona', false], // 10 ^ 24
95
+ ['kvadrilijarda', 'kvadrilijarde', 'kvadrilijarda', false], // 10 ^ 27
96
+ ['kvintilion', 'kvintiliona', 'kvintiliona', false] // 10 ^ 30
97
+ ]
98
+
99
+ pluralize (n, forms) {
100
+ const lastDigit = n % 10n
101
+ const lastTwoDigits = n % 100n
102
+
103
+ if ((lastTwoDigits < 10n || lastTwoDigits > 20n) && lastDigit === 1n) {
104
+ return forms[0]
105
+ }
106
+
107
+ if ((lastTwoDigits < 10n || lastTwoDigits > 20n) && lastDigit > 1n && lastDigit < 5n) {
108
+ return forms[1]
109
+ }
110
+
111
+ return forms[2]
112
+ }
113
+
114
+ convertWholePart (number) {
115
+ if (number === 0n) {
116
+ return this.zeroWord
117
+ }
118
+ const words = []
119
+ const chunks = this.splitByX(number.toString(), 3)
120
+ let index = chunks.length
121
+ for (const x of chunks) {
122
+ index = index - 1
123
+ const [n1, n2, n3] = this.getDigits(x)
124
+ if (n3 > 0) {
125
+ words.push(this.hundreds[n3])
126
+ }
127
+ if (n2 > 1) {
128
+ words.push(this.twenties[n2])
129
+ }
130
+ if (n2 === 1n) {
131
+ words.push(this.tens[n1])
132
+ } else if (n1 > 0) {
133
+ const isFeminine = (this.feminine || this.SCALE[index][3])
134
+ const genderIndex = isFeminine ? 1 : 0
135
+ words.push(this.ones[n1][genderIndex])
136
+ }
137
+ if ((index > 0) && (x !== 0n)) {
138
+ words.push(this.pluralize(x, this.SCALE[index]))
139
+ }
140
+ }
141
+ return words.join(' ')
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Converts a number to Serbian cardinal (written) form.
147
+ *
148
+ * @param {number|string|bigint} value The number to convert.
149
+ * @param {Object} [options={}] Configuration options.
150
+ * @param {boolean} [options.feminine=false] Use feminine forms for numbers.
151
+ * @returns {string} The number expressed in Serbian words.
152
+ * @throws {TypeError} If value is NaN or invalid type.
153
+ * @throws {Error} If value is an invalid number string.
154
+ */
155
+ export default function convertToWords (value, options = {}) {
156
+ return new Serbian(options).convertToWords(value)
157
+ }