Skip to content

Sort Lines & Remove Duplicates — Ruby source

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

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

# sort-lines — multi-mode line sorter. Language: Ruby (3.1+, stdlib only). 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. Ruby's sort is not stable, so every ordered mode decorates with the original index (the TS sort is stable — ties keep input order). Shuffle uses mulberry32, so a seed reproduces the TS order.

module SortLines
  # Deterministic PRNG (not cryptographic); a seed reproduces the same shuffle.
  # Ints are arbitrary precision, so each wrapping step is masked to 32 bits —
  # exactly the JS |0 / Math.imul semantics of the TS original.
  def self.mulberry32(seed)
    a = seed & 0xFFFFFFFF
    lambda do
      a = (a + 0x6D2B79F5) & 0xFFFFFFFF
      t = ((a ^ (a >> 15)) * (1 | a)) & 0xFFFFFFFF
      t = ((t + ((t ^ (t >> 7)) * (61 | t))) ^ t) & 0xFFFFFFFF
      ((t ^ (t >> 14)) & 0xFFFFFFFF) / 4294967296.0
    end
  end

  # Natural order: compare ASCII digit / non-digit runs chunk-wise, digit runs by
  # value — "file2" sorts before "file10".
  def self.natural_compare(a, b, case_sensitive)
    ax = case_sensitive ? a : a.downcase
    bx = case_sensitive ? b : b.downcase
    aa = ax.scan(/[0-9]+|[^0-9]+/); aa << '' if aa.empty? # "" -> [""], like the TS ?? fallback
    bb = bx.scan(/[0-9]+|[^0-9]+/); bb << '' if bb.empty?
    (0...[aa.length, bb.length].min).each do |i|
      x, y = aa[i], bb[i]
      dn = x.match?(/\A[0-9]/)
      dm = y.match?(/\A[0-9]/)
      if dn != dm # digit run vs text run: raw compare
        return x < y ? -1 : 1
      elsif dn
        vx = x.sub(/\A0+/, ''); vy = y.sub(/\A0+/, '') # numeric value: strip leading zeros
        c = vx.length <=> vy.length
        c = vx <=> vy if c.zero?
        return c unless c.zero?
      elsif x != y
        return x < y ? -1 : 1
      end
    end
    aa.length - bb.length
  end

  # All modes return a { lines:, text:, removed_duplicates: } hash. keyword args
  # carry the four TS options; omitted means "use the TS default".
  def self.sort_lines(input, mode, case_sensitive: true, trim: false, natural: false, seed: 1)
    norm = ->(s) { case_sensitive ? s : s.downcase }
    lines = (input || '').split("\n", -1) # -1 keeps the trailing empty line, like JS
    lines = lines.map(&:strip) if trim
    removed = 0
    case mode
    when 'unique' # keep each normalized line's first occurrence; count the rest
      seen = {}
      out = []
      lines.each do |l|
        if seen.key?(norm.call(l)) then removed += 1 else seen[norm.call(l)] = true; out << l end
      end
      lines = out
    when 'shuffle' # Fisher-Yates with the seeded PRNG -> reproducible order
      rng = mulberry32(seed)
      (lines.length - 1).downto(1) do |i|
        j = (rng.call * (i + 1)).floor
        lines[i], lines[j] = lines[j], lines[i]
      end
    when 'reverse'
      lines = lines.reverse
    when 'length-asc', 'length-desc' # stable by length, then reverse for desc
      lines = lines.each_with_index.sort_by { |l, i| [l.length, i] }.map(&:first)
      lines.reverse! if mode == 'length-desc'
    else # asc / desc — stable in both directions, ties keep input order
      dir = mode == 'asc' ? 1 : -1
      lines = lines.each_with_index.sort { |(x, xi), (y, yi)|
        c = natural ? natural_compare(x, y, case_sensitive) : norm.call(x) <=> norm.call(y)
        c.zero? ? xi <=> yi : c * dir
      }.map(&:first)
    end
    { lines: lines, text: lines.join("\n"), removed_duplicates: removed }
  end
end

text = "pear\napple\nBanana\napple\nfig10\nfig2"
puts "asc:    #{SortLines.sort_lines(text, 'asc')[:text]}"
puts "ci-asc: #{SortLines.sort_lines(text, 'asc', case_sensitive: false)[:text]}"
u = SortLines.sort_lines(text, 'unique', case_sensitive: false)
puts "uniq:   #{u[:text]}  (removed #{u[:removed_duplicates]})"
puts "shuf-7: #{SortLines.sort_lines(text, 'shuffle', seed: 7)[:text]}"
puts "nat-ci: #{SortLines.sort_lines(text, 'asc', case_sensitive: false, natural: true)[: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 →