n2words 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +182 -53
- package/dist/languages/ar.js +2 -0
- package/dist/languages/ar.js.map +1 -0
- package/dist/languages/az.js +2 -0
- package/dist/languages/az.js.map +1 -0
- package/dist/languages/bn.js +2 -0
- package/dist/languages/bn.js.map +1 -0
- package/dist/languages/cs.js +2 -0
- package/dist/languages/cs.js.map +1 -0
- package/dist/languages/da.js +2 -0
- package/dist/languages/da.js.map +1 -0
- package/dist/languages/de.js +2 -0
- package/dist/languages/de.js.map +1 -0
- package/dist/languages/el.js +2 -0
- package/dist/languages/el.js.map +1 -0
- package/dist/languages/en.js +2 -0
- package/dist/languages/en.js.map +1 -0
- package/dist/languages/es.js +2 -0
- package/dist/languages/es.js.map +1 -0
- package/dist/languages/fa.js +2 -0
- package/dist/languages/fa.js.map +1 -0
- package/dist/languages/fil.js +2 -0
- package/dist/languages/fil.js.map +1 -0
- package/dist/languages/fr-BE.js +2 -0
- package/dist/languages/fr-BE.js.map +1 -0
- package/dist/languages/fr.js +2 -0
- package/dist/languages/fr.js.map +1 -0
- package/dist/languages/gu.js +2 -0
- package/dist/languages/gu.js.map +1 -0
- package/dist/languages/he.js +2 -0
- package/dist/languages/he.js.map +1 -0
- package/dist/languages/hi.js +2 -0
- package/dist/languages/hi.js.map +1 -0
- package/dist/languages/hr.js +2 -0
- package/dist/languages/hr.js.map +1 -0
- package/dist/languages/hu.js +2 -0
- package/dist/languages/hu.js.map +1 -0
- package/dist/languages/id.js +2 -0
- package/dist/languages/id.js.map +1 -0
- package/dist/languages/it.js +2 -0
- package/dist/languages/it.js.map +1 -0
- package/dist/languages/ja.js +2 -0
- package/dist/languages/ja.js.map +1 -0
- package/dist/languages/kn.js +2 -0
- package/dist/languages/kn.js.map +1 -0
- package/dist/languages/ko.js +2 -0
- package/dist/languages/ko.js.map +1 -0
- package/dist/languages/lt.js +2 -0
- package/dist/languages/lt.js.map +1 -0
- package/dist/languages/lv.js +2 -0
- package/dist/languages/lv.js.map +1 -0
- package/dist/languages/mr.js +2 -0
- package/dist/languages/mr.js.map +1 -0
- package/dist/languages/ms.js +2 -0
- package/dist/languages/ms.js.map +1 -0
- package/dist/languages/nb.js +2 -0
- package/dist/languages/nb.js.map +1 -0
- package/dist/languages/nl.js +2 -0
- package/dist/languages/nl.js.map +1 -0
- package/dist/languages/pa-Guru.js +2 -0
- package/dist/languages/pa-Guru.js.map +1 -0
- package/dist/languages/pl.js +2 -0
- package/dist/languages/pl.js.map +1 -0
- package/dist/languages/pt.js +2 -0
- package/dist/languages/pt.js.map +1 -0
- package/dist/languages/ro.js +2 -0
- package/dist/languages/ro.js.map +1 -0
- package/dist/languages/ru.js +2 -0
- package/dist/languages/ru.js.map +1 -0
- package/dist/languages/sr-Latn.js +2 -0
- package/dist/languages/sr-Latn.js.map +1 -0
- package/dist/languages/sv.js +2 -0
- package/dist/languages/sv.js.map +1 -0
- package/dist/languages/sw.js +2 -0
- package/dist/languages/sw.js.map +1 -0
- package/dist/languages/ta.js +2 -0
- package/dist/languages/ta.js.map +1 -0
- package/dist/languages/te.js +2 -0
- package/dist/languages/te.js.map +1 -0
- package/dist/languages/th.js +2 -0
- package/dist/languages/th.js.map +1 -0
- package/dist/languages/tr.js +2 -0
- package/dist/languages/tr.js.map +1 -0
- package/dist/languages/uk.js +2 -0
- package/dist/languages/uk.js.map +1 -0
- package/dist/languages/ur.js +2 -0
- package/dist/languages/ur.js.map +1 -0
- package/dist/languages/vi.js +2 -0
- package/dist/languages/vi.js.map +1 -0
- package/dist/languages/zh-Hans.js +2 -0
- package/dist/languages/zh-Hans.js.map +1 -0
- package/dist/n2words.js +1 -1
- package/dist/n2words.js.map +1 -1
- package/lib/classes/abstract-language.js +211 -110
- package/lib/classes/greedy-scale-language.js +195 -0
- package/lib/classes/slavic-language.js +251 -0
- package/lib/classes/south-asian-language.js +161 -0
- package/lib/classes/turkic-language.js +63 -0
- package/lib/languages/ar.js +243 -0
- package/lib/languages/az.js +58 -0
- package/lib/languages/bn.js +126 -0
- package/lib/languages/cs.js +212 -0
- package/lib/languages/da.js +167 -0
- package/lib/languages/de.js +135 -0
- package/lib/languages/el.js +116 -0
- package/lib/languages/en.js +123 -0
- package/lib/languages/es.js +153 -0
- package/lib/languages/fa.js +127 -0
- package/lib/languages/fil.js +162 -0
- package/lib/languages/fr-BE.js +61 -0
- package/lib/languages/fr.js +145 -0
- package/lib/languages/gu.js +156 -0
- package/lib/languages/he.js +329 -0
- package/lib/languages/hi.js +126 -0
- package/lib/languages/hr.js +157 -0
- package/lib/languages/hu.js +155 -0
- package/lib/languages/id.js +174 -0
- package/lib/languages/it.js +148 -0
- package/lib/languages/ja.js +190 -0
- package/lib/languages/kn.js +71 -0
- package/lib/languages/ko.js +83 -0
- package/lib/languages/lt.js +171 -0
- package/lib/languages/lv.js +153 -0
- package/lib/languages/mr.js +156 -0
- package/lib/languages/ms.js +146 -0
- package/lib/languages/nb.js +120 -0
- package/lib/languages/nl.js +206 -0
- package/lib/languages/pa-Guru.js +126 -0
- package/lib/languages/pl.js +189 -0
- package/lib/languages/pt.js +147 -0
- package/lib/languages/ro.js +380 -0
- package/lib/languages/ru.js +116 -0
- package/lib/languages/sr-Latn.js +157 -0
- package/lib/languages/sv.js +127 -0
- package/lib/languages/sw.js +121 -0
- package/lib/languages/ta.js +226 -0
- package/lib/languages/te.js +229 -0
- package/lib/languages/th.js +123 -0
- package/lib/languages/tr.js +83 -0
- package/lib/{i18n → languages}/uk.js +50 -23
- package/lib/languages/ur.js +126 -0
- package/lib/languages/vi.js +193 -0
- package/lib/languages/zh-Hans.js +165 -0
- package/lib/n2words.js +246 -75
- package/package.json +80 -72
- package/typings/classes/abstract-language.d.ts +144 -0
- package/typings/classes/greedy-scale-language.d.ts +148 -0
- package/typings/classes/slavic-language.d.ts +145 -0
- package/typings/classes/south-asian-language.d.ts +101 -0
- package/typings/classes/turkic-language.d.ts +42 -0
- package/typings/languages/ar.d.ts +93 -0
- package/typings/languages/az.d.ts +25 -0
- package/typings/languages/bn.d.ts +1 -0
- package/typings/languages/cs.d.ts +120 -0
- package/typings/languages/da.d.ts +53 -0
- package/typings/languages/de.d.ts +26 -0
- package/typings/languages/el.d.ts +11 -0
- package/typings/languages/en.d.ts +30 -0
- package/typings/languages/es.d.ts +43 -0
- package/typings/languages/fa.d.ts +81 -0
- package/typings/languages/fil.d.ts +12 -0
- package/typings/languages/fr-BE.d.ts +41 -0
- package/typings/languages/fr.d.ts +43 -0
- package/typings/languages/gu.d.ts +12 -0
- package/typings/languages/he.d.ts +197 -0
- package/typings/languages/hi.d.ts +1 -0
- package/typings/languages/hr.d.ts +110 -0
- package/typings/languages/hu.d.ts +37 -0
- package/typings/languages/id.d.ts +69 -0
- package/typings/languages/it.d.ts +51 -0
- package/typings/languages/ja.d.ts +58 -0
- package/typings/languages/kn.d.ts +11 -0
- package/typings/languages/ko.d.ts +25 -0
- package/typings/languages/lt.d.ts +110 -0
- package/typings/languages/lv.d.ts +99 -0
- package/typings/languages/mr.d.ts +12 -0
- package/typings/languages/ms.d.ts +37 -0
- package/typings/languages/nb.d.ts +27 -0
- package/typings/languages/nl.d.ts +65 -0
- package/typings/languages/pa-Guru.d.ts +1 -0
- package/typings/languages/pl.d.ts +116 -0
- package/typings/languages/pt.d.ts +39 -0
- package/typings/languages/ro.d.ts +229 -0
- package/typings/languages/ru.d.ts +108 -0
- package/typings/languages/sr-Latn.d.ts +98 -0
- package/typings/languages/sv.d.ts +30 -0
- package/typings/languages/sw.d.ts +1 -0
- package/typings/languages/ta.d.ts +1 -0
- package/typings/languages/te.d.ts +1 -0
- package/typings/languages/th.d.ts +1 -0
- package/typings/languages/tr.d.ts +46 -0
- package/typings/languages/uk.d.ts +117 -0
- package/typings/languages/ur.d.ts +1 -0
- package/typings/languages/vi.d.ts +116 -0
- package/typings/languages/zh-Hans.d.ts +57 -0
- package/typings/n2words.d.ts +177 -0
- package/dist/ar.js +0 -2
- package/dist/ar.js.map +0 -1
- package/dist/az.js +0 -2
- package/dist/az.js.map +0 -1
- package/dist/cz.js +0 -2
- package/dist/cz.js.map +0 -1
- package/dist/de.js +0 -2
- package/dist/de.js.map +0 -1
- package/dist/dk.js +0 -2
- package/dist/dk.js.map +0 -1
- package/dist/en.js +0 -2
- package/dist/en.js.map +0 -1
- package/dist/es.js +0 -2
- package/dist/es.js.map +0 -1
- package/dist/fa.js +0 -2
- package/dist/fa.js.map +0 -1
- package/dist/fr-BE.js +0 -2
- package/dist/fr-BE.js.map +0 -1
- package/dist/fr.js +0 -2
- package/dist/fr.js.map +0 -1
- package/dist/he.js +0 -2
- package/dist/he.js.map +0 -1
- package/dist/hr.js +0 -2
- package/dist/hr.js.map +0 -1
- package/dist/hu.js +0 -2
- package/dist/hu.js.map +0 -1
- package/dist/id.js +0 -2
- package/dist/id.js.map +0 -1
- package/dist/it.js +0 -2
- package/dist/it.js.map +0 -1
- package/dist/ko.js +0 -2
- package/dist/ko.js.map +0 -1
- package/dist/lt.js +0 -2
- package/dist/lt.js.map +0 -1
- package/dist/lv.js +0 -2
- package/dist/lv.js.map +0 -1
- package/dist/n2words.d.ts +0 -2
- package/dist/nl.js +0 -2
- package/dist/nl.js.map +0 -1
- package/dist/no.js +0 -2
- package/dist/no.js.map +0 -1
- package/dist/pl.js +0 -2
- package/dist/pl.js.map +0 -1
- package/dist/pt.js +0 -2
- package/dist/pt.js.map +0 -1
- package/dist/ro.js +0 -2
- package/dist/ro.js.map +0 -1
- package/dist/ru.js +0 -2
- package/dist/ru.js.map +0 -1
- package/dist/sr.js +0 -2
- package/dist/sr.js.map +0 -1
- package/dist/tr.js +0 -2
- package/dist/tr.js.map +0 -1
- package/dist/uk.js +0 -2
- package/dist/uk.js.map +0 -1
- package/dist/vi.js +0 -2
- package/dist/vi.js.map +0 -1
- package/dist/zh.js +0 -2
- package/dist/zh.js.map +0 -1
- package/lib/classes/abstract-language.d.ts +0 -54
- package/lib/classes/base-language.d.ts +0 -58
- package/lib/classes/base-language.js +0 -172
- package/lib/i18n/ar.d.ts +0 -41
- package/lib/i18n/ar.js +0 -209
- package/lib/i18n/az.d.ts +0 -15
- package/lib/i18n/az.js +0 -66
- package/lib/i18n/cz.d.ts +0 -68
- package/lib/i18n/cz.js +0 -135
- package/lib/i18n/de.d.ts +0 -17
- package/lib/i18n/de.js +0 -103
- package/lib/i18n/dk.d.ts +0 -14
- package/lib/i18n/dk.js +0 -110
- package/lib/i18n/en.d.ts +0 -22
- package/lib/i18n/en.js +0 -86
- package/lib/i18n/es.d.ts +0 -16
- package/lib/i18n/es.js +0 -110
- package/lib/i18n/fa.d.ts +0 -54
- package/lib/i18n/fa.js +0 -106
- package/lib/i18n/fr-BE.d.ts +0 -11
- package/lib/i18n/fr-BE.js +0 -20
- package/lib/i18n/fr.d.ts +0 -15
- package/lib/i18n/fr.js +0 -99
- package/lib/i18n/he.d.ts +0 -61
- package/lib/i18n/he.js +0 -132
- package/lib/i18n/hr.d.ts +0 -68
- package/lib/i18n/hr.js +0 -129
- package/lib/i18n/hu.d.ts +0 -17
- package/lib/i18n/hu.js +0 -135
- package/lib/i18n/id.d.ts +0 -43
- package/lib/i18n/id.js +0 -156
- package/lib/i18n/it.d.ts +0 -29
- package/lib/i18n/it.js +0 -137
- package/lib/i18n/ko.d.ts +0 -15
- package/lib/i18n/ko.js +0 -56
- package/lib/i18n/lt.d.ts +0 -68
- package/lib/i18n/lt.js +0 -138
- package/lib/i18n/lv.d.ts +0 -57
- package/lib/i18n/lv.js +0 -120
- package/lib/i18n/nl.d.ts +0 -20
- package/lib/i18n/nl.js +0 -125
- package/lib/i18n/no.d.ts +0 -15
- package/lib/i18n/no.js +0 -77
- package/lib/i18n/pl.d.ts +0 -67
- package/lib/i18n/pl.js +0 -126
- package/lib/i18n/pt.d.ts +0 -26
- package/lib/i18n/pt.js +0 -118
- package/lib/i18n/ro.d.ts +0 -109
- package/lib/i18n/ro.js +0 -360
- package/lib/i18n/ru.d.ts +0 -30
- package/lib/i18n/ru.js +0 -198
- package/lib/i18n/sr.d.ts +0 -56
- package/lib/i18n/sr.js +0 -127
- package/lib/i18n/tr.d.ts +0 -15
- package/lib/i18n/tr.js +0 -64
- package/lib/i18n/uk.d.ts +0 -78
- package/lib/i18n/vi.d.ts +0 -70
- package/lib/i18n/vi.js +0 -151
- package/lib/i18n/zh.d.ts +0 -18
- package/lib/i18n/zh.js +0 -78
- package/lib/n2words.d.ts +0 -9
|
@@ -1,160 +1,261 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
|
4
21
|
*/
|
|
5
22
|
class AbstractLanguage {
|
|
6
|
-
#negativeWord;
|
|
7
|
-
#separatorWord;
|
|
8
|
-
#zero;
|
|
9
|
-
#spaceSeparator;
|
|
10
|
-
#wholeNumber;
|
|
11
|
-
|
|
12
23
|
/**
|
|
13
|
-
*
|
|
14
|
-
* @
|
|
15
|
-
* @param {string} options.separatorWord Word that separates cardinal numbers (i.e. "and").
|
|
16
|
-
* @param {string} options.zero Word for 0 (i.e. "zero").
|
|
17
|
-
* @param {string} [options.spaceSeparator] Character that separates words.
|
|
24
|
+
* Word that precedes negative numbers (e.g., "minus", "negative", "moins").
|
|
25
|
+
* @type {string}
|
|
18
26
|
*/
|
|
19
|
-
|
|
20
|
-
// Merge supplied options with defaults
|
|
21
|
-
options = Object.assign({
|
|
22
|
-
negativeWord: '',
|
|
23
|
-
separatorWord: '',
|
|
24
|
-
zero: '',
|
|
25
|
-
spaceSeparator: ' '
|
|
26
|
-
}, options);
|
|
27
|
-
|
|
28
|
-
// Make options available to class
|
|
29
|
-
this.#negativeWord = options.negativeWord;
|
|
30
|
-
this.#separatorWord = options.separatorWord;
|
|
31
|
-
this.#zero = options.zero;
|
|
32
|
-
this.#spaceSeparator = options.spaceSeparator;
|
|
33
|
-
}
|
|
27
|
+
negativeWord = ''
|
|
34
28
|
|
|
35
29
|
/**
|
|
36
|
-
*
|
|
30
|
+
* Word that separates whole and decimal parts (e.g., "point", "virgule", "comma").
|
|
31
|
+
* @type {string}
|
|
37
32
|
*/
|
|
38
|
-
|
|
39
|
-
return this.#negativeWord;
|
|
40
|
-
}
|
|
33
|
+
decimalSeparatorWord = ''
|
|
41
34
|
|
|
42
35
|
/**
|
|
43
|
-
*
|
|
36
|
+
* Word representation for the digit 0 (e.g., "zero", "zéro", "null").
|
|
37
|
+
* @type {string}
|
|
44
38
|
*/
|
|
45
|
-
|
|
46
|
-
return this.#separatorWord;
|
|
47
|
-
}
|
|
39
|
+
zeroWord = ''
|
|
48
40
|
|
|
49
41
|
/**
|
|
50
|
-
*
|
|
42
|
+
* Character(s) used to separate words in the output (typically a space).
|
|
43
|
+
* @type {string}
|
|
51
44
|
*/
|
|
52
|
-
|
|
53
|
-
return this.#zero;
|
|
54
|
-
}
|
|
45
|
+
wordSeparator = ' '
|
|
55
46
|
|
|
56
47
|
/**
|
|
57
|
-
*
|
|
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}
|
|
58
52
|
*/
|
|
59
|
-
|
|
60
|
-
return this.#spaceSeparator;
|
|
61
|
-
}
|
|
53
|
+
cachedWholeNumber = 0n
|
|
62
54
|
|
|
63
55
|
/**
|
|
64
|
-
*
|
|
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}
|
|
65
61
|
*/
|
|
66
|
-
|
|
67
|
-
return this.#wholeNumber;
|
|
68
|
-
}
|
|
62
|
+
convertDecimalsPerDigit = false
|
|
69
63
|
|
|
70
64
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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}
|
|
74
70
|
*/
|
|
75
|
-
|
|
76
|
-
const words = [];
|
|
77
|
-
|
|
78
|
-
// Split decimal string into an array of characters
|
|
79
|
-
const chars = [...decimal];
|
|
71
|
+
digits = null
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
return [...words, this.toCardinal(BigInt(decimal))];
|
|
102
|
+
return this.convertWholePart(digit)
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
/**
|
|
98
|
-
* Convert
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
*
|
|
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']
|
|
102
133
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
value = value.trim();
|
|
112
|
-
if (value.length === 0 || Number.isNaN(Number(value))) {
|
|
113
|
-
throw new Error('"' + value + '" is not a valid number.');
|
|
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))
|
|
114
142
|
}
|
|
115
|
-
|
|
116
|
-
|
|
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++
|
|
117
151
|
}
|
|
118
152
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
122
182
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
)
|
|
126
193
|
}
|
|
127
194
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
205
|
} else {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
206
|
+
// For strings, check first character before normalization
|
|
207
|
+
if (value[0] === '-') {
|
|
208
|
+
isNegative = true
|
|
209
|
+
value = value.slice(1)
|
|
210
|
+
}
|
|
135
211
|
}
|
|
136
212
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|
|
140
229
|
}
|
|
141
230
|
|
|
142
|
-
|
|
143
|
-
this.#wholeNumber = wholeNumber;
|
|
231
|
+
this.cachedWholeNumber = wholeNumber
|
|
144
232
|
|
|
145
|
-
|
|
146
|
-
words = [...words, this.toCardinal(wholeNumber)];
|
|
233
|
+
if (isNegative) words.push(this.negativeWord)
|
|
147
234
|
|
|
148
|
-
|
|
149
|
-
if (decimalNumber) {
|
|
150
|
-
words.push(this.separatorWord);
|
|
235
|
+
words.push(this.convertWholePart(wholeNumber))
|
|
151
236
|
|
|
152
|
-
|
|
237
|
+
// Append decimal portion if present (separator + fractional digits)
|
|
238
|
+
if (decimalPart) {
|
|
239
|
+
words.push(this.decimalSeparatorWord)
|
|
240
|
+
words.push(...this.decimalDigitsToWords(decimalPart))
|
|
153
241
|
}
|
|
154
242
|
|
|
155
|
-
|
|
156
|
-
|
|
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')
|
|
157
258
|
}
|
|
158
259
|
}
|
|
159
260
|
|
|
160
|
-
export default AbstractLanguage
|
|
261
|
+
export default AbstractLanguage
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|