3

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.

Dave
  • 53
  • 9

5 Answers5

1

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.

Maurits Rijk
  • 9,789
  • 2
  • 36
  • 53
  • Thanks Maurits. As per RaR, not against libraries but in this case, I'd would like to work out how to run this using JS only. – Dave Mar 19 '17 at 01:31
  • As with the answer from Rajesh, it does give me the correct result in terms of combining the records that are the same but it does include the count with each. – Dave Mar 29 '17 at 01:40
0

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)
cchamberlain
  • 17,444
  • 7
  • 59
  • 72
  • Thanks cchamberlain, 'm assuming that you are using underscore for this? – Dave Mar 19 '17 at 01:38
  • @Dave nope this is vanilla JavaScript. The underscore here is just a variable that is somewhat commonly used for the reducers accumulator. If you had underscore or lodash in the project, you'd be better off picking a different variable name for the accumulator. – cchamberlain Mar 19 '17 at 01:40
  • @Dave the answers been updated with a link to the reduce documentation on MDN. – cchamberlain Mar 19 '17 at 01:42
  • I was unfortunately not able to get this to run. My application keeps freaking out about the "...". I get that it is ES6 and my NodeJS is the latest so should be able to run this but keeps giving me an error about the spread attribute. – Dave Mar 29 '17 at 01:40
0

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)
Rajesh
  • 24,354
  • 5
  • 48
  • 79
  • Thanks Rajesh. When I run that it's grouping but I'm not receiving a count at all. – Dave Mar 16 '17 at 08:53
  • @Dave I was using ES6 code. That might have caused issues. Have updated my answer to use `ES5` syntax. Hope it helps – Rajesh Mar 16 '17 at 09:02
  • It was not so much the ES6/ES5, it is that the results do not actually return a count. I am only receiving the groups but also the _id field. – Dave Mar 19 '17 at 01:37
0

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>
RaR
  • 3,075
  • 3
  • 23
  • 48
  • Thanks RaR. I'm not against using a library and I know that lodash, underscore, etc. are good options I just know that this is something that should be pretty easily accomplished so am keen to know exactly how. – Dave Mar 16 '17 at 08:48
0

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)
David Lemon
  • 1,560
  • 10
  • 21