Skip to content

List Set Operations — Rust 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 Rust 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: Rust (edition 2021, 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 panics (public API returns Strings, no Result).
//!   - Functionally equivalent to the TS/Go reference: same inputs -> same outputs.
//!   - Self-contained: std only (no crates.io dependencies).
//!
//! Algorithm: each list is parsed (optionally trimmed + deduped by first-seen
//! casing), then compared by exact string equality unless `case_insensitive`
//! folds the comparison key (the original first-seen casing is still emitted).
//! Results are joined with "\n".

use std::collections::HashSet;

/// The set operation to compute over two lists. Matches the TS `SetOp` union
/// and the Go `Op` constants literal-for-literal.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetOp {
    Union,
    Intersect,
    Difference,
    SymmetricDifference,
}

/// Options mirror the TS `SetOptions` / Go `Options`. Every field has a
/// sensible default, so callers can construct it incrementally with
/// `..Default::default()`.
#[derive(Debug, Clone, Default)]
pub struct Options {
    /// Fold case for the membership comparison (the original first-seen casing
    /// is still emitted). Defaults to false.
    pub case_insensitive: bool,
    /// Trim items and drop empty lines. `None` -> true (the default); a
    /// non-`None` value is used verbatim, exactly like the TS lib's
    /// distinction between omitted (-> trim) and `trim: false`.
    pub trim: Option<bool>,
}

/// Resolve the effective trim flag: `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.
fn trim_enabled(opts: &Options) -> bool {
    opts.trim.unwrap_or(true)
}

/// Split a newline list into a deduped vector of items. Mirrors `parseList()`
/// in the TS lib and Go: 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.
fn parse_list(input: &str, opts: &Options) -> Vec<String> {
    if input.is_empty() {
        return Vec::new(); // empty input = no items
    }
    let trim = trim_enabled(opts);
    let ci = opts.case_insensitive;

    let mut items: Vec<String> = input.split('\n').map(String::from).collect();
    if trim {
        // Keep only non-empty trimmed items — `String::trim` is the Unicode-aware
        // whitespace trim (matches TS's String.prototype.trim).
        items = items
            .iter()
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
    }

    let mut seen: HashSet<String> = HashSet::with_capacity(items.len());
    let mut out: Vec<String> = Vec::with_capacity(items.len());
    for item in items {
        let key = if ci { item.to_lowercase() } else { item.clone() };
        if !seen.insert(key) {
            continue; // already seen — drop the duplicate
        }
        out.push(item);
    }
    out
}

/// Compute a set operation over two newline-separated lists. Pure and
/// deterministic; never panics. Items are compared by exact string equality
/// unless `Options::case_insensitive` 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)
pub fn set_ops(a: &str, b: &str, op: SetOp, opts: &Options) -> String {
    let ci = opts.case_insensitive;
    let key_of = |s: &str| -> String {
        if ci { s.to_lowercase() } else { s.to_string() }
    };

    let list_a = parse_list(a, opts);
    let list_b = parse_list(b, opts);

    let keys_a: HashSet<String> = list_a.iter().map(|i| key_of(i)).collect();
    let keys_b: HashSet<String> = list_b.iter().map(|i| key_of(i)).collect();

    let mut out: Vec<String> = Vec::new();
    match op {
        SetOp::Union => {
            let mut seen: HashSet<String> = HashSet::new();
            for item in &list_a {
                seen.insert(key_of(item));
                out.push(item.clone()); // list_a is already deduped by parse_list
            }
            for item in &list_b {
                let k = key_of(item);
                if seen.contains(&k) {
                    continue;
                }
                seen.insert(k);
                out.push(item.clone());
            }
        }
        SetOp::Intersect => {
            for item in &list_a {
                if keys_b.contains(&key_of(item)) {
                    out.push(item.clone());
                }
            }
        }
        SetOp::Difference => {
            for item in &list_a {
                if !keys_b.contains(&key_of(item)) {
                    out.push(item.clone());
                }
            }
        }
        SetOp::SymmetricDifference => {
            for item in &list_a {
                if !keys_b.contains(&key_of(item)) {
                    out.push(item.clone());
                }
            }
            for item in &list_b {
                if !keys_a.contains(&key_of(item)) {
                    out.push(item.clone());
                }
            }
        }
    }

    out.join("\n")
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
#[cfg(test)]
mod tests {
    use super::*;

    // The canonical fixtures shared with src/lib/list-set-ops.test.ts and the
    // Go twin's list-set-ops_test.go.
    const LIST_A: &str = "apple\nbanana\ncherry";
    const LIST_B: &str = "banana\ncherry\ndate";

    #[test]
    fn union_combines_deduped_a_then_b() {
        assert_eq!(
            set_ops(LIST_A, LIST_B, SetOp::Union, &Options::default()),
            "apple\nbanana\ncherry\ndate"
        );
    }

    #[test]
    fn intersect_items_present_in_both() {
        assert_eq!(
            set_ops(LIST_A, LIST_B, SetOp::Intersect, &Options::default()),
            "banana\ncherry"
        );
    }

    #[test]
    fn difference_items_in_a_not_in_b() {
        assert_eq!(
            set_ops(LIST_A, LIST_B, SetOp::Difference, &Options::default()),
            "apple"
        );
    }

    #[test]
    fn symmetric_difference_items_in_exactly_one() {
        assert_eq!(
            set_ops(LIST_A, LIST_B, SetOp::SymmetricDifference, &Options::default()),
            "apple\ndate"
        );
    }

    #[test]
    fn case_insensitive_emits_first_seen_casing() {
        let ci = Options { case_insensitive: true, ..Default::default() };
        // intersect matches HELLO/hello, emits a's casing.
        assert_eq!(set_ops("HELLO", "hello", SetOp::Intersect, &ci), "HELLO");
        // union keeps first-seen casing (a wins over b for duplicates).
        assert_eq!(
            set_ops("X\nApple", "x\nAPPLE\nmango", SetOp::Union, &ci),
            "X\nApple\nmango"
        );
    }
}

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 →