Dave
Dave

Reputation: 53

JavaScript Map / Reduce to return grouped by count

I have a JSON collection as an array. I would like to group by three fields within the collection and then return the result along with the count of the matching documents. The example below will hopefully make it clearer.

The JSON document collection returned:

[
    {
        _id: 1,
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com'
    },
    {
       _id: 2,
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com'
    },
    {
       _id: 3,
       browser: 'opera',
       ipAddress: '222.0.888.0',
       uri: 'example1.com'
    },
    {
       _id: 4,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 5,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 6,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 7,
       browser: 'opera',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    }
]

Should perform a grouping on browser, ipAddress and uri and then return the grouped result along with a count as per below (I checked a few times so I hope my numbers below add up to the instances of each combination above!).

[
    {
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com',
       count: 2
    },
    {
       browser: 'opera',
       ipAddress: '222.0.888.0',
       uri: 'example1.com',
       count: 1
    },
    {
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com',
       count: 3
    },
       browser: 'opera',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com',
       count: 1
]

I get that this should be easily doable using map / reduce but I cannot seem to get my confused brain around how to do this!

Thanks in advance.

Upvotes: 3

Views: 2806

Answers (5)

David Lemon
David Lemon

Reputation: 1560

You can achieve this by using a generic reducer generator. This code is based on my previous answer to another question . You can give it the fields by which you want to group and it returns function that can act as reducer giving a item count.

let arr = Object.freeze([
    {
        _id: 1,
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com'
    },
    {
       _id: 2,
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com'
    },
    {
       _id: 3,
       browser: 'opera',
       ipAddress: '222.0.888.0',
       uri: 'example1.com'
    },
    {
       _id: 4,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 5,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 6,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 7,
       browser: 'opera',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    }
]);

const groupByReducerCount = (group) =>
  (result, row) => {
    const keygroup = group.map((v) => row[v]);
    const key = keygroup.join(':');
    if (result[key])
      result[key] ++;
    else
      result[key] = 1;
    return result;
  };


const result = arr.reduce(groupByReducerCount(['uri','browser','ipAddress']),{});

console.log(result)

Upvotes: 1

cchamberlain
cchamberlain

Reputation: 17966

You can do this with vanilla JavaScript using a single reduce:

let arr = [
    {
        _id: 1,
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com'
    },
    {
       _id: 2,
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com'
    },
    {
       _id: 3,
       browser: 'opera',
       ipAddress: '222.0.888.0',
       uri: 'example1.com'
    },
    {
       _id: 4,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 5,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 6,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 7,
       browser: 'opera',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    }
]

let result = arr.reduce((_, x) => {
  for(let i = 0; i < _.length; i++) {
    if(_[i].browser === x.browser && _[i].ipAddress === x.ipAddress && _[i].uri === x.uri) {
      _[i].count++
      return _
    }
  }
  let { _id, ...rest } = x
  return [ ..._, { ...rest, count: 1 } ]
}, [])

console.log(result)

Upvotes: 1

Rajesh
Rajesh

Reputation: 24945

You can try something like this:

var data=[{_id:1,browser:"chrome",ipAddress:"222.111.111.0",uri:"example1.com"},{_id:2,browser:"chrome",ipAddress:"222.111.111.0",uri:"example1.com"},{_id:3,browser:"opera",ipAddress:"222.0.888.0",uri:"example1.com"},{_id:4,browser:"chrome",ipAddress:"222.111.222.0",uri:"sample1.com"},{_id:5,browser:"chrome",ipAddress:"222.111.222.0",uri:"sample1.com"},{_id:6,browser:"chrome",ipAddress:"222.111.222.0",uri:"sample1.com"},{_id:7,browser:"opera",ipAddress:"222.111.222.0",uri:"sample1.com"}];

function groupBy(array, keys) {
  var groups = array.reduce(function(p, c) {
    var hash = keys.map(function(k){ return c[k]; }).join("|")
    p[hash] = p[hash] || c;
    p[hash]["count"] = (p[hash]["count"] || 0) + 1
    delete p[hash]["_id"];
    return p;
  }, {});
  var result = Object.keys(groups).map(function(x){return groups[x] })
  console.log(result);
  return result
}

var keys = ["browser", "ipAddress", "uri"]
groupBy(data, keys)

Upvotes: 1

Maurits Rijk
Maurits Rijk

Reputation: 9985

Another (more functional) approach using lodash:

_(array).groupBy(v => ([v.browser, v.ipAddress, v.uri]))
        .map(v => _.merge(_.omit(v[0], '_id'), {count: v.length}))
        .value();

Short explanation: the groupBy uses browser, ipAddress and uri to create a grouping. In the map statement we remove the _id field and add the count based on the number of objects in the group.

Upvotes: 1

RaR
RaR

Reputation: 3223

If you are open to use lodash(better to use if not), you can do like the following,

var array = [
    {
        _id: 1,
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com'
    },
    {
       _id: 2,
       browser: 'chrome',
       ipAddress: '222.111.111.0',
       uri: 'example1.com'
    },
    {
       _id: 3,
       browser: 'opera',
       ipAddress: '222.0.888.0',
       uri: 'example1.com'
    },
    {
       _id: 4,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 5,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 6,
       browser: 'chrome',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    },
    {
       _id: 7,
       browser: 'opera',
       ipAddress: '222.111.222.0',
       uri: 'sample1.com'
    }
]

var res = _.reduce(array, function(acc, elem){
  delete elem._id;
  var obj = _.find(acc, elem)
  if(obj){
    obj.count++;
  }
  else{
    elem.count = 1;
    acc.push(elem);
  }
  return acc;
}, [])

console.log(res);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>

Upvotes: 0

Related Questions