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/vi.js
CHANGED
|
@@ -1,193 +1,316 @@
|
|
|
1
|
-
import AbstractLanguage from '../classes/abstract-language.js'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Vietnamese language converter
|
|
2
|
+
* Vietnamese language converter - Functional Implementation v2
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* - Special pronunciation rules for 5 and 15 (lăm instead of năm)
|
|
9
|
-
* - Special pronunciation for final 1 in compound numbers (mốt instead of một)
|
|
10
|
-
* - "Lẻ" (odd/extra) used when tens place is zero but units exist
|
|
4
|
+
* A performance-optimized number-to-words converter using precomputed lookup tables.
|
|
5
|
+
* Self-contained module with its own input validation, ready for subpath exports.
|
|
11
6
|
*
|
|
12
|
-
* Key
|
|
13
|
-
*
|
|
14
|
-
* - Tens mapping (hai mươi, ba mươi, etc.)
|
|
15
|
-
* - Group-based algorithm:
|
|
16
|
-
* 1. Split number into groups of 3 digits
|
|
17
|
-
* 2. For each group, build words using special pronunciation rules:
|
|
18
|
-
* - "Mốt" instead of "một" for final 1 (e.g., 21 → hai mươi mốt)
|
|
19
|
-
* - "Lăm" instead of "năm" for 15 and mid-group 5 (e.g., 15, 105)
|
|
20
|
-
* - "Lẻ" prefix when tens=0 but units>0 (e.g., 101 → một trăm lẻ một)
|
|
21
|
-
* 3. Append magnitude word (nghìn/triệu/tỷ)
|
|
22
|
-
* 4. Join all groups with spaces
|
|
23
|
-
* - Support for large numbers up to vigintillions
|
|
7
|
+
* Key optimization: Precompute all segment values (0-999) at module load.
|
|
8
|
+
* This eliminates all per-call string manipulation for segment conversion.
|
|
24
9
|
*
|
|
25
|
-
*
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
10
|
+
* Vietnamese-specific rules (handled in precomputation):
|
|
11
|
+
* - Special pronunciation: "lăm" for 5 in tens position, "mốt" for final 1
|
|
12
|
+
* - "Lẻ" (odd/extra) marker when tens place is zero after hundreds/scales
|
|
13
|
+
* - Short scale system with Vietnamese words (nghìn, triệu, tỷ)
|
|
29
14
|
*/
|
|
30
|
-
export class Vietnamese extends AbstractLanguage {
|
|
31
|
-
negativeWord = 'âm'
|
|
32
|
-
decimalSeparatorWord = 'phẩy'
|
|
33
|
-
zeroWord = 'không'
|
|
34
|
-
base = {
|
|
35
|
-
0: 'không',
|
|
36
|
-
1: 'một',
|
|
37
|
-
2: 'hai',
|
|
38
|
-
3: 'ba',
|
|
39
|
-
4: 'bốn',
|
|
40
|
-
5: 'năm',
|
|
41
|
-
6: 'sáu',
|
|
42
|
-
7: 'bảy',
|
|
43
|
-
8: 'tám',
|
|
44
|
-
9: 'chín',
|
|
45
|
-
10: 'mười',
|
|
46
|
-
11: 'mười một',
|
|
47
|
-
12: 'mười hai',
|
|
48
|
-
13: 'mười ba',
|
|
49
|
-
14: 'mười bốn',
|
|
50
|
-
15: 'mười lăm',
|
|
51
|
-
16: 'mười sáu',
|
|
52
|
-
17: 'mười bảy',
|
|
53
|
-
18: 'mười tám',
|
|
54
|
-
19: 'mười chín'
|
|
55
|
-
}
|
|
56
15
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
16
|
+
import { parseNumericValue } from '../utils/parse-numeric.js'
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Vocabulary (module-level constants)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
// Base vocabulary for building lookup tables
|
|
23
|
+
const ONES = ['không', 'một', 'hai', 'ba', 'bốn', 'năm', 'sáu', 'bảy', 'tám', 'chín']
|
|
24
|
+
|
|
25
|
+
// Scale words indexed by scale level (0 = units, 1 = thousands, etc.)
|
|
26
|
+
const SCALES = [
|
|
27
|
+
'', 'nghìn', 'triệu', 'tỷ', 'nghìn tỷ', 'trăm nghìn tỷ',
|
|
28
|
+
'Quintillion', 'Sextillion', 'Septillion', 'Octillion',
|
|
29
|
+
'Nonillion', 'Decillion', 'Undecillion', 'Duodecillion',
|
|
30
|
+
'Tredecillion', 'Quattuordecillion', 'Sexdecillion',
|
|
31
|
+
'Septendecillion', 'Octodecillion', 'Novemdecillion', 'Vigintillion'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const HUNDRED = 'trăm'
|
|
35
|
+
const ZERO = 'không'
|
|
36
|
+
const NEGATIVE = 'âm'
|
|
37
|
+
const DECIMAL_SEP = 'phẩy'
|
|
38
|
+
const LE = 'lẻ' // "odd/extra" marker for gaps
|
|
39
|
+
|
|
40
|
+
// Special forms
|
|
41
|
+
const MOT_FINAL = 'mốt' // 1 in tens position (21, 31, etc.)
|
|
42
|
+
const LAM = 'lăm' // 5 in tens position (25, 35, etc.)
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Precomputed Lookup Tables (built once at module load)
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Builds word for 0-99 with special forms (mốt, lăm).
|
|
50
|
+
* Only used during table construction.
|
|
51
|
+
*/
|
|
52
|
+
function buildBelowHundred (n) {
|
|
53
|
+
if (n === 0) return ONES[0]
|
|
54
|
+
if (n < 10) return ONES[n]
|
|
67
55
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
6: 'Quintillion',
|
|
75
|
-
7: 'Sextillion',
|
|
76
|
-
8: 'Septillion',
|
|
77
|
-
9: 'Octillion',
|
|
78
|
-
10: 'Nonillion',
|
|
79
|
-
11: 'Decillion',
|
|
80
|
-
12: 'Undecillion',
|
|
81
|
-
13: 'Duodecillion',
|
|
82
|
-
14: 'Tredecillion',
|
|
83
|
-
15: 'Quattuordecillion',
|
|
84
|
-
16: 'Sexdecillion',
|
|
85
|
-
17: 'Septendecillion',
|
|
86
|
-
18: 'Octodecillion',
|
|
87
|
-
19: 'Novemdecillion',
|
|
88
|
-
20: 'Vigintillion'
|
|
56
|
+
// Teens: 10-19
|
|
57
|
+
if (n < 20) {
|
|
58
|
+
const ones = n - 10
|
|
59
|
+
if (ones === 0) return 'mười'
|
|
60
|
+
if (ones === 5) return 'mười lăm'
|
|
61
|
+
return 'mười ' + ONES[ones]
|
|
89
62
|
}
|
|
90
63
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
64
|
+
// 20-99
|
|
65
|
+
const ones = n % 10
|
|
66
|
+
const tens = Math.floor(n / 10)
|
|
67
|
+
const tensWord = ONES[tens] + ' mươi'
|
|
68
|
+
|
|
69
|
+
if (ones === 0) return tensWord
|
|
70
|
+
if (ones === 1) return tensWord + ' ' + MOT_FINAL
|
|
71
|
+
if (ones === 5) return tensWord + ' ' + LAM
|
|
72
|
+
return tensWord + ' ' + ONES[ones]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds segment word for 0-999.
|
|
77
|
+
* Only used during table construction.
|
|
78
|
+
*/
|
|
79
|
+
function buildSegment (n) {
|
|
80
|
+
if (n === 0) return ''
|
|
81
|
+
|
|
82
|
+
const hundreds = Math.floor(n / 100)
|
|
83
|
+
const remainder = n % 100
|
|
84
|
+
|
|
85
|
+
let result = ''
|
|
86
|
+
|
|
87
|
+
if (hundreds > 0) {
|
|
88
|
+
result = ONES[hundreds] + ' ' + HUNDRED
|
|
113
89
|
}
|
|
114
90
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const words = []
|
|
123
|
-
const tensUnitsPart = number % 100
|
|
124
|
-
const hundredsPart = number - tensUnitsPart
|
|
125
|
-
if (hundredsPart > 0) {
|
|
126
|
-
words.push(this.base[hundredsPart / 100], 'trăm')
|
|
127
|
-
}
|
|
128
|
-
if (tensUnitsPart > 0 && tensUnitsPart < 10) {
|
|
129
|
-
if (words.length > 0) {
|
|
130
|
-
words.push('lẻ')
|
|
131
|
-
}
|
|
132
|
-
if (tensUnitsPart === 5) {
|
|
133
|
-
words.push('năm')
|
|
91
|
+
if (remainder > 0) {
|
|
92
|
+
if (remainder < 10) {
|
|
93
|
+
// Single digit after hundreds needs "lẻ"
|
|
94
|
+
if (result) {
|
|
95
|
+
result += ' ' + LE + ' '
|
|
96
|
+
// Use "năm" not "lăm" after lẻ
|
|
97
|
+
result += remainder === 5 ? 'năm' : ONES[remainder]
|
|
134
98
|
} else {
|
|
135
|
-
|
|
99
|
+
result = ONES[remainder]
|
|
136
100
|
}
|
|
101
|
+
} else {
|
|
102
|
+
// 10-99 after hundreds
|
|
103
|
+
if (result) result += ' '
|
|
104
|
+
result += BELOW_100[remainder]
|
|
137
105
|
}
|
|
138
|
-
if (tensUnitsPart >= 10) {
|
|
139
|
-
words.push(this.convertWholePart(tensUnitsPart))
|
|
140
|
-
}
|
|
141
|
-
return words.join(' ')
|
|
142
106
|
}
|
|
143
107
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Precompute all 100 below-hundred words (0-99)
|
|
112
|
+
// BELOW_100[n] gives the Vietnamese word for n
|
|
113
|
+
const BELOW_100 = new Array(100)
|
|
114
|
+
for (let i = 0; i < 100; i++) {
|
|
115
|
+
BELOW_100[i] = buildBelowHundred(i)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Precompute all 1000 segment words (0-999)
|
|
119
|
+
// SEGMENTS[n] gives the Vietnamese word for n within a segment
|
|
120
|
+
const SEGMENTS = new Array(1000)
|
|
121
|
+
for (let i = 0; i < 1000; i++) {
|
|
122
|
+
SEGMENTS[i] = buildSegment(i)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Precompute "lẻ" prefixed versions for small remainders after scale words
|
|
126
|
+
// LE_SEGMENTS[n] gives "lẻ X" for n in range 1-99
|
|
127
|
+
const LE_SEGMENTS = new Array(100)
|
|
128
|
+
LE_SEGMENTS[0] = ''
|
|
129
|
+
for (let i = 1; i < 10; i++) {
|
|
130
|
+
// Use "năm" not "lăm" after lẻ
|
|
131
|
+
LE_SEGMENTS[i] = LE + ' ' + (i === 5 ? 'năm' : ONES[i])
|
|
132
|
+
}
|
|
133
|
+
for (let i = 10; i < 100; i++) {
|
|
134
|
+
LE_SEGMENTS[i] = LE + ' ' + BELOW_100[i]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Conversion Functions
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Converts a non-negative integer to Vietnamese words.
|
|
143
|
+
*
|
|
144
|
+
* @param {bigint} n - Non-negative integer to convert
|
|
145
|
+
* @returns {string} Vietnamese words
|
|
146
|
+
*/
|
|
147
|
+
function integerToWords (n) {
|
|
148
|
+
if (n === 0n) return ZERO
|
|
149
|
+
|
|
150
|
+
// Fast path: numbers < 100 (direct lookup)
|
|
151
|
+
if (n < 100n) {
|
|
152
|
+
return BELOW_100[Number(n)]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Fast path: numbers < 1000 (direct lookup)
|
|
156
|
+
if (n < 1000n) {
|
|
157
|
+
return SEGMENTS[Number(n)]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fast path: numbers < 1,000,000 (thousands)
|
|
161
|
+
if (n < 1_000_000n) {
|
|
162
|
+
const thousands = Number(n / 1000n)
|
|
163
|
+
const remainder = Number(n % 1000n)
|
|
164
|
+
|
|
165
|
+
const thousandsWords = SEGMENTS[thousands] + ' ' + SCALES[1]
|
|
166
|
+
|
|
167
|
+
if (remainder === 0) {
|
|
168
|
+
return thousandsWords
|
|
157
169
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
words.push('lẻ')
|
|
163
|
-
}
|
|
164
|
-
words.push(this.convertWholePart(r))
|
|
170
|
+
|
|
171
|
+
// Check if remainder needs "lẻ" marker (< 100)
|
|
172
|
+
if (remainder < 100) {
|
|
173
|
+
return thousandsWords + ' ' + LE_SEGMENTS[remainder]
|
|
165
174
|
}
|
|
166
|
-
|
|
175
|
+
|
|
176
|
+
return thousandsWords + ' ' + SEGMENTS[remainder]
|
|
167
177
|
}
|
|
168
178
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
// For numbers >= 1,000,000, use scale decomposition
|
|
180
|
+
return buildLargeNumberWords(n)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Builds words for numbers >= 1,000,000.
|
|
185
|
+
*
|
|
186
|
+
* @param {bigint} n - Number >= 1,000,000
|
|
187
|
+
* @returns {string} Vietnamese words
|
|
188
|
+
*/
|
|
189
|
+
function buildLargeNumberWords (n) {
|
|
190
|
+
const numStr = n.toString()
|
|
191
|
+
const len = numStr.length
|
|
192
|
+
|
|
193
|
+
// Build segments of 3 digits from right to left
|
|
194
|
+
const segments = []
|
|
195
|
+
const segmentSize = 3
|
|
196
|
+
|
|
197
|
+
const remainderLen = len % segmentSize
|
|
198
|
+
let pos = 0
|
|
199
|
+
if (remainderLen > 0) {
|
|
200
|
+
segments.push(Number(numStr.slice(0, remainderLen)))
|
|
201
|
+
pos = remainderLen
|
|
202
|
+
}
|
|
203
|
+
while (pos < len) {
|
|
204
|
+
segments.push(Number(numStr.slice(pos, pos + segmentSize)))
|
|
205
|
+
pos += segmentSize
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Convert segments to words
|
|
209
|
+
const parts = []
|
|
210
|
+
let scaleIndex = segments.length - 1
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < segments.length; i++) {
|
|
213
|
+
const segment = segments[i]
|
|
214
|
+
if (segment !== 0) {
|
|
215
|
+
const words = SEGMENTS[segment]
|
|
216
|
+
if (words) {
|
|
217
|
+
if (scaleIndex > 0) {
|
|
218
|
+
parts.push(words + ' ' + SCALES[scaleIndex])
|
|
219
|
+
} else {
|
|
220
|
+
parts.push(words)
|
|
221
|
+
}
|
|
177
222
|
}
|
|
178
223
|
}
|
|
224
|
+
scaleIndex--
|
|
179
225
|
}
|
|
226
|
+
|
|
227
|
+
// Join with "lẻ" logic for small remainders
|
|
228
|
+
const partsLen = parts.length
|
|
229
|
+
if (partsLen === 0) return ZERO
|
|
230
|
+
if (partsLen === 1) return parts[0]
|
|
231
|
+
|
|
232
|
+
// Check if final segment needs "lẻ" marker (remainder <= 99 after scale word)
|
|
233
|
+
const lastSegment = segments[segments.length - 1]
|
|
234
|
+
if (lastSegment > 0 && lastSegment <= 99) {
|
|
235
|
+
// Last segment is small (no hundreds), needs "lẻ" after scale word
|
|
236
|
+
let result = parts[0]
|
|
237
|
+
for (let i = 1; i < partsLen - 1; i++) {
|
|
238
|
+
result += ' ' + parts[i]
|
|
239
|
+
}
|
|
240
|
+
return result + ' ' + LE_SEGMENTS[lastSegment]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Join with spaces
|
|
244
|
+
let result = parts[0]
|
|
245
|
+
for (let i = 1; i < partsLen; i++) {
|
|
246
|
+
result += ' ' + parts[i]
|
|
247
|
+
}
|
|
248
|
+
return result
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Converts decimal digits to Vietnamese words.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} decimalPart - Decimal digits (without the point)
|
|
255
|
+
* @returns {string} Vietnamese words for decimal part
|
|
256
|
+
*/
|
|
257
|
+
function decimalPartToWords (decimalPart) {
|
|
258
|
+
let result = ''
|
|
259
|
+
|
|
260
|
+
// Handle leading zeros
|
|
261
|
+
let i = 0
|
|
262
|
+
while (i < decimalPart.length && decimalPart[i] === '0') {
|
|
263
|
+
if (result) result += ' '
|
|
264
|
+
result += ZERO
|
|
265
|
+
i++
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Convert remainder as a single number
|
|
269
|
+
const remainder = decimalPart.slice(i)
|
|
270
|
+
if (remainder) {
|
|
271
|
+
if (result) result += ' '
|
|
272
|
+
result += integerToWords(BigInt(remainder))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result
|
|
180
276
|
}
|
|
181
277
|
|
|
182
278
|
/**
|
|
183
|
-
* Converts a
|
|
279
|
+
* Converts a numeric value to Vietnamese words.
|
|
184
280
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* @
|
|
189
|
-
* @
|
|
281
|
+
* This is the main public API. It accepts any valid numeric input
|
|
282
|
+
* (number, string, or bigint) and handles parsing internally.
|
|
283
|
+
*
|
|
284
|
+
* @param {number | string | bigint} value - The numeric value to convert
|
|
285
|
+
* @returns {string} The number in Vietnamese words
|
|
286
|
+
* @throws {TypeError} If value is not a valid numeric type
|
|
287
|
+
* @throws {Error} If value is not a valid number format
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* toWords(42) // 'bốn mươi hai'
|
|
291
|
+
* toWords(101) // 'một trăm lẻ một'
|
|
292
|
+
* toWords(1000000) // 'một triệu'
|
|
190
293
|
*/
|
|
191
|
-
|
|
192
|
-
|
|
294
|
+
function toWords (value) {
|
|
295
|
+
const { isNegative, integerPart, decimalPart } = parseNumericValue(value)
|
|
296
|
+
|
|
297
|
+
let result = ''
|
|
298
|
+
|
|
299
|
+
if (isNegative) {
|
|
300
|
+
result = NEGATIVE + ' '
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
result += integerToWords(integerPart)
|
|
304
|
+
|
|
305
|
+
if (decimalPart) {
|
|
306
|
+
result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return result
|
|
193
310
|
}
|
|
311
|
+
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// Public API
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
export { toWords }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a numeric value to Simplified Chinese words.
|
|
3
|
+
*
|
|
4
|
+
* @param {number | string | bigint} value - The numeric value to convert
|
|
5
|
+
* @param {Object} [options] - Optional configuration
|
|
6
|
+
* @param {boolean} [options.formal=true] - Use formal/financial numerals
|
|
7
|
+
* @returns {string} The number in Simplified Chinese words
|
|
8
|
+
*/
|
|
9
|
+
export function toWords(value: number | string | bigint, options?: {
|
|
10
|
+
formal?: boolean | undefined;
|
|
11
|
+
}): string;
|