Sort Lines & Remove Duplicates — PHP source
Alphabetize, reverse, shuffle, dedupe, or length-sort lines of text. Supports case-insensitive and natural sorting (file2 before file10).
This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* sort-lines — PHP polyglot showcase port.
*
* Pure line-sorting logic — deterministic, stdlib-only. 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.
*/
/**
* mulberry32 — small deterministic PRNG returning floats in [0, 1).
*
* Deterministic by design (not cryptographic): a given seed reproduces the
* same shuffle. PHP integers are 64-bit signed, so every wrapping step is
* masked back to 32 bits with & 0xFFFFFFFF to match JS's 32-bit bitwise
* semantics exactly.
*/
function mulberry32(int $seed): callable {
$a = $seed & 0xFFFFFFFF;
return function () use (&$a): float {
$a = ($a + 0x6d2b79f5) & 0xFFFFFFFF;
$t = (($a ^ ($a >> 15)) * (1 | $a)) & 0xFFFFFFFF;
$t = (($t + (($t ^ ($t >> 7)) * (61 | $t))) ^ $t) & 0xFFFFFFFF;
return (($t ^ ($t >> 14)) & 0xFFFFFFFF) / 4294967296.0;
};
}
/**
* Natural-order comparator: numeric chunks compare by value so "file2" sorts
* before "file10" instead of lexicographically.
*
* Internal helper (module-private in spirit); not part of the tool's surface.
*/
function sortLinesNaturalCompare(string $a, string $b, bool $caseSensitive): int {
$ax = $caseSensitive ? $a : mb_strtolower($a);
$bx = $caseSensitive ? $b : mb_strtolower($b);
// \d+|\D+ — runs of ASCII digits, then runs of anything else. PCRE's \d
// is ASCII-only without the /u modifier, matching JS without /u.
preg_match_all('/(\d+|\D+)/', $ax, $m1);
preg_match_all('/(\d+|\D+)/', $bx, $m2);
$aa = !empty($m1[0]) ? $m1[0] : [$ax];
$bb = !empty($m2[0]) ? $m2[0] : [$bx];
$isDigit = static fn(string $s): bool => $s !== '' && $s[0] >= '0' && $s[0] <= '9';
$n = min(count($aa), count($bb));
for ($i = 0; $i < $n; $i++) {
$an = $isDigit($aa[$i]);
$bn = $isDigit($bb[$i]);
if ($an !== $bn) {
// A digit-run vs a text-run: compare as raw strings.
return $aa[$i] < $bb[$i] ? -1 : 1;
}
if ($an) {
$diff = (int) $aa[$i] - (int) $bb[$i];
if ($diff !== 0) {
return $diff;
}
} elseif ($aa[$i] !== $bb[$i]) {
return $aa[$i] < $bb[$i] ? -1 : 1;
}
}
return count($aa) - count($bb);
}
/**
* Apply `mode` to the lines of `input`.
*
* @param string $input
* @param string $mode One of: asc, desc, length-asc, length-desc,
* reverse, shuffle, unique.
* @param array{caseSensitive?:bool,trim?:bool,natural?:bool,seed?:int} $opts
* @return array{lines:string[],text:string,removedDuplicates:int}
*/
function sortLines(string $input, string $mode, array $opts = []): array {
$caseSensitive = $opts['caseSensitive'] ?? true;
$shouldTrim = $opts['trim'] ?? false;
$natural = $opts['natural'] ?? false;
$seed = $opts['seed'] ?? 1;
// explode("\n", '') -> [''] (one empty line); the rest degrades gracefully.
$lines = explode("\n", $input);
if ($shouldTrim) {
$lines = array_map('trim', $lines);
}
$norm = fn($s) => $caseSensitive ? $s : mb_strtolower($s);
$removedDuplicates = 0;
switch ($mode) {
case 'unique':
// Keep first occurrence of each normalised line; count the rest.
$seen = [];
$out = [];
foreach ($lines as $l) {
$key = $norm($l);
if (isset($seen[$key])) {
$removedDuplicates++;
} else {
$seen[$key] = true;
$out[] = $l;
}
}
$lines = $out;
break;
case 'shuffle':
// Fisher-Yates with the seeded PRNG -> reproducible ordering.
$rng = mulberry32($seed);
$arr = $lines; // copy
for ($i = count($arr) - 1; $i > 0; $i--) {
$j = (int) floor($rng() * ($i + 1));
[$arr[$i], $arr[$j]] = [$arr[$j], $arr[$i]];
}
$lines = $arr;
break;
case 'reverse':
$lines = array_reverse($lines);
break;
case 'length-asc':
case 'length-desc':
// Decorate with [value, char-length, original-index]. PHP's usort
// is NOT stable, so the index tiebreak makes ordering explicit and
// matches the TS decorate-by-index step. mb_strlen counts Unicode
// codepoints (== TS .length for BMP characters).
$decorated = [];
foreach ($lines as $i => $l) {
$decorated[] = [$l, mb_strlen($l), $i];
}
usort($decorated, fn($x, $y) => $x[1] <=> $y[1] ?: $x[2] <=> $y[2]);
$lines = array_column($decorated, 0);
if ($mode === 'length-desc') {
$lines = array_reverse($lines);
}
break;
default: // asc / desc
$dir = $mode === 'asc' ? 1 : -1;
// Decorate with the original index to guarantee stable order for
// equal elements (usort is not stable). Direction applies to the
// value comparison ONLY — equal elements keep input order in both
// directions, matching the TS `c * dir` stable sort.
$decorated = [];
foreach ($lines as $i => $l) {
$decorated[] = [$l, $i];
}
usort($decorated, function ($x, $y) use ($natural, $caseSensitive, $dir, $norm) {
$c = $natural
? sortLinesNaturalCompare($x[0], $y[0], $caseSensitive)
: strcmp($norm($x[0]), $norm($y[0]));
if ($c !== 0) {
return $c * $dir;
}
return $x[1] <=> $y[1];
});
$lines = array_column($decorated, 0);
break;
}
return [
'lines' => $lines,
'text' => implode("\n", $lines),
'removedDuplicates' => $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 →