Skip to content

List Set Operations — Go 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 Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package listsetops is the Go twin of CosmoDev's src/lib/list-set-ops.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). It computes union / intersection / difference / symmetric
// difference of two newline-separated lists. Pure + deterministic, never
// panics. The table-driven tests in list-set-ops_test.go share vectors with
// src/lib/list-set-ops.test.ts so the two implementations are held to the
// same contract.
//
// Algorithm mirrors the TS lib exactly: each list is parsed (optionally
// trimmed + deduped by first-seen casing), then compared by exact string
// equality unless CaseInsensitive folds the comparison key (the original
// first-seen casing is still emitted). Results are joined with "\n".
package listsetops

import "strings"

// Op is the set operation to compute over two lists. It matches the TS
// SetOp union literal-for-literal.
type Op string

const (
	OpUnion               Op = "union"
	OpIntersect           Op = "intersect"
	OpDifference          Op = "difference"
	OpSymmetricDifference Op = "symmetricDifference"
)

// Options configures SetOps. The zero value (Options{}) matches the TS
// default (setOps(a, b, op) with no options): trim enabled,
// case-sensitive comparison.
//
// Trim is a *bool so the zero value means "trim enabled (default)" while a
// non-nil false still disables trimming — exactly like the TS lib's
// distinction between omitted (→ trim) and trim: false.
type Options struct {
	CaseInsensitive bool   // default false
	Trim            *bool  // nil → true (default); non-nil used verbatim
}

// parseList splits a newline list into a deduped slice of items. It mirrors
// parseList() in the TS lib: empty input yields no items (not a single empty
// item); when trim is enabled (default) items are whitespace-trimmed and
// empty lines are dropped; duplicates within the list are removed keeping
// the first-seen original casing.
func parseList(input string, opts Options) []string {
	if input == "" {
		return nil // empty input = no items
	}
	trim := opts.Trim == nil || *opts.Trim
	ci := opts.CaseInsensitive

	items := strings.Split(input, "\n")
	if trim {
		filtered := items[:0]
		for _, s := range items {
			s = strings.TrimSpace(s)
			if s != "" {
				filtered = append(filtered, s)
			}
		}
		items = filtered
	}

	seen := make(map[string]struct{}, len(items))
	out := make([]string, 0, len(items))
	for _, item := range items {
		key := item
		if ci {
			key = strings.ToLower(item)
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, item)
	}
	return out
}

// SetOps computes a set operation over two newline-separated lists. It is
// the Go twin of setOps() in src/lib/list-set-ops.ts and must agree with it
// on every shared vector. Items are compared by exact string equality unless
// Options.CaseInsensitive folds the key (first-seen original casing emitted).
// The result is joined with "\n".
//
//   - union               → every unique item, a then b (first-seen casing)
//   - intersect           → items present in both lists (a's order/casing)
//   - difference          → items in a not present in b
//   - symmetricDifference → items in exactly one list (a-only then b-only)
//
// An unknown op yields the empty string, matching the TS switch with no
// default case.
func SetOps(a, b string, op Op, opts Options) string {
	ci := opts.CaseInsensitive
	keyOf := func(s string) string {
		if ci {
			return strings.ToLower(s)
		}
		return s
	}

	listA := parseList(a, opts)
	listB := parseList(b, opts)

	keysA := make(map[string]struct{}, len(listA))
	for _, item := range listA {
		keysA[keyOf(item)] = struct{}{}
	}
	keysB := make(map[string]struct{}, len(listB))
	for _, item := range listB {
		keysB[keyOf(item)] = struct{}{}
	}

	var out []string
	switch op {
	case OpUnion:
		seen := make(map[string]struct{})
		for _, item := range listA {
			seen[keyOf(item)] = struct{}{}
			out = append(out, item) // listA is already deduped by parseList
		}
		for _, item := range listB {
			k := keyOf(item)
			if _, ok := seen[k]; ok {
				continue
			}
			seen[k] = struct{}{}
			out = append(out, item)
		}
	case OpIntersect:
		for _, item := range listA {
			if _, ok := keysB[keyOf(item)]; ok {
				out = append(out, item)
			}
		}
	case OpDifference:
		for _, item := range listA {
			if _, ok := keysB[keyOf(item)]; !ok {
				out = append(out, item)
			}
		}
	case OpSymmetricDifference:
		for _, item := range listA {
			if _, ok := keysB[keyOf(item)]; !ok {
				out = append(out, item)
			}
		}
		for _, item := range listB {
			if _, ok := keysA[keyOf(item)]; !ok {
				out = append(out, item)
			}
		}
	}

	return strings.Join(out, "\n")
}

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 →