JavaScript Tips | 配列ユーティリティ:共通要素抽出

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「共通要素抽出」

ここでの「共通要素抽出」は、複数の配列を比べて「両方(または全部)に含まれている要素だけ」を取り出す処理です。

イメージとしては「集合の積(intersection)」です。

A: [1, 2, 3]
B: [2, 3, 4]

共通要素: [2, 3]

業務だと、例えばこんな場面で使います。

  • 「権限 A と権限 B の両方を持っているユーザー」を知りたい。
  • 「前回も今回も存在しているコード」だけを対象にしたい。
  • 「複数条件を満たす ID の集合」を取りたい。

毎回 for 文を書くより、「共通要素抽出ユーティリティ」を 1 個決めておくと、コードがかなり読みやすくなります。


プリミティブ配列の共通要素抽出:Set を使う基本形

2 つの配列の共通要素

まずは、数値や文字列などのプリミティブ値だけの配列を対象にします。

やりたいことはシンプルで、

  • A の要素を順に見て、「B にもあるものだけ」を集める

です。

function intersectPrimitive(a, b) {
  const left = Array.isArray(a) ? a : [];
  const right = Array.isArray(b) ? b : [];

  const setRight = new Set(right);

  const result = [];
  const seen = new Set();

  for (const v of left) {
    if (setRight.has(v) && !seen.has(v)) {
      result.push(v);
      seen.add(v);
    }
  }

  return result;
}
JavaScript

重要なポイントをかみ砕いて説明する

setRight は「B に含まれている値の集合」です。

const setRight = new Set(right);
JavaScript

こうしておくと、「この値は B にあるか?」を setRight.has(v) で高速に判定できます。

さらに seen という Set を用意しているのは、「結果の重複」を防ぐためです。

if (setRight.has(v) && !seen.has(v)) {
  result.push(v);
  seen.add(v);
}
JavaScript

これで、「A に 3 が 2 回出てきても、結果には 1 回だけ入る」ようになります。

実際の動き

intersectPrimitive([1, 2, 3], [2, 3, 4]);
// [2, 3]

intersectPrimitive(["A", "B", "C"], ["B", "C", "D"]);
// ["B", "C"]

intersectPrimitive([1, 1, 2, 3], [1, 3, 3]);
// [1, 3]  // 結果はユニーク
JavaScript

「重複も含めて」共通部分を取りたい場合

マルチセット的な共通部分

ときどき、「ユニークな共通要素」ではなく、「重複も含めた共通部分」が欲しいことがあります。

例えば、

A: [1, 1, 2, 3]
B: [1, 2, 2, 3]

重複も含めた共通部分: [1, 2, 3]

この場合は、「各値の出現回数の最小値」だけ共通とみなすイメージです。

function intersectPrimitiveWithCounts(a, b) {
  const left = Array.isArray(a) ? a : [];
  const right = Array.isArray(b) ? b : [];

  const countA = new Map();
  const countB = new Map();

  for (const v of left) {
    countA.set(v, (countA.get(v) ?? 0) + 1);
  }
  for (const v of right) {
    countB.set(v, (countB.get(v) ?? 0) + 1);
  }

  const result = [];

  for (const [value, countInA] of countA.entries()) {
    const countInB = countB.get(value) ?? 0;
    const commonCount = Math.min(countInA, countInB);

    for (let i = 0; i < commonCount; i++) {
      result.push(value);
    }
  }

  return result;
}
JavaScript

実際の動き

intersectPrimitiveWithCounts([1, 1, 2, 3], [1, 2, 2, 3]);
// [1, 2, 3]

intersectPrimitiveWithCounts([1, 1, 1], [1, 1]);
// [1, 1]
JavaScript

業務では「ユニークな共通要素」で足りることが多いですが、
「出現回数も意味を持つ」ケースでは、こういう書き方もできます。


オブジェクト配列の共通要素抽出:キーを使う

「id が共通しているレコード」を取りたい

業務では、こんな配列同士を比べることがよくあります。

const listA = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

const listB = [
  { id: 2, name: "Bob (from B)" },
  { id: 3, name: "Carol" },
];
JavaScript

ここで、「id が共通しているレコード」を取りたい、という要件はかなり頻出です。

id をキーにした共通要素抽出

function intersectByKey(a, b, key) {
  const left = Array.isArray(a) ? a : [];
  const right = Array.isArray(b) ? b : [];

  const mapRight = new Map();

  for (const item of right) {
    if (item && typeof item === "object") {
      mapRight.set(item[key], item);
    }
  }

  const result = [];

  for (const item of left) {
    if (!item || typeof item !== "object") continue;

    const k = item[key];
    if (mapRight.has(k)) {
      result.push({
        left: item,
        right: mapRight.get(k),
      });
    }
  }

  return result;
}
JavaScript

重要なポイントを深掘りする

mapRight は「id → B 側のレコード」の対応表です。

mapRight.set(item[key], item);
JavaScript

こうしておくと、「A 側の id に対応する B 側のレコード」をすぐに取り出せます。

結果は { left, right } のペアにしておくと、

  • A 側の値
  • B 側の値

を並べて比較したり、差分を取ったりしやすくなります。

実際の動き

const listA = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

const listB = [
  { id: 2, name: "Bob (from B)" },
  { id: 3, name: "Carol" },
];

const common = intersectByKey(listA, listB, "id");

// common → [
//   {
//     left:  { id: 2, name: "Bob" },
//     right: { id: 2, name: "Bob (from B)" },
//   },
// ]
JavaScript

任意の「キー関数」で共通要素を抽出する

複数項目の組み合わせで共通性を決めたい場合

例えば、「codeversion の組み合わせが共通しているレコード」を取りたい場合、
単純な key 文字列では足りません。

そのときは、「要素から共通判定用のキーを作る関数」を渡せるようにします。

function intersectBy(a, b, keyFn) {
  const left = Array.isArray(a) ? a : [];
  const right = Array.isArray(b) ? b : [];

  const mapRight = new Map();

  for (const item of right) {
    const key = keyFn(item);
    mapRight.set(key, item);
  }

  const result = [];

  for (const item of left) {
    const key = keyFn(item);
    if (mapRight.has(key)) {
      result.push({
        left: item,
        right: mapRight.get(key),
      });
    }
  }

  return result;
}
JavaScript

実際の使い方

const listA = [
  { code: "A", version: 1 },
  { code: "A", version: 2 },
  { code: "B", version: 1 },
];

const listB = [
  { code: "A", version: 2 },
  { code: "B", version: 1 },
  { code: "B", version: 2 },
];

const common = intersectBy(
  listA,
  listB,
  (item) => `${item.code}:${item.version}`
);

// common → [
//   { left: { code: "A", version: 2 }, right: { code: "A", version: 2 } },
//   { left: { code: "B", version: 1 }, right: { code: "B", version: 1 } },
// ]
JavaScript

実務で意識してほしい設計のポイント

「何をもって共通とみなすか」をはっきりさせる

共通要素抽出は、「同じとは何か?」を決める作業でもあります。

  • プリミティブ配列なら「値が同じなら共通」。
  • オブジェクト配列なら「id が同じなら共通」。
  • 場合によっては「code+version の組み合わせが同じなら共通」。

これをユーティリティの関数名や引数(keykeyFn)で表現しておくと、
あとからコードを読んだ人にも意図が伝わりやすくなります。

「共通部分」だけを対象に処理したい場面は多い

例えば、

  • 「前回も今回も存在しているユーザーだけ、ステータスを更新する」
  • 「両方の権限を持っているユーザーだけ、特定の機能を許可する」
  • 「前回版と今回版の両方にあるコードだけ、単価を比較する」

といった処理は、すべて「共通要素抽出」が入り口になります。

intersectByKeyintersectBy のようなユーティリティを用意しておくと、
こうした処理が「for 文の塊」ではなく、「意図のはっきりした 1 行」に変わります。

差分抽出ユーティリティとの組み合わせ

前に話した「差分抽出」と組み合わせると、
「追加」「削除」「共通(+更新)」の 3 つをきれいに扱えます。

  • 差分抽出 → 追加・削除・更新候補を出す
  • 共通要素抽出 → 共通部分のペアを作る

この 2 つをセットでユーティリティ化しておくと、
マスタ比較や同期処理のコードがかなり整理されます。


少し手を動かして感覚をつかむ

コンソールで、次のようなコードを実際に打ってみてください。

intersectPrimitive([1, 2, 3], [2, 3, 4]);
intersectPrimitiveWithCounts([1, 1, 2, 3], [1, 2, 2, 3]);

const listA = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

const listB = [
  { id: 2, name: "Bob (from B)" },
  { id: 3, name: "Carol" },
];

intersectByKey(listA, listB, "id");

const listC = [
  { code: "A", version: 1 },
  { code: "A", version: 2 },
  { code: "B", version: 1 },
];

const listD = [
  { code: "A", version: 2 },
  { code: "B", version: 1 },
  { code: "B", version: 2 },
];

intersectBy(listC, listD, (item) => `${item.code}:${item.version}`);
JavaScript

「どの要素が“共通しているもの”として抽出されているか」を、自分の目で確認してみてください。

そのうえで、自分のプロジェクトに

export function intersectPrimitive(...) { ... }
export function intersectPrimitiveWithCounts(...) { ... }
export function intersectByKey(...) { ... }
export function intersectBy(...) { ... }
JavaScript

のような関数を置き、

「配列の共通部分を取りたくなったら、必ずこの“共通要素抽出ユーティリティ”を通す」

というルールを作ってみてください。
それだけで、あなたの配列処理は、場当たり的な for 文から、意図と一貫性を備えた業務レベルの設計に近づいていきます。

タイトルとURLをコピーしました