Abacus/Soroban CLI in Kotlin Script
I was tinkering with Kotlin Script (.kts) files today and built a small interactive abacus (soroban) CLI. Kotlin scripts run with kotlinc -script or the Kotlin CLI; no project or build file required—just a shebang and you can ./Abacus.kts and go.
Demo
Commands: +<n> / add <n> to add, -<n> / sub <n> to subtract. Type help for commands. Empty line or q quits.
./Abacus.kts
Total: 0
○ │○ │○ │○
────┼────┼────┼────
○○○○│○○○○│○○○○│○○○○
1K 100s 10s 1s
Commands:
+<n> or + <n> or add <n> add n to total
-<n> or - <n> or sub <n> subtract n from total
help show this help
q or empty line quit
How to run
Make the script executable and run it (Kotlin CLI required):
chmod +x Abacus.kts
./Abacus.kts
Or:
kotlinc -script Abacus.kts
The script
Layout: each column is one decimal digit. Upper row = 1 bead worth 5; lower row = 4 beads worth 1 each. Digit value = 5×upper + lower.
#!/usr/bin/env kotlin
/**
* Interactive visual abacus. Commands: +<n> / add <n> to add, -<n> / sub <n> to subtract.
* Type "help" for commands. Empty line or 'q' quits.
*/
// ─── Abacus layout: each column = one decimal digit ────────────────────────
// Upper row: 1 bead = 5 (0 or 1 bead)
// Lower row: 4 beads = 1 each (0–4 beads)
// Digit value = 5 * upper + lower
val UPPER_BEADS = 1 // one bead worth 5
val LOWER_BEADS = 4 // four beads worth 1 each
val BEAD_ON = '●'
val BEAD_OFF = '○'
val COLUMN_WIDTH = 4
val DEFAULT_DIGITS = 4 // minimum columns to show (e.g. 0 → 0000)
fun digitToBeads(d: Int): Pair<Int, Int> {
require(d in 0..9) { "digit must be 0-9: $d" }
val upper = d / 5
val lower = d % 5
return upper to lower
}
fun renderColumn(digit: Int): List<String> {
val (upper, lower) = digitToBeads(digit)
val upperRow = (BEAD_OFF.toString().repeat(UPPER_BEADS - upper)) + (BEAD_ON.toString().repeat(upper))
val lowerRow = (BEAD_ON.toString().repeat(lower)) + (BEAD_OFF.toString().repeat(LOWER_BEADS - lower))
return listOf(
upperRow.padEnd(COLUMN_WIDTH),
"─".repeat(COLUMN_WIDTH),
lowerRow.padEnd(COLUMN_WIDTH)
)
}
fun renderAbacus(total: Long, minDigits: Int = DEFAULT_DIGITS): String {
val value = if (total < 0) 0L else total
val digits = value.toString().map { it.digitToInt() }
val padded = if (digits.size < minDigits) {
List(minDigits - digits.size) { 0 } + digits
} else digits
val columnLines = padded.map { renderColumn(it) }
val line0 = columnLines.joinToString("│") { it[0] }
val line1 = columnLines.joinToString("┼") { it[1] }
val line2 = columnLines.joinToString("│") { it[2] }
val labels = padded.indices.reversed().map { i ->
when (i) {
0 -> "1s"
1 -> "10s"
2 -> "100s"
3 -> "1K"
4 -> "10K"
5 -> "100K"
6 -> "1M"
7 -> "10M"
else -> "10^$i"
}.padEnd(COLUMN_WIDTH)
}.joinToString(" ")
return buildString {
appendLine(" Total: $total")
appendLine()
appendLine(" $line0")
appendLine(" $line1")
appendLine(" $line2")
appendLine(" $labels")
}
}
fun isInteractive(): Boolean = System.console() != null
fun clearScreen() {
if (isInteractive()) print("\u001b[H\u001b[2J")
}
val HELP_TEXT = """
Commands:
+<n> or + <n> or add <n> add n to total
-<n> or - <n> or sub <n> subtract n from total
help show this help
q or empty line quit
""".trimIndent()
fun parseCommand(input: String): Pair<Long, Boolean>? {
val s = input.trim()
val addMatch = Regex("^\\+\\s*(\\d+)$").find(s) ?: Regex("^add\\s+(\\d+)$", RegexOption.IGNORE_CASE).find(s)
if (addMatch != null) return addMatch.groupValues[1].toLong() to true
val subMatch = Regex("^\\-\\s*(\\d+)$").find(s) ?: Regex("^sub(?:tract)?\\s+(\\d+)$", RegexOption.IGNORE_CASE).find(s)
if (subMatch != null) return subMatch.groupValues[1].toLong() to false
if (s.matches(Regex("^(\\d+)$"))) return s.toLong() to true // bare number = add
return null
}
fun main() {
var total = 0L
while (true) {
print(renderAbacus(total))
print("\n ")
System.out.flush()
val input = (System.console()?.readLine() ?: readLine())?.trim() ?: break
if (input.equals("q", ignoreCase = true) || input.isEmpty()) break
if (input.equals("help", ignoreCase = true)) {
println(HELP_TEXT)
continue
}
val parsed = parseCommand(input)
if (parsed == null) {
println(" Unknown command. Type 'help' for commands.")
continue
}
val (n, isAdd) = parsed
total += if (isAdd) n else -n
clearScreen()
}
println("Final total: $total")
}
main()
Nice for quick CLI tools and one-off automation without a full Kotlin project.
Arrived
Ninja Turdle