kemakino
kemakino

Reputation: 1122

Summarize the frequency of array of objects

Assume I have the following array of objects.

data = [
  { x: 1, y: 1 },
  { x: 2, y: 2 },
  { x: 3, y: 3 },
  { x: 2, y: 2 },
  { x: 1, y: 1 },
  { x: 1, y: 2 },
  { x: 1, y: 1 }
]

what I need is to summarize the frequency of identical object in the array. The output will look like:

summary = [
  { x: 1, y: 1, f: 3 },
  { x: 1, y: 2, f: 1 },
  { x: 2, y: 2, f: 2 },
  { x: 3, y: 3, f: 1 }
]

For now I have this code

const summary = data.map((item, index, array) => {
  return { x: item.x, y: item.y, f: array.filter(i => i === item).length };
});

But I suppose I can do better by using reduce or includes. Any ideas?

Upvotes: 4

Views: 157

Answers (7)

ugliest
ugliest

Reputation: 101

Object.values(data.reduce((sum, i) => {
    i_str = JSON.stringify(i); // objects can't be keys
    sum[i_str] = Object.assign({}, i, {f: sum[i_str] ? sum[i_str].f+1 : 1});
    return sum;
}, {}));

Note:

  1. This snippet will work on an array of any arbitrary objects, as long as they are stringifiable.
  2. Results are not ordered, since object keys aren’t ordered. If this is an issue, sort at will.
  3. What you’re doing, is counting the times an object exists in an array. You probably want results external to the objects, as opposed to embedded in them. Something along these lines might be more manageable, returning a mapping of descriptions of the objects to a count:
data.reduce((sum, i) => {
    i_str = JSON.stringify(i); // objects can't be keys
    sum[i_str] = sum[i_str] ? sum[i_str]+1 : 1;
    return sum;
}, {});

Upvotes: 1

Code Maniac
Code Maniac

Reputation: 37755

You can use reduce and Map, club the use x and y as key, on every iteration check if the same key is already present on Map than just increase f count by 1 if not than set it to 1

const data = [{ x: 1, y: 1 },{ x: 2, y: 2 },{ x: 3, y: 3 },{ x: 2, y: 2 },{ x: 1, y: 1 },{ x: 1, y: 2 },{ x: 1, y: 1 }];

const countObj = data.reduce((a, obj) => {
  const objString = obj.x + '_' + obj.y;
  let value = a.get(objString) || obj
  let f = value && value.f  || 0
  a.set(objString, { ...value, f: f+1 })
  return a;
}, new Map());

console.log([...countObj.values()]);

Upvotes: 1

Barmar
Barmar

Reputation: 781088

Create nested objects. The outer object uses x values as keys, the nested object contains y values as keys, and the values are the frequencies.

data = [
  { x: 1, y: 1 },
  { x: 2, y: 2 },
  { x: 3, y: 3 },
  { x: 2, y: 2 },
  { x: 1, y: 1 },
  { x: 1, y: 2 },
  { x: 1, y: 1 }
];

const nested = data.reduce((a, {x, y}) => {
  a[x] = a[x] || {};
  a[x][y] = a[x][y] ? a[x][y] + 1 : 1
  return a;
}, {});
const summary = [];
Object.keys(nested).forEach(x => Object.keys(nested[x]).forEach(y => summary.push({x, y, f: nested[x][y]})));

console.log(summary);

Upvotes: 1

wakakak
wakakak

Reputation: 842

I know using reduce is probably better, but I tend to use forEach and findIndex for better readability.

var data = [
  { x: 1, y: 1 },
  { x: 2, y: 2 },
  { x: 3, y: 3 },
  { x: 2, y: 2 },
  { x: 1, y: 1 },
  { x: 1, y: 2 },
  { x: 1, y: 1 }
];

var summary = [];

data.forEach(function(d){
  var idx = summary.findIndex(function(i){
    return i.x === d.x && i.y === d.y;
  });

  if(idx < 0){
    var sum = Object.assign({}, d);
    sum.f = 1;
    summary.push(sum);
  } else {
    summary[idx].f = summary[idx].f + 1;
  }
});

console.log(summary);

Upvotes: 1

Dacre Denny
Dacre Denny

Reputation: 30360

A simple solution based on Array#reduce would be as detailed below:

const data = [
  { x: 1, y: 1 },
  { x: 2, y: 2 },
  { x: 3, y: 3 },
  { x: 2, y: 2 },
  { x: 1, y: 1 },
  { x: 1, y: 2 },
  { x: 1, y: 1 }
];

const summary = data.reduce((frequencySummary, item) => {
  
  /* Find a match for current item in current list of frequency summaries */
  const itemMatch = frequencySummary.find(i => i.x === item.x && i.y === item.y)
  
  if(!itemMatch) {
    
    /* If no match found, add a new item with inital frequency of 1 to the result */
    frequencySummary.push({ ...item, f : 1 });
  }
  else {
    
    /* If match found, increment the frequency count of that match */
    itemMatch.f ++;
  }
  
  return frequencySummary;

}, []);

console.log(summary)

Upvotes: 1

Jack Bashford
Jack Bashford

Reputation: 44105

Don't use map - you're better off using reduce like so:

const summary = Object.values(data.reduce((a, { x, y }) => {
  a[`${x}-${y}`] = a[`${x}-${y}`] || { x, y, f: 0 };
  a[`${x}-${y}`].f++;
  return a;
}, {}));

Upvotes: 2

CertainPerformance
CertainPerformance

Reputation: 370789

Reduce into an object whose keys uniquely represent an object, whose values are the object (with x, y, and f properties). On each iteration, increment the appropriate key's f property, or create the key on the accumulator if it doesn't exist yet:

const data = [
  { x: 1, y: 1 },
  { x: 2, y: 2 },
  { x: 3, y: 3 },
  { x: 2, y: 2 },
  { x: 1, y: 1 },
  { x: 1, y: 2 },
  { x: 1, y: 1 }
];
const countObj = data.reduce((a, obj) => {
  const objString = obj.x + '_' + obj.y;
  if (!a[objString]) {
    a[objString] = { ...obj, f: 1 };
  } else {
    a[objString].f++;
  }
  return a;
}, {});
const output = Object.values(countObj);
console.log(output);

Upvotes: 4

Related Questions