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,261 +0,0 @@
1
- /**
2
- * Abstract base class for language converters.
3
- *
4
- * What this class handles:
5
- * - Validates and normalizes caller input (`number | string | bigint`), rejecting NaN/invalid strings.
6
- * - Splits sign, whole, and decimal parts, caching the whole part for languages that need it.
7
- * - Delegates whole-number wording to `convertWholePart(wholeNumber)` implemented by subclasses.
8
- * - Converts decimals via `decimalDigitsToWords()`, preserving leading zeros and supporting per-digit mode when `convertDecimalsPerDigit` is true.
9
- *
10
- * What subclasses must provide:
11
- * - `convertWholePart(wholeNumber)` method implementation (required; abstract).
12
- * - `negativeWord` class property (word preceding negative numbers, e.g., "minus").
13
- * - `zeroWord` class property (word for digit 0, e.g., "zero").
14
- * - `decimalSeparatorWord` class property (word between whole and decimal, e.g., "point").
15
- * - `wordSeparator` class property (word separator in output, typically a space).
16
- * Optional: override `convertDecimalsPerDigit`, `digits`, or `convertDigitToWord()` for custom behavior.
17
- *
18
- * This class stays minimal; language grammar lives in subclasses.
19
- *
20
- * @abstract
21
- */
22
- class AbstractLanguage {
23
- /**
24
- * Word that precedes negative numbers (e.g., "minus", "negative", "moins").
25
- * @type {string}
26
- */
27
- negativeWord = ''
28
-
29
- /**
30
- * Word that separates whole and decimal parts (e.g., "point", "virgule", "comma").
31
- * @type {string}
32
- */
33
- decimalSeparatorWord = ''
34
-
35
- /**
36
- * Word representation for the digit 0 (e.g., "zero", "zéro", "null").
37
- * @type {string}
38
- */
39
- zeroWord = ''
40
-
41
- /**
42
- * Character(s) used to separate words in the output (typically a space).
43
- * @type {string}
44
- */
45
- wordSeparator = ' '
46
-
47
- /**
48
- * Cached whole number portion from the most recent conversion.
49
- * Some languages need access to this value during conversion for
50
- * pluralization rules or special cases (e.g., Czech, Hebrew).
51
- * @type {bigint}
52
- */
53
- cachedWholeNumber = 0n
54
-
55
- /**
56
- * Whether to convert decimal digits individually rather than grouped.
57
- * - `true`: Each digit converted separately (e.g., "05" → "zero five")
58
- * - `false`: Leading zeros preserved, remaining grouped (e.g., "14" → "fourteen")
59
- * Used by languages like Japanese, Thai, Tamil, Telugu.
60
- * @type {boolean}
61
- */
62
- convertDecimalsPerDigit = false
63
-
64
- /**
65
- * Optional array of digit words for direct lookup in `convertDigitToWord()`.
66
- * - Length 10: indices 0–9 map directly to digit words
67
- * - Length 9: indices 0–8 map to words for digits 1–9
68
- * - `null`: Falls back to `convertWholePart()` for digit conversion
69
- * @type {string[]|null}
70
- */
71
- digits = null
72
-
73
- /**
74
- * Convert a single decimal digit (0-9) to its word representation.
75
- *
76
- * Default behavior:
77
- * - 0 returns the language's `zeroWord`
78
- * - If a `digits` array is present, use it for direct lookup
79
- * - Length 10: indices 0–9 map directly
80
- * - Length 9: indices 1–9 map via `idx - 1`
81
- * - Otherwise delegate to `convertWholePart(digit)`
82
- *
83
- * Subclasses may override this for custom logic.
84
- *
85
- * @protected
86
- * @param {bigint} digit A single digit value (0-9) as BigInt
87
- * @returns {string} The word representation of the digit
88
- */
89
- convertDigitToWord (digit) {
90
- const idx = Number(digit)
91
- if (idx === 0) return this.zeroWord
92
-
93
- if (Array.isArray(this.digits)) {
94
- if (this.digits.length === 10) {
95
- return this.digits[idx] ?? this.zeroWord
96
- }
97
- if (this.digits.length === 9) {
98
- return this.digits[idx - 1] ?? this.zeroWord
99
- }
100
- }
101
-
102
- return this.convertWholePart(digit)
103
- }
104
-
105
- /**
106
- * Convert the decimal fractional digits into words.
107
- *
108
- * Behavior depends on the `convertDecimalsPerDigit` class property:
109
- *
110
- * **Per-digit mode** (`convertDecimalsPerDigit: true`):
111
- * - Each decimal digit is converted individually using `convertDigitToWord()`
112
- * - Example: "05" -> [zero, 'five'], "14" -> ['one', 'four']
113
- * - Used by: Japanese, Thai, Tamil, Telugu
114
- *
115
- * **Grouped mode** (default, `convertDecimalsPerDigit: false`):
116
- * - Leading zeros are preserved as individual `zeroWord` entries
117
- * - Remaining digits are grouped and converted as a number
118
- * - Example: "05" -> [zero, 'five'], "14" -> ['fourteen']
119
- * - Used by: Most languages (English, Spanish, French, etc.)
120
- *
121
- * @protected
122
- * @param {string} decimalString The decimal digits as a string (e.g. `'05'` for 3.05).
123
- * @returns {Array<string>} Array of word tokens representing the fractional part.
124
- *
125
- * @example
126
- * // Per-digit mode
127
- * decimalDigitsToWords('05'); // -> [this.zeroWord, 'five']
128
- * decimalDigitsToWords('14'); // -> ['one', 'four']
129
- *
130
- * // Grouped mode
131
- * decimalDigitsToWords('05'); // -> [this.zeroWord, 'five']
132
- * decimalDigitsToWords('14'); // -> ['fourteen']
133
- */
134
- decimalDigitsToWords (decimalString) {
135
- const words = []
136
- const len = decimalString.length
137
-
138
- if (this.convertDecimalsPerDigit) {
139
- for (let i = 0; i < len; i++) {
140
- const decimalDigit = BigInt(decimalString[i])
141
- words.push(this.convertDigitToWord(decimalDigit))
142
- }
143
- return words
144
- }
145
-
146
- // Default grouped-decimal behavior with leading zero preservation
147
- let i = 0
148
- while (i < len && decimalString[i] === '0') {
149
- words.push(this.zeroWord)
150
- i++
151
- }
152
-
153
- if (i === len) return words
154
-
155
- const remainingDigits = decimalString.slice(i)
156
- words.push(this.convertWholePart(BigInt(remainingDigits)))
157
-
158
- return words
159
- }
160
-
161
- /**
162
- * Convert a numeric input into its language cardinal representation.
163
- *
164
- * This is the public entry point used by consumers. It normalizes the input
165
- * (accepting `number | string | bigint`), validates it, splits sign and
166
- * fractional parts, and delegates whole-number conversion to
167
- * `convertWholePart(BigInt)` and fractional conversion to `decimalDigitsToWords()`.
168
- *
169
- * Errors and validation:
170
- * - Passing `NaN` (as `number`) throws `TypeError`.
171
- * - Passing a non-numeric `string` throws `Error`.
172
- * - Passing an unsupported type throws `TypeError`.
173
- *
174
- * @public
175
- * @param {number|string|bigint} value Numeric input to convert. Strings may include a single `.` decimal marker.
176
- * @returns {string} The localized cardinal string.
177
- * @throws {TypeError|Error} For invalid input as described above.
178
- */
179
- convertToWords (value) {
180
- // Normalize and validate input
181
- const inputType = typeof value
182
-
183
- if (inputType === 'number') {
184
- if (Number.isNaN(value)) throw new TypeError('NaN is not an accepted number.')
185
- value = value.toString()
186
- } else if (inputType === 'string') {
187
- value = value.trim()
188
- if (value.length === 0 || Number.isNaN(Number(value))) { throw new Error('Invalid number format: "' + value + '"') }
189
- } else if (inputType !== 'bigint') {
190
- throw new TypeError(
191
- 'Invalid variable type, expected number|string|bigint, received: ' + inputType
192
- )
193
- }
194
-
195
- const words = []
196
-
197
- // Detect negativity and strip sign for further processing.
198
- // Must check before type coercion for BigInt (comparison works but slice does not).
199
- let isNegative = false
200
- if (inputType === 'bigint') {
201
- if (value < 0n) {
202
- isNegative = true
203
- value = -value
204
- }
205
- } else {
206
- // For strings, check first character before normalization
207
- if (value[0] === '-') {
208
- isNegative = true
209
- value = value.slice(1)
210
- }
211
- }
212
-
213
- // Extract whole and decimal parts based on type.
214
- // BigInt has no decimal point; strings may contain a single '.'.
215
- // Default whole part to '0' if empty (e.g., '.5' -> '0' + '.5')
216
- let wholeNumber
217
- let decimalPart
218
- if (inputType === 'bigint') {
219
- wholeNumber = value
220
- } else {
221
- const decimalPointIndex = value.indexOf('.')
222
- if (decimalPointIndex === -1) {
223
- wholeNumber = BigInt(value)
224
- } else {
225
- const wholePartString = value.slice(0, decimalPointIndex) || '0'
226
- wholeNumber = BigInt(wholePartString)
227
- decimalPart = value.slice(decimalPointIndex + 1)
228
- }
229
- }
230
-
231
- this.cachedWholeNumber = wholeNumber
232
-
233
- if (isNegative) words.push(this.negativeWord)
234
-
235
- words.push(this.convertWholePart(wholeNumber))
236
-
237
- // Append decimal portion if present (separator + fractional digits)
238
- if (decimalPart) {
239
- words.push(this.decimalSeparatorWord)
240
- words.push(...this.decimalDigitsToWords(decimalPart))
241
- }
242
-
243
- return words.join(this.wordSeparator)
244
- }
245
-
246
- /**
247
- * Convert a BigInt whole number to its cardinal word representation.
248
- *
249
- * This is a template method that subclasses MUST implement to provide
250
- * language-specific number conversion logic.
251
- *
252
- * @abstract
253
- * @param {bigint} wholeNumber The whole number part to convert
254
- * @returns {string} The cardinal representation in the target language
255
- */
256
- convertWholePart (wholeNumber) {
257
- throw new Error('convertWholePart() must be implemented by subclass')
258
- }
259
- }
260
-
261
- export default AbstractLanguage
@@ -1,195 +0,0 @@
1
- import AbstractLanguage from './abstract-language.js'
2
-
3
- /**
4
- * @typedef {Object} WordSet
5
- * @property {string} word - The language word or phrase
6
- * @property {bigint} value - The numeric value represented by the word
7
- */
8
-
9
- /**
10
- * @typedef {Array.<Array.<(bigint|string)>>} ScaleWordPairs
11
- * Array of scale word pairs in descending order. Each pair contains:
12
- * - [0]: BigInt numeric value
13
- * - [1]: String word representation
14
- * Must be ordered largest to smallest for the greedy algorithm.
15
- */
16
-
17
- /**
18
- * Greedy scale language converter implementing the "highest-matching scale word" algorithm.
19
- *
20
- * Responsibilities:
21
- * - Decompose a whole-number value into a sequence of scale word-sets.
22
- * - Provide helpers to merge and post-process matched word-sets.
23
- * - Inherits decimal handling from AbstractLanguage (supports grouped and per-digit
24
- * modes via the `convertDecimalsPerDigit` class property).
25
- *
26
- * Subclass requirements:
27
- * - Define `scaleWordPairs` (ordered descending) as `[BigInt, string]` tuples.
28
- * - Implement `mergeScales(leftWordSet, rightWordSet)` to combine adjacent word-sets
29
- * per language grammar.
30
- *
31
- * Scale words specification:
32
- * - `scaleWordPairs` is an Array of 2-tuples: `[BigInt, string]` where the first element
33
- * is the numeric value (BigInt) and the second is the word used for that value.
34
- * - Scale words MUST be ordered from largest to smallest (descending) for the algorithm
35
- * to function correctly.
36
- *
37
- * @abstract
38
- * @extends AbstractLanguage
39
- * @example
40
- * // Example `scaleWordPairs` for English (descending order):
41
- * // [[1000000000n, 'billion'], [1000000n, 'million'], [1000n, 'thousand'], [100n, 'hundred'], ..., [1n, 'one']]
42
- */
43
-
44
- class GreedyScaleLanguage extends AbstractLanguage {
45
- /**
46
- * Array of scale word pairs mapping numeric values to their word representations.
47
- *
48
- * Each element is a 2-tuple: `[BigInt, string]` where the first element is the
49
- * numeric value and the second is the word for that value. The array MUST be
50
- * ordered from largest to smallest (descending) for the greedy algorithm to work correctly.
51
- *
52
- * @type {Array.<Array.<(bigint|string)>>}
53
- * @example
54
- * // English scale words (descending order):
55
- * // [[1000000000n, 'billion'], [1000000n, 'million'], [1000n, 'thousand'], [100n, 'hundred'], ..., [1n, 'one']]
56
- */
57
- scaleWordPairs
58
-
59
- /**
60
- * Return the word associated with an exact scale word-set numeric value.
61
- *
62
- * @param {bigint|number} scaleValue Numeric word-set object key (prefer BigInt for exact matching).
63
- * @returns {string|undefined} The word for the provided scale value, or `undefined`.
64
- */
65
- getScaleWord (scaleValue) {
66
- const matchingPair = this.scaleWordPairs.find((pair) => pair[0] === scaleValue)
67
- return matchingPair?.[1]
68
- }
69
-
70
- /**
71
- * Decompose a whole-number into a sequence of scale word-sets.
72
- *
73
- * This internal helper returns a nested structure that represents quantities and
74
- * their matching scale words (e.g. `[{ 'one': 1n }, { 'hundred': 100n }, ...]`).
75
- * The result is designed to be reduced by `mergeWordSets()` using language-specific `mergeScales()`.
76
- *
77
- * For quantities > 1, the multiplier is recursively decomposed. For quantity = 1,
78
- * the implicit "one" is represented with `{ 'one': 1n }` and typically omitted during mergeScales().
79
- *
80
- * @protected
81
- * @param {bigint} wholeNumber The integer value to decompose (BigInt preferred).
82
- * @returns {Array<Object|Array>} An array of word-set objects and possibly nested arrays.
83
- */
84
- decomposeToScales (wholeNumber) {
85
- const decomposeWordSets = []
86
- let remainingValue = wholeNumber
87
-
88
- do {
89
- const matchingScaleWordPair = this.scaleWordPairs.find((scaleWordPair) => remainingValue >= scaleWordPair[0])
90
- if (!matchingScaleWordPair) break
91
-
92
- const multiplier = remainingValue === 0n ? 1n : remainingValue / matchingScaleWordPair[0]
93
-
94
- if (multiplier === 1n) {
95
- decomposeWordSets.push({ [this.getScaleWord(1n)]: 1n })
96
- } else {
97
- decomposeWordSets.push(this.decomposeToScales(multiplier))
98
- }
99
-
100
- decomposeWordSets.push({ [matchingScaleWordPair[1]]: matchingScaleWordPair[0] })
101
-
102
- remainingValue = remainingValue === 0n ? 0n : remainingValue % matchingScaleWordPair[0]
103
- } while (remainingValue > 0n)
104
-
105
- return decomposeWordSets
106
- }
107
-
108
- /**
109
- * Reduce a nested array of word-sets into a single merged word-set object.
110
- *
111
- * This method repeatedly applies the subclass `mergeScales()` operation until a single
112
- * object remains. It normalizes nested arrays by recursively merging them.
113
- *
114
- * @protected
115
- * @param {Array<Object|Array>} mergeWordSetsList Array of word-set objects and nested arrays.
116
- * @returns {Object} Merged word-set where the single object key is the language string
117
- * and its value is the numeric BigInt result for that string.
118
- */
119
- mergeWordSets (mergeWordSetsList) {
120
- while (mergeWordSetsList.length > 1) {
121
- const [firstWordSet, secondWordSet, ...remainingWordSets] = mergeWordSetsList
122
-
123
- if (!Array.isArray(firstWordSet) && !Array.isArray(secondWordSet)) {
124
- const merged = this.mergeScales(firstWordSet, secondWordSet)
125
- mergeWordSetsList = remainingWordSets.length > 0 ? [merged, remainingWordSets] : [merged]
126
- continue
127
- }
128
-
129
- const normalizedWordSets = mergeWordSetsList.map((wordSetElement) => {
130
- if (!Array.isArray(wordSetElement)) return wordSetElement
131
- return wordSetElement.length === 1 ? wordSetElement[0] : this.mergeWordSets(wordSetElement)
132
- })
133
-
134
- mergeWordSetsList = normalizedWordSets
135
- }
136
-
137
- return mergeWordSetsList[0]
138
- }
139
-
140
- /**
141
- * Combine two adjacent word-sets into a single merged word-set.
142
- *
143
- * This is the core language-specific method that must be implemented by subclasses
144
- * to define how adjacent word-sets are combined according to the language's grammar.
145
- * For example, English combines "twenty" + "three" → "twenty-three", while
146
- * French might combine "quatre-vingts" + "dix" → "quatre-vingt-dix".
147
- *
148
- * @abstract
149
- * @protected
150
- * @param {Object} leftWordSet Left operand as `{ word: bigint }` pair.
151
- * @param {Object} rightWordSet Right operand as `{ word: bigint }` pair.
152
- * @returns {Object} Single merged word-set object with combined text and numeric value.
153
- *
154
- * @example
155
- * // English implementation might handle:
156
- * // mergeScales({ 'twenty': 20n }, { 'three': 3n }) → { 'twenty-three': 23n }
157
- * // mergeScales({ 'one': 1n }, { 'hundred': 100n }) → { 'one hundred': 100n }
158
- */
159
- mergeScales (leftWordSet, rightWordSet) {
160
- throw new Error('mergeScales() must be implemented by subclass')
161
- }
162
-
163
- /**
164
- * Final string post-processing hook.
165
- *
166
- * Subclasses may override to apply language-specific whitespace, punctuation or
167
- * orthographic corrections.
168
- *
169
- * @protected
170
- * @param {string} output Merged language string produced by `convertWholePart` flow.
171
- * @returns {string} Final formatted string.
172
- */
173
- finalizeWords (output) {
174
- return output.trimEnd()
175
- }
176
-
177
- /**
178
- * Convert an integer value to its language-specific cardinal words.
179
- *
180
- * This method orchestrates decomposition, merging and final formatting. It does
181
- * not handle decimals or sign; those concerns are implemented in
182
- * `AbstractLanguage.convertToWords` which calls this helper for the whole-number part.
183
- *
184
- * @param {bigint|number} wholeNumber Whole-number value to convert (BigInt is preferred).
185
- * @returns {string} The cardinal representation for `value` in the language.
186
- */
187
- convertWholePart (wholeNumber) {
188
- const decomposeWordSets = this.decomposeToScales(wholeNumber)
189
- const mergedWordSet = this.mergeWordSets(decomposeWordSets)
190
- const resultString = Object.keys(mergedWordSet)[0]
191
- return this.finalizeWords(resultString)
192
- }
193
- }
194
-
195
- export default GreedyScaleLanguage
@@ -1,251 +0,0 @@
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