入れ子オブジェクト内のプロパティ値を合計する(Node.js)
入れ子オブジェクト内のプロパティ値を合計する(Node.js)
問題
以下のようなオブジェクトがあるとする。
オブジェクト内の各プロパティ値の中から、minutesの値を抽出して合計値を算出する。
const array = {
1: { minutes: 65, count: 3 },
2: { minutes: 20, count: 1 },
3: { minutes: 449, count: 7 },
4: { minutes: 25, count: 2 },
5: { minutes: 30, count: 1 },
6: { minutes: 108, count: 4 },
7: { minutes: 132, count: 1 },
8: { minutes: 155, count: 2 },
9: { minutes: 272, count: 2 },
10: { minutes: 15, count: 1 },
11: { minutes: 124, count: 5 },
12: { minutes: 76, count: 1 },
};
かかったじかん
3時間以上?実際は前後にまだ色々処理があったので正確ではないけど、解説が難しくて理解に時間かかった。。
結論
繰り返し処理で足していく
let total = 0;
Object.values(array).forEach((item) => {
total += item.minutes;
});
Objectのkeyとvalueを扱う方法として、Object.keys
とObject.values
があって、Object.values
でarray内のプロパティ値、つまり{ minutes: 65, count: 3 }の部分を取り出せる。(keys, valuesそれぞれ複数形なのに注意ね)
さらに、Object.values(array).forEach()でそれぞれのvaluesに対して処理が実行できるというわけ。ここではそれぞれのvaluesをitemに代入し、その中にあるプロパティ名minutesに対応するプロパティ値を取り出して、あらかじめ用意したtotal変数に加算する。
で、全て合算し終わった値が答え。
レデュース(reduce)関数を使う
頭から蛇が生えてそうな(それはメデューサ)、あるいは暴走族のような(多分レディース)名前だけど、reduceという言葉には「減らす」という意味以外にも
change a substance to (a different or more basic form)
というような意味があるらしい。これが正しい汲み取りかはわからないけれど。AIに説明を頼んだら以下の通りだった。
reduce
メソッドは、JavaScriptの配列操作において非常に強力なツールです。このメソッドは、配列の各要素に対して「リデューサ」関数を実行し、単一の累積結果を生成します。
へ?
全然理解できなかった。多分、取り出した要素に対して、同じ計算を繰り返すようなイメージ?
さらに説明してもらった。ここからAIのまとめ。
reduceメソッドの基本構文
array.reduce((accumulator, currentValue, currentIndex, array) => {
// コールバック関数の内容
}, initialValue);
- accumulator: 累積結果を保持する変数。前回のコールバック関数の戻り値が格納されます。
- currentValue: 現在処理されている配列の要素。
- currentIndex (オプション): 現在の要素のインデックス。
-
array (オプション):
reduce
メソッドが呼び出された配列。 - initialValue (オプション): 累積結果の初期値。
JavaScriptのreduce
メソッドを使用する際、accumulator
やcurrentValue
といった変数は、特別に宣言しなくても使うことができます。これは、reduce
メソッドがコールバック関数にこれらの引数を自動的に渡すためです。
動作の流れ
-
初期値の設定:
-
initialValue
が指定されている場合、accumulator
はその値から始まります。 -
initialValue
が指定されていない場合、accumulator
は配列の最初の要素から始まり、currentValue
は2番目の要素から始まります。
-
-
コールバック関数の実行:
- 配列の各要素に対して、コールバック関数が実行されます。
- コールバック関数は、
accumulator
とcurrentValue
を引数に取り、累積結果を返します。 - この累積結果が次のイテレーションの
accumulator
となります。
-
最終結果の返却:
- 配列の全要素に対してコールバック関数が実行された後、最終的な累積結果が返されます。
例1. 配列の合計を計算する
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 15
例2. 配列の最大値を見つける
const numbers = [1, 2, 3, 4, 5];
const max = numbers.reduce((accumulator, currentValue) => Math.max(accumulator, currentValue), -Infinity);
console.log(max); // 5
例3. オブジェクト配列のプロパティの合計を計算する
const items = [{ price: 10 }, { price: 20 }, { price: 30 }];
const total = items.reduce((accumulator, item) => accumulator + item.price, 0);
console.log(total); // 60
例4. 配列をフラット化する
const nestedArray = [[1, 2], [3, 4], [5, 6]];
const flatArray = nestedArray.reduce((accumulator, currentArray) => accumulator.concat(currentArray), []);
console.log(flatArray); // [1, 2, 3, 4, 5, 6]
注意点
-
初期値の指定: 初期値を指定しない場合、配列の最初の要素が初期値として使用されますが、これは空の配列に対して
reduce
を呼び出すとエラーを引き起こす可能性があります。そのため、初期値を指定することが推奨されます[1][3][6]。 -
不変性の保持:
reduce
メソッドは、関数型プログラミングの原則に従い、不変性を保持するために使用されることが多いです。累積結果を変更する代わりに、新しい累積結果を返すように設計されています[1][4][5]。
これらのポイントを理解することで、reduce
メソッドを効果的に使用し、複雑なデータ操作をシンプルに実装することができます。
Citations:
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
[2] https://www.youtube.com/watch?v=iDWtuWkuj8g
[3] https://www.w3schools.com/jsref/jsref_reduce.asp
[4] https://builtin.com/software-engineering-perspectives/javascript-reduce
[5] https://www.freecodecamp.org/news/how-to-use-javascript-array-reduce-method/
[6] https://www.geeksforgeeks.org/javascript-array-reduce-method/
[7] https://www.freecodecamp.org/news/javascript-reduce-method-code-examples/
[8] https://www.programiz.com/javascript/library/array/reduce
[9] https://www.simplilearn.com/tutorials/javascript-tutorial/array-reduce-javascript
[10] https://thecodebarbarian.com/javascript-reduce-in-5-examples.html
とのこと。ここまでAIのまとめ。
今回のケースだと例3に当たりそう。例2の -Infinity
はちょっとかっこいい。
このreduceメソッドのaccumulator
とかcurrentValue
の理解ができてなくて時間かかってたと思ってたけど、もしかしたらいまだにアロー関数がふわっとしか理解できてないからかも。。?
次はこれをちゃんと理解しよう。
今回のケースで書き直すと、
const total = Object.values(array).reduce(
(sum, item) => sum + item.minutes,
0
);
こんな感じかな。sumはreduce内で使ったローカル変数。でこれがtotalに返されるのか。
補足:どっちがいいのか
メモリ消費量とか処理速度とかを考えた時にどちらがベターなのか。
reduceメソッドは一旦配列を生成してメモリに展開するから、こちらの方が処理速度も遅いかと思いきや、今回のような小規模な計算だとforEachで地道にやった方が早かった。処理内容としては変わらないので、そこまで処理時間変わらないかと思ったけれど。
実験
試しに実験をしてみた。arrayのアイテム数を500まで増やした上で、以下のようなコードで比較してみた。
// forEachを使った処理
function measureTimeForEach(array) {
const startTime = performance.now();
let totalMinutes = 0;
Object.values(array).forEach((item) => {
totalMinutes += item.minutes;
});
const endTime = performance.now();
const timeTaken = endTime - startTime;
return { totalMinutes, timeTaken };
}
// reduceメソッドを使った処理
function measureTimeReduce(array) {
const startTime = performance.now();
const totalMinutes = Object.values(array).reduce(
(sum, item) => sum + item.minutes,
0
);
const endTime = performance.now();
const timeTaken = endTime - startTime;
return { totalMinutes, timeTaken };
}
// 結果の計算と出力
const foreachResult = measureTimeForEach(array);
console.log(
`ForEach結果:${foreachResult.totalMinutes}、Time Taken: ${foreachResult.timeTaken}ms`
);
const reduceResult = measureTimeReduce(array);
console.log(
`Reduce結果:${reduceResult.totalMinutes}、Time Taken: ${reduceResult.timeTaken}ms`
);
結果、面白いことがわかった。
1回目
ForEach結果:118463、計算時間:0.018875000000001307ms
Reduce結果:118463、計算時間:0.2308750000000046ms
2回目
ForEach結果:118463、計算時間:0.024374999999999147ms
Reduce結果:118463、計算時間:0.020291999999997756ms
3回目
ForEach結果:118463、計算時間:0.023707999999999174ms
Reduce結果:118463、計算時間:0.019999999999999574ms
何度か値を変えて試してみても、同じように1回目は圧倒的にForEachの方が早くて、以降は若干Reduceの方が早い。これ、もしかすると初回はメモリ展開するために時間がかかっていて、以降はそれを元に再処理しているからとか?
いずれまた調べてみよう。
Discussion