Skip to content

Sort Lines & Remove Duplicates — Zig source

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

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

// sort-lines — multi-mode line sorter. Language: Zig (0.12+). 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. std.sort.block is stable (ties keep input order, like the TS sort); shuffle uses mulberry32 with u32 wraparound, so a seed reproduces the TS order.
const std = @import("std");

const Mode = enum { asc, desc, len_asc, len_desc, reverse, shuffle, unique };
const Options = struct { // TS defaults: case sensitive, no trim, no natural, seed 1
    case_sensitive: bool = true,
    trim: bool = false,
    natural: bool = false,
    seed: u32 = 1,
};
const Result = struct { lines: [][]const u8, text: []const u8, removed: usize };

// mulberry32 — deterministic PRNG (not cryptographic); a seed reproduces the same shuffle.
const Mulberry32 = struct {
    a: u32,
    fn next(self: *Mulberry32) f64 {
        self.a +%= 0x6d2b79f5;
        var t: u32 = (self.a ^ (self.a >> 15)) *% (1 | self.a);
        t = (t +% ((t ^ (t >> 7)) *% (61 | t))) ^ t;
        return @as(f64, @floatFromInt(t ^ (t >> 14))) / 4294967296.0;
    }
};

fn isDigit(c: u8) bool { return c >= '0' and c <= '9'; }

// Case-(un)sensitive byte compare — the plain (non-natural) order.
fn textCompare(x: []const u8, y: []const u8, case_sensitive: bool) i8 {
    const n = @min(x.len, y.len);
    var i: usize = 0;
    while (i < n) : (i += 1) {
        const a = if (case_sensitive) x[i] else std.ascii.toLower(x[i]);
        const b = if (case_sensitive) y[i] else std.ascii.toLower(y[i]);
        if (a != b) return if (a < b) -1 else 1;
    }
    if (x.len != y.len) return if (x.len < y.len) -1 else 1;
    return 0;
}

// Natural order: walk both strings in ASCII digit / non-digit runs, comparing
// chunk-wise so numbers order by value — "file2" sorts before "file10". Digit
// runs compare by numeric value: strip leading zeros, longer run wins, then lex.
fn naturalCompare(a: []const u8, b: []const u8, case_sensitive: bool) i8 {
    var ai: usize = 0;
    var bi: usize = 0;
    while (ai < a.len or bi < b.len) {
        const da = ai < a.len and isDigit(a[ai]);
        const db = bi < b.len and isDigit(b[bi]);
        const as = ai;
        const bs = bi;
        while (ai < a.len and isDigit(a[ai]) == da) ai += 1;
        while (bi < b.len and isDigit(b[bi]) == db) bi += 1;
        const x = a[as..ai];
        const y = b[bs..bi];
        if (da != db) { // digit run vs text run: raw byte compare, shorter prefix first
            if (std.mem.lessThan(u8, x, y)) return -1;
            if (std.mem.lessThan(u8, y, x)) return 1;
            return if (x.len < y.len) -1 else 1;
        }
        if (da) {
            var xs = as;
            var ys = bs;
            while (xs < ai and a[xs] == '0') xs += 1;
            while (ys < bi and b[ys] == '0') ys += 1;
            const vx = a[xs..ai];
            const vy = b[ys..bi];
            if (vx.len != vy.len) return if (vx.len < vy.len) -1 else 1;
            if (!std.mem.eql(u8, vx, vy)) return if (std.mem.lessThan(u8, vx, vy)) -1 else 1;
        } else if (textCompare(x, y, case_sensitive) != 0) {
            return textCompare(x, y, case_sensitive);
        }
    }
    return 0;
}

const SortCtx = struct { case_sensitive: bool, natural: bool, desc: bool };
fn textLess(ctx: SortCtx, x: []const u8, y: []const u8) bool {
    const c = if (ctx.natural) naturalCompare(x, y, ctx.case_sensitive) else textCompare(x, y, ctx.case_sensitive);
    return if (ctx.desc) c > 0 else c < 0;
}
fn lenLess(_: void, x: []const u8, y: []const u8) bool { return x.len < y.len; }

fn sortLines(alloc: std.mem.Allocator, input: []const u8, mode: Mode, opts: Options) !Result {
    var lines = std.ArrayList([]const u8).init(alloc);
    var it = std.mem.splitScalar(u8, input, '\n');
    while (it.next()) |raw| {
        try lines.append(if (opts.trim) std.mem.trim(u8, raw, " \t\r") else raw);
    }
    var removed: usize = 0;
    switch (mode) {
        .unique => { // keep each normalized line's first occurrence; count the rest
            var out = std.ArrayList([]const u8).init(alloc);
            defer out.deinit();
            for (lines.items) |l| {
                var dup = false;
                for (out.items) |k| {
                    if (textCompare(l, k, opts.case_sensitive) == 0) { dup = true; break; }
                }
                if (dup) { removed += 1; } else { try out.append(l); }
            }
            lines.clearRetainingCapacity();
            try lines.appendSlice(out.items);
        },
        .shuffle => { // Fisher-Yates with the seeded PRNG -> reproducible order
            var rng = Mulberry32{ .a = opts.seed };
            const arr = lines.items;
            var i: usize = arr.len -% 1;
            while (i > 0) : (i -= 1) {
                const j: usize = @intFromFloat(rng.next() * @as(f64, @floatFromInt(i + 1)));
                std.mem.swap([]const u8, &arr[i], &arr[j]);
            }
        },
        .reverse => std.mem.reverse([]const u8, lines.items),
        .len_asc, .len_desc => { // stable by length, then reverse for desc
            std.sort.block([]const u8, lines.items, {}, lenLess);
            if (mode == .len_desc) std.mem.reverse([]const u8, lines.items);
        },
        .asc, .desc => std.sort.block([]const u8, lines.items, SortCtx{
            .case_sensitive = opts.case_sensitive,
            .natural = opts.natural,
            .desc = mode == .desc,
        }, textLess),
    }
    const owned = try lines.toOwnedSlice();
    const text = try std.mem.join(alloc, "\n", owned);
    return .{ .lines = owned, .text = text, .removed = removed };
}

pub fn main() !void {
    const alloc = std.heap.page_allocator;
    const text = "pear\napple\nBanana\napple\nfig10\nfig2";
    const demos = [_]struct { mode: Mode, opts: Options, label: []const u8 }{
        .{ .mode = .asc, .opts = .{}, .label = "asc:    " },
        .{ .mode = .asc, .opts = .{ .case_sensitive = false }, .label = "ci-asc: " },
        .{ .mode = .unique, .opts = .{ .case_sensitive = false }, .label = "uniq:   " },
        .{ .mode = .shuffle, .opts = .{ .seed = 7 }, .label = "shuf-7: " },
        .{ .mode = .asc, .opts = .{ .case_sensitive = false, .natural = true }, .label = "nat-ci: " },
    };
    for (demos) |d| {
        const r = try sortLines(alloc, text, d.mode, d.opts);
        defer alloc.free(r.lines);
        defer alloc.free(r.text);
        if (r.removed > 0) {
            std.debug.print("{s}{s}  (removed {d})\n", .{ d.label, r.text, r.removed });
        } else {
            std.debug.print("{s}{s}\n", .{ d.label, r.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 →