Skip to content

List Set Operations — JavaScript 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 JavaScript 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:   JavaScript (ES2020+, runs unmodified in Node 16+ and modern browsers)
 * 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 throws.
 *   - Functionally equivalent to the TS/Go reference: same inputs -> same outputs.
 *   - Self-contained: stdlib only (no npm dependencies).
 *
 * 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".
 */

'use strict';

/**
 * The set operation to compute over two lists. (No `export type` in plain JS -
 * the JSDoc union is the contract.)
 * @typedef {('union' | 'intersect' | 'difference' | 'symmetricDifference')} SetOp
 */

/**
 * Options shape. Keys are all optional.
 * @typedef {Object} SetOptions
 * @property {boolean} [caseInsensitive] Fold case for the membership comparison (the original first-seen casing is still emitted). Defaults to false.
 * @property {boolean} [trim]            Trim items and drop empty lines. Defaults to true; an explicit false disables it.
 */

/**
 * Split a newline list into a deduped array 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.
 *
 * @param {string} input
 * @param {SetOptions} opts
 * @returns {string[]}
 */
function parseList(input, opts) {
  if (input === '') return []; // empty input = no items
  const trim = opts.trim !== false; // default true
  const ci = !!opts.caseInsensitive;

  let items = input.split('\n');
  if (trim) {
    items = items.map((s) => s.trim()).filter((s) => s !== '');
  }

  const seen = new Set();
  const out = [];
  for (const item of items) {
    const key = ci ? item.toLowerCase() : item;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(item);
  }
  return out;
}

/**
 * Compute a set operation over two newline-separated lists. Never throws.
 *
 * 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).
 *
 * @param {string} inputA
 * @param {string} inputB
 * @param {SetOp} op
 * @param {SetOptions} [opts={}]
 * @returns {string}
 */
function setOps(inputA, inputB, op, opts = {}) {
  const ci = !!opts.caseInsensitive;
  const keyOf = (s) => (ci ? s.toLowerCase() : s);

  const listA = parseList(inputA, opts);
  const listB = parseList(inputB, opts);

  const keysA = new Set(listA.map(keyOf));
  const keysB = new Set(listB.map(keyOf));

  const out = [];
  switch (op) {
    case 'union': {
      const seen = new Set();
      for (const item of listA) {
        seen.add(keyOf(item));
        out.push(item); // listA is already deduped by parseList
      }
      for (const item of listB) {
        const k = keyOf(item);
        if (seen.has(k)) continue;
        seen.add(k);
        out.push(item);
      }
      break;
    }
    case 'intersect': {
      for (const item of listA) {
        if (keysB.has(keyOf(item))) out.push(item);
      }
      break;
    }
    case 'difference': {
      for (const item of listA) {
        if (!keysB.has(keyOf(item))) out.push(item);
      }
      break;
    }
    case 'symmetricDifference': {
      for (const item of listA) {
        if (!keysB.has(keyOf(item))) out.push(item);
      }
      for (const item of listB) {
        if (!keysA.has(keyOf(item))) out.push(item);
      }
      break;
    }
  }

  return out.join('\n');
}

// CommonJS export so the file is consumable from Node without a build step,
// while staying dependency-free and framework-agnostic.
module.exports = { setOps, parseList };

// ---------- showcase (the canonical suite lives in src/lib) ----------
if (require.main === module) {
  const A = 'apple\nbanana\ncherry';
  const B = 'banana\ncherry\ndate';
  const ci = { caseInsensitive: true };
  const eq = (actual, expected, msg) => {
    if (actual !== expected) {
      throw new Error(`${msg}: got ${JSON.stringify(actual)}, want ${JSON.stringify(expected)}`);
    }
  };

  eq(setOps(A, B, 'union'), 'apple\nbanana\ncherry\ndate', 'union');
  eq(setOps(A, B, 'intersect'), 'banana\ncherry', 'intersect');
  eq(setOps(A, B, 'difference'), 'apple', 'difference');
  eq(setOps(A, B, 'symmetricDifference'), 'apple\ndate', 'symmetricDifference');
  // case-folding compares keys, but the first-seen original casing is emitted.
  eq(setOps('HELLO', 'hello', 'intersect', ci), 'HELLO', 'ci intersect');
  eq(setOps('X\nApple', 'x\nAPPLE\nmango', 'union', ci), 'X\nApple\nmango', 'ci union');
  console.log('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 →