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,251 @@
1
+ import AbstractLanguage from './abstract-language.js'
2
+
3
+ /**
4
+ * @typedef {string[]} SlavicPluralForms
5
+ * Array of three plural forms for Slavic languages:
6
+ * - [0]: Singular form (for numbers ending in 1, except 11)
7
+ * - [1]: Few form (for numbers ending in 2-4, except 12-14)
8
+ * - [2]: Many form (for all other numbers: 0, 5-20, and numbers ending in 0, 5-9, 11-19)
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object.<string, SlavicPluralForms>} SlavicThousandsMap
13
+ * Mapping from power indices to their plural forms.
14
+ * Example: { '0': ['тысяча', 'тысячи', 'тысяч'], '1': ['миллион', 'миллиона', 'миллионов'] }
15
+ */
16
+
17
+ /**
18
+ * Base class for Slavic and related languages with complex pluralization.
19
+ *
20
+ * This class provides a reusable implementation for languages that share:
21
+ * - Three-form pluralization (singular/few/many)
22
+ * - Gender-aware number forms (masculine/feminine for 1, 2)
23
+ * - Hundreds, tens, ones decomposition
24
+ * - Chunk-based large number handling (thousands, millions, etc.)
25
+ * - Inherits decimal handling from AbstractLanguage (supports both grouped and
26
+ * per-digit modes via the `convertDecimalsPerDigit` class property).
27
+ *
28
+ * Used by: Russian, Czech, Polish, Ukrainian, Serbian, Croatian,
29
+ * as well as Baltic (Lithuanian, Latvian) and Hebrew languages.
30
+ *
31
+ * Subclasses MUST define these properties with language-specific vocabulary:
32
+ * - `ones` - Object mapping 1-9 to masculine forms
33
+ * - `onesFeminine` - Object mapping 1-9 to feminine forms
34
+ * - `tens` - Object mapping 0-9 to teen numbers (10-19)
35
+ * - `twenties` - Object mapping 2-9 to tens (20-90)
36
+ * - `hundreds` - Object mapping 1-9 to hundreds (100-900)
37
+ * - `thousands` - Object mapping power indices to [singular, few, many] forms
38
+ * - `feminine` - Boolean indicating if feminine forms should be used (optional)
39
+ *
40
+ * @abstract
41
+ * @extends AbstractLanguage
42
+ */
43
+ class SlavicLanguage extends AbstractLanguage {
44
+ /**
45
+ * Masculine forms for digits 1-9.
46
+ *
47
+ * @type {object}
48
+ */
49
+ ones = {}
50
+
51
+ /**
52
+ * Feminine forms for digits 1-9.
53
+ *
54
+ * @type {object}
55
+ */
56
+ onesFeminine = {}
57
+
58
+ /**
59
+ * Words for tens (10, 20, 30, etc.).
60
+ *
61
+ * @type {object}
62
+ */
63
+ tens = {}
64
+
65
+ /**
66
+ * Special forms for 21-29 in some languages.
67
+ *
68
+ * @type {object}
69
+ */
70
+ twenties = {}
71
+
72
+ /**
73
+ * Words for hundreds (100, 200, 300, etc.).
74
+ *
75
+ * @type {object}
76
+ */
77
+ hundreds = {}
78
+
79
+ /**
80
+ * Scale words for thousands, millions, etc.
81
+ *
82
+ * @type {object}
83
+ */
84
+ thousands = {}
85
+
86
+ /**
87
+ * Use feminine forms for numbers (affects 1-9).
88
+ *
89
+ * @type {boolean}
90
+ */
91
+ feminine
92
+
93
+ /**
94
+ * Initializes the Slavic language converter with language-specific options.
95
+ *
96
+ * @param {Object} [options={}] Configuration options.
97
+ * @param {boolean} [options.feminine=false] Use feminine forms for numbers (affects gender agreement).
98
+ */
99
+ constructor ({ feminine = false } = {}) {
100
+ super()
101
+
102
+ this.feminine = feminine
103
+ }
104
+
105
+ /**
106
+ * Converts a whole number to its word representation.
107
+ *
108
+ * This method implements the Slavic number construction algorithm:
109
+ * 1. Split number into 3-digit chunks (right to left)
110
+ * 2. For each chunk: convert hundreds, tens, ones
111
+ * 3. Apply gender rules for ones (feminine for thousands, or when feminine=true)
112
+ * 4. Add pluralized power word (thousand/million/billion/etc.)
113
+ * 5. Join all parts with spaces
114
+ *
115
+ * @param {bigint} number The whole number to convert (non-negative).
116
+ * @returns {string} The number in words.
117
+ */
118
+ convertWholePart (number) {
119
+ if (number === 0n) {
120
+ return this.zeroWord
121
+ }
122
+
123
+ const words = []
124
+ const chunks = this.splitByX(number.toString(), 3)
125
+ let chunkIndex = chunks.length
126
+
127
+ for (const chunkValue of chunks) {
128
+ chunkIndex = chunkIndex - 1
129
+
130
+ if (chunkValue === 0n) {
131
+ continue
132
+ }
133
+
134
+ const [onesDigit, tensDigit, hundredsDigit] = this.getDigits(chunkValue)
135
+
136
+ if (hundredsDigit > 0n) {
137
+ words.push(this.hundreds[hundredsDigit])
138
+ }
139
+
140
+ if (tensDigit > 1n) {
141
+ words.push(this.twenties[tensDigit])
142
+ }
143
+
144
+ // Handle teens (10-19) or ones (1-9)
145
+ if (tensDigit === 1n) {
146
+ // Teens: use tens array directly
147
+ words.push(this.tens[onesDigit])
148
+ } else if (onesDigit > 0n) {
149
+ // Ones: use feminine form for thousands (chunkIndex 1) or when feminine=true (chunkIndex 0)
150
+ const onesArray =
151
+ chunkIndex === 1 || (this.feminine && chunkIndex === 0)
152
+ ? this.onesFeminine
153
+ : this.ones
154
+ words.push(onesArray[onesDigit])
155
+ }
156
+
157
+ // Add power word (thousand, million, etc.) with proper pluralization
158
+ if (chunkIndex > 0) {
159
+ words.push(this.pluralize(chunkValue, this.thousands[chunkIndex]))
160
+ }
161
+ }
162
+
163
+ return words.join(' ')
164
+ }
165
+
166
+ /**
167
+ * Splits a number string into chunks of X digits.
168
+ *
169
+ * Example: splitByX('1234567', 3) => [1n, 234n, 567n]
170
+ *
171
+ * @param {string} numberString The number as a string.
172
+ * @param {number} chunkSize Chunk size (typically 3 for thousands grouping).
173
+ * @returns {bigint[]} Array of BigInt chunks.
174
+ */
175
+ splitByX (numberString, chunkSize) {
176
+ const chunks = []
177
+ const stringLength = numberString.length
178
+
179
+ if (stringLength > chunkSize) {
180
+ const remainderLength = stringLength % chunkSize
181
+
182
+ if (remainderLength > 0) {
183
+ chunks.push(BigInt(numberString.slice(0, remainderLength)))
184
+ }
185
+
186
+ for (let i = remainderLength; i < stringLength; i += chunkSize) {
187
+ chunks.push(BigInt(numberString.slice(i, i + chunkSize)))
188
+ }
189
+ } else {
190
+ chunks.push(BigInt(numberString))
191
+ }
192
+
193
+ return chunks
194
+ }
195
+
196
+ /**
197
+ * Extracts individual digits from a number (units, tens, hundreds).
198
+ *
199
+ * Returns digits in reverse order: [ones, tens, hundreds]
200
+ * Example: 456 => [6n, 5n, 4n]
201
+ *
202
+ * @param {bigint} value The number to extract digits from (0-999).
203
+ * @returns {bigint[]} Array of [ones, tens, hundreds] as BigInts.
204
+ */
205
+ getDigits (value) {
206
+ // Direct BigInt arithmetic is faster than string manipulation
207
+ const onesPlace = value % 10n
208
+ const tensPlace = (value / 10n) % 10n
209
+ const hundredsPlace = value / 100n
210
+ return [onesPlace, tensPlace, hundredsPlace]
211
+ }
212
+
213
+ /**
214
+ * Selects the correct plural form based on Slavic pluralization rules.
215
+ *
216
+ * Slavic languages use three forms:
217
+ * - Form 0 (singular): numbers ending in 1 (but not 11)
218
+ * - Form 1 (few): numbers ending in 2-4 (but not 12-14)
219
+ * - Form 2 (many): all other numbers (0, 5-20, 25-30, etc.)
220
+ *
221
+ * Examples (Russian):
222
+ * - 1, 21, 31... => тысяча (form 0)
223
+ * - 2-4, 22-24, 32-34... => тысячи (form 1)
224
+ * - 0, 5-20, 25-30... => тысяч (form 2)
225
+ *
226
+ * @param {bigint} n The number to check.
227
+ * @param {string[]} forms Array of [singular, few, many] forms.
228
+ * @returns {string} The appropriate form for the number.
229
+ */
230
+ pluralize (number, pluralForms) {
231
+ const remainder100 = number % 100n
232
+ const remainder10 = number % 10n
233
+
234
+ // Check if in 11-19 range (special case)
235
+ if (remainder100 >= 10n && remainder100 <= 20n) {
236
+ return pluralForms[2] // Always use "many" form for 11-20
237
+ }
238
+
239
+ if (remainder10 === 1n) {
240
+ return pluralForms[0] // Singular
241
+ }
242
+
243
+ if (remainder10 >= 2n && remainder10 <= 4n) {
244
+ return pluralForms[1] // Few (2-4)
245
+ }
246
+
247
+ return pluralForms[2] // Many
248
+ }
249
+ }
250
+
251
+ export default SlavicLanguage
@@ -0,0 +1,161 @@
1
+ import AbstractLanguage from './abstract-language.js'
2
+
3
+ /**
4
+ * @typedef {Array<string>} SouthAsianScaleWords
5
+ * Array of scale words for the Indian numbering system, in ascending order.
6
+ * - Index 0: Usually empty/unused (ones place)
7
+ * - Index 1: Thousands (hazaar/হাজার/हजार)
8
+ * - Index 2: Lakhs (lakh/লাখ/लाख)
9
+ * - Index 3: Crores (crore/কোটি/करोड़)
10
+ * - Index 4: Arabs (arab/আরব/अरब)
11
+ * Each index i represents the scale word for groups at position i in the Indian system.
12
+ */
13
+
14
+ /**
15
+ * @typedef {Array<string>} SouthAsianBelowHundred
16
+ * Array of words for numbers 0-99, indexed directly.
17
+ * belowHundred[0] = word for 0, belowHundred[42] = word for 42, etc.
18
+ */
19
+
20
+ /**
21
+ * Base class for South Asian languages with shared grouping patterns.
22
+ *
23
+ * This class provides a reusable implementation for South Asian languages that share:
24
+ * - Indian-style number grouping: last 3 digits, then 2-2 (1,23,45,67,89)
25
+ * - Lakh (100,000), Crore (10,000,000), Arab (1,000,000,000) scale words
26
+ * - Standard negative and decimal handling (inherits AbstractLanguage decimal logic,
27
+ * including `convertDecimalsPerDigit` support when set by subclasses)
28
+ *
29
+ * Used by: Hindi (hi), Bengali (bn), Urdu (ur), Punjabi (pa), Marathi (mr), Gujarati (gu), Kannada (kn)
30
+ *
31
+ * Subclasses MUST define language-specific vocabulary via class properties:
32
+ * - `belowHundred` array with digit and teen words (0-99)
33
+ * - `hundredWord` string used inside `convertBelowThousand`
34
+ * - `scaleWords` array with grouping words (hazaar, lakh, crore, etc.) indexed by grouping level
35
+ * - `negativeWord`, `decimalSeparatorWord`, `zeroWord`, `wordSeparator`
36
+ *
37
+ * @abstract
38
+ * @extends AbstractLanguage
39
+ */
40
+ class SouthAsianLanguage extends AbstractLanguage {
41
+ /**
42
+ * Array of words for numbers 0-99 (digits and teens).
43
+ * Index directly: belowHundred[0] through belowHundred[99].
44
+ * @type {Array<string>}
45
+ */
46
+ belowHundred
47
+
48
+ /**
49
+ * Word for "hundred" in the language (e.g., 'सौ' in Hindi, 'শত' in Bengali).
50
+ * Used to construct hundreds (e.g., "1 hundred", "2 hundred").
51
+ * @type {string}
52
+ */
53
+ hundredWord
54
+
55
+ /**
56
+ * Array of scale words for Indian-style grouping (hazaar, lakh, crore, arab, etc.).
57
+ * Index 0 is typically unused (ones place, no scale word).
58
+ * Index 1 is for thousands, Index 2 for lakhs, Index 3 for crores, etc.
59
+ * @type {Array<string>}
60
+ */
61
+ scaleWords
62
+
63
+ /**
64
+ * Split a number into Indian numbering system groups.
65
+ *
66
+ * The Indian system groups differently than Western (3-3-3) systems:
67
+ * - First group (rightmost): Up to 3 digits (ones, tens, hundreds)
68
+ * - Subsequent groups: Exactly 2 digits each (thousands, lakhs, crores, etc.)
69
+ *
70
+ * This creates the familiar Indian comma pattern: 1,23,45,67,890
71
+ *
72
+ * @protected
73
+ * @param {bigint} number The number to split into groups
74
+ * @returns {Array<number>} Array of groups from most significant to least significant
75
+ *
76
+ * @example
77
+ * // splitToGroups(1234567n) → [12, 34, 567]
78
+ * // Reads as: 12 lakhs, 34 thousands, 567 units
79
+ * // splitToGroups(98765432n) → [9, 87, 65, 432]
80
+ * // Reads as: 9 crores, 87 lakhs, 65 thousands, 432 units
81
+ */
82
+ splitToGroups (number) {
83
+ const numStr = number.toString()
84
+
85
+ if (numStr.length <= 3) {
86
+ return [Number(numStr)]
87
+ }
88
+
89
+ const groups = []
90
+ const last3 = numStr.slice(-3)
91
+ groups.unshift(Number(last3))
92
+
93
+ let remaining = numStr.slice(0, -3)
94
+ while (remaining.length > 0) {
95
+ const group = remaining.slice(-2)
96
+ groups.unshift(Number(group))
97
+ remaining = remaining.slice(0, -2)
98
+ }
99
+
100
+ return groups
101
+ }
102
+
103
+ /**
104
+ * Convert a number below 1000 to words (0-999).
105
+ *
106
+ * @protected
107
+ * @param {number} number Value between 0 and 999
108
+ * @returns {string} Language-specific word representation
109
+ */
110
+ convertBelowThousand (number) {
111
+ if (number === 0) return ''
112
+ if (number < 100) return this.belowHundred[number]
113
+
114
+ const hundreds = Math.trunc(number / 100)
115
+ const remainder = number % 100
116
+ const parts = []
117
+
118
+ if (hundreds === 1) {
119
+ parts.push(this.belowHundred[1] + ' ' + this.hundredWord)
120
+ } else {
121
+ parts.push(this.belowHundred[hundreds] + ' ' + this.hundredWord)
122
+ }
123
+
124
+ if (remainder > 0) {
125
+ parts.push(this.belowHundred[remainder])
126
+ }
127
+
128
+ return parts.join(' ')
129
+ }
130
+
131
+ /**
132
+ * Convert whole number to cardinal words using South Asian grouping.
133
+ *
134
+ * @param {bigint} number Number to convert
135
+ * @returns {string} Cardinal representation
136
+ */
137
+ convertWholePart (number) {
138
+ if (number === 0n) {
139
+ return this.zeroWord
140
+ }
141
+
142
+ const groups = this.splitToGroups(number)
143
+ const groupCount = groups.length
144
+ const words = []
145
+
146
+ for (let i = 0; i < groupCount; i++) {
147
+ const groupValue = groups[i]
148
+ if (groupValue === 0) continue
149
+
150
+ const scaleIndex = groupCount - i - 1
151
+ words.push(this.convertBelowThousand(groupValue))
152
+ if (scaleIndex > 0 && this.scaleWords[scaleIndex]) {
153
+ words.push(this.scaleWords[scaleIndex])
154
+ }
155
+ }
156
+
157
+ return words.join(' ').trim()
158
+ }
159
+ }
160
+
161
+ export default SouthAsianLanguage
@@ -0,0 +1,63 @@
1
+ import GreedyScaleLanguage from './greedy-scale-language.js'
2
+
3
+ /**
4
+ * @typedef {Object} TurkicWordPair
5
+ * @property {string} word - The Turkic word or phrase
6
+ * @property {bigint} value - The numeric value represented by the word
7
+ */
8
+
9
+ /**
10
+ * Base class for Turkic languages with shared grammar patterns.
11
+ *
12
+ * This class provides a reusable implementation for Turkic languages that share:
13
+ * - Space-separated number combinations
14
+ * - Implicit 'bir' (one) before hundreds and thousands
15
+ * - Simple multiplication/addition logic
16
+ * - Consistent magnitude handling
17
+ * - Inherits decimal handling from AbstractLanguage via GreedyScaleLanguage
18
+ * (supports both grouped and per-digit modes via the `convertDecimalsPerDigit` class property).
19
+ *
20
+ * Used by: Turkish (TR), Azerbaijani (AZ)
21
+ *
22
+ * Subclasses MUST define (from GreedyScaleLanguage requirements):
23
+ * - `scaleWordPairs` array of [value, word] pairs as a class property (ordered descending by value).
24
+ * Optionally, language-specific class properties (e.g., `negativeWord`, `zeroWord`, `decimalSeparatorWord`, `wordSeparator`).
25
+ *
26
+ * TurkicLanguage provides a default `mergeScales()` implementation; subclasses may override
27
+ * if specialized merge logic is needed (unlikely for Turkic languages).
28
+ *
29
+ * @abstract
30
+ * @extends GreedyScaleLanguage
31
+ */
32
+ class TurkicLanguage extends GreedyScaleLanguage {
33
+ /**
34
+ * Merges two adjacent word-number pairs according to Turkic grammar rules.
35
+ *
36
+ * Shared Turkic patterns:
37
+ * - Implicit 'bir' (one) before hundreds and thousands
38
+ * - Space separator (wordSeparator property) for all combinations
39
+ * - Multiplies when right > left (crossing magnitude boundary)
40
+ * - Adds otherwise (combining same-magnitude components)
41
+ *
42
+ * @param {Object} leftPair The left operand as `{ word: bigint }`.
43
+ * @param {Object} rightPair The right operand as `{ word: bigint }`.
44
+ * @returns {Object} Merged pair with combined word and resulting number (bigint).
45
+ */
46
+ mergeScales (leftPair, rightPair) {
47
+ const [[leftWord, leftNumber]] = Object.entries(leftPair)
48
+ const [[rightWord, rightNumber]] = Object.entries(rightPair)
49
+
50
+ // Implicit 'bir' (one) before certain magnitudes:
51
+ // Omit '1' before hundreds (100n) and thousands (1000n) to form natural combinations
52
+ if (leftNumber === 1n && (rightNumber <= 100n || rightNumber === 1000n)) {
53
+ return rightPair // Return just the magnitude word (e.g., "yüz", not "bir yüz")
54
+ }
55
+
56
+ // Combine numbers with space separator (wordSeparator from GreedyScaleLanguage):
57
+ // Multiply when crossing magnitude boundary, add otherwise
58
+ const mergedNumber = rightNumber > leftNumber ? leftNumber * rightNumber : leftNumber + rightNumber
59
+ return { [`${leftWord}${this.wordSeparator}${rightWord}`]: mergedNumber }
60
+ }
61
+ }
62
+
63
+ export default TurkicLanguage