Skip to content

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 →