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 →