Skip to content

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 →