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 →