JavaScript Tips | 配列ユーティリティ:グループ化

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「配列のグループ化」

ここでの「グループ化」は、配列の要素を「ある条件(キー)」ごとにまとめ直す処理です。
SQL の GROUP BY や、Excel の「ピボットテーブルのグループ化」に近いイメージです。

例えば、商品一覧を「カテゴリごと」にまとめたい。
ユーザー一覧を「都道府県ごと」にまとめたい。
ログを「レベル(info / warn / error)ごと」にまとめたい。

こういうときに毎回 for 文を書くのではなく、「グループ化ユーティリティ」として関数を用意しておくと、業務コードがかなり読みやすくなります。


まずはイメージ:グループ化すると何がうれしいか

配列を「キー → 配列」の形に変える

例えば、こんな商品配列があるとします。

const products = [
  { id: 1, name: "Laptop", category: "Electronics" },
  { id: 2, name: "Shirt", category: "Clothing" },
  { id: 3, name: "Phone", category: "Electronics" },
  { id: 4, name: "Pants", category: "Clothing" },
];
JavaScript

これを「category ごと」にグループ化したいとします。
欲しい形はこうです。

{
  Electronics: [
    { id: 1, name: "Laptop", category: "Electronics" },
    { id: 3, name: "Phone", category: "Electronics" },
  ],
  Clothing: [
    { id: 2, name: "Shirt", category: "Clothing" },
    { id: 4, name: "Pants", category: "Clothing" },
  ],
}
JavaScript

つまり、
「キー(ここでは category の値)」 → 「そのキーに属する要素の配列」
という形に変換するのが「グループ化」です。

この形にしておくと、「カテゴリごとの件数」「カテゴリごとの合計金額」などを計算しやすくなります。


reduce で書く「基本の groupBy」

シンプルな groupBy 実装

まずは、reduce を使った、よくある実装から見てみます。

function groupBy(array, key) {
  if (!Array.isArray(array)) {
    return {};
  }

  return array.reduce((groups, item) => {
    if (!item || typeof item !== "object") {
      return groups;
    }

    const groupKey = item[key];

    const safeKey = String(groupKey);

    if (!Object.prototype.hasOwnProperty.call(groups, safeKey)) {
      groups[safeKey] = [];
    }

    groups[safeKey].push(item);

    return groups;
  }, {});
}
JavaScript

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

reduce の第 2 引数 {} が、「グループをためていく入れ物(オブジェクト)」の初期値です。
groups が「今までのグループ結果」、item が「今見ている要素」です。

groupKey は、「この要素が属するグループ名」です。
ここでは単純に item[key] を使っていますが、あとで「関数で決める版」もやります。

safeKeyString(groupKey) としているのは、undefined や数値が来ても、とりあえず文字列キーにして扱えるようにするためです。

groups[safeKey] がまだなければ空配列で初期化し、
その配列に itempush していきます。
最後に groups を返せば、「キー → 配列」の形が完成します。

実際の動き

const products = [
  { id: 1, name: "Laptop", category: "Electronics" },
  { id: 2, name: "Shirt", category: "Clothing" },
  { id: 3, name: "Phone", category: "Electronics" },
  { id: 4, name: "Pants", category: "Clothing" },
];

const grouped = groupBy(products, "category");

console.log(grouped);
/*
{
  Electronics: [
    { id: 1, name: "Laptop", category: "Electronics" },
    { id: 3, name: "Phone", category: "Electronics" },
  ],
  Clothing: [
    { id: 2, name: "Shirt", category: "Clothing" },
    { id: 4, name: "Pants", category: "Clothing" },
  ],
}
*/
JavaScript

ES2024 の Object.groupBy を使う(対応ブラウザなら最強)

Object.groupBy の基本

新しめの JavaScript には、ネイティブの Object.groupBy があります。
「配列をグループ化するための専用メソッド」です。【MDN / W3Schools など】

基本形はこうです。

const grouped = Object.groupBy(items, (item) => {
  return /* グループ名(文字列) */;
});
JavaScript

コールバック関数が返した値が「グループ名」になり、
同じ値を返した要素が同じグループに入ります。

先ほどの例を Object.groupBy で書き直す

const products = [
  { id: 1, name: "Laptop", category: "Electronics" },
  { id: 2, name: "Shirt", category: "Clothing" },
  { id: 3, name: "Phone", category: "Electronics" },
  { id: 4, name: "Pants", category: "Clothing" },
];

const grouped = Object.groupBy(products, (item) => item.category);

console.log(grouped);
JavaScript

やっていることは groupBy(products, "category") と同じですが、
Object.groupBy を使うと「グループ化している」ことが一目で分かり、コードがかなり短くなります。【MDN / Qiita 記事など】

条件でグループ分けする例

例えば、在庫を「在庫あり」「要補充」で分けたい場合。

const inventory = [
  { name: "asparagus", type: "vegetable", quantity: 5 },
  { name: "bananas", type: "fruit", quantity: 0 },
  { name: "cherries", type: "fruit", quantity: 5 },
  { name: "broccoli", type: "vegetable", quantity: 2 },
];

const grouped = Object.groupBy(inventory, (item) => {
  return item.quantity > 0 ? "inStock" : "outOfStock";
});

console.log(grouped);
/*
{
  inStock: [
    { name: "asparagus", ... },
    { name: "cherries", ... },
    { name: "broccoli", ... },
  ],
  outOfStock: [
    { name: "bananas", ... },
  ],
}
*/
JavaScript

「どのプロパティでグループ化するか」だけでなく、
「どんな条件でグループ名を決めるか」を自由に書けるのが強みです。


関数でグループキーを決める汎用 groupBy

「キー名」ではなく「関数」で柔軟に書く

groupBy(array, key) だと「単純なプロパティ名」でしかグループ化できません。
より柔軟にするには、「要素を受け取ってグループ名を返す関数」を渡せるようにします。

function groupByFn(array, keyFn) {
  if (!Array.isArray(array)) {
    return {};
  }

  return array.reduce((groups, item, index) => {
    const key = keyFn(item, index);

    const safeKey = String(key);

    if (!Object.prototype.hasOwnProperty.call(groups, safeKey)) {
      groups[safeKey] = [];
    }

    groups[safeKey].push(item);

    return groups;
  }, {});
}
JavaScript

keyFn は、「要素(とインデックス)からグループ名を決める関数」です。
Object.groupBy とほぼ同じインターフェースだと思ってください。

実際の使い方

年齢で年代ごとにグループ化する例です。

const users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 35 },
  { name: "Charlie", age: 28 },
  { name: "Diana", age: 42 },
];

const grouped = groupByFn(users, (user) => {
  if (user.age < 30) return "20代";
  if (user.age < 40) return "30代";
  return "40代以上";
});

console.log(grouped);
/*
{
  "20代": [
    { name: "Alice", age: 25 },
    { name: "Charlie", age: 28 },
  ],
  "30代": [
    { name: "Bob", age: 35 },
  ],
  "40代以上": [
    { name: "Diana", age: 42 },
  ],
}
*/
JavaScript

このように、「グループ名をどう決めるか」を関数に閉じ込めておくと、
ユーティリティ側は「ひたすらグループに突っ込むだけ」で済みます。


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

返り値の形を「オブジェクト」にするか「Map」にするか

ここまでの例はすべて「普通のオブジェクト {}」を返しています。
これは扱いやすい一方で、「キーが何でもアリ」な場合には少し不便です。

ES2024 には Map.groupBy もあり、「キーに任意の値を使える」グループ化もできます。【MDN】
ただし、業務でよくある「文字列キーでグループ化」なら、まずはオブジェクトで十分です。

「キーが文字列で表現できるかどうか」で使い分ける、という考え方を覚えておくとよいです。

「グループ化したあとに何をしたいか」までセットで考える

グループ化はゴールではなく、「集計や表示のための前処理」です。
例えば、グループ化したあとに次のようなことをしたくなります。

各グループの件数を数える。
各グループの合計・平均を出す。
各グループの最大・最小を出す。

このとき、「グループ化ユーティリティ」と「合計・平均・最大・最小ユーティリティ」を組み合わせると、
SQL の GROUP BYSUM / AVG / MAX / MIN のようなことが、JavaScript だけで自然に書けるようになります。

「キーが存在しない」「値が不正」のときの扱い

現実のデータでは、
categorynull だったり、空文字だったり、そもそもプロパティがなかったりします。

方針としては、例えば次のように決めておくとよいです。

キーがない・空・不正 → "__unknown__" のような特別なグループにまとめる。
または、そういう要素はグループ化の対象から外す。

どちらにするかは要件次第ですが、ユーティリティの中で「どう扱うか」を一度決めてしまうと、
呼び出し側のコードがスッキリします。


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

ブラウザのコンソールや Node.js で、次のようなコードを実際に打ってみてください。

const products = [
  { id: 1, name: "Laptop", category: "Electronics" },
  { id: 2, name: "Shirt", category: "Clothing" },
  { id: 3, name: "Phone", category: "Electronics" },
  { id: 4, name: "Pants", category: "Clothing" },
];

groupBy(products, "category");

groupByFn(products, (p) => p.category);

Object.groupBy(products, (p) => p.category);
JavaScript

それぞれの結果を見比べて、
「どれも同じような形になっていること」
「書き方の違い(key 版と関数版、ネイティブ版)」
を自分の目で確認してみてください。

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

export function groupBy(...) { ... }
export function groupByFn(...) { ... }
// 対応ブラウザなら Object.groupBy を優先して使う
JavaScript

のようなユーティリティを置き、
「配列をグループ化したくなったら、必ずこの“グループ化ユーティリティ”を通す」
というルールを作ってみてください。

そうすると、あなたのコードは、「その場しのぎの for 文」から、「意図がはっきりしたデータ変換」に一段レベルアップしていきます。

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