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.
- package/CHANGELOG.md +49 -0
- package/README.md +183 -156
- package/dist/languages/am-Latn.js +3 -0
- package/dist/languages/am-Latn.js.map +1 -0
- package/dist/languages/am.js +3 -0
- package/dist/languages/am.js.map +1 -0
- package/dist/languages/ar.js +3 -2
- package/dist/languages/ar.js.map +1 -1
- package/dist/languages/az.js +3 -2
- package/dist/languages/az.js.map +1 -1
- package/dist/languages/bn.js +3 -2
- package/dist/languages/bn.js.map +1 -1
- package/dist/languages/cs.js +3 -2
- package/dist/languages/cs.js.map +1 -1
- package/dist/languages/da.js +3 -2
- package/dist/languages/da.js.map +1 -1
- package/dist/languages/de.js +3 -2
- package/dist/languages/de.js.map +1 -1
- package/dist/languages/el.js +3 -2
- package/dist/languages/el.js.map +1 -1
- package/dist/languages/en.js +3 -2
- package/dist/languages/en.js.map +1 -1
- package/dist/languages/es.js +3 -2
- package/dist/languages/es.js.map +1 -1
- package/dist/languages/fa.js +3 -2
- package/dist/languages/fa.js.map +1 -1
- package/dist/languages/fi.js +3 -0
- package/dist/languages/fi.js.map +1 -0
- package/dist/languages/fil.js +3 -2
- package/dist/languages/fil.js.map +1 -1
- package/dist/languages/fr-BE.js +3 -2
- package/dist/languages/fr-BE.js.map +1 -1
- package/dist/languages/fr.js +3 -2
- package/dist/languages/fr.js.map +1 -1
- package/dist/languages/gu.js +3 -2
- package/dist/languages/gu.js.map +1 -1
- package/dist/languages/ha.js +3 -0
- package/dist/languages/ha.js.map +1 -0
- package/dist/languages/hbo.js +3 -0
- package/dist/languages/hbo.js.map +1 -0
- package/dist/languages/he.js +3 -2
- package/dist/languages/he.js.map +1 -1
- package/dist/languages/hi.js +3 -2
- package/dist/languages/hi.js.map +1 -1
- package/dist/languages/hr.js +3 -2
- package/dist/languages/hr.js.map +1 -1
- package/dist/languages/hu.js +3 -2
- package/dist/languages/hu.js.map +1 -1
- package/dist/languages/id.js +3 -2
- package/dist/languages/id.js.map +1 -1
- package/dist/languages/it.js +3 -2
- package/dist/languages/it.js.map +1 -1
- package/dist/languages/ja.js +3 -2
- package/dist/languages/ja.js.map +1 -1
- package/dist/languages/kn.js +3 -2
- package/dist/languages/kn.js.map +1 -1
- package/dist/languages/ko.js +3 -2
- package/dist/languages/ko.js.map +1 -1
- package/dist/languages/lt.js +3 -2
- package/dist/languages/lt.js.map +1 -1
- package/dist/languages/lv.js +3 -2
- package/dist/languages/lv.js.map +1 -1
- package/dist/languages/mr.js +3 -2
- package/dist/languages/mr.js.map +1 -1
- package/dist/languages/ms.js +3 -2
- package/dist/languages/ms.js.map +1 -1
- package/dist/languages/nb.js +3 -2
- package/dist/languages/nb.js.map +1 -1
- package/dist/languages/nl.js +3 -2
- package/dist/languages/nl.js.map +1 -1
- package/dist/languages/pa.js +3 -0
- package/dist/languages/pa.js.map +1 -0
- package/dist/languages/pl.js +3 -2
- package/dist/languages/pl.js.map +1 -1
- package/dist/languages/pt.js +3 -2
- package/dist/languages/pt.js.map +1 -1
- package/dist/languages/ro.js +3 -2
- package/dist/languages/ro.js.map +1 -1
- package/dist/languages/ru.js +3 -2
- package/dist/languages/ru.js.map +1 -1
- package/dist/languages/sr-Cyrl.js +3 -0
- package/dist/languages/sr-Cyrl.js.map +1 -0
- package/dist/languages/sr-Latn.js +3 -2
- package/dist/languages/sr-Latn.js.map +1 -1
- package/dist/languages/sv.js +3 -2
- package/dist/languages/sv.js.map +1 -1
- package/dist/languages/sw.js +3 -2
- package/dist/languages/sw.js.map +1 -1
- package/dist/languages/ta.js +3 -2
- package/dist/languages/ta.js.map +1 -1
- package/dist/languages/te.js +3 -2
- package/dist/languages/te.js.map +1 -1
- package/dist/languages/th.js +3 -2
- package/dist/languages/th.js.map +1 -1
- package/dist/languages/tr.js +3 -2
- package/dist/languages/tr.js.map +1 -1
- package/dist/languages/uk.js +3 -2
- package/dist/languages/uk.js.map +1 -1
- package/dist/languages/ur.js +3 -2
- package/dist/languages/ur.js.map +1 -1
- package/dist/languages/vi.js +3 -2
- package/dist/languages/vi.js.map +1 -1
- package/dist/languages/zh-Hans.js +3 -2
- package/dist/languages/zh-Hans.js.map +1 -1
- package/dist/languages/zh-Hant.js +3 -0
- package/dist/languages/zh-Hant.js.map +1 -0
- package/dist/n2words.js +3 -2
- package/dist/n2words.js.map +1 -1
- package/lib/languages/am-Latn.d.ts +7 -0
- package/lib/languages/am-Latn.js +164 -0
- package/lib/languages/am.d.ts +7 -0
- package/lib/languages/am.js +164 -0
- package/lib/languages/ar.d.ts +17 -0
- package/lib/languages/ar.js +171 -209
- package/lib/languages/az.d.ts +7 -0
- package/lib/languages/az.js +167 -49
- package/lib/languages/bn.d.ts +7 -0
- package/lib/languages/bn.js +142 -123
- package/lib/languages/cs.d.ts +18 -0
- package/lib/languages/cs.js +303 -176
- package/lib/languages/da.d.ts +14 -0
- package/lib/languages/da.js +267 -139
- package/lib/languages/de.d.ts +17 -0
- package/lib/languages/de.js +310 -113
- package/lib/languages/el.d.ts +14 -0
- package/lib/languages/el.js +225 -98
- package/lib/languages/en.d.ts +17 -0
- package/lib/languages/en.js +235 -102
- package/lib/languages/es.d.ts +21 -0
- package/lib/languages/es.js +307 -125
- package/lib/languages/fa.d.ts +7 -0
- package/lib/languages/fa.js +115 -108
- package/lib/languages/fi.d.ts +14 -0
- package/lib/languages/fi.js +245 -0
- package/lib/languages/fil.d.ts +7 -0
- package/lib/languages/fil.js +199 -139
- package/lib/languages/fr-BE.d.ts +11 -0
- package/lib/languages/fr-BE.js +287 -48
- package/lib/languages/fr.d.ts +21 -0
- package/lib/languages/fr.js +343 -119
- package/lib/languages/gu.d.ts +7 -0
- package/lib/languages/gu.js +125 -144
- package/lib/languages/ha.d.ts +7 -0
- package/lib/languages/ha.js +230 -0
- package/lib/languages/hbo.d.ts +13 -0
- package/lib/languages/hbo.js +300 -0
- package/lib/languages/he.d.ts +13 -0
- package/lib/languages/he.js +230 -283
- package/lib/languages/hi.d.ts +7 -0
- package/lib/languages/hi.js +142 -123
- package/lib/languages/hr.d.ts +11 -0
- package/lib/languages/hr.js +190 -129
- package/lib/languages/hu.d.ts +7 -0
- package/lib/languages/hu.js +194 -133
- package/lib/languages/id.d.ts +7 -0
- package/lib/languages/id.js +167 -140
- package/lib/languages/it.d.ts +19 -0
- package/lib/languages/it.js +337 -108
- package/lib/languages/ja.d.ts +17 -0
- package/lib/languages/ja.js +224 -155
- package/lib/languages/kn.d.ts +7 -0
- package/lib/languages/kn.js +128 -62
- package/lib/languages/ko.d.ts +14 -0
- package/lib/languages/ko.js +250 -70
- package/lib/languages/lt.d.ts +18 -0
- package/lib/languages/lt.js +287 -148
- package/lib/languages/lv.d.ts +18 -0
- package/lib/languages/lv.js +291 -123
- package/lib/languages/mr.d.ts +7 -0
- package/lib/languages/mr.js +125 -144
- package/lib/languages/ms.d.ts +7 -0
- package/lib/languages/ms.js +171 -112
- package/lib/languages/nb.d.ts +14 -0
- package/lib/languages/nb.js +275 -100
- package/lib/languages/nl.d.ts +26 -0
- package/lib/languages/nl.js +307 -174
- package/lib/languages/pa.d.ts +7 -0
- package/lib/languages/pa.js +163 -0
- package/lib/languages/pl.d.ts +22 -0
- package/lib/languages/pl.js +299 -158
- package/lib/languages/pt.d.ts +17 -0
- package/lib/languages/pt.js +279 -120
- package/lib/languages/ro.d.ts +18 -0
- package/lib/languages/ro.js +214 -337
- package/lib/languages/ru.d.ts +11 -0
- package/lib/languages/ru.js +219 -95
- package/lib/languages/sr-Cyrl.d.ts +11 -0
- package/lib/languages/sr-Cyrl.js +215 -0
- package/lib/languages/sr-Latn.d.ts +11 -0
- package/lib/languages/sr-Latn.js +190 -132
- package/lib/languages/sv.d.ts +14 -0
- package/lib/languages/sv.js +280 -103
- package/lib/languages/sw.d.ts +7 -0
- package/lib/languages/sw.js +135 -103
- package/lib/languages/ta.d.ts +7 -0
- package/lib/languages/ta.js +133 -205
- package/lib/languages/te.d.ts +7 -0
- package/lib/languages/te.js +148 -213
- package/lib/languages/th.d.ts +7 -0
- package/lib/languages/th.js +139 -101
- package/lib/languages/tr.d.ts +18 -0
- package/lib/languages/tr.js +246 -66
- package/lib/languages/uk.d.ts +11 -0
- package/lib/languages/uk.js +197 -101
- package/lib/languages/ur.d.ts +7 -0
- package/lib/languages/ur.js +160 -123
- package/lib/languages/vi.d.ts +17 -0
- package/lib/languages/vi.js +287 -164
- package/lib/languages/zh-Hans.d.ts +11 -0
- package/lib/languages/zh-Hans.js +159 -142
- package/lib/languages/zh-Hant.d.ts +11 -0
- package/lib/languages/zh-Hant.js +202 -0
- package/lib/n2words.d.ts +53 -0
- package/lib/n2words.js +91 -227
- package/lib/utils/is-plain-object.d.ts +13 -0
- package/lib/utils/is-plain-object.js +17 -0
- package/lib/utils/parse-numeric.d.ts +17 -0
- package/lib/utils/parse-numeric.js +108 -0
- package/lib/utils/validate-options.d.ts +8 -0
- package/lib/utils/validate-options.js +16 -0
- package/package.json +118 -67
- package/dist/languages/pa-Guru.js +0 -2
- package/dist/languages/pa-Guru.js.map +0 -1
- package/lib/classes/abstract-language.js +0 -261
- package/lib/classes/greedy-scale-language.js +0 -195
- package/lib/classes/slavic-language.js +0 -251
- package/lib/classes/south-asian-language.js +0 -161
- package/lib/classes/turkic-language.js +0 -63
- package/lib/languages/pa-Guru.js +0 -126
- package/typings/classes/abstract-language.d.ts +0 -144
- package/typings/classes/greedy-scale-language.d.ts +0 -148
- package/typings/classes/slavic-language.d.ts +0 -145
- package/typings/classes/south-asian-language.d.ts +0 -101
- package/typings/classes/turkic-language.d.ts +0 -42
- package/typings/languages/ar.d.ts +0 -93
- package/typings/languages/az.d.ts +0 -25
- package/typings/languages/bn.d.ts +0 -1
- package/typings/languages/cs.d.ts +0 -120
- package/typings/languages/da.d.ts +0 -53
- package/typings/languages/de.d.ts +0 -26
- package/typings/languages/el.d.ts +0 -11
- package/typings/languages/en.d.ts +0 -30
- package/typings/languages/es.d.ts +0 -43
- package/typings/languages/fa.d.ts +0 -81
- package/typings/languages/fil.d.ts +0 -12
- package/typings/languages/fr-BE.d.ts +0 -41
- package/typings/languages/fr.d.ts +0 -43
- package/typings/languages/gu.d.ts +0 -12
- package/typings/languages/he.d.ts +0 -197
- package/typings/languages/hi.d.ts +0 -1
- package/typings/languages/hr.d.ts +0 -110
- package/typings/languages/hu.d.ts +0 -37
- package/typings/languages/id.d.ts +0 -69
- package/typings/languages/it.d.ts +0 -51
- package/typings/languages/ja.d.ts +0 -58
- package/typings/languages/kn.d.ts +0 -11
- package/typings/languages/ko.d.ts +0 -25
- package/typings/languages/lt.d.ts +0 -110
- package/typings/languages/lv.d.ts +0 -99
- package/typings/languages/mr.d.ts +0 -12
- package/typings/languages/ms.d.ts +0 -37
- package/typings/languages/nb.d.ts +0 -27
- package/typings/languages/nl.d.ts +0 -65
- package/typings/languages/pa-Guru.d.ts +0 -1
- package/typings/languages/pl.d.ts +0 -116
- package/typings/languages/pt.d.ts +0 -39
- package/typings/languages/ro.d.ts +0 -229
- package/typings/languages/ru.d.ts +0 -108
- package/typings/languages/sr-Latn.d.ts +0 -98
- package/typings/languages/sv.d.ts +0 -30
- package/typings/languages/sw.d.ts +0 -1
- package/typings/languages/ta.d.ts +0 -1
- package/typings/languages/te.d.ts +0 -1
- package/typings/languages/th.d.ts +0 -1
- package/typings/languages/tr.d.ts +0 -46
- package/typings/languages/uk.d.ts +0 -117
- package/typings/languages/ur.d.ts +0 -1
- package/typings/languages/vi.d.ts +0 -116
- package/typings/languages/zh-Hans.d.ts +0 -57
- package/typings/n2words.d.ts +0 -177
package/lib/languages/ru.js
CHANGED
|
@@ -1,116 +1,240 @@
|
|
|
1
|
-
import SlavicLanguage from '../classes/slavic-language.js'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
210
|
+
* Converts a numeric value to Russian words.
|
|
107
211
|
*
|
|
108
|
-
* @param {number|string|bigint} value The
|
|
109
|
-
* @param {
|
|
110
|
-
* @
|
|
111
|
-
* @
|
|
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
|
-
|
|
115
|
-
|
|
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;
|