JavaScript Tips | 配列ユーティリティ:差分検出

JavaScript JavaScript
スポンサーリンク

テーマの整理:「差分検出」とは何か

「差分検出」は、“前の配列”と“今の配列”を比べて、「何が増えたか」「何が消えたか」「何が変わったか」をはっきりさせるためのテクニックです。 業務だと、次のような場面でほぼ必ず出てきます。

前回のユーザー一覧と今回の一覧の違いをログに残したい。 画面の一覧で「追加された行」「削除された行」「更新された行」をハイライトしたい。 DB に同期するときに、「新規」「更新」「削除」を判定したい。

ここでは、初心者でも扱いやすい「配列差分検出」のユーティリティを、例題付きでかみ砕いて説明します。

基本コンセプト:ID を軸に「前」と「今」を比べる

なぜ「ID」が大事なのか

差分検出でいちばん大事なのは、「同じものかどうかを判定する軸」です。 業務データなら、だいたい「id」や「コード」などの一意なキーがあるはずです。

この「id」を使って、

前の配列にあって今の配列にない → 削除された。 前になくて今にある → 追加された。 両方にあるが中身が違う → 更新された。

というふうに差分を分類していきます。

差分検出ユーティリティの基本形

まずは、id をキーにしたシンプルな差分検出関数を作ります。

function diffById(prev, next) {
  const prevMap = new Map(prev.map((item) => [item.id, item]));
  const nextMap = new Map(next.map((item) => [item.id, item]));

  const added = [];
  const removed = [];
  const updated = [];
  const unchanged = [];

  for (const [id, nextItem] of nextMap.entries()) {
    const prevItem = prevMap.get(id);
    if (!prevItem) {
      added.push(nextItem);
      continue;
    }

    const isSame =
      JSON.stringify(prevItem) === JSON.stringify(nextItem);

    if (isSame) {
      unchanged.push(nextItem);
    } else {
      updated.push({ before: prevItem, after: nextItem });
    }
  }

  for (const [id, prevItem] of prevMap.entries()) {
    if (!nextMap.has(id)) {
      removed.push(prevItem);
    }
  }

  return { added, removed, updated, unchanged };
}
JavaScript

ここでの重要ポイントをかみ砕いて説明します。

prev は「前の配列」、next は「今の配列」。 prevMapnextMap は、「id → 要素」を引けるようにした Map。 added は「新しく増えた要素」、removed は「消えた要素」。 updated は「id は同じだが中身が変わった要素」、unchanged は「全く同じ要素」。 中身の比較には、初心者向けに分かりやすい JSON.stringify を使っている(厳密にはもっと工夫の余地あり)。

この関数が返すのは、「差分の分類結果」です。

例題 1:ユーザー一覧の差分を検出する

シナリオ

前回のユーザー一覧と今回のユーザー一覧がある。 「追加されたユーザー」「削除されたユーザー」「更新されたユーザー」を知りたい。

コード

const prevUsers = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob",   active: false },
  { id: 3, name: "Carol", active: true },
];

const nextUsers = [
  { id: 1, name: "Alice", active: true },        // 変更なし
  { id: 2, name: "Bob Jr.", active: false },     // 名前変更
  { id: 4, name: "Dave",  active: true },        // 新規
];

const diff = diffById(prevUsers, nextUsers);

console.log("added:", diff.added);
console.log("removed:", diff.removed);
console.log("updated:", diff.updated);
console.log("unchanged:", diff.unchanged);
JavaScript

ここで起きていることを言葉で追うと、

id=4 は前になくて今にある → added。 id=3 は前にあって今にない → removed。 id=2 は両方にあるが name が変わっている → updated。 id=1 は両方にあり、中身も同じ → unchanged。

というふうに、差分がきれいに分類されます。

例題 2:差分を使って「DB 同期用の指示」を作る

シナリオ

画面側で編集された一覧を、DB に反映したい。 差分から「INSERT すべきもの」「UPDATE すべきもの」「DELETE すべきもの」を作りたい。

コード

function buildSyncCommands(prev, next) {
  const diff = diffById(prev, next);

  const inserts = diff.added.map((item) => ({
    type: "INSERT",
    payload: item,
  }));

  const updates = diff.updated.map(({ before, after }) => ({
    type: "UPDATE",
    id: before.id,
    payload: after,
  }));

  const deletes = diff.removed.map((item) => ({
    type: "DELETE",
    id: item.id,
  }));

  return [...inserts, ...updates, ...deletes];
}

const prevUsers = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob",   active: false },
];

const nextUsers = [
  { id: 1, name: "Alice", active: true },
  { id: 3, name: "Carol", active: true },
];

const commands = buildSyncCommands(prevUsers, nextUsers);
console.log(commands);
JavaScript

ここでは、

差分検出の結果をもとに、「DB に対して何をすべきか」を明示的なコマンドに変換している。 INSERT は added、UPDATE は updated、DELETE は removed から作る。

という流れになっています。

差分検出は、「何が変わったか」を知るだけでなく、 「何をすべきか」に直結する情報として使えるのがポイントです。

例題 3:画面で「追加・削除・更新」をハイライトする

シナリオ

前回の一覧と今回の一覧を比べて、 追加された行は緑、削除された行は赤、更新された行は黄色で表示したい。

コード(概念的な例)

const prevUsers = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob",   active: false },
];

const nextUsers = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob Jr.", active: false },
  { id: 3, name: "Carol", active: true },
];

const diff = diffById(prevUsers, nextUsers);

const rows = nextUsers.map((user) => {
  const isAdded   = diff.added.some((u) => u.id === user.id);
  const isUpdated = diff.updated.some(({ after }) => after.id === user.id);

  let status = "normal";
  if (isAdded) status = "added";
  else if (isUpdated) status = "updated";

  return { ...user, status };
});

console.log(rows);
JavaScript

ここでは、

nextUsers をベースに行を作りつつ、「この行は added か updated か」を差分から判定している。 status に応じて、画面側で色を変えたりアイコンを出したりできる。

という使い方をしています。

差分検出は、「UI の表現」にも直結する情報になります。

差分検出で意識してほしい重要ポイント

「何をもって“同じもの”とみなすか」を決める

差分検出の設計でいちばん大事なのは、 「何をもって同じものとみなすか」です。

業務データなら、だいたい次のようなパターンがあります。

id が同じなら同じレコード。 コードやメールアドレスなど、ビジネス的なキーが同じなら同じ。

ここを曖昧にしたまま差分を取ると、 「本当は同じものなのに別物扱い」「本当は別物なのに同じ扱い」になってしまいます。

初心者向けには、

まずは「id」を軸にする。 もし id がないなら、「何が一意なキーになりうるか」をチームで決める。

というステップを必ず踏んでほしいです。

「中身の比較」をどこまでやるか

上の例では、簡単のために JSON.stringify(prevItem) === JSON.stringify(nextItem) で比較しましたが、 実務では「どのフィールドの変更を“更新”とみなすか」を決める必要があります。

例えば、

name や active が変わったら更新。 内部的なメタ情報(更新日時など)は差分として扱わない。

など、ビジネス的な意味に合わせて比較対象を絞ると、 「本当に意味のある変更だけ」を更新として扱えるようになります。

手を動かして「差分検出」の感覚をつかむ

次のコードを実行して、「added / removed / updated / unchanged」の動きを体感してみてください。

function diffById(prev, next) {
  const prevMap = new Map(prev.map((item) => [item.id, item]));
  const nextMap = new Map(next.map((item) => [item.id, item]));

  const added = [];
  const removed = [];
  const updated = [];
  const unchanged = [];

  for (const [id, nextItem] of nextMap.entries()) {
    const prevItem = prevMap.get(id);
    if (!prevItem) {
      added.push(nextItem);
      continue;
    }

    const isSame =
      JSON.stringify(prevItem) === JSON.stringify(nextItem);

    if (isSame) {
      unchanged.push(nextItem);
    } else {
      updated.push({ before: prevItem, after: nextItem });
    }
  }

  for (const [id, prevItem] of prevMap.entries()) {
    if (!nextMap.has(id)) {
      removed.push(prevItem);
    }
  }

  return { added, removed, updated, unchanged };
}

const prev = [
  { id: 1, name: "A" },
  { id: 2, name: "B" },
  { id: 3, name: "C" },
];

const next = [
  { id: 1, name: "A" },      // unchanged
  { id: 2, name: "B2" },     // updated
  { id: 4, name: "D" },      // added
];

const diff = diffById(prev, next);

console.log("added   :", diff.added);
console.log("removed :", diff.removed);
console.log("updated :", diff.updated);
console.log("unchanged:", diff.unchanged);
JavaScript

ログを見れば、

何が追加され、何が削除され、何が更新され、何が変わっていないかが、 とてもはっきり見えるはずです。

まとめ:差分検出ユーティリティで「変化を構造化して扱う」

「差分検出」は、 「前と今の違いを、ただ“なんとなく”ではなく、“構造化された情報”として扱うためのユーティリティ」です。

プロジェクトに例えば次のような形で置いておくイメージです。

export function diffById(prev, next) { ... }
export function buildSyncCommands(prev, next) { ... }
JavaScript

そして、

一覧データの更新や DB 同期、ログ・監査、UI のハイライトなど、 「変化に意味があるところ」では必ず差分検出を通す。

こう決めておくと、 「何が変わったか」「何をすべきか」がコードから読み取りやすくなり、 業務ロジックがぐっと整理されていきます。

差分検出は、“変化を怖がらずに、ちゃんと見て、ちゃんと扱う”ための道具です。 そこを意識してユーティリティを育てていくと、配列まわりのコードが一段落ち着いたものになります。

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