groupBy を書いてみたメモ。

こういうのが揃っているライブラリもあるけれど、 バニラで書きたいときもあるし、 色々と応用も効きそうだしということで書き留め。

何かゲームの個人記録があったとして、 各チームの平均点が知りたい、みたいなケース。

const records = [
    { player: 'a', team: 'red', score: 10 },
    { player: 'b', team: 'blue', score: 3 },
    { player: 'c', team: 'red', score: 4 },
    { player: 'd', team: 'green', score: 15 },
    // ...
];

/*
const result = [
    { team: 'red', score: ... },
    { team: 'blue', score: ... },
    { team: 'green', score: ... },
    // ...
]
*/

filter で振り分けちゃおうか、という発想。

const result = ['red', 'blue', 'green']
    .map(team => {
        const list = records.filter(r => r.team === team);
        return {
            team,
            score: list.reduce((sum, r) => sum + r.score, 0) / list.length
        };
    });

書くのは楽だけれど、filter のループ回数が要素数×分割数 になるので、 件数多いときにちょっと使いたくない感じ。

チームも決め打ちになってしまっているので、拡張性に乏しい。

やはり汎用的な groupBy が欲しい。

グループに分け、結果を { key: values } なオブジェクトで返す。

わりとよく見る実装。(underscore.js とか)

js

const groupBy = (array, getKey) =>
    array.reduce((obj, cur, idx, src) => {
        const key = getKey(cur, idx, src);
        (obj[key] || (obj[key] = [])).push(cur);
        return obj;
    }, {});

ts

const groupBy = <K extends PropertyKey, V>(
    array: readonly V[],
    getKey: (cur: V, idx: number, src: readonly V[]) => K
) =>
    array.reduce((obj, cur, idx, src) => {
        const key = getKey(cur, idx, src);
        (obj[key] || (obj[key] = []))!.push(cur);
        return obj;
    }, {} as Partial<Record<K, V[]>>);

振り分けのループ回数が要素数に抑えられている。

Usage

const groups = groupBy(records, r => r.team);
const result = Object.entries(groups)
    .map(([team, list]) => ({
        team,
        score: list.reduce((sum, r) => sum + r.score, 0) / list.length
    }));

単に振り分けるだけならいいのだけれど、その後になにか続けようとすると、ちょっと使い勝手が悪い。

他にも色々と不満点がある。