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 →