Skip to content

Sort Lines & Remove Duplicates — TypeScript source

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

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

// Pure line-sorting logic - no React, no DOM, deterministic.
// Splits on newlines and applies the requested operation. Never throws.

export type SortMode =
  | 'asc'
  | 'desc'
  | 'length-asc'
  | 'length-desc'
  | 'reverse'
  | 'shuffle'
  | 'unique';

export interface SortOptions {
  caseSensitive?: boolean; // default true
  trim?: boolean; // default false
  natural?: boolean; // default false
  seed?: number; // for shuffle, default 1
}

export interface SortResult {
  lines: string[];
  text: string;
  removedDuplicates: number;
}

/** mulberry32 - small deterministic PRNG returning floats in [0, 1). */
export function mulberry32(seed: number): () => number {
  let a = seed >>> 0;
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// Natural comparator: splits into alternating text/numeric chunks.
function naturalCompare(a: string, b: string, caseSensitive: boolean): number {
  const ax = caseSensitive ? a : a.toLowerCase();
  const bx = caseSensitive ? b : b.toLowerCase();
  const re = /(\d+|\D+)/g;
  const aa = ax.match(re) ?? [ax];
  const bb = bx.match(re) ?? [bx];
  const len = Math.min(aa.length, bb.length);
  for (let i = 0; i < len; i++) {
    const an = /^\d/.test(aa[i]);
    const bn = /^\d/.test(bb[i]);
    if (an !== bn) return aa[i] < bb[i] ? -1 : 1;
    if (an) {
      const diff = parseInt(aa[i], 10) - parseInt(bb[i], 10);
      if (diff !== 0) return diff;
    } else if (aa[i] !== bb[i]) {
      return aa[i] < bb[i] ? -1 : 1;
    }
  }
  return aa.length - bb.length;
}

export function sortLines(input: string, mode: SortMode, opts: SortOptions = {}): SortResult {
  const caseSensitive = opts.caseSensitive ?? true;
  const shouldTrim = opts.trim ?? false;
  const natural = opts.natural ?? false;
  const seed = opts.seed ?? 1;

  let lines = (input ?? '').split('\n');
  if (shouldTrim) lines = lines.map((l) => l.trim());

  const norm = (s: string) => (caseSensitive ? s : s.toLowerCase());
  let removedDuplicates = 0;

  if (mode === 'unique') {
    const seen = new Set<string>();
    const out: string[] = [];
    for (const l of lines) {
      const key = norm(l);
      if (seen.has(key)) {
        removedDuplicates++;
      } else {
        seen.add(key);
        out.push(l);
      }
    }
    lines = out;
  } else if (mode === 'shuffle') {
    const rng = mulberry32(seed);
    const arr = [...lines];
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(rng() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    lines = arr;
  } else if (mode === 'reverse') {
    lines = [...lines].reverse();
  } else if (mode === 'length-asc' || mode === 'length-desc') {
    const dir = mode === 'length-asc' ? 1 : -1;
    lines = [...lines]
      .map((l, i) => ({ l, i }))
      .sort((a, b) => a.l.length - b.l.length || a.i - b.i)
      .map((x) => x.l);
    if (dir === -1) lines.reverse();
  } else {
    // asc / desc
    const dir = mode === 'asc' ? 1 : -1;
    lines = [...lines].sort((a, b) => {
      const c = natural
        ? naturalCompare(a, b, caseSensitive)
        : norm(a).localeCompare(norm(b));
      return c * dir;
    });
  }

  return { lines, text: lines.join('\n'), removedDuplicates };
}

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 →