🚀

【JavaScript】groupBy をカスタム実装した話

2024/09/04に公開

こんにちは!
ラブグラフエンジニアのひろです。

多くのプログラミング言語では、データを特定のキーでグループ化する groupBy 関数が標準で提供されています。
しかし、 JavaScript にはそのような機能が標準で組み込まれていないため、他言語から移行する際に不便を感じることがあります。

この記事では、 JavaScript で groupBy 関数をカスタム実装する一例を紹介します。

リファクタリングの動機と目的

当社では groupBy を以下のような形で用意していました。

group_by.js
export default f => xs => xs.reduce((acc, x) => Object.assign({}, acc, { [f(x)]: [...acc[f(x)] || [], x] }), {});

動作に問題はなかったものの、ワンライナーで書かれており、可読性が低下していました。
この問題を解決するため、より読みやすい groupBy を求めて、リファクタリングをおこなうことにしました。

新しい groupBy メソッドの実装

新しい groupBy メソッドでも、元の実装と同じく reduce 関数を使用してデータをキーに基づいてグループ化します。
完成したコードがこちらです。

array_util.js
static groupBy(objectsArray, key) {
  return objectsArray.reduce((result, currentObject) => {
    // グループ化のためのキーを取得
    const keyName = currentObject[key];

    // キーが存在しない場合は、空のオブジェクトを返す
    if (!keyName) {
      return result;
    }

    // まだそのキーのグループが存在しない場合は、新たに空の配列を作成
    if (!result[keyName]) {
      result[keyName] = [];
    }

    // 現在のアイテムをグループに追加
    result[keyName].push(currentObject);

    return result;
  }, {}); // 初期値は空のオブジェクト
}

このコードでは、配列の各要素を取り出し、指定されたキーに基づいて値を分類しています。
各キーの値ごとに新しい配列を作成し、該当するオブジェクトを配列に追加していきます。

実装した groupBy メソッドの実用例

例えば、以下のようなオブジェクトの配列があるとします。

const people = [
  { name: "二郎", age: 25 },
  { name: "太朗", age: 30 },
  { name: "花子", age: 25 }
];

const peopleGroupedByAge = groupBy(people, 'age');
console.log(peopleGroupedByAge);

出力結果は次のようになります。

{
  "25": [{ name: "二郎", age: 25 }, { name: "花子", age: 25 }],
  "30": [{ name: "太朗", age: 30 }]
}

このように、任意の属性でオブジェクトを効果的にグループ化できるようになっています。

パフォーマンスの評価

最初のワンライナーのコードも、改善後のコードも、どちらも返り値は同じで引数もほぼ同じものですが、リファクタリングにより、コードの可読性が大幅に向上しました。

ではパフォーマンスについてはどうでしょうか?
以下のようなコードを用意して計測してみました。

class Utils {
  static oldGroupBy = (xs, f) => 
    xs.reduce((acc, x) => 
      Object.assign({}, acc, { [f(x)]: [...acc[f(x)] || [], x] }), 
    {});

  static newGroupBy(objectsArray, key) {
    return objectsArray.reduce((result, currentObject) => {
      const keyName = currentObject[key];
    
      if (!keyName) {
        return result;
      }
    
      if (!result[keyName]) {
        result[keyName] = [];
      }
    
      result[keyName].push(currentObject);
    
      return result;
    }, {});
  }
}

// テスト用データを作成
const largeDataSet = [];
for (let i = 0; i < 10000; i++) {
  largeDataSet.push({ name: `Person${i}`, age: Math.floor(Math.random() * 100) });
}

// oldGroupBy の実行時間を計測
console.time("oldGroupBy");
const oldPeopleGroupedByAge = Utils.oldGroupBy(largeDataSet, x => x.age);
console.timeEnd("oldGroupBy");

// newGroupBy の実行時間を計測
console.time("newGroupBy");
const newPeopleGroupedByAge = Utils.newGroupBy(largeDataSet, 'age');
console.timeEnd("newGroupBy");

結果は以下の通り。

回数 oldGroupBy newGroupBy
1回目 254.275ms 1.676ms
2回目 250.351ms 1.725ms
3回目 238.392ms 1.573ms
4回目 250.11ms 1.623ms
5回目 243.045ms 1.61ms

約150倍の速度ということで、テストデータの形式においてはかなりの改善が見られました。
reduce を使っている点は同じなので、 oldGroupBy で Object.assign によって新しいオブジェクトを生成している部分でここまでの差が生まれたのではないかと考えています。

まとめ

この記事では、 JavaScript における groupBy メソッドの実装例を紹介しました。

何事もバランスが大事ではありますが、行数が増えたとしても、読みやすいコードにする意義はあると思うので、これからも意識していこうと思っています。

groupBy の実装に役立つ reduce 関数の詳細については、こちらの記事で詳しく解説していますので、ぜひご覧ください。

https://zenn.dev/lovegraph/articles/a2ebaedbcd03db

ラブグラフのエンジニアブログ

Discussion