Skip to content

Sort Lines & Remove Duplicates — Swift source

Alphabetize, reverse, shuffle, dedupe, or length-sort lines of text. Supports case-insensitive and natural sorting (file2 before file10).

This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// sort-lines — multi-mode line sorter. Language: Swift (5.9+, stdlib only). 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. Swift's sorted(by:) is not guaranteed stable, so ordered modes decorate with the original index (the TS sort is stable — ties keep input order). Shuffle uses mulberry32, so a seed reproduces the TS order.

enum SortMode: String { case asc, desc, lengthAsc = "length-asc", lengthDesc = "length-desc", reverse, shuffle, unique }

struct SortOptions { // TS defaults: case sensitive, no trim, no natural, seed 1
    var caseSensitive = true, trim = false, natural = false, seed = 1
}

struct SortResult { let lines: [String]; let text: String; let removedDuplicates: Int }

// mulberry32 — deterministic PRNG (not cryptographic); a seed reproduces the same shuffle.
struct Mulberry32 {
    var a: UInt32
    init(seed: Int) { a = UInt32(bitPattern: Int32(truncatingIfNeeded: seed)) }
    mutating func next() -> Double {
        a &+= 0x6d2b79f5
        var t = (a ^ (a >> 15)) &* (1 | a)
        t = (t &+ (t ^ (t >> 7)) &* (61 | t)) ^ t
        return Double(t ^ (t >> 14)) / 4294967296.0
    }
}

private func isDigit(_ c: Character) -> Bool { c >= "0" && c <= "9" }

private func trimWS(_ s: String) -> String { // spaces / tabs / CR both ends
    var t = Substring(s)
    while let f = t.first, f == " " || f == "\t" || f == "\r" { t = t.dropFirst() }
    while let l = t.last, l == " " || l == "\t" || l == "\r" { t = t.dropLast() }
    return String(t)
}

// Maximal digit / non-digit runs; "" yields [""], like the TS ?? fallback.
private func chunks(_ s: String) -> [Substring] {
    var out: [Substring] = [], start = s.startIndex, i = s.startIndex, digit = false
    while i < s.endIndex {
        let d = isDigit(s[i])
        if i == s.startIndex { digit = d } else if d != digit { out.append(s[start..<i]); start = i; digit = d }
        i = s.index(after: i)
    }
    out.append(s[start...])
    return out
}

func plainCompare(_ a: String, _ b: String, caseSensitive: Bool) -> Int {
    let x = caseSensitive ? a : a.lowercased(), y = caseSensitive ? b : b.lowercased()
    return x == y ? 0 : x < y ? -1 : 1
}

// Natural order: compare ASCII digit / non-digit runs chunk-wise so numbers order
// by value — "file2" sorts before "file10".
func naturalCompare(_ a: String, _ b: String, caseSensitive: Bool) -> Int {
    let aa = chunks(caseSensitive ? a : a.lowercased())
    let bb = chunks(caseSensitive ? b : b.lowercased())
    for i in 0..<min(aa.count, bb.count) {
        let x = aa[i], y = bb[i]
        let dn = x.first.map(isDigit) ?? false, dm = y.first.map(isDigit) ?? false
        if dn != dm { return x < y ? -1 : 1 } // digit run vs text run: raw compare
        if dn {
            let vx = x.drop { $0 == "0" }, vy = y.drop { $0 == "0" } // numeric value: strip leading zeros
            if vx.count != vy.count { return vx.count < vy.count ? -1 : 1 }
            if vx != vy { return vx < vy ? -1 : 1 }
        } else if x != y {
            return x < y ? -1 : 1
        }
    }
    return aa.count - bb.count
}

func sortLines(_ input: String, _ mode: SortMode, _ opts: SortOptions = SortOptions()) -> SortResult {
    var lines = input.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
    if opts.trim { lines = lines.map(trimWS) }
    let norm = { (s: String) -> String in opts.caseSensitive ? s : s.lowercased() }
    var removed = 0
    switch mode {
    case .unique: // keep each normalized line's first occurrence; count the rest
        var seen = Set<String>(), out: [String] = []
        for l in lines {
            if seen.contains(norm(l)) { removed += 1 } else { seen.insert(norm(l)); out.append(l) }
        }
        lines = out
    case .shuffle: // Fisher-Yates with the seeded PRNG -> reproducible order
        var rng = Mulberry32(seed: opts.seed)
        var i = lines.count - 1
        while i > 0 { let j = Int(rng.next() * Double(i + 1)); lines.swapAt(i, j); i -= 1 }
    case .reverse:
        lines.reverse()
    case .lengthAsc, .lengthDesc: // stable by length (ties keep input order), then reverse for desc
        let decorated = Array(lines.enumerated())
        let ordered = decorated.sorted(by: { (a, b) -> Bool in
            if a.element.count != b.element.count { return a.element.count < b.element.count }
            return a.offset < b.offset
        }).map { $0.element }
        lines = mode == .lengthAsc ? ordered : ordered.reversed()
    case .asc, .desc: // stable in both directions — ties keep input order
        let dir = mode == .asc ? 1 : -1
        lines = lines.enumerated().sorted { a, b in
            let c = opts.natural ? naturalCompare(a.element, b.element, caseSensitive: opts.caseSensitive)
                                 : plainCompare(a.element, b.element, caseSensitive: opts.caseSensitive)
            return c != 0 ? c * dir < 0 : a.offset < b.offset
        }.map { $0.element }
    }
    return SortResult(lines: lines, text: lines.joined(separator: "\n"), removedDuplicates: removed)
}

let text = "pear\napple\nBanana\napple\nfig10\nfig2"
print("asc:    " + sortLines(text, .asc).text)
print("ci-asc: " + sortLines(text, .asc, SortOptions(caseSensitive: false)).text)
let u = sortLines(text, .unique, SortOptions(caseSensitive: false))
print("uniq:   \(u.text)  (removed \(u.removedDuplicates))")
print("shuf-7: " + sortLines(text, .shuffle, SortOptions(seed: 7)).text)
print("nat-ci: " + sortLines(text, .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 →