Sort Lines & Remove Duplicates — Python source
Alphabetize, reverse, shuffle, dedupe, or length-sort lines of text. Supports case-insensitive and natural sorting (file2 before file10).
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""sort-lines — Python polyglot showcase port.
Pure line-sorting logic — deterministic, stdlib-only. Splits on newlines and
applies the requested operation. Never raises.
CosmoDev polyglot showcase port of sort-lines, ported from
src/lib/sortLines.ts. Display source — part of CosmoDev's polyglot tool pages.
"""
from __future__ import annotations
import functools
import re
from dataclasses import dataclass
from enum import Enum
from typing import Callable, List, Optional
class SortMode(str, Enum):
"""The available line operations. String values mirror the TS union."""
ASC = "asc"
DESC = "desc"
LENGTH_ASC = "length-asc"
LENGTH_DESC = "length-desc"
REVERSE = "reverse"
SHUFFLE = "shuffle"
UNIQUE = "unique"
@dataclass
class SortOptions:
# `None` means "use the default", mirroring the TS optional fields
# (omitted is not the same as False).
case_sensitive: Optional[bool] = None # default True
trim: Optional[bool] = None # default False
natural: Optional[bool] = None # default False
seed: Optional[int] = None # default 1
@dataclass
class SortResult:
lines: List[str]
text: str
removed_duplicates: int
# Chunk splitter: alternating runs of ASCII digits and non-digits. We use the
# explicit [0-9] form rather than \d because Python's \d is Unicode-aware by
# default, whereas JS's \d (no /u flag) is ASCII-only — the explicit form keeps
# the two ports identical for any input.
_CHUNK_RE = re.compile(r"[0-9]+|[^0-9]+")
def mulberry32(seed: int) -> Callable[[], float]:
"""Return a deterministic PRNG closure producing floats in [0, 1).
Deterministic by design (not cryptographic): a given seed reproduces the
same shuffle. Python ints are arbitrary precision, so every wrapping step
is masked with & 0xFFFFFFFF to match JS's 32-bit bitwise semantics.
"""
a = seed & 0xFFFFFFFF
def rng() -> float:
nonlocal a
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
return rng
def _natural_compare(a: str, b: str, case_sensitive: bool) -> int:
"""Order strings chunk-by-chunk so numeric runs compare by value.
Lets "file2" sort before "file10" rather than lexicographically.
"""
ax = a if case_sensitive else a.lower()
bx = b if case_sensitive else b.lower()
aa = _CHUNK_RE.findall(ax) or [ax]
bb = _CHUNK_RE.findall(bx) or [bx]
for x, y in zip(aa, bb):
an = x[:1].isdigit() # "" -> "" -> isdigit() is False (empty-safe)
bn = y[:1].isdigit()
if an != bn:
# A digit-run vs a text-run: compare as raw strings.
return -1 if x < y else 1
if an:
diff = int(x) - int(y)
if diff != 0:
return diff
elif x != y:
return -1 if x < y else 1
# All compared chunks equal: the shorter run-list wins.
return len(aa) - len(bb)
def sort_lines(
input: str,
mode: "str | SortMode",
opts: Optional[SortOptions] = None,
) -> SortResult:
"""Apply ``mode`` to the lines of ``input`` and return the result.
All non-unique modes return ``removed_duplicates == 0``.
"""
if opts is None:
opts = SortOptions()
case_sensitive = opts.case_sensitive if opts.case_sensitive is not None else True
should_trim = opts.trim if opts.trim is not None else False
natural = opts.natural if opts.natural is not None else False
seed = opts.seed if opts.seed is not None else 1
def norm(s: str) -> str:
return s if case_sensitive else s.lower()
# str.split("\n") on "" yields [""] — one empty line — matching JS. The
# `(input or "")` guard mirrors the TS `(input ?? "")` nullish fallback.
lines: List[str] = (input or "").split("\n")
if should_trim:
lines = [line.strip() for line in lines]
removed = 0
m = mode.value if isinstance(mode, SortMode) else mode
if m == "unique":
# Keep first occurrence of each normalised line; count the rest.
seen: set = set()
out: List[str] = []
for line in lines:
key = norm(line)
if key in seen:
removed += 1
else:
seen.add(key)
out.append(line)
lines = out
elif m == "shuffle":
# Fisher-Yates with the seeded PRNG -> reproducible ordering.
rng = mulberry32(seed)
arr = list(lines)
for i in range(len(arr) - 1, 0, -1):
j = int(rng() * (i + 1))
arr[i], arr[j] = arr[j], arr[i]
lines = arr
elif m == "reverse":
lines = list(reversed(lines))
elif m in ("length-asc", "length-desc"):
# Timsort is stable, so equal lengths keep input order — equivalent to
# the TS decorate-by-index step. len(str) is a codepoint count.
ordered = sorted(lines, key=len)
if m == "length-desc":
ordered = list(reversed(ordered))
lines = ordered
else: # asc / desc
# Python's sorted(reverse=...) is still stable, so equal keys retain
# their original order in BOTH directions — matching the TS `c * dir`
# stable sort.
reverse = m == "desc"
if natural:
ordered = sorted(
lines,
key=functools.cmp_to_key(
lambda x, y: _natural_compare(x, y, case_sensitive)
),
reverse=reverse,
)
else:
# Codepoint ordering (Python's default str comparison) matches JS
# localeCompare's default for ASCII input.
ordered = sorted(lines, key=norm, reverse=reverse)
lines = ordered
return SortResult(lines=lines, text="\n".join(lines), removed_duplicates=removed)
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 →