Skip to the code
cf.

Set operations by key

Write three functions that treat two arrays of items as sets, where two items count as the same when they have the same key. Each takes arrays a and b and a key, and returns a new array.

key is either a function that takes an item and returns its key, or a property name, in which case an item's key is the value of that property. Keys are compared the way a Set compares values, so NaN matches NaN and 1 doesn't match "1".

intersectionBy(a, b, key) returns the items of a whose key is also in b. differenceBy(a, b, key) returns the items of a whose key isn't in b. unionBy(a, b, key) returns the items of a, followed by the items of b whose key isn't in a.

Like a set, a result never holds two items with the same key: keep the first one. Items keep the order they had, and when both arrays have an item with the same key, the result uses the one from a.

key may be slow, so call it at most once per item: a.length + b.length calls in all. None of the functions change a or b.

const selected = [{ id: 1, name: "Ada" }, { id: 3, name: "Cy" }];
const visible = [{ id: 2, name: "Bo" }, { id: 3, name: "Cy" }, { id: 4, name: "Di" }];
 
intersectionBy(selected, visible, "id"); // [{ id: 3, name: "Cy" }]
differenceBy(selected, visible, "id");   // [{ id: 1, name: "Ada" }]
unionBy(selected, visible, "id");
// [{ id: 1, name: "Ada" }, { id: 3, name: "Cy" }, { id: 2, name: "Bo" }, { id: 4, name: "Di" }]
 
intersectionBy([1.2, 2.5, 3.1], [3.9, 1.0], Math.floor); // [1.2, 3.1]

What the tests check

  1. intersectionBy keeps the items of a whose key is in b
    const a = [{ id: 1, from: "a" }, { id: 2, from: "a" }, { id: 3, from: "a" }];
    const b = [{ id: 3, from: "b" }, { id: 1, from: "b" }];
    const result = intersectionBy(a, b, "id");
    expect(result).toEqual([{ id: 1, from: "a" }, { id: 3, from: "a" }]);
    expect(result[0]).toBe(a[0]);
    
  2. differenceBy keeps the items of a whose key isn't in b
    const a = [{ id: 1, from: "a" }, { id: 2, from: "a" }, { id: 3, from: "a" }];
    const b = [{ id: 3, from: "b" }, { id: 1, from: "b" }];
    expect(differenceBy(a, b, "id")).toEqual([{ id: 2, from: "a" }]);
    expect(differenceBy(a, [], "id")).toEqual(a);
    
  3. unionBy adds the items of b whose key isn't in a
    const a = [{ id: 1, from: "a" }, { id: 2, from: "a" }];
    const b = [{ id: 2, from: "b" }, { id: 3, from: "b" }];
    expect(unionBy(a, b, "id")).toEqual([{ id: 1, from: "a" }, { id: 2, from: "a" }, { id: 3, from: "b" }]);
    
  4. accepts a key function
    expect(intersectionBy([1.2, 2.5, 3.1], [3.9, 1.0], Math.floor)).toEqual([1.2, 3.1]);
    expect(differenceBy(["a", "B", "c"], ["b"], (s) => s.toLowerCase())).toEqual(["a", "c"]);
    expect(unionBy(["a", "B"], ["b", "C"], (s) => s.toLowerCase())).toEqual(["a", "B", "C"]);
    
  5. never returns two items with the same key
    expect(unionBy([{ id: 1, n: 1 }, { id: 1, n: 2 }], [{ id: 1, n: 3 }, { id: 2, n: 4 }, { id: 2, n: 5 }], "id")).toEqual([
      { id: 1, n: 1 },
      { id: 2, n: 4 },
    ]);
    expect(intersectionBy([{ id: 1, n: 1 }, { id: 1, n: 2 }], [{ id: 1 }], "id")).toEqual([{ id: 1, n: 1 }]);
    expect(differenceBy([{ id: 2, n: 1 }, { id: 2, n: 2 }], [], "id")).toEqual([{ id: 2, n: 1 }]);
    
  6. compares keys the way a Set does
    expect(intersectionBy([NaN, 1, "2"], [NaN, "1", 2], (x) => x)).toEqual([NaN]);
    expect(differenceBy([1, "1"], [1], (x) => x)).toEqual(["1"]);
    expect(unionBy([1], ["1"], (x) => x)).toEqual([1, "1"]);
    
  7. calls key at most once per item
    const a = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
    const b = [{ id: 4 }, { id: 5 }, { id: 2 }];
    for (const operation of [intersectionBy, differenceBy, unionBy]) {
      const key = spy((item) => item.id);
      operation(a, b, key);
      expect(key.callCount).toBeLessThanOrEqual(7);
    }
    
  8. returns new arrays and leaves a and b alone
    const a = [{ id: 1 }, { id: 2 }];
    const b = [{ id: 2 }, { id: 3 }];
    expect(differenceBy(a, [], "id")).not.toBe(a);
    expect(unionBy(a, [], "id")).not.toBe(a);
    intersectionBy(a, b, "id");
    differenceBy(a, b, "id");
    unionBy(a, b, "id");
    expect(a).toEqual([{ id: 1 }, { id: 2 }]);
    expect(b).toEqual([{ id: 2 }, { id: 3 }]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).