⏱️

入れ子オブジェクト内のプロパティ値を合計する(Node.js)

2024/07/12に公開

入れ子オブジェクト内のプロパティ値を合計する(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.keysObject.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メソッドを使用する際、accumulatorcurrentValueといった変数は、特別に宣言しなくても使うことができます。これは、reduceメソッドがコールバック関数にこれらの引数を自動的に渡すためです。

動作の流れ

  1. 初期値の設定:

    • initialValueが指定されている場合、accumulatorはその値から始まります。
    • initialValueが指定されていない場合、accumulatorは配列の最初の要素から始まり、currentValueは2番目の要素から始まります。
  2. コールバック関数の実行:

    • 配列の各要素に対して、コールバック関数が実行されます。
    • コールバック関数は、accumulatorcurrentValueを引数に取り、累積結果を返します。
    • この累積結果が次のイテレーションのaccumulatorとなります。
  3. 最終結果の返却:

    • 配列の全要素に対してコールバック関数が実行された後、最終的な累積結果が返されます。

例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