How to use javascript reduce function to calculate average of items meeting a specific condition?

主宰稳场 提交于 2019-12-21 09:12:04

问题


So assume I have the following array of objects:

var arr = [
  {"name": "John", "score": "8.8"},
  {"name": "John", "score": "8.6"},
  {"name": "John", "score": "9.0"},
  {"name": "John", "score": "8.3"},
  {"name": "Tom",  "score": "7.9"}
];

var count = 0;
var avgScore = arr.reduce(function (sum,person) {
  if (person.name == "John") {
    count+=1;
    return sum + parseFloat(person.score);
  }
  return sum;
},0)/count);

Question: Is there a way way to calculate the average score for "John" without creating a global count variable. Ideally, the count would be internal to the anonymous function in the arr.reduce.


回答1:


To avoid global variables, use a standard solution like IIFEs or block scopes. However I guess you're looking for a way to avoid a mutable counter.

The simplest would be to drop all other persons beforehand:

var johns = arr.filter(function(person) {
  return person.name == "John";
});
var avgScore = johns.reduce(function (sum, person) {
  return sum + parseFloat(person.score);
}, 0) / johns.length;

But you can also use a count that is passed along with the sum in an object:

var stats = arr.reduce(function ({count, sum}, person) {
  return (person.name == "John")
    ? {count: count+1, sum: sum + parseFloat(person.score)}
    : {count, sum};
}, {count:0, sum:0})
var avgScore = stats.sum / stats.count);

(using ES6 object property shorthands and destructuring)




回答2:


Here is yet another ES6 variant, which (ab)uses the third argument of reduce as temporary storage, and calls reduce again for a chained calculation of the average from the sum and count:

const arr = [
  {"name": "John", "score": "8.8"},
  {"name": "John", "score": "8.6"},
  {"name": "John", "score": "9.0"},
  {"name": "John", "score": "8.3"},
  {"name": "Tom",  "score": "7.9"}
];

const avg = arr.reduce( ([sum, count], {name, score}, i) =>
                        (i = name == 'John', [sum + i * score, count + i]), [0, 0] )
               .reduce( (sum, count) => sum/count );

console.log(avg);



回答3:


You could return an object with the average in it, calculated on every loop with an update.

var arr = [{ name: "John", score: "8.8" }, { name: "John", score: "8.6" }, { name: "John", score: "9.0" }, { name: "John", score: "8.3" }, { name: "Tom", score: "7.9" }],
    avgScore = arr.reduce(function (r, person) {
        if (person.name === "John") {
            r.sum += +person.score;
            r.avg = r.sum / ++r.count;
        }
        return r;
    }, { sum: 0, count: 0, avg: 0 }).avg;

console.log(avgScore);

A version with a closure and a direct return of the average.

var arr = [{ name: "John", score: "8.8" }, { name: "John", score: "8.6" }, { name: "John", score: "9.0" }, { name: "John", score: "8.3" }, { name: "Tom", score: "7.9" }],
    avgScore = arr.reduce(function (sum, count) {
        return function (avg, person) {
            if (person.name === "John") {
                sum += +person.score;
                return sum / ++count;
            }
            return avg;
        };
    }(0, 0), 0);

console.log(avgScore);

Above as ES6

var arr = [{ name: "John", score: "8.8" }, { name: "John", score: "8.6" }, { name: "John", score: "9.0" }, { name: "John", score: "8.3" }, { name: "Tom", score: "7.9" }],
    avgScore = arr.reduce(((sum, count) => (avg, person) => person.name === "John" ? (sum += +person.score) / ++count : avg)(0, 0), 0);

console.log(avgScore);



回答4:


You can use an IIFE to confine count to a private scope:

var arr = [
  {"name": "John", "score": "8.8"},
  {"name": "John", "score": "8.6"},
  {"name": "John", "score": "9.0"},
  {"name": "John", "score": "8.3"},
  {"name": "Tom",  "score": "7.9"}
];

var avgScore = arr.reduce(
  (function() {
    var count = 0;

    return function (average, person) {
      if (person.name == "John") {
        count += 1;
        return average * (count - 1) / count + parseFloat(person.score) / count;
      }
      return average;
    };
  })(),
  0
);

console.log(avgScore);



回答5:


The solution using custom object as initialValue parameter for Array.prototype.reduce():

var arr = [{"name": "John", "score": "8.8"},{"name": "John", "score": "8.6"}, {"name": "John", "score": "9.0"}, {"name": "John", "score": "8.3"}, {"name": "Tom",  "score": "7.9"}];

var result = arr.reduce(function (r, o) {
    if (o.name === 'John') ++r.count && (r.sum += Number(o.score));
    return r;
}, {sum: 0, count: 0});

console.log(result.sum/result.count);  // `John's` average score



回答6:


A 2-pass works well without extra calculations, globals, or wrapper objects:

var arr = [
  {"name": "John", "score": "8.8"},
  {"name": "John", "score": "8.6"},
  {"name": "John", "score": "9.0"},
  {"name": "John", "score": "8.3"},
  {"name": "Tom",  "score": "7.9"}
];


var avgScore = arr.filter(x=>x.name=="John")
   .reduce(function(v, n, c, r) {
        return r.length-1 === c ? 
            (v + +n.score) / r.length : 
             v + +n.score;
    },0);

console.log(avgScore);

If you are doing several different shapes, you should work in arrays of primitives so you can re-use methods:

var arr = [
  {"name": "John", "score": "8.8"},
  {"name": "John", "score": "8.6"},
  {"name": "John", "score": "9.0"},
  {"name": "John", "score": "8.3"},
  {"name": "Tom",  "score": "7.9"}
];

// define a few simple helpers
function pluck(o){ return o[this];}    
function avg (v, n, c, r) { // calcs an average w/o a sum
        return r.length-1 === c ? 
            (v + n) / r.length : 
            v + n ;
}

//now use the helpers to write succinct custom code:
var avgScore = arr.filter(x=>x.name=="John")
   .map(pluck, "score")
   .reduce(avg, 0);

console.log(avgScore);

The orig idea came from a custom report generator where users could pass in parameters and do some calcs on the backend without running custom code. the lib of generic methods such as avg can be used without a custom callback function. it's different, so i mention it...




回答7:


This function takes the filter as an argument if you would want to filter on something else another time. It also uses filteredPersons.length; instead of count.

var arr = [
  {"name": "John", "score": "8.8"},
  {"name": "John", "score": "8.6"},
  {"name": "John", "score": "9.0"},
  {"name": "John", "score": "8.3"},
  {"name": "Tom",  "score": "7.9"}
];

function filterJohn(person){
  return person.name === 'John';
};

function calculateAverageScore(persons, filterFunc){
  const filteredPersons = persons.filter(filterFunc);
  return filteredPersons.reduce((sum, person) => { return sum +   parseFloat(person.score); }, 0)/filteredPersons.length;
};

calculateAverageScore(arr, filterJohn);


来源:https://stackoverflow.com/questions/42146272/how-to-use-javascript-reduce-function-to-calculate-average-of-items-meeting-a-sp

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!