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 →