何をしたいユーティリティか:「共通要素抽出」
ここでの「共通要素抽出」は、複数の配列を比べて「両方(または全部)に含まれている要素だけ」を取り出す処理です。
イメージとしては「集合の積(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任意の「キー関数」で共通要素を抽出する
複数項目の組み合わせで共通性を決めたい場合
例えば、「code と version の組み合わせが共通しているレコード」を取りたい場合、
単純な 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 の組み合わせが同じなら共通」。
これをユーティリティの関数名や引数(key や keyFn)で表現しておくと、
あとからコードを読んだ人にも意図が伝わりやすくなります。
「共通部分」だけを対象に処理したい場面は多い
例えば、
- 「前回も今回も存在しているユーザーだけ、ステータスを更新する」
- 「両方の権限を持っているユーザーだけ、特定の機能を許可する」
- 「前回版と今回版の両方にあるコードだけ、単価を比較する」
といった処理は、すべて「共通要素抽出」が入り口になります。
intersectByKey や intersectBy のようなユーティリティを用意しておくと、
こうした処理が「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 文から、意図と一貫性を備えた業務レベルの設計に近づいていきます。
