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/sv.js
CHANGED
|
@@ -1,127 +1,304 @@
|
|
|
1
|
-
import GreedyScaleLanguage from '../classes/greedy-scale-language.js'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Swedish language converter
|
|
2
|
+
* Swedish language converter - Functional Implementation
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
* - Negative numbers (prepended with "minus")
|
|
8
|
-
* - Decimal numbers (word "komma" between whole and fractional parts)
|
|
9
|
-
* - Numbers up to quadrillions
|
|
4
|
+
* A performance-optimized number-to-words converter using precomputed lookup tables.
|
|
10
5
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* - "och"
|
|
14
|
-
* -
|
|
6
|
+
* Key features:
|
|
7
|
+
* - Hyphenation for tens-ones (tjugo-ett)
|
|
8
|
+
* - "och" after hundreds before small numbers
|
|
9
|
+
* - Omit "ett" before hundra and tusen
|
|
10
|
+
* - Use "en" (not "ett") before million+ scales
|
|
11
|
+
* - Long scale naming with -ard forms
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parseNumericValue } from '../utils/parse-numeric.js'
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Vocabulary (module-level constants)
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const ONES = ['', 'ett', 'två', 'tre', 'fyra', 'fem', 'sex', 'sju', 'åtta', 'nio']
|
|
21
|
+
|
|
22
|
+
const TEENS = ['tio', 'elva', 'tolv', 'tretton', 'fjorton', 'femton', 'sexton', 'sjutton', 'arton', 'nitton']
|
|
23
|
+
const TENS = ['', '', 'tjugo', 'trettio', 'fyrtio', 'femtio', 'sextio', 'sjuttio', 'åttio', 'nittio']
|
|
24
|
+
|
|
25
|
+
const HUNDRED = 'hundra'
|
|
26
|
+
|
|
27
|
+
const ZERO = 'noll'
|
|
28
|
+
const NEGATIVE = 'minus'
|
|
29
|
+
const DECIMAL_SEP = 'komma'
|
|
30
|
+
|
|
31
|
+
// Scale words (long scale with -ard forms)
|
|
32
|
+
const SCALES = ['tusen', 'miljon', 'miljard', 'biljon', 'biljard', 'triljon', 'triljard', 'kvadriljon']
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Precomputed Lookup Tables (built once at module load)
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds segment word for 0-999.
|
|
40
|
+
* Returns object with word and metadata for "och" logic.
|
|
15
41
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
[70n, 'sjuttio'],
|
|
34
|
-
[60n, 'sextio'],
|
|
35
|
-
[50n, 'femtio'],
|
|
36
|
-
[40n, 'fyrtio'],
|
|
37
|
-
[30n, 'trettio'],
|
|
38
|
-
[20n, 'tjugo'],
|
|
39
|
-
[19n, 'nitton'],
|
|
40
|
-
[18n, 'arton'],
|
|
41
|
-
[17n, 'sjutton'],
|
|
42
|
-
[16n, 'sexton'],
|
|
43
|
-
[15n, 'femton'],
|
|
44
|
-
[14n, 'fjorton'],
|
|
45
|
-
[13n, 'tretton'],
|
|
46
|
-
[12n, 'tolv'],
|
|
47
|
-
[11n, 'elva'],
|
|
48
|
-
[10n, 'tio'],
|
|
49
|
-
[9n, 'nio'],
|
|
50
|
-
[8n, 'åtta'],
|
|
51
|
-
[7n, 'sju'],
|
|
52
|
-
[6n, 'sex'],
|
|
53
|
-
[5n, 'fem'],
|
|
54
|
-
[4n, 'fyra'],
|
|
55
|
-
[3n, 'tre'],
|
|
56
|
-
[2n, 'två'],
|
|
57
|
-
[1n, 'ett'],
|
|
58
|
-
[0n, 'noll']
|
|
59
|
-
]
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Merges two adjacent word-number pairs according to Swedish grammar rules.
|
|
63
|
-
*
|
|
64
|
-
* Swedish-specific rules:
|
|
65
|
-
* - Implicit "ett": `mergeScales({ 'ett': 1n }, { 'hundra': 100n })` → `{ 'hundra': 100n }`
|
|
66
|
-
* - Hyphenated compounds: `mergeScales({ 'tjugo': 20n }, { 'tre': 3n })` → `{ 'tjugo-tre': 23n }`
|
|
67
|
-
* - "och" after hundreds: `mergeScales({ 'hundra': 100n }, { 'ett': 1n })` → `{ 'hundra och ett': 101n }`
|
|
68
|
-
* - "en" for millions: `mergeScales({ 'ett': 1n }, { 'miljon': 1000000n })` → `{ 'en miljon': 1000000n }`
|
|
69
|
-
*
|
|
70
|
-
* @param {Object} leftPair Left word-set as `{ word: BigInt }`.
|
|
71
|
-
* @param {Object} rightPair Right word-set as `{ word: BigInt }`.
|
|
72
|
-
* @returns {Object} Merged pair with combined word and resulting numeric value.
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* mergeScales({ 'ett': 1n }, { 'hundra': 100n }); // { 'hundra': 100n }
|
|
76
|
-
* mergeScales({ 'tjugo': 20n }, { 'tre': 3n }); // { 'tjugo-tre': 23n }
|
|
77
|
-
*/
|
|
78
|
-
mergeScales (leftPair, rightPair) {
|
|
79
|
-
const leftWord = Object.keys(leftPair)[0]
|
|
80
|
-
const rightWord = Object.keys(rightPair)[0]
|
|
81
|
-
const leftNumber = Object.values(leftPair)[0]
|
|
82
|
-
const rightNumber = Object.values(rightPair)[0]
|
|
83
|
-
|
|
84
|
-
if (leftNumber === 1n && rightNumber < 100n) {
|
|
85
|
-
return rightPair
|
|
42
|
+
function buildSegment (n) {
|
|
43
|
+
if (n === 0) return { word: '', hasHundred: false, lessThan100: false }
|
|
44
|
+
|
|
45
|
+
const ones = n % 10
|
|
46
|
+
const tens = Math.floor(n / 10) % 10
|
|
47
|
+
const hundreds = Math.floor(n / 100)
|
|
48
|
+
|
|
49
|
+
const parts = []
|
|
50
|
+
let hasHundred = false
|
|
51
|
+
|
|
52
|
+
// Hundreds - omit "ett" before hundra
|
|
53
|
+
if (hundreds > 0) {
|
|
54
|
+
hasHundred = true
|
|
55
|
+
if (hundreds === 1) {
|
|
56
|
+
parts.push(HUNDRED)
|
|
57
|
+
} else {
|
|
58
|
+
parts.push(ONES[hundreds] + ' ' + HUNDRED)
|
|
86
59
|
}
|
|
60
|
+
}
|
|
87
61
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
62
|
+
// Tens and ones with hyphenation
|
|
63
|
+
let tensOnesWord = ''
|
|
64
|
+
if (tens === 1) {
|
|
65
|
+
tensOnesWord = TEENS[ones]
|
|
66
|
+
} else if (tens >= 2) {
|
|
67
|
+
if (ones > 0) {
|
|
68
|
+
tensOnesWord = TENS[tens] + '-' + ONES[ones]
|
|
69
|
+
} else {
|
|
70
|
+
tensOnesWord = TENS[tens]
|
|
91
71
|
}
|
|
72
|
+
} else if (ones > 0) {
|
|
73
|
+
tensOnesWord = ONES[ones]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Combine with "och" after hundreds if there's a remainder
|
|
77
|
+
if (hasHundred && tensOnesWord) {
|
|
78
|
+
return { word: parts[0] + ' och ' + tensOnesWord, hasHundred: true, lessThan100: false }
|
|
79
|
+
} else if (hasHundred) {
|
|
80
|
+
return { word: parts[0], hasHundred: true, lessThan100: false }
|
|
81
|
+
} else {
|
|
82
|
+
return { word: tensOnesWord, hasHundred: false, lessThan100: true }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
92
85
|
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
// Precompute all 1000 segment words (0-999)
|
|
87
|
+
const SEGMENTS = new Array(1000)
|
|
88
|
+
const SEGMENTS_HAS_HUNDRED = new Array(1000)
|
|
89
|
+
const SEGMENTS_LESS_THAN_100 = new Array(1000)
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < 1000; i++) {
|
|
92
|
+
const result = buildSegment(i)
|
|
93
|
+
SEGMENTS[i] = result.word
|
|
94
|
+
SEGMENTS_HAS_HUNDRED[i] = result.hasHundred
|
|
95
|
+
SEGMENTS_LESS_THAN_100[i] = result.lessThan100
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Conversion Functions
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Converts a non-negative integer to Swedish words.
|
|
104
|
+
*
|
|
105
|
+
* @param {bigint} n - Non-negative integer to convert
|
|
106
|
+
* @returns {string} Swedish words
|
|
107
|
+
*/
|
|
108
|
+
function integerToWords (n) {
|
|
109
|
+
if (n === 0n) return ZERO
|
|
110
|
+
|
|
111
|
+
// Fast path: numbers < 1000 (direct lookup)
|
|
112
|
+
if (n < 1000n) {
|
|
113
|
+
return SEGMENTS[Number(n)]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fast path: numbers < 1,000,000 (thousands)
|
|
117
|
+
if (n < 1_000_000n) {
|
|
118
|
+
const thousands = Number(n / 1000n)
|
|
119
|
+
const remainder = Number(n % 1000n)
|
|
120
|
+
|
|
121
|
+
// Omit "ett" before tusen
|
|
122
|
+
let result = thousands === 1 ? SCALES[0] : SEGMENTS[thousands] + ' ' + SCALES[0]
|
|
123
|
+
|
|
124
|
+
if (remainder > 0) {
|
|
125
|
+
const remainderWord = SEGMENTS[remainder]
|
|
126
|
+
// Insert "och" if remainder < 100 (doesn't have hundred)
|
|
127
|
+
if (SEGMENTS_LESS_THAN_100[remainder]) {
|
|
128
|
+
result += ' och ' + remainderWord
|
|
129
|
+
} else {
|
|
130
|
+
result += ' ' + remainderWord
|
|
131
|
+
}
|
|
95
132
|
}
|
|
96
133
|
|
|
97
|
-
|
|
98
|
-
|
|
134
|
+
return result
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// For numbers >= 1,000,000, use scale decomposition
|
|
138
|
+
return buildLargeNumberWords(n)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Builds words for numbers >= 1,000,000.
|
|
143
|
+
*
|
|
144
|
+
* @param {bigint} n - Number >= 1,000,000
|
|
145
|
+
* @returns {string} Swedish words
|
|
146
|
+
*/
|
|
147
|
+
function buildLargeNumberWords (n) {
|
|
148
|
+
const numStr = n.toString()
|
|
149
|
+
const len = numStr.length
|
|
150
|
+
|
|
151
|
+
// Build segments of 3 digits from right to left
|
|
152
|
+
const segments = []
|
|
153
|
+
const segmentSize = 3
|
|
154
|
+
|
|
155
|
+
const remainderLen = len % segmentSize
|
|
156
|
+
let pos = 0
|
|
157
|
+
if (remainderLen > 0) {
|
|
158
|
+
segments.push(Number(numStr.slice(0, remainderLen)))
|
|
159
|
+
pos = remainderLen
|
|
160
|
+
}
|
|
161
|
+
while (pos < len) {
|
|
162
|
+
segments.push(Number(numStr.slice(pos, pos + segmentSize)))
|
|
163
|
+
pos += segmentSize
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Convert segments to words
|
|
167
|
+
const parts = []
|
|
168
|
+
let scaleIndex = segments.length - 1
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < segments.length; i++) {
|
|
171
|
+
const segment = segments[i]
|
|
172
|
+
|
|
173
|
+
if (segment !== 0) {
|
|
174
|
+
if (scaleIndex === 0) {
|
|
175
|
+
// Units segment
|
|
176
|
+
parts.push({
|
|
177
|
+
word: SEGMENTS[segment],
|
|
178
|
+
hasHundred: SEGMENTS_HAS_HUNDRED[segment],
|
|
179
|
+
isScale: false
|
|
180
|
+
})
|
|
181
|
+
} else {
|
|
182
|
+
// Segment with scale word
|
|
183
|
+
const scaleWord = SCALES[scaleIndex - 1]
|
|
184
|
+
|
|
185
|
+
let segmentWord
|
|
186
|
+
if (segment === 1) {
|
|
187
|
+
// Omit "ett" before tusen, use "en" before million+
|
|
188
|
+
if (scaleIndex === 1) {
|
|
189
|
+
segmentWord = '' // Just "tusen"
|
|
190
|
+
} else {
|
|
191
|
+
segmentWord = 'en' // "en miljon"
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
segmentWord = SEGMENTS[segment]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (segmentWord) {
|
|
198
|
+
parts.push({ word: segmentWord, hasHundred: false, isScale: false })
|
|
199
|
+
}
|
|
200
|
+
parts.push({ word: scaleWord, hasHundred: false, isScale: true })
|
|
201
|
+
}
|
|
99
202
|
}
|
|
100
203
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
204
|
+
scaleIndex--
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Join with Swedish "och" rules
|
|
208
|
+
return joinSwedishParts(parts)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Joins parts with Swedish "och" rules.
|
|
213
|
+
* Insert "och" before final segment if it follows a scale word and doesn't have "hundra".
|
|
214
|
+
*
|
|
215
|
+
* @param {Array} parts - Parts with metadata
|
|
216
|
+
* @returns {string} Joined string
|
|
217
|
+
*/
|
|
218
|
+
function joinSwedishParts (parts) {
|
|
219
|
+
if (parts.length === 0) return ZERO
|
|
220
|
+
if (parts.length === 1) return parts[0].word
|
|
221
|
+
|
|
222
|
+
const result = []
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < parts.length; i++) {
|
|
225
|
+
const part = parts[i]
|
|
226
|
+
const isLast = i === parts.length - 1
|
|
227
|
+
|
|
228
|
+
if (isLast && parts.length > 1) {
|
|
229
|
+
const prevPart = parts[i - 1]
|
|
230
|
+
// Insert "och" if previous was scale and this doesn't have hundred
|
|
231
|
+
if (prevPart.isScale && !part.hasHundred) {
|
|
232
|
+
result.push('och')
|
|
104
233
|
}
|
|
105
|
-
return { [`${leftWord} ${rightWord}`]: leftNumber * rightNumber }
|
|
106
234
|
}
|
|
107
235
|
|
|
108
|
-
|
|
236
|
+
result.push(part.word)
|
|
109
237
|
}
|
|
238
|
+
|
|
239
|
+
return result.join(' ')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Converts decimal digits to Swedish words.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} decimalPart - Decimal digits (without the point)
|
|
246
|
+
* @returns {string} Swedish words for decimal part
|
|
247
|
+
*/
|
|
248
|
+
function decimalPartToWords (decimalPart) {
|
|
249
|
+
let result = ''
|
|
250
|
+
|
|
251
|
+
// Handle leading zeros
|
|
252
|
+
let i = 0
|
|
253
|
+
while (i < decimalPart.length && decimalPart[i] === '0') {
|
|
254
|
+
if (result) result += ' '
|
|
255
|
+
result += ZERO
|
|
256
|
+
i++
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Convert remainder as a single number
|
|
260
|
+
const remainder = decimalPart.slice(i)
|
|
261
|
+
if (remainder) {
|
|
262
|
+
if (result) result += ' '
|
|
263
|
+
result += integerToWords(BigInt(remainder))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return result
|
|
110
267
|
}
|
|
111
268
|
|
|
112
269
|
/**
|
|
113
|
-
* Converts a
|
|
270
|
+
* Converts a numeric value to Swedish words.
|
|
114
271
|
*
|
|
115
|
-
* @param {number|string|bigint} value The
|
|
116
|
-
* @
|
|
117
|
-
* @
|
|
118
|
-
* @throws {
|
|
119
|
-
* @throws {Error} If value is an invalid number string.
|
|
272
|
+
* @param {number | string | bigint} value - The numeric value to convert
|
|
273
|
+
* @returns {string} The number in Swedish words
|
|
274
|
+
* @throws {TypeError} If value is not a valid numeric type
|
|
275
|
+
* @throws {Error} If value is not a valid number format
|
|
120
276
|
*
|
|
121
277
|
* @example
|
|
122
|
-
*
|
|
123
|
-
*
|
|
278
|
+
* toWords(42) // 'fyrtio-två'
|
|
279
|
+
* toWords(101) // 'hundra och ett'
|
|
280
|
+
* toWords(1000000) // 'en miljon'
|
|
124
281
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
282
|
+
function toWords (value) {
|
|
283
|
+
const { isNegative, integerPart, decimalPart } = parseNumericValue(value)
|
|
284
|
+
|
|
285
|
+
let result = ''
|
|
286
|
+
|
|
287
|
+
if (isNegative) {
|
|
288
|
+
result = NEGATIVE + ' '
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
result += integerToWords(integerPart)
|
|
292
|
+
|
|
293
|
+
if (decimalPart) {
|
|
294
|
+
result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return result
|
|
127
298
|
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// Public API
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
export { toWords }
|
package/lib/languages/sw.js
CHANGED
|
@@ -1,121 +1,153 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Swahili language converter - Functional Implementation
|
|
3
|
+
*
|
|
4
|
+
* Self-contained converter with precomputed lookup tables.
|
|
5
|
+
*
|
|
6
|
+
* Key features:
|
|
7
|
+
* - "na" connector for compound numbers
|
|
8
|
+
* - Reversed hundreds: "mia moja" (one hundred)
|
|
9
|
+
* - Scale words: elfu, milioni, bilioni
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseNumericValue } from '../utils/parse-numeric.js'
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Vocabulary
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const ONES = ['sifuri', 'moja', 'mbili', 'tatu', 'nne', 'tano', 'sita', 'saba', 'nane', 'tisa']
|
|
19
|
+
const TENS = { 10: 'kumi', 20: 'ishirini', 30: 'thelathini', 40: 'arobaini', 50: 'hamsini', 60: 'sitini', 70: 'sabini', 80: 'themanini', 90: 'tisini' }
|
|
20
|
+
|
|
21
|
+
const SCALE_WORDS = ['', 'elfu', 'milioni', 'bilioni', 'trilioni', 'kwadrilioni', 'kwintilioni']
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
const ZERO = 'sifuri'
|
|
24
|
+
const NEGATIVE = 'minus'
|
|
25
|
+
const DECIMAL_SEP = 'nukta'
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Conversion Functions
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
function wordsUnder100 (n) {
|
|
32
|
+
if (n < 10) return ONES[n]
|
|
33
|
+
if (n === 10) return TENS[10]
|
|
34
|
+
if (n < 20) {
|
|
35
|
+
// 11-19: 'kumi na <digit>'
|
|
36
|
+
return TENS[10] + ' na ' + ONES[n - 10]
|
|
30
37
|
}
|
|
38
|
+
const tens = Math.trunc(n / 10) * 10
|
|
39
|
+
const ones = n % 10
|
|
40
|
+
if (ones === 0) return TENS[tens]
|
|
41
|
+
return TENS[tens] + ' na ' + ONES[ones]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function wordsUnder1000 (n) {
|
|
45
|
+
if (n < 100) return wordsUnder100(n)
|
|
46
|
+
if (n === 100) return 'mia moja'
|
|
47
|
+
const hundreds = Math.trunc(n / 100)
|
|
48
|
+
const rest = n % 100
|
|
49
|
+
const parts = []
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
wordsUnder100 (n) {
|
|
41
|
-
if (n < 10) return this.digits[n]
|
|
42
|
-
if (n === 10) return this.tens[10]
|
|
43
|
-
if (n < 20) {
|
|
44
|
-
// 11-19: 'kumi na <digit>'
|
|
45
|
-
return this.tens[10] + ' na ' + this.digits[n - 10]
|
|
51
|
+
// Hundreds: 'mia <digit>'
|
|
52
|
+
parts.push('mia ' + ONES[hundreds])
|
|
53
|
+
if (rest > 0) {
|
|
54
|
+
if (rest < 10) {
|
|
55
|
+
parts.push('na ' + ONES[rest])
|
|
56
|
+
} else {
|
|
57
|
+
parts.push(wordsUnder100(rest))
|
|
46
58
|
}
|
|
47
|
-
const tens = Math.trunc(n / 10) * 10
|
|
48
|
-
const ones = n % 10
|
|
49
|
-
if (ones === 0) return this.tens[tens]
|
|
50
|
-
return this.tens[tens] + ' na ' + this.digits[ones]
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (n === 100) return 'mia moja'
|
|
56
|
-
const hundreds = Math.trunc(n / 100)
|
|
57
|
-
const rest = n % 100
|
|
58
|
-
const parts = []
|
|
59
|
-
|
|
60
|
-
// Hundreds: 'mia <digit>'
|
|
61
|
-
parts.push('mia ' + this.digits[hundreds])
|
|
62
|
-
if (rest > 0) {
|
|
63
|
-
if (rest < 10) {
|
|
64
|
-
parts.push('na ' + this.digits[rest])
|
|
65
|
-
} else {
|
|
66
|
-
parts.push(this.wordsUnder100(rest))
|
|
67
|
-
}
|
|
68
|
-
}
|
|
61
|
+
return parts.join(' ')
|
|
62
|
+
}
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
function extractSegments (n) {
|
|
65
|
+
const segments = []
|
|
66
|
+
let temp = n
|
|
67
|
+
while (temp > 0n) {
|
|
68
|
+
segments.push(Number(temp % 1000n))
|
|
69
|
+
temp = temp / 1000n
|
|
71
70
|
}
|
|
71
|
+
return segments
|
|
72
|
+
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let remaining = s.slice(0, -3)
|
|
80
|
-
while (remaining.length > 0) {
|
|
81
|
-
const group = remaining.slice(-3)
|
|
82
|
-
groups.unshift(Number(group))
|
|
83
|
-
remaining = remaining.slice(0, -3)
|
|
84
|
-
}
|
|
85
|
-
return groups
|
|
86
|
-
}
|
|
74
|
+
function integerToWords (n) {
|
|
75
|
+
if (n === 0n) return ZERO
|
|
76
|
+
|
|
77
|
+
// segments stored least-significant first: [ones, thousands, millions, ...]
|
|
78
|
+
const segments = extractSegments(n)
|
|
79
|
+
const parts = []
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (val < 10 && parts.length > 0) {
|
|
101
|
-
parts.push('na ' + this.digits[val])
|
|
102
|
-
} else if (val === 100 && parts.length > 0) {
|
|
103
|
-
// In compound numbers (e.g., 1100 -> 'elfu moja mia'), use 'mia' not 'mia moja'
|
|
104
|
-
parts.push('mia')
|
|
105
|
-
} else {
|
|
106
|
-
parts.push(this.wordsUnder1000(val))
|
|
107
|
-
}
|
|
81
|
+
// Iterate from highest scale to lowest
|
|
82
|
+
for (let scaleIndex = segments.length - 1; scaleIndex >= 0; scaleIndex--) {
|
|
83
|
+
const val = segments[scaleIndex]
|
|
84
|
+
if (val === 0) continue
|
|
85
|
+
|
|
86
|
+
if (scaleIndex === 0) {
|
|
87
|
+
// Units segment
|
|
88
|
+
if (val < 10 && parts.length > 0) {
|
|
89
|
+
parts.push('na ' + ONES[val])
|
|
90
|
+
} else if (val === 100 && parts.length > 0) {
|
|
91
|
+
// In compound numbers (e.g., 1100 -> 'elfu moja mia'), use 'mia' not 'mia moja'
|
|
92
|
+
parts.push('mia')
|
|
108
93
|
} else {
|
|
109
|
-
|
|
110
|
-
const unit = (val === 1) ? 'moja' : this.wordsUnder1000(val)
|
|
111
|
-
parts.push(this.scales[scaleIndex] + ' ' + unit)
|
|
94
|
+
parts.push(wordsUnder1000(val))
|
|
112
95
|
}
|
|
96
|
+
} else {
|
|
97
|
+
// Scale segments: 'elfu moja', 'milioni mbili'
|
|
98
|
+
const unit = (val === 1) ? 'moja' : wordsUnder1000(val)
|
|
99
|
+
parts.push(SCALE_WORDS[scaleIndex] + ' ' + unit)
|
|
113
100
|
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return parts.join(' ').trim()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function decimalPartToWords (decimalPart) {
|
|
107
|
+
let result = ''
|
|
108
|
+
let i = 0
|
|
114
109
|
|
|
115
|
-
|
|
110
|
+
while (i < decimalPart.length && decimalPart[i] === '0') {
|
|
111
|
+
if (result) result += ' '
|
|
112
|
+
result += ZERO
|
|
113
|
+
i++
|
|
116
114
|
}
|
|
115
|
+
|
|
116
|
+
const remainder = decimalPart.slice(i)
|
|
117
|
+
if (remainder) {
|
|
118
|
+
if (result) result += ' '
|
|
119
|
+
result += integerToWords(BigInt(remainder))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result
|
|
117
123
|
}
|
|
118
124
|
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Converts a numeric value to Swahili words.
|
|
127
|
+
*
|
|
128
|
+
* @param {number | string | bigint} value - The numeric value to convert
|
|
129
|
+
* @returns {string} The number in Swahili words
|
|
130
|
+
*/
|
|
131
|
+
function toWords (value) {
|
|
132
|
+
const { isNegative, integerPart, decimalPart } = parseNumericValue(value)
|
|
133
|
+
|
|
134
|
+
let result = ''
|
|
135
|
+
|
|
136
|
+
if (isNegative) {
|
|
137
|
+
result = NEGATIVE + ' '
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
result += integerToWords(integerPart)
|
|
141
|
+
|
|
142
|
+
if (decimalPart) {
|
|
143
|
+
result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result
|
|
121
147
|
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Exports
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
export { toWords }
|