Sort Lines & Remove Duplicates — Kotlin source
Alphabetize, reverse, shuffle, dedupe, or length-sort lines of text. Supports case-insensitive and natural sorting (file2 before file10).
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// sort-lines — multi-mode line sorter. Language: Kotlin (1.9+). Port of src/lib/sortLines.ts — same contract as this dir's go.go (the live Go twin): split on "\n", apply the mode (asc/desc/length-asc/length-desc/reverse/shuffle/unique), join back. Int arithmetic wraps mod 2^32 exactly like the TS Math.imul/|0 ops, so mulberry32 shuffles reproduce the TS order; sortedWith is stable, matching the TS stable sort.
enum class SortMode { ASC, DESC, LENGTH_ASC, LENGTH_DESC, REVERSE, SHUFFLE, UNIQUE }
data class SortOptions(
val caseSensitive: Boolean = true, // TS defaults: case sensitive, no trim, no natural, seed 1
val trim: Boolean = false,
val natural: Boolean = false,
val seed: Int = 1,
)
data class SortResult(val lines: List<String>, val text: String, val removedDuplicates: Int)
// mulberry32 — deterministic PRNG (not cryptographic); a seed reproduces the same shuffle.
fun mulberry32(seed: Int): () -> Double {
var a = seed
return {
a += 0x6d2b79f5
var t = (a xor (a ushr 15)) * (1 or a)
t = (t + (t xor (t ushr 7)) * (61 or t)) xor t
((t xor (t ushr 14)).toLong() and 0xFFFFFFFFL) / 4294967296.0
}
}
private fun isDigit(c: Char) = c in '0'..'9'
// Maximal ASCII digit / non-digit runs; "" -> [""] like the TS ?? fallback.
private fun chunks(s: String): List<String> {
val out = mutableListOf<String>()
var i = 0
while (i < s.length) {
val d = isDigit(s[i])
var j = i + 1
while (j < s.length && isDigit(s[j]) == d) j++
out += s.substring(i, j)
i = j
}
if (out.isEmpty()) out += ""
return out
}
// Natural order: compare chunk-wise, digit runs by value — "file2" sorts before "file10".
fun naturalCompare(a: String, b: String, caseSensitive: Boolean): Int {
val ax = if (caseSensitive) a else a.lowercase()
val bx = if (caseSensitive) b else b.lowercase()
val aa = chunks(ax)
val bb = chunks(bx)
for (i in 0 until minOf(aa.size, bb.size)) {
val x = aa[i]
val y = bb[i]
val dn = x.isNotEmpty() && isDigit(x[0])
val dm = y.isNotEmpty() && isDigit(y[0])
if (dn != dm) return if (x < y) -1 else 1 // digit run vs text run: raw compare
if (dn) {
val vx = x.dropWhile { it == '0' } // numeric value: strip leading zeros
val vy = y.dropWhile { it == '0' }
if (vx.length != vy.length) return vx.length - vy.length
if (vx != vy) return if (vx < vy) -1 else 1
} else if (x != y) {
return if (x < y) -1 else 1
}
}
return aa.size - bb.size
}
fun sortLines(input: String, mode: SortMode, opts: SortOptions = SortOptions()): SortResult {
val norm = { s: String -> if (opts.caseSensitive) s else s.lowercase() }
var lines = input.split('\n') // trailing empty line kept, like JS split("\n")
if (opts.trim) lines = lines.map { it.trim() }
var removed = 0
lines = when (mode) {
SortMode.UNIQUE -> { // keep each normalized line's first occurrence; count the rest
val seen = HashSet<String>()
val out = mutableListOf<String>()
for (l in lines) if (!seen.add(norm(l))) removed++ else out += l
out
}
SortMode.SHUFFLE -> { // Fisher-Yates with the seeded PRNG -> reproducible order
val rng = mulberry32(opts.seed)
val arr = lines.toMutableList()
for (i in arr.size - 1 downTo 1) {
val j = (rng() * (i + 1)).toInt()
arr[i] = arr[j].also { arr[j] = arr[i] }
}
arr
}
SortMode.REVERSE -> lines.asReversed()
SortMode.LENGTH_ASC, SortMode.LENGTH_DESC -> { // stable by length (ties keep input order), then reverse for desc
val ordered = lines.withIndex().sortedWith(compareBy({ it.value.length }, { it.index })).map { it.value }
if (mode == SortMode.LENGTH_ASC) ordered else ordered.asReversed()
}
SortMode.ASC, SortMode.DESC -> { // stable in both directions — ties keep input order
val dir = if (mode == SortMode.ASC) 1 else -1
lines.withIndex().sortedWith { a, b ->
val c = if (opts.natural) naturalCompare(a.value, b.value, opts.caseSensitive)
else norm(a.value).compareTo(norm(b.value))
if (c != 0) c * dir else a.index - b.index
}.map { it.value }
}
}
return SortResult(lines, lines.joinToString("\n"), removed)
}
fun main() {
val text = "pear\napple\nBanana\napple\nfig10\nfig2"
println("asc: " + sortLines(text, SortMode.ASC).text)
println("ci-asc: " + sortLines(text, SortMode.ASC, SortOptions(caseSensitive = false)).text)
val u = sortLines(text, SortMode.UNIQUE, SortOptions(caseSensitive = false))
println("uniq: ${u.text} (removed ${u.removedDuplicates})")
println("shuf-7: " + sortLines(text, SortMode.SHUFFLE, SortOptions(seed = 7)).text)
println("nat-ci: " + sortLines(text, SortMode.ASC, SortOptions(caseSensitive = false, natural = true)).text)
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →