n2words 2.0.0 → 3.1.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 (335) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +86 -188
  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 -0
  8. package/dist/languages/ar.js.map +1 -0
  9. package/dist/languages/az.js +3 -0
  10. package/dist/languages/az.js.map +1 -0
  11. package/dist/languages/bn.js +3 -0
  12. package/dist/languages/bn.js.map +1 -0
  13. package/dist/languages/cs.js +3 -0
  14. package/dist/languages/cs.js.map +1 -0
  15. package/dist/languages/da.js +3 -0
  16. package/dist/languages/da.js.map +1 -0
  17. package/dist/languages/de.js +3 -0
  18. package/dist/languages/de.js.map +1 -0
  19. package/dist/languages/el.js +3 -0
  20. package/dist/languages/el.js.map +1 -0
  21. package/dist/languages/en.js +3 -0
  22. package/dist/languages/en.js.map +1 -0
  23. package/dist/languages/es.js +3 -0
  24. package/dist/languages/es.js.map +1 -0
  25. package/dist/languages/fa.js +3 -0
  26. package/dist/languages/fa.js.map +1 -0
  27. package/dist/languages/fi.js +3 -0
  28. package/dist/languages/fi.js.map +1 -0
  29. package/dist/languages/fil.js +3 -0
  30. package/dist/languages/fil.js.map +1 -0
  31. package/dist/languages/fr-BE.js +3 -0
  32. package/dist/languages/fr-BE.js.map +1 -0
  33. package/dist/languages/fr.js +3 -0
  34. package/dist/languages/fr.js.map +1 -0
  35. package/dist/languages/gu.js +3 -0
  36. package/dist/languages/gu.js.map +1 -0
  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 -0
  42. package/dist/languages/he.js.map +1 -0
  43. package/dist/languages/hi.js +3 -0
  44. package/dist/languages/hi.js.map +1 -0
  45. package/dist/languages/hr.js +3 -0
  46. package/dist/languages/hr.js.map +1 -0
  47. package/dist/languages/hu.js +3 -0
  48. package/dist/languages/hu.js.map +1 -0
  49. package/dist/languages/id.js +3 -0
  50. package/dist/languages/id.js.map +1 -0
  51. package/dist/languages/it.js +3 -0
  52. package/dist/languages/it.js.map +1 -0
  53. package/dist/languages/ja.js +3 -0
  54. package/dist/languages/ja.js.map +1 -0
  55. package/dist/languages/ka.js +3 -0
  56. package/dist/languages/ka.js.map +1 -0
  57. package/dist/languages/kn.js +3 -0
  58. package/dist/languages/kn.js.map +1 -0
  59. package/dist/languages/ko.js +3 -0
  60. package/dist/languages/ko.js.map +1 -0
  61. package/dist/languages/lt.js +3 -0
  62. package/dist/languages/lt.js.map +1 -0
  63. package/dist/languages/lv.js +3 -0
  64. package/dist/languages/lv.js.map +1 -0
  65. package/dist/languages/mr.js +3 -0
  66. package/dist/languages/mr.js.map +1 -0
  67. package/dist/languages/ms.js +3 -0
  68. package/dist/languages/ms.js.map +1 -0
  69. package/dist/languages/nb.js +3 -0
  70. package/dist/languages/nb.js.map +1 -0
  71. package/dist/languages/nl.js +3 -0
  72. package/dist/languages/nl.js.map +1 -0
  73. package/dist/languages/pa.js +3 -0
  74. package/dist/languages/pa.js.map +1 -0
  75. package/dist/languages/pl.js +3 -0
  76. package/dist/languages/pl.js.map +1 -0
  77. package/dist/languages/pt.js +3 -0
  78. package/dist/languages/pt.js.map +1 -0
  79. package/dist/languages/ro.js +3 -0
  80. package/dist/languages/ro.js.map +1 -0
  81. package/dist/languages/ru.js +3 -0
  82. package/dist/languages/ru.js.map +1 -0
  83. package/dist/languages/sr-Cyrl.js +3 -0
  84. package/dist/languages/sr-Cyrl.js.map +1 -0
  85. package/dist/languages/sr-Latn.js +3 -0
  86. package/dist/languages/sr-Latn.js.map +1 -0
  87. package/dist/languages/sv.js +3 -0
  88. package/dist/languages/sv.js.map +1 -0
  89. package/dist/languages/sw.js +3 -0
  90. package/dist/languages/sw.js.map +1 -0
  91. package/dist/languages/ta.js +3 -0
  92. package/dist/languages/ta.js.map +1 -0
  93. package/dist/languages/te.js +3 -0
  94. package/dist/languages/te.js.map +1 -0
  95. package/dist/languages/th.js +3 -0
  96. package/dist/languages/th.js.map +1 -0
  97. package/dist/languages/tr.js +3 -0
  98. package/dist/languages/tr.js.map +1 -0
  99. package/dist/languages/uk.js +3 -0
  100. package/dist/languages/uk.js.map +1 -0
  101. package/dist/languages/ur.js +3 -0
  102. package/dist/languages/ur.js.map +1 -0
  103. package/dist/languages/vi.js +3 -0
  104. package/dist/languages/vi.js.map +1 -0
  105. package/dist/languages/yo.js +3 -0
  106. package/dist/languages/yo.js.map +1 -0
  107. package/dist/languages/zh-Hans.js +3 -0
  108. package/dist/languages/zh-Hans.js.map +1 -0
  109. package/dist/languages/zh-Hant.js +3 -0
  110. package/dist/languages/zh-Hant.js.map +1 -0
  111. package/dist/n2words.js +2 -2
  112. package/dist/n2words.js.map +1 -1
  113. package/lib/languages/am-Latn.d.ts +7 -0
  114. package/lib/languages/am-Latn.js +159 -0
  115. package/lib/languages/am.d.ts +7 -0
  116. package/lib/languages/am.js +159 -0
  117. package/lib/languages/ar.d.ts +14 -27
  118. package/lib/languages/ar.js +175 -129
  119. package/lib/languages/az.d.ts +4 -9
  120. package/lib/languages/az.js +166 -37
  121. package/lib/languages/bn.d.ts +4 -8
  122. package/lib/languages/bn.js +159 -124
  123. package/lib/languages/cs.d.ts +15 -85
  124. package/lib/languages/cs.js +293 -114
  125. package/lib/languages/da.d.ts +11 -12
  126. package/lib/languages/da.js +269 -101
  127. package/lib/languages/de.d.ts +14 -11
  128. package/lib/languages/de.js +305 -86
  129. package/lib/languages/el.d.ts +11 -11
  130. package/lib/languages/el.js +224 -78
  131. package/lib/languages/en.d.ts +14 -13
  132. package/lib/languages/en.js +228 -72
  133. package/lib/languages/es.d.ts +18 -12
  134. package/lib/languages/es.js +297 -103
  135. package/lib/languages/fa.d.ts +4 -44
  136. package/lib/languages/fa.js +112 -122
  137. package/lib/languages/fi.d.ts +14 -0
  138. package/lib/languages/fi.js +238 -0
  139. package/lib/languages/fil.d.ts +4 -13
  140. package/lib/languages/fil.js +196 -106
  141. package/lib/languages/fr-BE.d.ts +8 -8
  142. package/lib/languages/fr-BE.js +285 -19
  143. package/lib/languages/fr.d.ts +18 -12
  144. package/lib/languages/fr.js +339 -89
  145. package/lib/languages/gu.d.ts +4 -8
  146. package/lib/languages/gu.js +143 -125
  147. package/lib/languages/ha.d.ts +7 -0
  148. package/lib/languages/ha.js +225 -0
  149. package/lib/languages/hbo.d.ts +10 -110
  150. package/lib/languages/hbo.js +245 -214
  151. package/lib/languages/he.d.ts +10 -77
  152. package/lib/languages/he.js +231 -172
  153. package/lib/languages/hi.d.ts +4 -8
  154. package/lib/languages/hi.js +163 -124
  155. package/lib/languages/hr.d.ts +8 -77
  156. package/lib/languages/hr.js +200 -89
  157. package/lib/languages/hu.d.ts +4 -19
  158. package/lib/languages/hu.js +198 -119
  159. package/lib/languages/id.d.ts +4 -34
  160. package/lib/languages/id.js +166 -129
  161. package/lib/languages/it.d.ts +16 -34
  162. package/lib/languages/it.js +307 -97
  163. package/lib/languages/ja.d.ts +14 -14
  164. package/lib/languages/ja.js +221 -111
  165. package/lib/languages/ka.d.ts +17 -0
  166. package/lib/languages/ka.js +291 -0
  167. package/lib/languages/kn.d.ts +4 -8
  168. package/lib/languages/kn.js +143 -35
  169. package/lib/languages/ko.d.ts +11 -11
  170. package/lib/languages/ko.js +250 -49
  171. package/lib/languages/lt.d.ts +15 -67
  172. package/lib/languages/lt.js +287 -122
  173. package/lib/languages/lv.d.ts +15 -67
  174. package/lib/languages/lv.js +288 -106
  175. package/lib/languages/mr.d.ts +4 -8
  176. package/lib/languages/mr.js +143 -125
  177. package/lib/languages/ms.d.ts +4 -28
  178. package/lib/languages/ms.js +166 -116
  179. package/lib/languages/nb.d.ts +11 -9
  180. package/lib/languages/nb.js +272 -87
  181. package/lib/languages/nl.d.ts +23 -13
  182. package/lib/languages/nl.js +299 -133
  183. package/lib/languages/pa.d.ts +4 -8
  184. package/lib/languages/pa.js +151 -124
  185. package/lib/languages/pl.d.ts +19 -77
  186. package/lib/languages/pl.js +294 -87
  187. package/lib/languages/pt.d.ts +14 -26
  188. package/lib/languages/pt.js +272 -92
  189. package/lib/languages/ro.d.ts +15 -155
  190. package/lib/languages/ro.js +219 -235
  191. package/lib/languages/ru.d.ts +8 -82
  192. package/lib/languages/ru.js +239 -90
  193. package/lib/languages/sr-Cyrl.d.ts +8 -77
  194. package/lib/languages/sr-Cyrl.js +197 -89
  195. package/lib/languages/sr-Latn.d.ts +8 -77
  196. package/lib/languages/sr-Latn.js +197 -89
  197. package/lib/languages/sv.d.ts +11 -11
  198. package/lib/languages/sv.js +278 -74
  199. package/lib/languages/sw.d.ts +4 -36
  200. package/lib/languages/sw.js +133 -106
  201. package/lib/languages/ta.d.ts +4 -17
  202. package/lib/languages/ta.js +143 -202
  203. package/lib/languages/te.d.ts +4 -19
  204. package/lib/languages/te.js +133 -196
  205. package/lib/languages/th.d.ts +4 -14
  206. package/lib/languages/th.js +135 -91
  207. package/lib/languages/tr.d.ts +15 -9
  208. package/lib/languages/tr.js +245 -49
  209. package/lib/languages/uk.d.ts +8 -82
  210. package/lib/languages/uk.js +206 -78
  211. package/lib/languages/ur.d.ts +4 -8
  212. package/lib/languages/ur.js +151 -124
  213. package/lib/languages/vi.d.ts +14 -69
  214. package/lib/languages/vi.js +278 -129
  215. package/lib/languages/yo.d.ts +7 -0
  216. package/lib/languages/yo.js +303 -0
  217. package/lib/languages/zh-Hans.d.ts +8 -18
  218. package/lib/languages/zh-Hans.js +163 -92
  219. package/lib/languages/zh-Hant.d.ts +8 -18
  220. package/lib/languages/zh-Hant.js +181 -90
  221. package/lib/n2words.d.ts +55 -209
  222. package/lib/n2words.js +115 -530
  223. package/lib/utils/is-plain-object.d.ts +13 -0
  224. package/lib/utils/is-plain-object.js +17 -0
  225. package/lib/utils/parse-numeric.d.ts +17 -0
  226. package/lib/utils/parse-numeric.js +108 -0
  227. package/lib/utils/validate-options.d.ts +8 -0
  228. package/lib/utils/validate-options.js +16 -0
  229. package/package.json +26 -14
  230. package/dist/ArabicConverter.js +0 -3
  231. package/dist/ArabicConverter.js.map +0 -1
  232. package/dist/AzerbaijaniConverter.js +0 -3
  233. package/dist/AzerbaijaniConverter.js.map +0 -1
  234. package/dist/BanglaConverter.js +0 -3
  235. package/dist/BanglaConverter.js.map +0 -1
  236. package/dist/BiblicalHebrewConverter.js +0 -3
  237. package/dist/BiblicalHebrewConverter.js.map +0 -1
  238. package/dist/CroatianConverter.js +0 -3
  239. package/dist/CroatianConverter.js.map +0 -1
  240. package/dist/CzechConverter.js +0 -3
  241. package/dist/CzechConverter.js.map +0 -1
  242. package/dist/DanishConverter.js +0 -3
  243. package/dist/DanishConverter.js.map +0 -1
  244. package/dist/DutchConverter.js +0 -3
  245. package/dist/DutchConverter.js.map +0 -1
  246. package/dist/EnglishConverter.js +0 -3
  247. package/dist/EnglishConverter.js.map +0 -1
  248. package/dist/FilipinoConverter.js +0 -3
  249. package/dist/FilipinoConverter.js.map +0 -1
  250. package/dist/FrenchBelgiumConverter.js +0 -3
  251. package/dist/FrenchBelgiumConverter.js.map +0 -1
  252. package/dist/FrenchConverter.js +0 -3
  253. package/dist/FrenchConverter.js.map +0 -1
  254. package/dist/GermanConverter.js +0 -3
  255. package/dist/GermanConverter.js.map +0 -1
  256. package/dist/GreekConverter.js +0 -3
  257. package/dist/GreekConverter.js.map +0 -1
  258. package/dist/GujaratiConverter.js +0 -3
  259. package/dist/GujaratiConverter.js.map +0 -1
  260. package/dist/HebrewConverter.js +0 -3
  261. package/dist/HebrewConverter.js.map +0 -1
  262. package/dist/HindiConverter.js +0 -3
  263. package/dist/HindiConverter.js.map +0 -1
  264. package/dist/HungarianConverter.js +0 -3
  265. package/dist/HungarianConverter.js.map +0 -1
  266. package/dist/IndonesianConverter.js +0 -3
  267. package/dist/IndonesianConverter.js.map +0 -1
  268. package/dist/ItalianConverter.js +0 -3
  269. package/dist/ItalianConverter.js.map +0 -1
  270. package/dist/JapaneseConverter.js +0 -3
  271. package/dist/JapaneseConverter.js.map +0 -1
  272. package/dist/KannadaConverter.js +0 -3
  273. package/dist/KannadaConverter.js.map +0 -1
  274. package/dist/KoreanConverter.js +0 -3
  275. package/dist/KoreanConverter.js.map +0 -1
  276. package/dist/LatvianConverter.js +0 -3
  277. package/dist/LatvianConverter.js.map +0 -1
  278. package/dist/LithuanianConverter.js +0 -3
  279. package/dist/LithuanianConverter.js.map +0 -1
  280. package/dist/MalayConverter.js +0 -3
  281. package/dist/MalayConverter.js.map +0 -1
  282. package/dist/MarathiConverter.js +0 -3
  283. package/dist/MarathiConverter.js.map +0 -1
  284. package/dist/NorwegianBokmalConverter.js +0 -3
  285. package/dist/NorwegianBokmalConverter.js.map +0 -1
  286. package/dist/PersianConverter.js +0 -3
  287. package/dist/PersianConverter.js.map +0 -1
  288. package/dist/PolishConverter.js +0 -3
  289. package/dist/PolishConverter.js.map +0 -1
  290. package/dist/PortugueseConverter.js +0 -3
  291. package/dist/PortugueseConverter.js.map +0 -1
  292. package/dist/PunjabiConverter.js +0 -3
  293. package/dist/PunjabiConverter.js.map +0 -1
  294. package/dist/RomanianConverter.js +0 -3
  295. package/dist/RomanianConverter.js.map +0 -1
  296. package/dist/RussianConverter.js +0 -3
  297. package/dist/RussianConverter.js.map +0 -1
  298. package/dist/SerbianCyrillicConverter.js +0 -3
  299. package/dist/SerbianCyrillicConverter.js.map +0 -1
  300. package/dist/SerbianLatinConverter.js +0 -3
  301. package/dist/SerbianLatinConverter.js.map +0 -1
  302. package/dist/SimplifiedChineseConverter.js +0 -3
  303. package/dist/SimplifiedChineseConverter.js.map +0 -1
  304. package/dist/SpanishConverter.js +0 -3
  305. package/dist/SpanishConverter.js.map +0 -1
  306. package/dist/SwahiliConverter.js +0 -3
  307. package/dist/SwahiliConverter.js.map +0 -1
  308. package/dist/SwedishConverter.js +0 -3
  309. package/dist/SwedishConverter.js.map +0 -1
  310. package/dist/TamilConverter.js +0 -3
  311. package/dist/TamilConverter.js.map +0 -1
  312. package/dist/TeluguConverter.js +0 -3
  313. package/dist/TeluguConverter.js.map +0 -1
  314. package/dist/ThaiConverter.js +0 -3
  315. package/dist/ThaiConverter.js.map +0 -1
  316. package/dist/TraditionalChineseConverter.js +0 -3
  317. package/dist/TraditionalChineseConverter.js.map +0 -1
  318. package/dist/TurkishConverter.js +0 -3
  319. package/dist/TurkishConverter.js.map +0 -1
  320. package/dist/UkrainianConverter.js +0 -3
  321. package/dist/UkrainianConverter.js.map +0 -1
  322. package/dist/UrduConverter.js +0 -3
  323. package/dist/UrduConverter.js.map +0 -1
  324. package/dist/VietnameseConverter.js +0 -3
  325. package/dist/VietnameseConverter.js.map +0 -1
  326. package/lib/classes/abstract-language.d.ts +0 -178
  327. package/lib/classes/abstract-language.js +0 -268
  328. package/lib/classes/greedy-scale-language.d.ts +0 -109
  329. package/lib/classes/greedy-scale-language.js +0 -201
  330. package/lib/classes/slavic-language.d.ts +0 -148
  331. package/lib/classes/slavic-language.js +0 -281
  332. package/lib/classes/south-asian-language.d.ts +0 -70
  333. package/lib/classes/south-asian-language.js +0 -154
  334. package/lib/classes/turkic-language.d.ts +0 -26
  335. package/lib/classes/turkic-language.js +0 -59
@@ -1,90 +1,294 @@
1
- import { GreedyScaleLanguage } from '../classes/greedy-scale-language.js'
2
-
3
1
  /**
4
- * Swedish language converter.
2
+ * Swedish language converter - Functional Implementation
3
+ *
4
+ * Self-contained module with its own input validation, ready for subpath exports.
5
5
  *
6
- * Supports:
7
- * - Hyphenation for compound tens (tjugo-tre)
8
- * - "och" (and) after hundreds
9
- * - Space-separated larger composites
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
+ // Segment Building
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Builds segment word for 0-999.
40
+ * Returns object with word and metadata for "och" logic.
10
41
  */
11
- export class Swedish extends GreedyScaleLanguage {
12
- negativeWord = 'minus'
13
- decimalSeparatorWord = 'komma'
14
- zeroWord = 'noll'
15
- wordSeparator = ' '
16
-
17
- scaleWords = [
18
- [1_000_000_000_000_000_000_000_000_000n, 'kvadriljon'],
19
- [1_000_000_000_000_000_000_000_000n, 'triljon'],
20
- [1_000_000_000_000_000_000_000n, 'biljon'],
21
- [1_000_000_000_000_000_000n, 'miljard'],
22
- [1_000_000_000_000n, 'biljon'],
23
- [1_000_000_000n, 'miljard'],
24
- [1_000_000n, 'miljon'],
25
- [1000n, 'tusen'],
26
- [100n, 'hundra'],
27
- [90n, 'nittio'],
28
- [80n, 'åttio'],
29
- [70n, 'sjuttio'],
30
- [60n, 'sextio'],
31
- [50n, 'femtio'],
32
- [40n, 'fyrtio'],
33
- [30n, 'trettio'],
34
- [20n, 'tjugo'],
35
- [19n, 'nitton'],
36
- [18n, 'arton'],
37
- [17n, 'sjutton'],
38
- [16n, 'sexton'],
39
- [15n, 'femton'],
40
- [14n, 'fjorton'],
41
- [13n, 'tretton'],
42
- [12n, 'tolv'],
43
- [11n, 'elva'],
44
- [10n, 'tio'],
45
- [9n, 'nio'],
46
- [8n, 'åtta'],
47
- [7n, 'sju'],
48
- [6n, 'sex'],
49
- [5n, 'fem'],
50
- [4n, 'fyra'],
51
- [3n, 'tre'],
52
- [2n, 'två'],
53
- [1n, 'ett'],
54
- [0n, 'noll']
55
- ]
56
-
57
- /** Combines two word-sets according to Swedish grammar rules. */
58
- combineWordSets (preceding, following) {
59
- const precedingWord = Object.keys(preceding)[0]
60
- const followingWord = Object.keys(following)[0]
61
- const precedingValue = Object.values(preceding)[0]
62
- const followingValue = Object.values(following)[0]
63
-
64
- if (precedingValue === 1n && followingValue < 100n) {
65
- return following
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)
66
59
  }
60
+ }
67
61
 
68
- // Omit 'ett' before 'hundra' and 'tusen'
69
- if (precedingValue === 1n && (followingValue === 100n || followingValue === 1000n)) {
70
- return following
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]
71
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
+ }
85
+
86
+ // ============================================================================
87
+ // Conversion Functions
88
+ // ============================================================================
72
89
 
73
- if (precedingValue < 100n && precedingValue > followingValue) {
74
- return { [`${precedingWord}-${followingWord}`]: precedingValue + followingValue }
90
+ /**
91
+ * Converts a non-negative integer to Swedish words.
92
+ *
93
+ * @param {bigint} n - Non-negative integer to convert
94
+ * @returns {string} Swedish words
95
+ */
96
+ function integerToWords (n) {
97
+ if (n === 0n) return ZERO
98
+
99
+ // Fast path: numbers < 1000
100
+ if (n < 1000n) {
101
+ return buildSegment(Number(n)).word
102
+ }
103
+
104
+ // Fast path: numbers < 1,000,000 (thousands)
105
+ if (n < 1_000_000n) {
106
+ const thousands = Number(n / 1000n)
107
+ const remainder = Number(n % 1000n)
108
+
109
+ // Omit "ett" before tusen
110
+ let result = thousands === 1 ? SCALES[0] : buildSegment(thousands).word + ' ' + SCALES[0]
111
+
112
+ if (remainder > 0) {
113
+ const remainderResult = buildSegment(remainder)
114
+ // Insert "och" if remainder < 100 (doesn't have hundred)
115
+ if (remainderResult.lessThan100) {
116
+ result += ' och ' + remainderResult.word
117
+ } else {
118
+ result += ' ' + remainderResult.word
119
+ }
75
120
  }
76
121
 
77
- if (precedingValue >= 100n && followingValue < 100n) {
78
- return { [`${precedingWord} och ${followingWord}`]: precedingValue + followingValue }
122
+ return result
123
+ }
124
+
125
+ // For numbers >= 1,000,000, use scale decomposition
126
+ return buildLargeNumberWords(n)
127
+ }
128
+
129
+ /**
130
+ * Builds words for numbers >= 1,000,000.
131
+ *
132
+ * @param {bigint} n - Number >= 1,000,000
133
+ * @returns {string} Swedish words
134
+ */
135
+ function buildLargeNumberWords (n) {
136
+ const numStr = n.toString()
137
+ const len = numStr.length
138
+
139
+ // Build segments of 3 digits from right to left
140
+ const segments = []
141
+ const segmentSize = 3
142
+
143
+ const remainderLen = len % segmentSize
144
+ let pos = 0
145
+ if (remainderLen > 0) {
146
+ segments.push(Number(numStr.slice(0, remainderLen)))
147
+ pos = remainderLen
148
+ }
149
+ while (pos < len) {
150
+ segments.push(Number(numStr.slice(pos, pos + segmentSize)))
151
+ pos += segmentSize
152
+ }
153
+
154
+ // Convert segments to words
155
+ const parts = []
156
+ let scaleIndex = segments.length - 1
157
+
158
+ for (let i = 0; i < segments.length; i++) {
159
+ const segment = segments[i]
160
+
161
+ if (segment !== 0) {
162
+ const segmentResult = buildSegment(segment)
163
+
164
+ if (scaleIndex === 0) {
165
+ // Units segment
166
+ parts.push({
167
+ word: segmentResult.word,
168
+ hasHundred: segmentResult.hasHundred,
169
+ isScale: false
170
+ })
171
+ } else {
172
+ // Segment with scale word
173
+ const scaleWord = SCALES[scaleIndex - 1]
174
+
175
+ let segmentWord
176
+ if (segment === 1) {
177
+ // Omit "ett" before tusen, use "en" before million+
178
+ if (scaleIndex === 1) {
179
+ segmentWord = '' // Just "tusen"
180
+ } else {
181
+ segmentWord = 'en' // "en miljon"
182
+ }
183
+ } else {
184
+ segmentWord = segmentResult.word
185
+ }
186
+
187
+ if (segmentWord) {
188
+ parts.push({ word: segmentWord, hasHundred: false, isScale: false })
189
+ }
190
+ parts.push({ word: scaleWord, hasHundred: false, isScale: true })
191
+ }
79
192
  }
80
193
 
81
- if (followingValue > precedingValue) {
82
- if (precedingValue === 1n && followingValue >= 1_000_000n) {
83
- return { [`en ${followingWord}`]: precedingValue * followingValue }
194
+ scaleIndex--
195
+ }
196
+
197
+ // Join with Swedish "och" rules
198
+ return joinSwedishParts(parts)
199
+ }
200
+
201
+ /**
202
+ * Joins parts with Swedish "och" rules.
203
+ * Insert "och" before final segment if it follows a scale word and doesn't have "hundra".
204
+ *
205
+ * @param {Array} parts - Parts with metadata
206
+ * @returns {string} Joined string
207
+ */
208
+ function joinSwedishParts (parts) {
209
+ if (parts.length === 0) return ZERO
210
+ if (parts.length === 1) return parts[0].word
211
+
212
+ const result = []
213
+
214
+ for (let i = 0; i < parts.length; i++) {
215
+ const part = parts[i]
216
+ const isLast = i === parts.length - 1
217
+
218
+ if (isLast && parts.length > 1) {
219
+ const prevPart = parts[i - 1]
220
+ // Insert "och" if previous was scale and this doesn't have hundred
221
+ if (prevPart.isScale && !part.hasHundred) {
222
+ result.push('och')
84
223
  }
85
- return { [`${precedingWord} ${followingWord}`]: precedingValue * followingValue }
86
224
  }
87
225
 
88
- return { [`${precedingWord} ${followingWord}`]: precedingValue + followingValue }
226
+ result.push(part.word)
227
+ }
228
+
229
+ return result.join(' ')
230
+ }
231
+
232
+ /**
233
+ * Converts decimal digits to Swedish words.
234
+ *
235
+ * @param {string} decimalPart - Decimal digits (without the point)
236
+ * @returns {string} Swedish words for decimal part
237
+ */
238
+ function decimalPartToWords (decimalPart) {
239
+ let result = ''
240
+
241
+ // Handle leading zeros
242
+ let i = 0
243
+ while (i < decimalPart.length && decimalPart[i] === '0') {
244
+ if (result) result += ' '
245
+ result += ZERO
246
+ i++
89
247
  }
248
+
249
+ // Convert remainder as a single number
250
+ const remainder = decimalPart.slice(i)
251
+ if (remainder) {
252
+ if (result) result += ' '
253
+ result += integerToWords(BigInt(remainder))
254
+ }
255
+
256
+ return result
90
257
  }
258
+
259
+ /**
260
+ * Converts a numeric value to Swedish words.
261
+ *
262
+ * @param {number | string | bigint} value - The numeric value to convert
263
+ * @returns {string} The number in Swedish words
264
+ * @throws {TypeError} If value is not a valid numeric type
265
+ * @throws {Error} If value is not a valid number format
266
+ *
267
+ * @example
268
+ * toWords(42) // 'fyrtio-två'
269
+ * toWords(101) // 'hundra och ett'
270
+ * toWords(1000000) // 'en miljon'
271
+ */
272
+ function toWords (value) {
273
+ const { isNegative, integerPart, decimalPart } = parseNumericValue(value)
274
+
275
+ let result = ''
276
+
277
+ if (isNegative) {
278
+ result = NEGATIVE + ' '
279
+ }
280
+
281
+ result += integerToWords(integerPart)
282
+
283
+ if (decimalPart) {
284
+ result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
285
+ }
286
+
287
+ return result
288
+ }
289
+
290
+ // ============================================================================
291
+ // Public API
292
+ // ============================================================================
293
+
294
+ export { toWords }
@@ -1,39 +1,7 @@
1
1
  /**
2
- * Swahili language converter.
2
+ * Converts a numeric value to Swahili words.
3
3
  *
4
- * Supports:
5
- * - Swahili number words (moja, mbili, tatu)
6
- * - Space-separated components
7
- * - Simple decimal point notation
4
+ * @param {number | string | bigint} value - The numeric value to convert
5
+ * @returns {string} The number in Swahili words
8
6
  */
9
- export class Swahili extends AbstractLanguage {
10
- onesWords: {
11
- 0: string;
12
- 1: string;
13
- 2: string;
14
- 3: string;
15
- 4: string;
16
- 5: string;
17
- 6: string;
18
- 7: string;
19
- 8: string;
20
- 9: string;
21
- };
22
- tensWords: {
23
- 10: string;
24
- 20: string;
25
- 30: string;
26
- 40: string;
27
- 50: string;
28
- 60: string;
29
- 70: string;
30
- 80: string;
31
- 90: string;
32
- };
33
- scaleWords: string[];
34
- wordsUnder100(n: any): any;
35
- wordsUnder1000(n: any): any;
36
- splitBy3(number: any): number[];
37
- integerToWords(integerPart: any): string;
38
- }
39
- import { AbstractLanguage } from '../classes/abstract-language.js';
7
+ export function toWords(value: number | string | bigint): string;
@@ -1,126 +1,153 @@
1
- import { AbstractLanguage } from '../classes/abstract-language.js'
2
-
3
1
  /**
4
- * Swahili language converter.
2
+ * Swahili language converter - Functional Implementation
3
+ *
4
+ * Self-contained converter with precomputed lookup tables.
5
5
  *
6
- * Supports:
7
- * - Swahili number words (moja, mbili, tatu)
8
- * - Space-separated components
9
- * - Simple decimal point notation
6
+ * Key features:
7
+ * - "na" connector for compound numbers
8
+ * - Reversed hundreds: "mia moja" (one hundred)
9
+ * - Scale words: elfu, milioni, bilioni
10
10
  */
11
- export class Swahili extends AbstractLanguage {
12
- negativeWord = 'minus'
13
- decimalSeparatorWord = 'nukta'
14
- zeroWord = 'sifuri'
15
-
16
- onesWords = {
17
- 0: 'sifuri',
18
- 1: 'moja',
19
- 2: 'mbili',
20
- 3: 'tatu',
21
- 4: 'nne',
22
- 5: 'tano',
23
- 6: 'sita',
24
- 7: 'saba',
25
- 8: 'nane',
26
- 9: 'tisa'
27
- }
28
11
 
29
- tensWords = {
30
- 10: 'kumi',
31
- 20: 'ishirini',
32
- 30: 'thelathini',
33
- 40: 'arobaini',
34
- 50: 'hamsini',
35
- 60: 'sitini',
36
- 70: 'sabini',
37
- 80: 'themanini',
38
- 90: 'tisini'
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']
22
+
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]
39
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
+ }
40
43
 
41
- scaleWords = [
42
- '',
43
- 'elfu',
44
- 'milioni',
45
- 'bilioni',
46
- 'trilioni'
47
- ]
48
-
49
- wordsUnder100 (n) {
50
- if (n < 10) return this.onesWords[n]
51
- if (n === 10) return this.tensWords[10]
52
- if (n < 20) {
53
- // 11-19: 'kumi na <digit>'
54
- return this.tensWords[10] + ' na ' + this.onesWords[n - 10]
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 = []
50
+
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))
55
58
  }
56
- const tens = Math.trunc(n / 10) * 10
57
- const ones = n % 10
58
- if (ones === 0) return this.tensWords[tens]
59
- return this.tensWords[tens] + ' na ' + this.onesWords[ones]
60
59
  }
61
60
 
62
- wordsUnder1000 (n) {
63
- if (n < 100) return this.wordsUnder100(n)
64
- if (n === 100) return 'mia moja'
65
- const hundreds = Math.trunc(n / 100)
66
- const rest = n % 100
67
- const parts = []
68
-
69
- // Hundreds: 'mia <digit>'
70
- parts.push('mia ' + this.onesWords[hundreds])
71
- if (rest > 0) {
72
- if (rest < 10) {
73
- parts.push('na ' + this.onesWords[rest])
61
+ return parts.join(' ')
62
+ }
63
+
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
70
+ }
71
+ return segments
72
+ }
73
+
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 = []
80
+
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')
74
93
  } else {
75
- parts.push(this.wordsUnder100(rest))
94
+ parts.push(wordsUnder1000(val))
76
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)
77
100
  }
101
+ }
78
102
 
79
- return parts.join(' ')
103
+ return parts.join(' ').trim()
104
+ }
105
+
106
+ function decimalPartToWords (decimalPart) {
107
+ let result = ''
108
+ let i = 0
109
+
110
+ while (i < decimalPart.length && decimalPart[i] === '0') {
111
+ if (result) result += ' '
112
+ result += ZERO
113
+ i++
80
114
  }
81
115
 
82
- splitBy3 (number) {
83
- const s = number.toString()
84
- if (s.length <= 3) return [Number(s)]
85
- const groups = []
86
- const last3 = s.slice(-3)
87
- groups.unshift(Number(last3))
88
- let remaining = s.slice(0, -3)
89
- while (remaining.length > 0) {
90
- const group = remaining.slice(-3)
91
- groups.unshift(Number(group))
92
- remaining = remaining.slice(0, -3)
93
- }
94
- return groups
116
+ const remainder = decimalPart.slice(i)
117
+ if (remainder) {
118
+ if (result) result += ' '
119
+ result += integerToWords(BigInt(remainder))
95
120
  }
96
121
 
97
- integerToWords (integerPart) {
98
- if (integerPart === 0n) return this.zeroWord
99
-
100
- const groups = this.splitBy3(integerPart)
101
- const parts = []
102
-
103
- for (let i = 0; i < groups.length; i++) {
104
- const val = groups[i]
105
- if (val === 0) continue
106
- const scaleIndex = groups.length - i - 1
107
- // scale word
108
- if (scaleIndex === 0) {
109
- if (val < 10 && parts.length > 0) {
110
- parts.push('na ' + this.onesWords[val])
111
- } else if (val === 100 && parts.length > 0) {
112
- // In compound numbers (e.g., 1100 -> 'elfu moja mia'), use 'mia' not 'mia moja'
113
- parts.push('mia')
114
- } else {
115
- parts.push(this.wordsUnder1000(val))
116
- }
117
- } else {
118
- // e.g., 'elfu moja', 'milioni mbili'
119
- const unit = (val === 1) ? 'moja' : this.wordsUnder1000(val)
120
- parts.push(this.scaleWords[scaleIndex] + ' ' + unit)
121
- }
122
- }
122
+ return result
123
+ }
123
124
 
124
- return parts.join(' ').trim()
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 + ' '
125
138
  }
139
+
140
+ result += integerToWords(integerPart)
141
+
142
+ if (decimalPart) {
143
+ result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
144
+ }
145
+
146
+ return result
126
147
  }
148
+
149
+ // ============================================================================
150
+ // Exports
151
+ // ============================================================================
152
+
153
+ export { toWords }