JavaScript Tips | 配列ユーティリティ:部分一致判定

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「部分一致判定」

「部分一致判定」は、「完全に同じかどうか」ではなく、「一部が条件を満たしているかどうか」を判定する処理です。
ここでは主に、配列に対して次のような問いを投げるユーティリティだと思ってください。

ある値が1つでも含まれているか。
ある集合のうち、少なくとも1つは含まれているか。
ある集合のすべてが含まれているか。
先頭からの並びが、特定の配列と一致しているか。

業務だと、例えばこういう場面で使います。
「ユーザーの権限リストに、必要な権限が1つでも含まれているか」。
「選択されたタグの中に、禁止タグが含まれていないか」。
「履歴の先頭数件が、期待しているシーケンスと一致しているか」。

「完全一致判定」が「全部同じか」を見るのに対して、
「部分一致判定」は「一部が条件を満たしているか」を見るイメージです。


基本形1:1つでも含まれているか(any 部分一致)

includes をラップした contains

一番シンプルな部分一致は、「この値が配列に1つでも含まれているか」です。
これは Array.prototype.includes をそのまま使ってもいいですが、業務ユーティリティとしては安全側に倒した関数にしておくと使いやすくなります。

function contains(array, value) {
  if (!Array.isArray(array)) {
    return false;
  }
  return array.includes(value);
}
JavaScript

ここでのポイントは、「配列じゃないものが来たら false にする」ことです。
これで、「部分一致(1つでも含まれているか)」の一番基本が書けます。

動作イメージ

const roles = ["admin", "editor", "viewer"];

contains(roles, "admin");  // true
contains(roles, "guest");  // false
contains(null, "admin");   // false
JavaScript

「この配列に、特定の値が“部分的に”含まれているか」を見るときは、まずこのレベルです。


基本形2:候補のうち、少なくとも1つは含まれているか(any 部分一致・集合版)

some と Set を使った containsAny

次に、「候補が複数ある場合に、そのうち1つでも含まれているか」を判定するユーティリティです。

function containsAny(array, candidates) {
  if (!Array.isArray(array) || !Array.isArray(candidates)) {
    return false;
  }

  const candidateSet = new Set(candidates);
  return array.some((item) => candidateSet.has(item));
}
JavaScript

重要なポイントをかみ砕きます。

配列でなければ false。
candidatesSet にしておくことで、has で高速に判定できる。
array.some(...) は「1つでも条件を満たす要素があれば true」を返す。

つまり、「候補のうち、少なくとも1つは含まれているか」を判定する関数です。

動作例

const userRoles = ["editor", "viewer"];
const requiredAny = ["admin", "editor"];

containsAny(userRoles, requiredAny); // true(editor が共通)
containsAny(userRoles, ["admin"]);   // false
JavaScript

業務では、「このユーザーは、許可されたロールのどれかを持っているか?」のような判定にそのまま使えます。


基本形3:候補のすべてが含まれているか(all 部分一致・集合版)

every と Set を使った containsAll

今度は、「候補のすべてが配列に含まれているか」を判定するユーティリティです。

function containsAll(array, candidates) {
  if (!Array.isArray(array) || !Array.isArray(candidates)) {
    return false;
  }

  const valueSet = new Set(array);
  return candidates.every((c) => valueSet.has(c));
}
JavaScript

ここでのポイントは、「どの候補も配列の中にあるか」を every で見ていることです。

arraySet にしておき、
candidates.every((c) => valueSet.has(c)) で「候補のすべてが含まれているか」を判定します。

動作例

const userRoles = ["admin", "editor", "viewer"];

containsAll(userRoles, ["admin", "editor"]); // true
containsAll(userRoles, ["admin", "guest"]);  // false
JavaScript

業務では、「このユーザーは、必要な権限を全部持っているか?」のような判定に使えます。
containsAny が「どれか1つでもあればOK」、containsAll が「全部必要」という違いです。


先頭からの部分一致:配列の prefix 判定

startsWithArray(配列版 startsWith)

文字列には startsWith がありますが、配列にも「先頭からの部分一致」を判定したくなる場面があります。
例えば、「履歴の先頭数件が、期待しているシーケンスと一致しているか」などです。

function startsWithArray(array, prefix) {
  if (!Array.isArray(array) || !Array.isArray(prefix)) {
    return false;
  }

  if (prefix.length > array.length) {
    return false;
  }

  for (let i = 0; i < prefix.length; i++) {
    if (array[i] !== prefix[i]) {
      return false;
    }
  }

  return true;
}
JavaScript

ここでの重要ポイントは、「prefix の長さだけ先頭から比較する」ことです。

配列じゃなければ false。
prefix のほうが長ければ、絶対に先頭一致しないので false。
先頭から順に !== で比較し、1つでも違えば false。
全部同じなら true。

動作例

const history = ["login", "view", "edit", "logout"];

startsWithArray(history, ["login"]);           // true
startsWithArray(history, ["login", "view"]);   // true
startsWithArray(history, ["view"]);            // false
startsWithArray(history, ["login", "edit"]);   // false
JavaScript

「先頭からの部分一致」は、「完全一致」ではなく「prefix として一致しているか」を見る部分一致の一種です。


部分一致判定ユーティリティで意識してほしいポイント

「何をもって“部分一致”とみなすか」を関数ごとに分ける

部分一致と一言で言っても、実は種類があります。

1つでも含まれていればいい(contains / containsAny)。
全部含まれていてほしい(containsAll)。
先頭からの並びが一致していてほしい(startsWithArray)。

これらを1つの関数に押し込めるのではなく、
関数名で意図が分かるように分けておくと、コードを読む人が迷いません。

「この判定は、“どれか1つでも含まれていればOK”なのか、“全部必要”なのか、“先頭だけ見ている”のか」が、関数名だけで伝わる状態を目指します。

配列じゃないものが来ても安全に扱う

すべてのユーティリティで、最初に Array.isArray をチェックしています。

if (!Array.isArray(array)) {
  return false;
}
JavaScript

現実の業務コードでは、「本当は配列のはずだけど、何かの拍子に null やオブジェクトが来る」ことが普通にあります。
そこで例外を投げて落ちるより、「false を返す」と決めておくほうが、呼び出し側のコードがシンプルになります。

Set を使って「部分一致判定」を効率よく書く

containsAnycontainsAllSet を使っているのは、「含まれているかどうか」を高速に判定するためです。

配列のまま includes を何度も呼ぶと、要素数が増えるほど重くなります。
一方、Set にしておけば、has で一発です。

「同じ配列に対して、何度も部分一致判定をする」ような場面では、
先に Set に変換しておくのが定石です。


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

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

function contains(array, value) {
  if (!Array.isArray(array)) return false;
  return array.includes(value);
}

function containsAny(array, candidates) {
  if (!Array.isArray(array) || !Array.isArray(candidates)) return false;
  const candidateSet = new Set(candidates);
  return array.some((item) => candidateSet.has(item));
}

function containsAll(array, candidates) {
  if (!Array.isArray(array) || !Array.isArray(candidates)) return false;
  const valueSet = new Set(array);
  return candidates.every((c) => valueSet.has(c));
}

function startsWithArray(array, prefix) {
  if (!Array.isArray(array) || !Array.isArray(prefix)) return false;
  if (prefix.length > array.length) return false;
  for (let i = 0; i < prefix.length; i++) {
    if (array[i] !== prefix[i]) return false;
  }
  return true;
}

const roles = ["admin", "editor", "viewer"];
console.log(contains(roles, "admin"));
console.log(containsAny(roles, ["guest", "editor"]));
console.log(containsAll(roles, ["admin", "editor"]));

const history = ["login", "view", "edit", "logout"];
console.log(startsWithArray(history, ["login", "view"]));
console.log(startsWithArray(history, ["view"]));
JavaScript

「どの判定が true になって、どれが false になるか」を自分の目で確かめてみてください。

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

export function contains(...) { ... }
export function containsAny(...) { ... }
export function containsAll(...) { ... }
export function startsWithArray(...) { ... }
JavaScript

のような関数を置き、「配列が“部分的に条件を満たしているか”を判定したくなったら、必ずこの“部分一致判定ユーティリティ”を通す」と決めてみてください。
それだけで、「なんとなく includes や some を書いているコード」から、「意図がはっきりした判定ロジック」に一段レベルアップできます。

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