List Set Operations — PHP 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 PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* list-set-ops — union / intersection / difference / symmetric difference of
* two newline-separated lists.
*
* Language: PHP (8.1+, standard library only — mbstring for safe multibyte
* lowercasing, which is effectively universal in modern PHP)
* 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 Composer 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".
*/
declare(strict_types=1);
/**
* The set operation to compute over two lists. Matches the TS `SetOp` union
* and the Go `Op` constants literal-for-literal. (PHP has no string-union
* type, so these consts are the contract.)
*/
const SET_OP_UNION = 'union';
const SET_OP_INTERSECT = 'intersect';
const SET_OP_DIFFERENCE = 'difference';
const SET_OP_SYMMETRIC_DIFFERENCE = 'symmetricDifference';
/**
* Default options for set_ops(). A missing/null key means "use the default".
*
* - caseInsensitive: bool default false fold case for the comparison
* - trim: ?bool default null null -> true (trim on); an
* explicit false disables trimming
*/
function set_ops_default_options(): array
{
return [
'caseInsensitive' => false,
'trim' => null,
];
}
/**
* 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.
*
* @return list<string>
*/
function set_ops_parse_list(string $input, array $options): array
{
if ($input === '') {
return []; // empty input = no items
}
$trim = $options['trim'] ?? true; // null/missing -> true (default)
$ci = !empty($options['caseInsensitive']);
$items = explode("\n", $input);
if ($trim) {
// trim() is the ASCII whitespace trim here; the tool's inputs are
// plain lines so mbstring isn't needed for the trim step.
$items = array_values(array_filter(
array_map('trim', $items),
fn ($s) => $s !== ''
));
}
$seen = [];
$out = [];
foreach ($items as $item) {
$key = $ci ? mb_strtolower($item, 'UTF-8') : $item;
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$out[] = $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).
*/
function set_ops(string $a, string $b, string $op, array $options = []): string
{
$options = array_merge(set_ops_default_options(), $options);
$ci = !empty($options['caseInsensitive']);
$key_of = fn (string $s): string => $ci ? mb_strtolower($s, 'UTF-8') : $s;
$listA = set_ops_parse_list($a, $options);
$listB = set_ops_parse_list($b, $options);
// Build the key sets for both lists. Array keys give O(1) isset() lookup.
$keysA = [];
foreach ($listA as $i) {
$keysA[$key_of($i)] = true;
}
$keysB = [];
foreach ($listB as $i) {
$keysB[$key_of($i)] = true;
}
$out = [];
switch ($op) {
case SET_OP_UNION:
$seen = [];
foreach ($listA as $item) {
$seen[$key_of($item)] = true;
$out[] = $item; // listA is already deduped by parse_list
}
foreach ($listB as $item) {
$k = $key_of($item);
if (isset($seen[$k])) {
continue;
}
$seen[$k] = true;
$out[] = $item;
}
break;
case SET_OP_INTERSECT:
foreach ($listA as $item) {
if (isset($keysB[$key_of($item)])) {
$out[] = $item;
}
}
break;
case SET_OP_DIFFERENCE:
foreach ($listA as $item) {
if (!isset($keysB[$key_of($item)])) {
$out[] = $item;
}
}
break;
case SET_OP_SYMMETRIC_DIFFERENCE:
foreach ($listA as $item) {
if (!isset($keysB[$key_of($item)])) {
$out[] = $item;
}
}
foreach ($listB as $item) {
if (!isset($keysA[$key_of($item)])) {
$out[] = $item;
}
}
break;
}
return implode("\n", $out);
}
// ---------- showcase (the canonical suite lives in src/lib) ----------
// Runs only when this file is executed directly from the CLI, so including it
// from elsewhere has no side effects.
$is_main = PHP_SAPI === 'cli'
&& !empty($argv[0])
&& realpath($argv[0]) === __FILE__;
if ($is_main) {
$A = "apple\nbanana\ncherry";
$B = "banana\ncherry\ndate";
$ci = ['caseInsensitive' => true];
assert(set_ops($A, $B, SET_OP_UNION) === "apple\nbanana\ncherry\ndate");
assert(set_ops($A, $B, SET_OP_INTERSECT) === "banana\ncherry");
assert(set_ops($A, $B, SET_OP_DIFFERENCE) === "apple");
assert(set_ops($A, $B, SET_OP_SYMMETRIC_DIFFERENCE) === "apple\ndate");
// case-folding compares keys, but the first-seen original casing is emitted.
assert(set_ops('HELLO', 'hello', SET_OP_INTERSECT, $ci) === 'HELLO');
assert(set_ops("X\nApple", "x\nAPPLE\nmango", SET_OP_UNION, $ci) === "X\nApple\nmango");
echo "ok\n";
}
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 →