reduceを集計目的で使うときのメモ。
reduceの簡単な説明
mapとかfilterとかと同じ部類の関数ですが、他の関数に比べ(名前的にも)何してるのかわかりにくいですが、
[結果] = array.reduce([callback],初期値);
というような形式をとり(初期値は省略可能ですが、prevの意味を理解するためにも付けてたほうがいいです)、前の実行結果の影響を受ける(値を利用する)のが特徴です。
で、下記のように使います。
const array = [1,2,3];
const result = array.reduce((prev,current)=>prev + current,0);
reduceもmap等とどうようループします。で今回は配列が3つなので3回ループし、それぞれの処理として下記のような処理が実行されます。
- 1回目:prevの値は初期値の0、currentには1が入ります。なので、0 + 1 = 1が(次のprev値として)返ります。
- 2回目:prevの値は1、currentには2が入るので、 1 + 2 = 3が返ります。
- 3回目:prevの値は3、currentには3が入るので、 3 + 3 = 6が返ります。で、これが最終的な結果として返ります。
いかがでしょうか。
基本
プリミティブ
合計、平均値、最大値、最小値とか。
平均値に関してだけは、sumを利用して別途計算するのがてっとり速いかと。
const numbers = [1, 2, 3, 4, 5];
//合計
const sum = numbers.reduce((prev, current) => prev + current, 10);
console.log(sum);
//平均値
const average = sum / numbers.length;
console.log(average);
//最大値
const max = numbers.reduce((prev, current) => prev > current ? prev : current);
console.log(max);
//最小値
const min = numbers.reduce((prev, current) => prev < current ? prev : current);
console.log(min);
25
5
5
1
オブジェクト
次にオブジェクト。実際の現場ではこちらのほうが一般的かと。
ま、対象とするkeyを指定するだけで、基本的にはプリミティブと同じ。
とりあえず、合計と最大値のサンプル(後は割愛)。
//data
const members = [
{ name: "foo", score: 10, dep: 'sales1' },
{ name: "bar", score: 20, dep: 'sales2' },
{ name: "baz", score: 30, dep: 'sales2' },
]
//合計
const scoreSum = members.reduce((prev, current) => {
return prev.score + current.score;
}, 0);
console.log(scoreSum);
//最大値
const maxScore = members.reduce((prev, current) => {
return prev.score > current.score ? prev.score : current.score;
}, 0);
console.log(maxScore);
60
30
応用
GroupBy的な
実開発ではよく使うのでメモ。
出力するデータ・フォーマットをイメージしておく必要があります。今回は、部署(dep)毎に下記のような形式でデータを出力する想定。
{
dep:DEP_NAME, //部署名
count COUNT, //何件あるか
score: SCORE //スコアの合計
}
では作ってみます。
//souce data
const members = [
{ name: "foo", score: 10, dep: 'sales1' },
{ name: "bar", score: 20, dep: 'sales2' },
{ name: "baz", score: 30, dep: 'sales2' },
]
//groupBy
const groupBy = members.reduce((result, current) => {
//部署がprevにあるか
const element = result.find(value => value.dep === current.dep);
if (element) {
//ある時(下記、初期データを操作)
element.count++;
element.score += current.score;
} else {
//無いとき(新規に初期データを作成)
result.push({
dep: current.dep,
count: 1,
score: current.score,
})
}
return result;
}, []); //初期値は[]
console.log(groupBy);
[
{ dep: 'sales1', count: 1, score: 10 },
{ dep: 'sales2', count: 2, score: 50 }
]
Average
平均を追記してみます。出力されたデータをmapでループして平均値を追加しています。
const withAverage = groupBy.map(item => ({ ...item, ave: item.score / item.count }));
console.log(withAverage);
[
{ dep: 'sales1', count: 1, score: 10, ave: 10 },
{ dep: 'sales2', count: 2, score: 50, ave: 25 }
]
平均には合計と合計カウントが必要なので1ループでは無理ですかね。。。mapをつなげて書くことはできるかと思います。
Sort
ソート。まあ基本の応用。
//sort
const sorted = withAverage.sort((prev, current) => prev.score < current.score ? 1 : -1); //大きい順
// const sorted = groupBy.sort((prev, current) => prev.score > current.score ? 1 : -1); //小さい順
console.log(sorted);
[
{ dep: 'sales2', count: 2, score: 50, ave: 25 },
{ dep: 'sales1', count: 1, score: 10, ave: 10 }
]
ひとまず以上です。必要に応じて随時追加予定。
番外編:配列データのJOIN(client side join)
集計の際によく2つの配列をJOINすることがあるのでついでに書いておきます。
const deps = [
{ depId: 1, depName: "経理" },
{ depId: 2, depName: "営業" },
{ depId: 3, depName: "その他" },
];
const members = [
{ name: "foo", score: 10, depId: 1 },
{ name: "bar", score: 20, depId: 2 },
{ name: "baz", score: 30, depId: 3 },
]
const membersWithDep = members.map(member => {
//既存のmemberの要素を展開しつつ、depIdがマッチしたdepNameの値を追記する
return { ...member, depName: deps.find(value => value.depId === member.depId).depName }
});
console.log(membersWithDep);
[
{ name: 'foo', score: 10, depId: 1, depName: '経理' },
{ name: 'bar', score: 20, depId: 2, depName: '営業' },
{ name: 'baz', score: 30, depId: 3, depName: 'その他' }
]
その他
Firestoreでの適用事例?を記述しました。