Skip to content

Sort Lines & Remove Duplicates — Java source

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

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

// sort-lines — multi-mode line sorter. Language: Java (17+). Port of src/lib/sortLines.ts — same contract as this dir's go.go (the live Go twin): split on "\n", apply the mode (asc/desc/length-asc/length-desc/reverse/shuffle/unique), join back. Java int arithmetic wraps mod 2^32 exactly like the TS Math.imul/|0 ops, so mulberry32 shuffles reproduce the TS order; Collections.sort (TimSort) is stable, matching the TS stable sort.
import java.util.*;
import java.util.function.DoubleSupplier;

public class SortLines {
    enum Mode { ASC, DESC, LENGTH_ASC, LENGTH_DESC, REVERSE, SHUFFLE, UNIQUE }

    static final class Options {
        boolean caseSensitive = true;  // TS defaults: case sensitive, no trim, no natural, seed 1
        boolean trim = false, natural = false;
        int seed = 1;
    }

    static final class Result {
        final List<String> lines; final String text; final int removedDuplicates;
        Result(List<String> lines, int removedDuplicates) {
            this.lines = lines;
            this.text = String.join("\n", lines);
            this.removedDuplicates = removedDuplicates;
        }
    }

    // mulberry32 — deterministic PRNG (not cryptographic); a seed reproduces the same shuffle.
    static DoubleSupplier mulberry32(int seed) {
        int[] a = { seed };
        return () -> {
            a[0] += 0x6d2b79f5;
            int t = (a[0] ^ (a[0] >>> 15)) * (1 | a[0]);
            t = (t + (t ^ (t >>> 7)) * (61 | t)) ^ t;
            return ((t ^ (t >>> 14)) & 0xFFFFFFFFL) / 4294967296.0;
        };
    }

    static boolean isDigitCh(char c) { return c >= '0' && c <= '9'; }

    static String norm(String s, boolean caseSensitive) {
        return caseSensitive ? s : s.toLowerCase(Locale.ROOT);
    }

    // Maximal ASCII digit / non-digit runs; "" -> [""] like the TS ?? fallback.
    static List<String> chunks(String s) {
        List<String> out = new ArrayList<>();
        for (int i = 0; i < s.length(); ) {
            boolean d = isDigitCh(s.charAt(i));
            int j = i + 1;
            while (j < s.length() && isDigitCh(s.charAt(j)) == d) j++;
            out.add(s.substring(i, j));
            i = j;
        }
        if (out.isEmpty()) out.add("");
        return out;
    }

    // Natural order: compare chunk-wise, digit runs by value — "file2" sorts before "file10".
    static int naturalCompare(String a, String b, boolean caseSensitive) {
        List<String> aa = chunks(norm(a, caseSensitive)), bb = chunks(norm(b, caseSensitive));
        for (int i = 0; i < Math.min(aa.size(), bb.size()); i++) {
            String x = aa.get(i), y = bb.get(i);
            boolean dn = !x.isEmpty() && isDigitCh(x.charAt(0)), dm = !y.isEmpty() && isDigitCh(y.charAt(0));
            if (dn != dm) return x.compareTo(y) < 0 ? -1 : 1; // digit vs text run: raw compare
            if (dn) {
                String vx = x.replaceFirst("^0+", ""), vy = y.replaceFirst("^0+", ""); // numeric value
                if (vx.length() != vy.length()) return vx.length() < vy.length() ? -1 : 1;
                int c = vx.compareTo(vy);
                if (c != 0) return c < 0 ? -1 : 1;
            } else if (!x.equals(y)) {
                return x.compareTo(y) < 0 ? -1 : 1;
            }
        }
        return aa.size() - bb.size();
    }

    static Result sort(String input, Mode mode, Options o) {
        List<String> lines = new ArrayList<>(Arrays.asList(input.split("\n", -1))); // trailing empty kept
        if (o.trim) lines.replaceAll(String::strip);
        int removed = 0;
        switch (mode) {
            case UNIQUE -> { // keep each normalized line's first occurrence; count the rest
                Set<String> seen = new HashSet<>();
                List<String> out = new ArrayList<>();
                for (String l : lines) {
                    if (!seen.add(norm(l, o.caseSensitive))) removed++;
                    else out.add(l);
                }
                lines = out;
            }
            case SHUFFLE -> { // Fisher-Yates with the seeded PRNG -> reproducible order
                DoubleSupplier rng = mulberry32(o.seed);
                for (int i = lines.size() - 1; i > 0; i--) {
                    int j = (int) (rng.getAsDouble() * (i + 1));
                    Collections.swap(lines, i, j);
                }
            }
            case REVERSE -> Collections.reverse(lines);
            case LENGTH_ASC, LENGTH_DESC -> { // stable by length, then reverse for desc
                lines.sort(Comparator.comparingInt(String::length)); // TimSort is stable
                if (mode == Mode.LENGTH_DESC) Collections.reverse(lines);
            }
            default -> { // ASC / DESC — stable, ties keep input order in both directions
                int dir = mode == Mode.ASC ? 1 : -1;
                lines.sort((x, y) -> {
                    int c = o.natural ? naturalCompare(x, y, o.caseSensitive)
                                      : norm(x, o.caseSensitive).compareTo(norm(y, o.caseSensitive));
                    return c * dir;
                });
            }
        }
        return new Result(lines, removed);
    }

    public static void main(String[] args) {
        String text = "pear\napple\nBanana\napple\nfig10\nfig2";
        Options ci = new Options(); ci.caseSensitive = false;
        Options sh = new Options(); sh.seed = 7;
        Options nat = new Options(); nat.caseSensitive = false; nat.natural = true;
        System.out.println("asc:    " + sort(text, Mode.ASC, new Options()).text);
        System.out.println("ci-asc: " + sort(text, Mode.ASC, ci).text);
        System.out.println("uniq:   " + sort(text, Mode.UNIQUE, ci).text + "  (removed "
                + sort(text, Mode.UNIQUE, ci).removedDuplicates + ")");
        System.out.println("shuf-7: " + sort(text, Mode.SHUFFLE, sh).text);
        System.out.println("nat-ci: " + sort(text, Mode.ASC, nat).text);
    }
}

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 →