Skip to content

Sort Lines & Remove Duplicates — JavaScript source

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

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

/**
 * sort-lines - JavaScript polyglot showcase port.
 *
 * Pure line-sorting logic - no React, no DOM, deterministic. Splits on
 * newlines and applies the requested operation. Never throws.
 *
 * CosmoDev polyglot showcase port of sort-lines, ported from
 * src/lib/sortLines.ts. Display source - part of CosmoDev's polyglot
 * tool pages.
 */

/**
 * @typedef {'asc'|'desc'|'length-asc'|'length-desc'|'reverse'|'shuffle'|'unique'} SortMode
 * @typedef {{ caseSensitive?: boolean, trim?: boolean, natural?: boolean, seed?: number }} SortOptions
 * @typedef {{ lines: string[], text: string, removedDuplicates: number }} SortResult
 */

/**
 * mulberry32 - small deterministic PRNG returning floats in [0, 1).
 *
 * Intentionally deterministic (not cryptographic): a given seed reproduces
 * the exact same shuffle, which is what a "sort lines" tool wants.
 *
 * @param {number} seed - 32-bit unsigned integer seed.
 * @returns {() => number} A stateful RNG function.
 */
export function mulberry32(seed) {
  let a = seed >>> 0;
  return function () {
    // 32-bit bitwise ops keep `a` and `t` in the int32 ring; `>>>` is the
    // unsigned shift that mirrors the algorithm's reference implementation.
    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-order comparator: splits each string into alternating numeric and
 * text chunks so that "file2" sorts before "file10" - numeric chunks compare
 * by value rather than lexicographically.
 *
 * @param {string} a
 * @param {string} b
 * @param {boolean} caseSensitive
 * @returns {number} negative / zero / positive, per comparator contract.
 */
function naturalCompare(a, b, caseSensitive) {
  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]);
    // A digit-run and a text-run are compared as raw strings.
    if (an !== bn) return aa[i] < bb[i] ? -1 : 1;
    if (an) {
      // Two digit-runs compare by their numeric value.
      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;
    }
  }
  // If all compared chunks are equal, the shorter run-list wins.
  return aa.length - bb.length;
}

/**
 * Sort the lines of `input` according to `mode`.
 *
 * @param {string} input
 * @param {SortMode} mode
 * @param {SortOptions} [opts]
 * @returns {SortResult}
 */
export function sortLines(input, mode, opts = {}) {
  const caseSensitive = opts.caseSensitive ?? true;
  const shouldTrim = opts.trim ?? false;
  const natural = opts.natural ?? false;
  const seed = opts.seed ?? 1;

  // ''.split('\n') -> [''] (one empty line); the rest degrades gracefully.
  let lines = (input ?? '').split('\n');
  if (shouldTrim) lines = lines.map((l) => l.trim());

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

  if (mode === 'unique') {
    // Keep first occurrence of each (normalised) line, count the rest.
    const seen = new Set();
    const out = [];
    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') {
    // Fisher-Yates with the seeded PRNG so output is reproducible.
    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') {
    // Decorate with the original index so equal lengths stay in input order
    // (an explicit, sort-stability-independent tiebreak).
    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: direction is folded into the comparator, so equal elements
    // retain their original order in BOTH directions (stable).
    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 →