Skip to content

List Set Operations — Python 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 Python 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.

Language: Python (3.9+, standard library only)
Source:   CosmoDev polyglot showcase port of the List Set Operations tool,
          ported from src/lib/list-set-ops.ts (the canonical TypeScript
          implementation) and cli/list-set-ops/list-set-ops.go (the Go twin).
License:  display source — part of CosmoDev's polyglot tool pages.

Design goals:
  - Pure + deterministic; never raises.
  - Functionally equivalent to the TS/Go reference: same inputs -> same outputs.
  - Self-contained: stdlib only (no pip packages).

Algorithm: 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".
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import List, Literal, Optional

__all__ = ["SetOp", "SetOptions", "set_ops", "parse_list"]

SetOp = Literal["union", "intersect", "difference", "symmetricDifference"]


@dataclass
class SetOptions:
    """Options mirror the TS ``SetOptions`` / Go ``Options`` struct.

    Every field defaults to the TS default.
    """

    caseInsensitive: bool = False
    """Fold case for the membership comparison (the original first-seen casing
    is still emitted)."""

    trim: Optional[bool] = None
    """Trim items and drop empty lines. ``None`` -> True (the default); an
    explicit ``False`` disables trimming — exactly like the TS lib's
    distinction between omitted (-> trim) and ``trim: False``."""


def _trim_enabled(options: SetOptions) -> bool:
    """``None`` (default) means trim ON; a concrete bool is used as-is. Mirrors
    the TS ``opts.trim !== false`` and Go ``opts.Trim == nil || *opts.Trim``
    rules."""
    return True if options.trim is None else options.trim


def parse_list(input: str, options: SetOptions) -> List[str]:
    """Split a newline list into a deduped list of items.

    Empty input yields no items (not a single empty item). When trimming is
    enabled (default) items are whitespace-trimmed and empty lines dropped.
    Duplicates within the list are removed keeping the first-seen original
    casing.
    """
    if input == "":
        return []  # empty input = no items
    trim = _trim_enabled(options)
    ci = options.caseInsensitive

    items = input.split("\n")
    if trim:
        # str.strip is the Unicode-aware whitespace trim (matches JS String.trim).
        items = [s for s in (x.strip() for x in items) if s != ""]

    seen: set[str] = set()
    out: List[str] = []
    for item in items:
        key = item.lower() if ci else item
        if key in seen:
            continue
        seen.add(key)
        out.append(item)
    return out


def set_ops(a: str, b: str, op: SetOp, options: Optional[SetOptions] = None) -> str:
    """Compute a set operation over two newline-separated lists. Never raises.

    Items are compared by exact string equality unless ``caseInsensitive`` is
    set, in which case the comparison is case-folded but the first-seen
    original casing is 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 (no default branch).
    """
    if options is None:
        options = SetOptions()
    ci = options.caseInsensitive
    key_of = (lambda s: s.lower()) if ci else (lambda s: s)

    list_a = parse_list(a, options)
    list_b = parse_list(b, options)

    keys_a = {key_of(i) for i in list_a}
    keys_b = {key_of(i) for i in list_b}

    out: List[str] = []
    if op == "union":
        seen: set[str] = set()
        for item in list_a:
            seen.add(key_of(item))
            out.append(item)  # list_a is already deduped by parse_list
        for item in list_b:
            k = key_of(item)
            if k in seen:
                continue
            seen.add(k)
            out.append(item)
    elif op == "intersect":
        for item in list_a:
            if key_of(item) in keys_b:
                out.append(item)
    elif op == "difference":
        for item in list_a:
            if key_of(item) not in keys_b:
                out.append(item)
    elif op == "symmetricDifference":
        for item in list_a:
            if key_of(item) not in keys_b:
                out.append(item)
        for item in list_b:
            if key_of(item) not in keys_a:
                out.append(item)
    # unknown op -> out stays empty (no default branch)

    return "\n".join(out)


# ---------- showcase (the canonical suite lives in src/lib) ----------
if __name__ == "__main__":
    A = "apple\nbanana\ncherry"
    B = "banana\ncherry\ndate"
    ci = SetOptions(caseInsensitive=True)

    assert set_ops(A, B, "union") == "apple\nbanana\ncherry\ndate"
    assert set_ops(A, B, "intersect") == "banana\ncherry"
    assert set_ops(A, B, "difference") == "apple"
    assert set_ops(A, B, "symmetricDifference") == "apple\ndate"
    # case-folding compares keys, but the first-seen original casing is emitted.
    assert set_ops("HELLO", "hello", "intersect", ci) == "HELLO"
    assert set_ops("X\nApple", "x\nAPPLE\nmango", "union", ci) == "X\nApple\nmango"
    print("ok")

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 →