Iterating through an object and updating values based on existing values

Viewed 56

I have a data json object structured like this:

data: [{
  "aggregate": {
    "path": "/home/page1",
    "query": "name=todd"
  },
  "visits": 4,
  "clicks": 7
},{
  "aggregate": {
    "path": "/home/page1",
    "query": "name=matt"
  },
  "visits": 5,
  "clicks": 17
},{
  "aggregate": {
    "path": "/home/page2",
    "query": ""
  },
  "visits": 4,
  "clicks": 7
},{
  "aggregate": {
    "path": "/home/page3",
    "query": "term=dig"
  },
  "visits": 2,
  "clicks": 20
},{
  "aggregate": {
    "path": "/home/page1",
    "query": "term=dug"
  },
  "visits": 2,
  "clicks": 11
}]

I am looking to end up with an aggregatedObject object like this:

[{
  "path": "/home/page1",
  "visits": 9,
  "clicks": 24
},{
  "path": "/home/page2",
  "visits": 4,
  "clicks": 7
},{
  "path": "/home/page1",
  "visits": 4,
  "clicks": 31
}]

This is what I have so far:

let aggregatedObject = [];

_.each(data, function (item) {

  if (!_.find(aggregatedObject, { path: item.aggregate.path })) {
    aggregatedObject.push({
      path: item.aggregate.path,
      visits: item.visits,
      clicks: item.clicks
    });
    //console.log('not found');

  } else {
    // so lost here

  }
});

What I'm trying to do is if my new object doesn't have an item that matches the item I'm currently iterating through, I push it to the new object. So far I can successfully do this.

However, if I do find an item that matches the path in my new object (ignoring query), I have no idea how to update that item with the sum of the existing item's visits and clicks, and the one I'm iterating through.

4 Answers

You can group based on path and add visits and clicks for same path using array#reduce.

const data = [{ "aggregate": { "path": "/home/page1", "query": "name=todd" }, "visits": 4, "clicks": 7 },{ "aggregate": { "path": "/home/page1", "query": "name=matt" }, "visits": 5, "clicks": 17 },{ "aggregate": { "path": "/home/page2", "query": "" }, "visits": 4, "clicks": 7 },{ "aggregate": { "path": "/home/page3", "query": "term=dig" }, "visits": 2, "clicks": 20 },{ "aggregate": { "path": "/home/page1", "query": "term=dug" }, "visits": 2, "clicks": 11 }],
    result = Object.values(data.reduce((r, o) => {
      const path = o.aggregate.path;
      r[path] ??= { path, visits: 0, clicks: 0};
      r[path].visits += o.visits;
      r[path].clicks += o.clicks;
      return r;
    },{}));
console.log(result);

I think you want to group data based on path, but your example result doesn't do that, it has two entries for "/home/page1" and none for "/home/page3", I'll assume that's just an error in the post.

You need to test to see if path already exists and if not, add a new aggregate object for it, then add clicks and visits.

Looking through the aggregated array every time for path can get expensive if the array gets very large, so consider making an index object of path to aggregated data array so it's just two lookups (once to get the index then another to get the object) rather than searching through the array every time. E.g. (no lodash I'm afraid):

let data = [{
  "aggregate": {
    "path": "/home/page1",
    "query": "name=todd"
  },
  "visits": 4,
  "clicks": 7
},{
  "aggregate": {
    "path": "/home/page1",
    "query": "name=matt"
  },
  "visits": 5,
  "clicks": 17
},{
  "aggregate": {
    "path": "/home/page2",
    "query": ""
  },
  "visits": 4,
  "clicks": 7
},{
  "aggregate": {
    "path": "/home/page3",
    "query": "term=dig"
  },
  "visits": 2,
  "clicks": 20
},{
  "aggregate": {
    "path": "/home/page1",
    "query": "term=dug"
  },
  "visits": 2,
  "clicks": 11
}];

// Map of path to index in aggArr {path: index}
let pathMap = {};

// Get aggregated array data
let aggData = data.reduce((agg, obj) => {
  let path = obj.aggregate.path;
  
  // If not in index, add it and a new aggregation object
  if (!pathMap.hasOwnProperty(path)) {
    pathMap[path] = agg.length;
    agg.push({path: path, visits:0, clicks:0});
  }

  // target is for convenience, add visits and clicks
  let target = agg[pathMap[path]];
  target.visits += obj.visits; 
  target.clicks += obj.clicks;
  return agg;
}, []);

// Show result
console.log(aggData);

Map array with initial destructuring, using ES6 spread syntax. Then reduce accounting for paths previously traversed, summing visits and clicks on appropriate path.


 const res = data
     .map(({ aggregate, ...rest }) => ({ path: aggregate.path, ...rest }))
     .reduce((acc, c, i, arr) => {
         let curr = Object.assign({}, c);
         const traversed = acc.some(item => item.hasOwnProperty('path') && item.path === curr.path);
         if (traversed) return acc;

         arr.forEach((v, idx) => {
            if (idx !== i && c.path === v.path) {
               curr = { 
                  ...curr, 
                  visits: v.visits + curr.visits, 
                 clicks: v.clicks + curr.clicks 
                } 
           }
        })

       return [...acc, curr];
    }, [])

I would create an object and use the path names as the aggregator. Then change it back to an array.

let data = [ {"aggregate": { "path": "/home/page1", "query": "name=todd" }, "visits": 4, "clicks": 7 },
{"aggregate": { "path": "/home/page1", "query": "name=matt"}, "visits": 5, "clicks": 17},
{"aggregate": { "path": "/home/page2", "query": "" },"visits": 4,"clicks": 7},
{"aggregate": { "path": "/home/page3", "query": "term=dig" },"visits": 2, "clicks": 20},
{"aggregate": { "path": "/home/page1", "query": "term=dug" }, "visits": 2, "clicks": 11}];

//let the object path act as the aggregator
let new_data = data.reduce((acc,obj) => {
  let path = obj.aggregate.path;
  acc[path] = { 
      path: path,
      clicks: obj.clicks + (acc[path]?.clicks || 0),
      visits: obj.visits + (acc[path]?.visits || 0)
      };
  return acc;
},{});

//turn the object into an array
new_data = Object.entries(new_data).map(([key,value]) => value);

console.log(new_data);

Related