Skip to content

Sort Lines & Remove Duplicates — C# source

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

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

// sort-lines — multi-mode line sorter. Language: C# (10+). 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. uint arithmetic wraps unchecked exactly like the TS Math.imul/|0 ops, so mulberry32 shuffles reproduce the TS order; OrderBy is stable, matching the TS stable sort.
using System;
using System.Collections.Generic;
using System.Linq;

enum SortMode { Asc, Desc, LengthAsc, LengthDesc, Reverse, Shuffle, Unique }

record SortOptions(bool CaseSensitive = true, bool Trim = false, bool Natural = false, int Seed = 1); // TS defaults

record SortResult(string[] Lines, string Text, int RemovedDuplicates);

static class SortLinesTool
{
    // mulberry32 — deterministic PRNG (not cryptographic); a seed reproduces the same shuffle.
    static Func<double> Mulberry32(uint seed) {
        uint a = seed;
        return () => {
            a += 0x6d2b79f5;
            uint t = (a ^ (a >> 15)) * (1 | a);
            t = (t + (t ^ (t >> 7)) * (61 | t)) ^ t;
            return (t ^ (t >> 14)) / 4294967296.0;
        };
    }

    static bool IsDigit(char c) => c is >= '0' and <= '9';

    // Maximal ASCII digit / non-digit runs; "" -> [""] like the TS ?? fallback.
    static List<string> Chunks(string s) {
        var outList = new List<string>();
        for (int i = 0; i < s.Length; ) {
            bool d = IsDigit(s[i]);
            int j = i + 1;
            while (j < s.Length && IsDigit(s[j]) == d) j++;
            outList.Add(s[i..j]);
            i = j;
        }
        if (outList.Count == 0) outList.Add("");
        return outList;
    }

    // Natural order: compare chunk-wise, digit runs by value — "file2" sorts before "file10".
    public static int NaturalCompare(string a, string b, bool caseSensitive) {
        string ax = caseSensitive ? a : a.ToLowerInvariant(), bx = caseSensitive ? b : b.ToLowerInvariant();
        var aa = Chunks(ax); var bb = Chunks(bx);
        for (int i = 0; i < Math.Min(aa.Count, bb.Count); i++) {
            string x = aa[i], y = bb[i];
            bool dn = x.Length > 0 && IsDigit(x[0]), dm = y.Length > 0 && IsDigit(y[0]);
            if (dn != dm) return string.CompareOrdinal(x, y) < 0 ? -1 : 1; // digit vs text run: raw compare
            if (dn) {
                string vx = x.TrimStart('0'), vy = y.TrimStart('0'); // numeric value
                if (vx.Length != vy.Length) return vx.Length < vy.Length ? -1 : 1;
                int c = string.CompareOrdinal(vx, vy);
                if (c != 0) return c < 0 ? -1 : 1;
            } else if (x != y) {
                return string.CompareOrdinal(x, y) < 0 ? -1 : 1;
            }
        }
        return aa.Count - bb.Count;
    }

    public static SortResult Sort(string input, SortMode mode, SortOptions? opts = null) {
        var o = opts ?? new SortOptions();
        string Norm(string s) => o.CaseSensitive ? s : s.ToLowerInvariant();
        string[] lines = (input ?? "").Split('\n'); // trailing empty line kept, like JS split("\n")
        if (o.Trim) lines = lines.Select(l => l.Trim()).ToArray();
        int removed = 0;
        switch (mode) {
            case SortMode.Unique: { // keep each normalized line's first occurrence; count the rest
                var seen = new HashSet<string>();
                var kept = new List<string>();
                foreach (var l in lines)
                    if (!seen.Add(Norm(l))) removed++;
                    else kept.Add(l);
                lines = kept.ToArray();
                break;
            }
            case SortMode.Shuffle: { // Fisher-Yates with the seeded PRNG -> reproducible order
                var rng = Mulberry32((uint)o.Seed);
                for (int i = lines.Length - 1; i > 0; i--) {
                    int j = (int)(rng() * (i + 1));
                    (lines[i], lines[j]) = (lines[j], lines[i]);
                }
                break;
            }
            case SortMode.Reverse:
                Array.Reverse(lines);
                break;
            case SortMode.LengthAsc or SortMode.LengthDesc: { // stable by length, then reverse for desc
                var ordered = lines.OrderBy(l => l.Length).ToArray(); // OrderBy is stable
                lines = mode == SortMode.LengthAsc ? ordered : ordered.Reverse().ToArray();
                break;
            }
            default: { // Asc / Desc — stable, ties keep input order in both directions
                var cmp = o.Natural
                    ? Comparer<string>.Create((x, y) => NaturalCompare(x, y, o.CaseSensitive))
                    : Comparer<string>.Create((x, y) => string.CompareOrdinal(Norm(x), Norm(y)));
                lines = mode == SortMode.Asc ? lines.OrderBy(l => l, cmp).ToArray()
                                             : lines.OrderByDescending(l => l, cmp).ToArray();
                break;
            }
        }
        return new SortResult(lines, string.Join("\n", lines), removed);
    }

    public static void Main() {
        var text = "pear\napple\nBanana\napple\nfig10\nfig2";
        Console.WriteLine("asc:    " + Sort(text, SortMode.Asc).Text);
        Console.WriteLine("ci-asc: " + Sort(text, SortMode.Asc, new(CaseSensitive: false)).Text);
        var u = Sort(text, SortMode.Unique, new(CaseSensitive: false));
        Console.WriteLine($"uniq:   {u.Text}  (removed {u.RemovedDuplicates})");
        Console.WriteLine("shuf-7: " + Sort(text, SortMode.Shuffle, new(Seed: 7)).Text);
        Console.WriteLine("nat-ci: " + Sort(text, SortMode.Asc, new(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 →