n2words 1.24.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +183 -156
  3. package/dist/languages/am-Latn.js +3 -0
  4. package/dist/languages/am-Latn.js.map +1 -0
  5. package/dist/languages/am.js +3 -0
  6. package/dist/languages/am.js.map +1 -0
  7. package/dist/languages/ar.js +3 -2
  8. package/dist/languages/ar.js.map +1 -1
  9. package/dist/languages/az.js +3 -2
  10. package/dist/languages/az.js.map +1 -1
  11. package/dist/languages/bn.js +3 -2
  12. package/dist/languages/bn.js.map +1 -1
  13. package/dist/languages/cs.js +3 -2
  14. package/dist/languages/cs.js.map +1 -1
  15. package/dist/languages/da.js +3 -2
  16. package/dist/languages/da.js.map +1 -1
  17. package/dist/languages/de.js +3 -2
  18. package/dist/languages/de.js.map +1 -1
  19. package/dist/languages/el.js +3 -2
  20. package/dist/languages/el.js.map +1 -1
  21. package/dist/languages/en.js +3 -2
  22. package/dist/languages/en.js.map +1 -1
  23. package/dist/languages/es.js +3 -2
  24. package/dist/languages/es.js.map +1 -1
  25. package/dist/languages/fa.js +3 -2
  26. package/dist/languages/fa.js.map +1 -1
  27. package/dist/languages/fi.js +3 -0
  28. package/dist/languages/fi.js.map +1 -0
  29. package/dist/languages/fil.js +3 -2
  30. package/dist/languages/fil.js.map +1 -1
  31. package/dist/languages/fr-BE.js +3 -2
  32. package/dist/languages/fr-BE.js.map +1 -1
  33. package/dist/languages/fr.js +3 -2
  34. package/dist/languages/fr.js.map +1 -1
  35. package/dist/languages/gu.js +3 -2
  36. package/dist/languages/gu.js.map +1 -1
  37. package/dist/languages/ha.js +3 -0
  38. package/dist/languages/ha.js.map +1 -0
  39. package/dist/languages/hbo.js +3 -0
  40. package/dist/languages/hbo.js.map +1 -0
  41. package/dist/languages/he.js +3 -2
  42. package/dist/languages/he.js.map +1 -1
  43. package/dist/languages/hi.js +3 -2
  44. package/dist/languages/hi.js.map +1 -1
  45. package/dist/languages/hr.js +3 -2
  46. package/dist/languages/hr.js.map +1 -1
  47. package/dist/languages/hu.js +3 -2
  48. package/dist/languages/hu.js.map +1 -1
  49. package/dist/languages/id.js +3 -2
  50. package/dist/languages/id.js.map +1 -1
  51. package/dist/languages/it.js +3 -2
  52. package/dist/languages/it.js.map +1 -1
  53. package/dist/languages/ja.js +3 -2
  54. package/dist/languages/ja.js.map +1 -1
  55. package/dist/languages/kn.js +3 -2
  56. package/dist/languages/kn.js.map +1 -1
  57. package/dist/languages/ko.js +3 -2
  58. package/dist/languages/ko.js.map +1 -1
  59. package/dist/languages/lt.js +3 -2
  60. package/dist/languages/lt.js.map +1 -1
  61. package/dist/languages/lv.js +3 -2
  62. package/dist/languages/lv.js.map +1 -1
  63. package/dist/languages/mr.js +3 -2
  64. package/dist/languages/mr.js.map +1 -1
  65. package/dist/languages/ms.js +3 -2
  66. package/dist/languages/ms.js.map +1 -1
  67. package/dist/languages/nb.js +3 -2
  68. package/dist/languages/nb.js.map +1 -1
  69. package/dist/languages/nl.js +3 -2
  70. package/dist/languages/nl.js.map +1 -1
  71. package/dist/languages/pa.js +3 -0
  72. package/dist/languages/pa.js.map +1 -0
  73. package/dist/languages/pl.js +3 -2
  74. package/dist/languages/pl.js.map +1 -1
  75. package/dist/languages/pt.js +3 -2
  76. package/dist/languages/pt.js.map +1 -1
  77. package/dist/languages/ro.js +3 -2
  78. package/dist/languages/ro.js.map +1 -1
  79. package/dist/languages/ru.js +3 -2
  80. package/dist/languages/ru.js.map +1 -1
  81. package/dist/languages/sr-Cyrl.js +3 -0
  82. package/dist/languages/sr-Cyrl.js.map +1 -0
  83. package/dist/languages/sr-Latn.js +3 -2
  84. package/dist/languages/sr-Latn.js.map +1 -1
  85. package/dist/languages/sv.js +3 -2
  86. package/dist/languages/sv.js.map +1 -1
  87. package/dist/languages/sw.js +3 -2
  88. package/dist/languages/sw.js.map +1 -1
  89. package/dist/languages/ta.js +3 -2
  90. package/dist/languages/ta.js.map +1 -1
  91. package/dist/languages/te.js +3 -2
  92. package/dist/languages/te.js.map +1 -1
  93. package/dist/languages/th.js +3 -2
  94. package/dist/languages/th.js.map +1 -1
  95. package/dist/languages/tr.js +3 -2
  96. package/dist/languages/tr.js.map +1 -1
  97. package/dist/languages/uk.js +3 -2
  98. package/dist/languages/uk.js.map +1 -1
  99. package/dist/languages/ur.js +3 -2
  100. package/dist/languages/ur.js.map +1 -1
  101. package/dist/languages/vi.js +3 -2
  102. package/dist/languages/vi.js.map +1 -1
  103. package/dist/languages/zh-Hans.js +3 -2
  104. package/dist/languages/zh-Hans.js.map +1 -1
  105. package/dist/languages/zh-Hant.js +3 -0
  106. package/dist/languages/zh-Hant.js.map +1 -0
  107. package/dist/n2words.js +3 -2
  108. package/dist/n2words.js.map +1 -1
  109. package/lib/languages/am-Latn.d.ts +7 -0
  110. package/lib/languages/am-Latn.js +164 -0
  111. package/lib/languages/am.d.ts +7 -0
  112. package/lib/languages/am.js +164 -0
  113. package/lib/languages/ar.d.ts +17 -0
  114. package/lib/languages/ar.js +171 -209
  115. package/lib/languages/az.d.ts +7 -0
  116. package/lib/languages/az.js +167 -49
  117. package/lib/languages/bn.d.ts +7 -0
  118. package/lib/languages/bn.js +142 -123
  119. package/lib/languages/cs.d.ts +18 -0
  120. package/lib/languages/cs.js +303 -176
  121. package/lib/languages/da.d.ts +14 -0
  122. package/lib/languages/da.js +267 -139
  123. package/lib/languages/de.d.ts +17 -0
  124. package/lib/languages/de.js +310 -113
  125. package/lib/languages/el.d.ts +14 -0
  126. package/lib/languages/el.js +225 -98
  127. package/lib/languages/en.d.ts +17 -0
  128. package/lib/languages/en.js +235 -102
  129. package/lib/languages/es.d.ts +21 -0
  130. package/lib/languages/es.js +307 -125
  131. package/lib/languages/fa.d.ts +7 -0
  132. package/lib/languages/fa.js +115 -108
  133. package/lib/languages/fi.d.ts +14 -0
  134. package/lib/languages/fi.js +245 -0
  135. package/lib/languages/fil.d.ts +7 -0
  136. package/lib/languages/fil.js +199 -139
  137. package/lib/languages/fr-BE.d.ts +11 -0
  138. package/lib/languages/fr-BE.js +287 -48
  139. package/lib/languages/fr.d.ts +21 -0
  140. package/lib/languages/fr.js +343 -119
  141. package/lib/languages/gu.d.ts +7 -0
  142. package/lib/languages/gu.js +125 -144
  143. package/lib/languages/ha.d.ts +7 -0
  144. package/lib/languages/ha.js +230 -0
  145. package/lib/languages/hbo.d.ts +13 -0
  146. package/lib/languages/hbo.js +300 -0
  147. package/lib/languages/he.d.ts +13 -0
  148. package/lib/languages/he.js +230 -283
  149. package/lib/languages/hi.d.ts +7 -0
  150. package/lib/languages/hi.js +142 -123
  151. package/lib/languages/hr.d.ts +11 -0
  152. package/lib/languages/hr.js +190 -129
  153. package/lib/languages/hu.d.ts +7 -0
  154. package/lib/languages/hu.js +194 -133
  155. package/lib/languages/id.d.ts +7 -0
  156. package/lib/languages/id.js +167 -140
  157. package/lib/languages/it.d.ts +19 -0
  158. package/lib/languages/it.js +337 -108
  159. package/lib/languages/ja.d.ts +17 -0
  160. package/lib/languages/ja.js +224 -155
  161. package/lib/languages/kn.d.ts +7 -0
  162. package/lib/languages/kn.js +128 -62
  163. package/lib/languages/ko.d.ts +14 -0
  164. package/lib/languages/ko.js +250 -70
  165. package/lib/languages/lt.d.ts +18 -0
  166. package/lib/languages/lt.js +287 -148
  167. package/lib/languages/lv.d.ts +18 -0
  168. package/lib/languages/lv.js +291 -123
  169. package/lib/languages/mr.d.ts +7 -0
  170. package/lib/languages/mr.js +125 -144
  171. package/lib/languages/ms.d.ts +7 -0
  172. package/lib/languages/ms.js +171 -112
  173. package/lib/languages/nb.d.ts +14 -0
  174. package/lib/languages/nb.js +275 -100
  175. package/lib/languages/nl.d.ts +26 -0
  176. package/lib/languages/nl.js +307 -174
  177. package/lib/languages/pa.d.ts +7 -0
  178. package/lib/languages/pa.js +163 -0
  179. package/lib/languages/pl.d.ts +22 -0
  180. package/lib/languages/pl.js +299 -158
  181. package/lib/languages/pt.d.ts +17 -0
  182. package/lib/languages/pt.js +279 -120
  183. package/lib/languages/ro.d.ts +18 -0
  184. package/lib/languages/ro.js +214 -337
  185. package/lib/languages/ru.d.ts +11 -0
  186. package/lib/languages/ru.js +219 -95
  187. package/lib/languages/sr-Cyrl.d.ts +11 -0
  188. package/lib/languages/sr-Cyrl.js +215 -0
  189. package/lib/languages/sr-Latn.d.ts +11 -0
  190. package/lib/languages/sr-Latn.js +190 -132
  191. package/lib/languages/sv.d.ts +14 -0
  192. package/lib/languages/sv.js +280 -103
  193. package/lib/languages/sw.d.ts +7 -0
  194. package/lib/languages/sw.js +135 -103
  195. package/lib/languages/ta.d.ts +7 -0
  196. package/lib/languages/ta.js +133 -205
  197. package/lib/languages/te.d.ts +7 -0
  198. package/lib/languages/te.js +148 -213
  199. package/lib/languages/th.d.ts +7 -0
  200. package/lib/languages/th.js +139 -101
  201. package/lib/languages/tr.d.ts +18 -0
  202. package/lib/languages/tr.js +246 -66
  203. package/lib/languages/uk.d.ts +11 -0
  204. package/lib/languages/uk.js +197 -101
  205. package/lib/languages/ur.d.ts +7 -0
  206. package/lib/languages/ur.js +160 -123
  207. package/lib/languages/vi.d.ts +17 -0
  208. package/lib/languages/vi.js +287 -164
  209. package/lib/languages/zh-Hans.d.ts +11 -0
  210. package/lib/languages/zh-Hans.js +159 -142
  211. package/lib/languages/zh-Hant.d.ts +11 -0
  212. package/lib/languages/zh-Hant.js +202 -0
  213. package/lib/n2words.d.ts +53 -0
  214. package/lib/n2words.js +91 -227
  215. package/lib/utils/is-plain-object.d.ts +13 -0
  216. package/lib/utils/is-plain-object.js +17 -0
  217. package/lib/utils/parse-numeric.d.ts +17 -0
  218. package/lib/utils/parse-numeric.js +108 -0
  219. package/lib/utils/validate-options.d.ts +8 -0
  220. package/lib/utils/validate-options.js +16 -0
  221. package/package.json +118 -67
  222. package/dist/languages/pa-Guru.js +0 -2
  223. package/dist/languages/pa-Guru.js.map +0 -1
  224. package/lib/classes/abstract-language.js +0 -261
  225. package/lib/classes/greedy-scale-language.js +0 -195
  226. package/lib/classes/slavic-language.js +0 -251
  227. package/lib/classes/south-asian-language.js +0 -161
  228. package/lib/classes/turkic-language.js +0 -63
  229. package/lib/languages/pa-Guru.js +0 -126
  230. package/typings/classes/abstract-language.d.ts +0 -144
  231. package/typings/classes/greedy-scale-language.d.ts +0 -148
  232. package/typings/classes/slavic-language.d.ts +0 -145
  233. package/typings/classes/south-asian-language.d.ts +0 -101
  234. package/typings/classes/turkic-language.d.ts +0 -42
  235. package/typings/languages/ar.d.ts +0 -93
  236. package/typings/languages/az.d.ts +0 -25
  237. package/typings/languages/bn.d.ts +0 -1
  238. package/typings/languages/cs.d.ts +0 -120
  239. package/typings/languages/da.d.ts +0 -53
  240. package/typings/languages/de.d.ts +0 -26
  241. package/typings/languages/el.d.ts +0 -11
  242. package/typings/languages/en.d.ts +0 -30
  243. package/typings/languages/es.d.ts +0 -43
  244. package/typings/languages/fa.d.ts +0 -81
  245. package/typings/languages/fil.d.ts +0 -12
  246. package/typings/languages/fr-BE.d.ts +0 -41
  247. package/typings/languages/fr.d.ts +0 -43
  248. package/typings/languages/gu.d.ts +0 -12
  249. package/typings/languages/he.d.ts +0 -197
  250. package/typings/languages/hi.d.ts +0 -1
  251. package/typings/languages/hr.d.ts +0 -110
  252. package/typings/languages/hu.d.ts +0 -37
  253. package/typings/languages/id.d.ts +0 -69
  254. package/typings/languages/it.d.ts +0 -51
  255. package/typings/languages/ja.d.ts +0 -58
  256. package/typings/languages/kn.d.ts +0 -11
  257. package/typings/languages/ko.d.ts +0 -25
  258. package/typings/languages/lt.d.ts +0 -110
  259. package/typings/languages/lv.d.ts +0 -99
  260. package/typings/languages/mr.d.ts +0 -12
  261. package/typings/languages/ms.d.ts +0 -37
  262. package/typings/languages/nb.d.ts +0 -27
  263. package/typings/languages/nl.d.ts +0 -65
  264. package/typings/languages/pa-Guru.d.ts +0 -1
  265. package/typings/languages/pl.d.ts +0 -116
  266. package/typings/languages/pt.d.ts +0 -39
  267. package/typings/languages/ro.d.ts +0 -229
  268. package/typings/languages/ru.d.ts +0 -108
  269. package/typings/languages/sr-Latn.d.ts +0 -98
  270. package/typings/languages/sv.d.ts +0 -30
  271. package/typings/languages/sw.d.ts +0 -1
  272. package/typings/languages/ta.d.ts +0 -1
  273. package/typings/languages/te.d.ts +0 -1
  274. package/typings/languages/th.d.ts +0 -1
  275. package/typings/languages/tr.d.ts +0 -46
  276. package/typings/languages/uk.d.ts +0 -117
  277. package/typings/languages/ur.d.ts +0 -1
  278. package/typings/languages/vi.d.ts +0 -116
  279. package/typings/languages/zh-Hans.d.ts +0 -57
  280. package/typings/n2words.d.ts +0 -177
@@ -1,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
- * Converts numbers to Swedish words, supporting:
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
- * Merge rules:
12
- * - Hyphenated for compound tens (e.g., "tjugo-tre")
13
- * - "och" (and) after hundreds (e.g., "hundra och ett")
14
- * - Space-separated for larger composites (e.g., "en miljon")
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
- export class Swedish extends GreedyScaleLanguage {
17
- negativeWord = 'minus'
18
- decimalSeparatorWord = 'komma'
19
- zeroWord = 'noll'
20
- wordSeparator = ' '
21
- scaleWordPairs = [
22
- [1_000_000_000_000_000_000_000_000_000n, 'kvadriljon'],
23
- [1_000_000_000_000_000_000_000_000n, 'triljon'],
24
- [1_000_000_000_000_000_000_000n, 'biljon'],
25
- [1_000_000_000_000_000_000n, 'miljard'],
26
- [1_000_000_000_000n, 'biljon'],
27
- [1_000_000_000n, 'miljard'],
28
- [1_000_000n, 'miljon'],
29
- [1000n, 'tusen'],
30
- [100n, 'hundra'],
31
- [90n, 'nittio'],
32
- [80n, 'åttio'],
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
- // Omit 'ett' before 'hundra' and 'tusen'
89
- if (leftNumber === 1n && (rightNumber === 100n || rightNumber === 1000n)) {
90
- return rightPair
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
- if (leftNumber < 100n && leftNumber > rightNumber) {
94
- return { [`${leftWord}-${rightWord}`]: leftNumber + rightNumber }
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
- if (leftNumber >= 100n && rightNumber < 100n) {
98
- return { [`${leftWord} och ${rightWord}`]: leftNumber + rightNumber }
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
- if (rightNumber > leftNumber) {
102
- if (leftNumber === 1n && rightNumber >= 1_000_000n) {
103
- return { [`en ${rightWord}`]: leftNumber * rightNumber }
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
- return { [`${leftWord} ${rightWord}`]: leftNumber + rightNumber }
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 number to Swedish cardinal (written) form.
270
+ * Converts a numeric value to Swedish words.
114
271
  *
115
- * @param {number|string|bigint} value The number to convert.
116
- * @param {Object} [options] Conversion options (see Swedish class options).
117
- * @returns {string} The number expressed in Swedish words.
118
- * @throws {TypeError} If value is NaN or invalid type.
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
- * convertToWords(42); // 'fyrtio-två'
123
- * convertToWords('1.5'); // 'ett komma fem'
278
+ * toWords(42) // 'fyrtio-två'
279
+ * toWords(101) // 'hundra och ett'
280
+ * toWords(1000000) // 'en miljon'
124
281
  */
125
- export default function convertToWords (value, options = {}) {
126
- return new Swedish(options).convertToWords(value)
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 }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Converts a numeric value to Swahili words.
3
+ *
4
+ * @param {number | string | bigint} value - The numeric value to convert
5
+ * @returns {string} The number in Swahili words
6
+ */
7
+ export function toWords(value: number | string | bigint): string;
@@ -1,121 +1,153 @@
1
- import AbstractLanguage from '../classes/abstract-language.js'
2
-
3
- class Swahili extends AbstractLanguage {
4
- negativeWord = 'minus'
5
- decimalSeparatorWord = 'nukta'
6
- zeroWord = 'sifuri'
7
- digits = {
8
- 0: 'sifuri',
9
- 1: 'moja',
10
- 2: 'mbili',
11
- 3: 'tatu',
12
- 4: 'nne',
13
- 5: 'tano',
14
- 6: 'sita',
15
- 7: 'saba',
16
- 8: 'nane',
17
- 9: 'tisa'
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
- tens = {
21
- 10: 'kumi',
22
- 20: 'ishirini',
23
- 30: 'thelathini',
24
- 40: 'arobaini',
25
- 50: 'hamsini',
26
- 60: 'sitini',
27
- 70: 'sabini',
28
- 80: 'themanini',
29
- 90: 'tisini'
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
- scales = [
33
- '',
34
- 'elfu',
35
- 'milioni',
36
- 'bilioni',
37
- 'trilioni'
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
- wordsUnder1000 (n) {
54
- if (n < 100) return this.wordsUnder100(n)
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
- return parts.join(' ')
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
- splitBy3 (number) {
74
- const s = number.toString()
75
- if (s.length <= 3) return [Number(s)]
76
- const groups = []
77
- const last3 = s.slice(-3)
78
- groups.unshift(Number(last3))
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
- convertWholePart (number) {
89
- if (number === 0n) return this.zeroWord
90
-
91
- const groups = this.splitBy3(number)
92
- const parts = []
93
-
94
- for (let i = 0; i < groups.length; i++) {
95
- const val = groups[i]
96
- if (val === 0) continue
97
- const scaleIndex = groups.length - i - 1
98
- // scale word
99
- if (scaleIndex === 0) {
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
- // e.g., 'elfu moja', 'milioni mbili'
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
- return parts.join(' ').trim()
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
- export default function convertToWords (value, options = {}) {
120
- return new Swahili(options).convertToWords(value)
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 }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Converts a numeric value to Tamil words.
3
+ *
4
+ * @param {number | string | bigint} value - The numeric value to convert
5
+ * @returns {string} The number in Tamil words
6
+ */
7
+ export function toWords(value: number | string | bigint): string;