Skip to content

List Set Operations — Ruby source

Compute the union, intersection, difference, or symmetric difference of two newline-separated lists. Deduped results with optional trim and case-insensitive matching. Runs entirely in your browser, with a shareable link.

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

# list-set-ops — union / intersection / difference / symmetric difference of
# two newline-separated lists. Ruby port (canonical TS:
# src/lib/list-set-ops.ts; Go twin: cli/list-set-ops). Set is the membership
# structure; trim defaults on like the TS `trim !== false`.
require 'set'

module ListSetOps
  class SetOptions
    def initialize(case_insensitive: false, trim: true)
      @case_insensitive = case_insensitive
      @trim = trim
    end
    attr_reader :case_insensitive, :trim
  end

  module_function

  # Split a newline list into a deduped array (first-seen casing kept).
  # split(..., -1) keeps trailing empty lines so trim-then-drop matches TS.
  def parse_list(input, o = SetOptions.new)
    return [] if input.empty? # empty input = no items
    key = ->(s) { o.case_insensitive ? s.downcase : s }
    seen = Set.new
    input.split("\n", -1).filter_map do |raw|
      item = o.trim ? raw.strip : raw
      next if o.trim && item.empty?
      next if seen.include?(key.call(item))
      seen << key.call(item)
      item
    end
  end

  # Compute a set op over two lists; the result is "\n"-joined.
  def set_ops(a, b, op, o = SetOptions.new)
    key = ->(s) { o.case_insensitive ? s.downcase : s }
    la = parse_list(a, o)
    lb = parse_list(b, o)
    ka = la.map { |s| key.call(s) }.to_set
    kb = lb.map { |s| key.call(s) }.to_set
    out = []
    case op
    when 'union' # every unique item, a then b
      seen = Set.new
      (la + lb).each { |s| out << s if seen.add?(key.call(s)) }
    when 'intersect'            then la.each { |s| out << s if kb.include?(key.call(s)) }
    when 'difference'           then la.each { |s| out << s unless kb.include?(key.call(s)) }
    when 'symmetricDifference'  # exactly one list, a-only then b-only
      la.each { |s| out << s unless kb.include?(key.call(s)) }
      lb.each { |s| out << s unless ka.include?(key.call(s)) }
    end # unknown op -> ""
    out.join("\n")
  end
end

if __FILE__ == $PROGRAM_NAME
  a = "apple\nbanana\ncherry"
  b = "banana\ncherry\ndate"
  puts ListSetOps.set_ops(a, b, 'union')               # apple\nbanana\ncherry\ndate
  puts ListSetOps.set_ops(a, b, 'intersect')           # banana\ncherry
  puts ListSetOps.set_ops(a, b, 'difference')          # apple
  puts ListSetOps.set_ops(a, b, 'symmetricDifference') # apple\ndate
  # case-folded compare, first-seen original casing emitted
  puts ListSetOps.set_ops('HELLO', 'hello', 'intersect', ListSetOps::SetOptions.new(case_insensitive: true)) # HELLO
end

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 →