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 →