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,116 +1,240 @@
1
- import SlavicLanguage from '../classes/slavic-language.js'
2
-
3
1
  /**
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
2
+ * Russian language converter - Functional Implementation
16
3
  *
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
4
+ * Self-contained converter using shared Slavic utilities.
22
5
  *
23
- * This class extends SlavicLanguage, which provides the conversion algorithm
24
- * shared by Czech, Polish, Ukrainian, Serbian, Croatian, Hebrew, Lithuanian, Latvian.
6
+ * Key features:
7
+ * - Three-form pluralization (one/few/many)
8
+ * - Gender: thousands are feminine, millions+ are masculine
9
+ * - Irregular hundreds (двести, триста, etc.)
10
+ * - Long scale naming
25
11
  */
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: 'девять'
12
+
13
+ import { parseNumericValue } from '../utils/parse-numeric.js'
14
+ import { validateOptions } from '../utils/validate-options.js'
15
+
16
+ // ============================================================================
17
+ // Slavic Utilities (inlined for performance)
18
+ // ============================================================================
19
+
20
+ function pluralize (n, forms) {
21
+ const num = typeof n === 'bigint' ? Number(n) : n
22
+ const lastDigit = num % 10
23
+ const lastTwoDigits = num % 100
24
+
25
+ if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
26
+ return forms[2]
41
27
  }
42
28
 
43
- onesFeminine = {
44
- 1: 'одна',
45
- 2: 'две',
46
- 3: 'три',
47
- 4: 'четыре',
48
- 5: 'пять',
49
- 6: 'шесть',
50
- 7: 'семь',
51
- 8: 'восемь',
52
- 9: 'девять'
29
+ if (lastDigit === 1) return forms[0]
30
+ if (lastDigit >= 2 && lastDigit <= 4) return forms[1]
31
+ return forms[2]
32
+ }
33
+
34
+ function buildAllSegments (onesMasc, onesFem, teens, tens, hundreds) {
35
+ const masc = new Array(1000)
36
+ const fem = new Array(1000)
37
+
38
+ for (let i = 0; i < 1000; i++) {
39
+ masc[i] = buildSegment(i, onesMasc, teens, tens, hundreds)
40
+ fem[i] = buildSegment(i, onesFem, teens, tens, hundreds)
53
41
  }
54
42
 
55
- tens = {
56
- 0: 'десять',
57
- 1: 'одиннадцать',
58
- 2: 'двенадцать',
59
- 3: 'тринадцать',
60
- 4: 'четырнадцать',
61
- 5: 'пятнадцать',
62
- 6: 'шестнадцать',
63
- 7: 'семнадцать',
64
- 8: 'восемнадцать',
65
- 9: 'девятнадцать'
43
+ return { masc, fem }
44
+ }
45
+
46
+ function buildSegment (n, ones, teens, tens, hundreds) {
47
+ if (n === 0) return ''
48
+
49
+ const onesDigit = n % 10
50
+ const tensDigit = Math.floor(n / 10) % 10
51
+ const hundredsDigit = Math.floor(n / 100)
52
+
53
+ const parts = []
54
+
55
+ if (hundredsDigit > 0) {
56
+ parts.push(hundreds[hundredsDigit])
66
57
  }
67
58
 
68
- twenties = {
69
- 2: 'двадцать',
70
- 3: 'тридцать',
71
- 4: 'сорок',
72
- 5: 'пятьдесят',
73
- 6: 'шестьдесят',
74
- 7: 'семьдесят',
75
- 8: 'восемьдесят',
76
- 9: 'девяносто'
59
+ if (tensDigit > 1) {
60
+ parts.push(tens[tensDigit])
77
61
  }
78
62
 
79
- hundreds = {
80
- 1: 'сто',
81
- 2: 'двести',
82
- 3: 'триста',
83
- 4: 'четыреста',
84
- 5: 'пятьсот',
85
- 6: 'шестьсот',
86
- 7: 'семьсот',
87
- 8: 'восемьсот',
88
- 9: 'девятьсот'
63
+ if (tensDigit === 1) {
64
+ parts.push(teens[onesDigit])
65
+ } else if (onesDigit > 0) {
66
+ parts.push(ones[onesDigit])
89
67
  }
90
68
 
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
69
+ return parts.join(' ')
70
+ }
71
+
72
+ // ============================================================================
73
+ // Vocabulary
74
+ // ============================================================================
75
+
76
+ const ONES_MASC = ['', 'один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']
77
+ const ONES_FEM = ['', 'одна', 'две', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']
78
+
79
+ const TEENS = ['десять', 'одиннадцать', 'двенадцать', 'тринадцать', 'четырнадцать', 'пятнадцать', 'шестнадцать', 'семнадцать', 'восемнадцать', 'девятнадцать']
80
+ const TENS = ['', '', 'двадцать', 'тридцать', 'сорок', 'пятьдесят', 'шестьдесят', 'семьдесят', 'восемьдесят', 'девяносто']
81
+
82
+ // Irregular hundreds
83
+ const HUNDREDS = ['', 'сто', 'двести', 'триста', 'четыреста', 'пятьсот', 'шестьсот', 'семьсот', 'восемьсот', 'девятьсот']
84
+
85
+ const ZERO = 'ноль'
86
+ const NEGATIVE = 'минус'
87
+ const DECIMAL_SEP = 'запятая'
88
+
89
+ // Scale words: [singular, few, many]
90
+ // Thousands (index 0) are feminine, rest are masculine
91
+ const SCALE_FORMS = [
92
+ ['тысяча', 'тысячи', 'тысяч'],
93
+ ['миллион', 'миллиона', 'миллионов'],
94
+ ['миллиард', 'миллиарда', 'миллиардов'],
95
+ ['триллион', 'триллиона', 'триллионов'],
96
+ ['квадриллион', 'квадриллиона', 'квадриллионов'],
97
+ ['квинтиллион', 'квинтиллиона', 'квинтиллионов'],
98
+ ['секстиллион', 'секстиллиона', 'секстиллионов'],
99
+ ['септиллион', 'септиллиона', 'септиллионов'],
100
+ ['октиллион', 'октиллиона', 'октиллионов'],
101
+ ['нониллион', 'нониллиона', 'нониллионов']
102
+ ]
103
+
104
+ // ============================================================================
105
+ // Precomputed Lookup Tables
106
+ // ============================================================================
107
+
108
+ const { masc: SEGMENTS_MASC, fem: SEGMENTS_FEM } = buildAllSegments(ONES_MASC, ONES_FEM, TEENS, TENS, HUNDREDS)
109
+
110
+ // ============================================================================
111
+ // Conversion Functions
112
+ // ============================================================================
113
+
114
+ function integerToWords (n, options = {}) {
115
+ if (n === 0n) return ZERO
116
+
117
+ const feminine = options.gender === 'feminine'
118
+
119
+ if (n < 1000n) {
120
+ const segments = feminine ? SEGMENTS_FEM : SEGMENTS_MASC
121
+ return segments[Number(n)]
102
122
  }
123
+
124
+ if (n < 1_000_000n) {
125
+ const thousands = Number(n / 1000n)
126
+ const remainder = Number(n % 1000n)
127
+
128
+ // Thousands are always feminine in Russian
129
+ const thousandsWord = SEGMENTS_FEM[thousands]
130
+ const scaleWord = pluralize(thousands, SCALE_FORMS[0])
131
+
132
+ let result = thousandsWord + ' ' + scaleWord
133
+
134
+ if (remainder > 0) {
135
+ const segments = feminine ? SEGMENTS_FEM : SEGMENTS_MASC
136
+ result += ' ' + segments[remainder]
137
+ }
138
+
139
+ return result
140
+ }
141
+
142
+ return buildLargeNumberWords(n, options)
143
+ }
144
+
145
+ function buildLargeNumberWords (n, options) {
146
+ const feminine = options.gender === 'feminine'
147
+ const numStr = n.toString()
148
+ const len = numStr.length
149
+
150
+ const segments = []
151
+ const segmentSize = 3
152
+
153
+ const remainderLen = len % segmentSize
154
+ let pos = 0
155
+ if (remainderLen > 0) {
156
+ segments.push(Number(numStr.slice(0, remainderLen)))
157
+ pos = remainderLen
158
+ }
159
+ while (pos < len) {
160
+ segments.push(Number(numStr.slice(pos, pos + segmentSize)))
161
+ pos += segmentSize
162
+ }
163
+
164
+ const parts = []
165
+ let scaleIndex = segments.length - 1
166
+
167
+ for (let i = 0; i < segments.length; i++) {
168
+ const segment = segments[i]
169
+
170
+ if (segment !== 0) {
171
+ if (scaleIndex === 0) {
172
+ const segmentWords = feminine ? SEGMENTS_FEM : SEGMENTS_MASC
173
+ parts.push(segmentWords[segment])
174
+ } else {
175
+ const scaleForms = SCALE_FORMS[scaleIndex - 1]
176
+ const scaleWord = pluralize(segment, scaleForms)
177
+ // Thousands (scaleIndex=1) are feminine, others masculine
178
+ const isFeminine = scaleIndex === 1
179
+ const segmentWords = isFeminine ? SEGMENTS_FEM : SEGMENTS_MASC
180
+ parts.push(segmentWords[segment] + ' ' + scaleWord)
181
+ }
182
+ }
183
+
184
+ scaleIndex--
185
+ }
186
+
187
+ return parts.join(' ')
188
+ }
189
+
190
+ function decimalPartToWords (decimalPart, options) {
191
+ let result = ''
192
+ let i = 0
193
+
194
+ while (i < decimalPart.length && decimalPart[i] === '0') {
195
+ if (result) result += ' '
196
+ result += ZERO
197
+ i++
198
+ }
199
+
200
+ const remainder = decimalPart.slice(i)
201
+ if (remainder) {
202
+ if (result) result += ' '
203
+ result += integerToWords(BigInt(remainder), options)
204
+ }
205
+
206
+ return result
103
207
  }
104
208
 
105
209
  /**
106
- * Converts a number to Russian cardinal (written) form.
210
+ * Converts a numeric value to Russian words.
107
211
  *
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.
212
+ * @param {number | string | bigint} value - The numeric value to convert
213
+ * @param {Object} [options] - Optional configuration
214
+ * @param {('masculine'|'feminine')} [options.gender='masculine'] - Grammatical gender
215
+ * @returns {string} The number in Russian words
113
216
  */
114
- export default function convertToWords (value, options = {}) {
115
- return new Russian(options).convertToWords(value)
217
+ function toWords (value, options) {
218
+ options = validateOptions(options)
219
+ const { isNegative, integerPart, decimalPart } = parseNumericValue(value)
220
+
221
+ let result = ''
222
+
223
+ if (isNegative) {
224
+ result = NEGATIVE + ' '
225
+ }
226
+
227
+ result += integerToWords(integerPart, options)
228
+
229
+ if (decimalPart) {
230
+ result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart, options)
231
+ }
232
+
233
+ return result
116
234
  }
235
+
236
+ // ============================================================================
237
+ // Exports
238
+ // ============================================================================
239
+
240
+ export { toWords }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Converts a numeric value to Serbian (Cyrillic) 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 Serbian Cyrillic words
8
+ */
9
+ export function toWords(value: number | string | bigint, options?: {
10
+ gender?: "masculine" | "feminine" | undefined;
11
+ }): string;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Serbian Cyrillic language converter - Functional Implementation
3
+ *
4
+ * Self-contained converter using shared Slavic utilities.
5
+ *
6
+ * Key features:
7
+ * - Three-form pluralization (one/few/many)
8
+ * - Gender: thousands are feminine, millions+ are masculine
9
+ * - Irregular hundreds
10
+ * - Long scale naming with -ard forms
11
+ * - Cyrillic script
12
+ */
13
+
14
+ import { parseNumericValue } from '../utils/parse-numeric.js'
15
+ import { validateOptions } from '../utils/validate-options.js'
16
+
17
+ // ============================================================================
18
+ // Slavic Utilities (inlined for performance)
19
+ // ============================================================================
20
+
21
+ function pluralize (n, forms) {
22
+ const num = typeof n === 'bigint' ? Number(n) : n
23
+ const lastDigit = num % 10
24
+ const lastTwoDigits = num % 100
25
+
26
+ if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
27
+ return forms[2]
28
+ }
29
+
30
+ if (lastDigit === 1) return forms[0]
31
+ if (lastDigit >= 2 && lastDigit <= 4) return forms[1]
32
+ return forms[2]
33
+ }
34
+
35
+ function buildAllSegments (onesMasc, onesFem, teens, tens, hundreds) {
36
+ const masc = new Array(1000)
37
+ const fem = new Array(1000)
38
+
39
+ for (let i = 0; i < 1000; i++) {
40
+ masc[i] = buildSegment(i, onesMasc, teens, tens, hundreds)
41
+ fem[i] = buildSegment(i, onesFem, teens, tens, hundreds)
42
+ }
43
+
44
+ return { masc, fem }
45
+ }
46
+
47
+ function buildSegment (n, ones, teens, tens, hundreds) {
48
+ if (n === 0) return ''
49
+
50
+ const onesDigit = n % 10
51
+ const tensDigit = Math.floor(n / 10) % 10
52
+ const hundredsDigit = Math.floor(n / 100)
53
+
54
+ const parts = []
55
+
56
+ if (hundredsDigit > 0) {
57
+ parts.push(hundreds[hundredsDigit])
58
+ }
59
+
60
+ if (tensDigit > 1) {
61
+ parts.push(tens[tensDigit])
62
+ }
63
+
64
+ if (tensDigit === 1) {
65
+ parts.push(teens[onesDigit])
66
+ } else if (onesDigit > 0) {
67
+ parts.push(ones[onesDigit])
68
+ }
69
+
70
+ return parts.join(' ')
71
+ }
72
+
73
+ // ============================================================================
74
+ // Vocabulary
75
+ // ============================================================================
76
+
77
+ const ONES_MASC = ['', 'један', 'два', 'три', 'четири', 'пет', 'шест', 'седам', 'осам', 'девет']
78
+ const ONES_FEM = ['', 'једна', 'две', 'три', 'четири', 'пет', 'шест', 'седам', 'осам', 'девет']
79
+ const TEENS = ['десет', 'једанаест', 'дванаест', 'тринаест', 'четрнаест', 'петнаест', 'шеснаест', 'седамнаест', 'осамнаест', 'деветнаест']
80
+ const TENS = ['', '', 'двадесет', 'тридесет', 'четрдесет', 'педесет', 'шездесет', 'седамдесет', 'осамдесет', 'деведесет']
81
+ const HUNDREDS = ['', 'сто', 'двеста', 'триста', 'четиристо', 'петсто', 'шесто', 'седамсто', 'осамсто', 'девестo']
82
+
83
+ const ZERO = 'нула'
84
+ const NEGATIVE = 'минус'
85
+ const DECIMAL_SEP = 'запета'
86
+
87
+ // Scale words: [singular, few, many]
88
+ const SCALE_FORMS = [
89
+ ['хиљада', 'хиљаде', 'хиљада'],
90
+ ['милион', 'милиона', 'милиона'],
91
+ ['милијарда', 'милијарде', 'милијарда'],
92
+ ['билион', 'билиона', 'билиона'],
93
+ ['билијарда', 'билијарде', 'билијарда'],
94
+ ['трилион', 'трилиона', 'трилиона'],
95
+ ['трилијарда', 'трилијарде', 'трилијарда'],
96
+ ['квадрилион', 'квадрилиона', 'квадрилиона'],
97
+ ['квадрилијарда', 'квадрилијарде', 'квадрилијарда']
98
+ ]
99
+
100
+ // ============================================================================
101
+ // Precomputed Lookup Tables
102
+ // ============================================================================
103
+
104
+ const { masc: SEGMENTS_MASC, fem: SEGMENTS_FEM } = buildAllSegments(ONES_MASC, ONES_FEM, TEENS, TENS, HUNDREDS)
105
+
106
+ // ============================================================================
107
+ // Conversion Functions
108
+ // ============================================================================
109
+
110
+ function integerToWords (n, options = {}) {
111
+ if (n === 0n) return ZERO
112
+
113
+ if (n < 1000n) {
114
+ const segments = options.gender === 'feminine' ? SEGMENTS_FEM : SEGMENTS_MASC
115
+ return segments[Number(n)]
116
+ }
117
+
118
+ return buildLargeNumberWords(n, options)
119
+ }
120
+
121
+ function buildLargeNumberWords (n, options) {
122
+ const numStr = n.toString()
123
+ const len = numStr.length
124
+
125
+ const segments = []
126
+ const segmentSize = 3
127
+
128
+ const remainderLen = len % segmentSize
129
+ let pos = 0
130
+ if (remainderLen > 0) {
131
+ segments.push(Number(numStr.slice(0, remainderLen)))
132
+ pos = remainderLen
133
+ }
134
+ while (pos < len) {
135
+ segments.push(Number(numStr.slice(pos, pos + segmentSize)))
136
+ pos += segmentSize
137
+ }
138
+
139
+ const parts = []
140
+ let scaleIndex = segments.length - 1
141
+
142
+ for (let i = 0; i < segments.length; i++) {
143
+ const segment = segments[i]
144
+
145
+ if (segment !== 0) {
146
+ if (scaleIndex === 0) {
147
+ const segmentWords = options.gender === 'feminine' ? SEGMENTS_FEM : SEGMENTS_MASC
148
+ parts.push(segmentWords[segment])
149
+ } else {
150
+ const scaleForms = SCALE_FORMS[scaleIndex - 1]
151
+ const scaleWord = pluralize(segment, scaleForms)
152
+ // Thousands (scaleIndex=1) are feminine, others masculine
153
+ const isFeminine = scaleIndex === 1
154
+ const segmentWords = isFeminine ? SEGMENTS_FEM : SEGMENTS_MASC
155
+ parts.push(segmentWords[segment] + ' ' + scaleWord)
156
+ }
157
+ }
158
+
159
+ scaleIndex--
160
+ }
161
+
162
+ return parts.join(' ')
163
+ }
164
+
165
+ function decimalPartToWords (decimalPart, options) {
166
+ let result = ''
167
+ let i = 0
168
+
169
+ while (i < decimalPart.length && decimalPart[i] === '0') {
170
+ if (result) result += ' '
171
+ result += ZERO
172
+ i++
173
+ }
174
+
175
+ const remainder = decimalPart.slice(i)
176
+ if (remainder) {
177
+ if (result) result += ' '
178
+ result += integerToWords(BigInt(remainder), options)
179
+ }
180
+
181
+ return result
182
+ }
183
+
184
+ /**
185
+ * Converts a numeric value to Serbian (Cyrillic) words.
186
+ *
187
+ * @param {number | string | bigint} value - The numeric value to convert
188
+ * @param {Object} [options] - Optional configuration
189
+ * @param {('masculine'|'feminine')} [options.gender='masculine'] - Grammatical gender
190
+ * @returns {string} The number in Serbian Cyrillic words
191
+ */
192
+ function toWords (value, options) {
193
+ options = validateOptions(options)
194
+ const { isNegative, integerPart, decimalPart } = parseNumericValue(value)
195
+
196
+ let result = ''
197
+
198
+ if (isNegative) {
199
+ result = NEGATIVE + ' '
200
+ }
201
+
202
+ result += integerToWords(integerPart, options)
203
+
204
+ if (decimalPart) {
205
+ result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart, options)
206
+ }
207
+
208
+ return result
209
+ }
210
+
211
+ // ============================================================================
212
+ // Exports
213
+ // ============================================================================
214
+
215
+ export { toWords }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Converts a numeric value to Serbian (Latin) 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 Serbian Latin words
8
+ */
9
+ export function toWords(value: number | string | bigint, options?: {
10
+ gender?: "masculine" | "feminine" | undefined;
11
+ }): string;