13

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.

Randell D
  • 133
  • 1
  • 1
  • 4
  • I think this the best way to do it. As you said `count` would be wrapped inside a function so it won't be a problem! Plus you can accumulate both the sum and the count using an object inside reduce (`{sum: ..., count: ...}`)! – ibrahim mahrir Feb 09 '17 at 20:36

9 Answers9

14

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)

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
4

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);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
3

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);
trincot
  • 317,000
  • 35
  • 244
  • 286
1

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
RomanPerekhrest
  • 88,541
  • 4
  • 65
  • 105
1

This is one line code to get average score

let avgScore = arr.reduce((sum, a) => { return sum + parseFloat(a.score) },0)/(arr.length||1)
Manoj Rana
  • 3,068
  • 1
  • 24
  • 34
1
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"}
    ];

    const johnAvgScore = function(scores) {
      return scores.filter(score => score.name === 'John').reduce((acc, score, i, arr) => acc + parseFloat(score.score)/ arr.length, 0)
      
      }
      
    console.log(johnAvgScore(arr))
Stan
  • 11
  • 4
0

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);
Jordan Running
  • 102,619
  • 17
  • 182
  • 182
  • 2
    Dear downvoter: Please leave a comment explaining the reason for your downvote. My answer fulfills OP's requirements, but nevertheless if my answer is lacking I'd appreciate the opportunity to improve it. – Jordan Running Feb 09 '17 at 20:45
  • Forgive my ignorance. I did not purposely downvote your solution. I just accepted the one I thought best met the requirements. I did not realize I could accept multiple solutions. – Randell D Feb 09 '17 at 20:56
  • You cannot *accept* multiple solutions. Voting does not have this limitation though. But only downvote if an answer does not meet the expectations. Upvote when you find it useful, even if not accepted. – trincot Feb 09 '17 at 20:58
  • 1
    @RandellD Did you click the down-arrow? If so, you can retract your downvote by clicking it again. This won't change your accepted answer. (As you've discovered, there are several good ways to solve this problem; I don't mind at all that you accepted a different answer.) – Jordan Running Feb 09 '17 at 21:00
  • I did not click the down-arrow. – Randell D Feb 10 '17 at 05:25
  • @RandellD Then it was someone else. You're off the hook! – Jordan Running Feb 10 '17 at 05:59
0

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...

dandavis
  • 16,370
  • 5
  • 40
  • 36
  • What do you mean by "*without extra calculations*"? – Bergi Feb 09 '17 at 20:47
  • @Bergi: a running sum/avg in a shell object – dandavis Feb 09 '17 at 20:50
  • This solution does, however, iterate over the same data twice (well, three times in the second example), which will probably cancel out any performance gain from avoiding "extra calculations." – Jordan Running Feb 09 '17 at 20:53
  • @Jordan: for-loops would be much faster, "functional" is about writing concise semantic code. – dandavis Feb 09 '17 at 20:54
  • I still really dislike that `r.length-1 === c ? …` hack – Bergi Feb 09 '17 at 20:54
  • @Bergi: yeah, there's no other way to get that, without bind, but bind kills reduce perf... – dandavis Feb 09 '17 at 20:55
  • A simple helper function wrapped *around* the `reduce` calculation would do… `function avg(arr) { return arr.reduce(sum, 0) / arr.length }` `avg(arr.filter(…).map(…))` – Bergi Feb 09 '17 at 20:56
  • interesting. i wonder if the extra closure would negate the r.length "penalty". it seems like V8 caches the length anyway, even if it's ugly. something to consider and try, thanks! – dandavis Feb 09 '17 at 20:58
  • I didn't care so much about the penalty of accessing `r.length`, but about the penalty of evaluating the condition. And of course about the ugliness of the construct :-) – Bergi Feb 09 '17 at 21:15
0

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);
Swoot
  • 1,304
  • 11
  • 12