4

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?

Jack Bashford
  • 43,180
  • 11
  • 50
  • 79
kemakino
  • 1,041
  • 13
  • 33
  • 1
    You should use `reduce`. `map` always returns an array with the same number of elements, but you want to combine equivalent elements. – Barmar Aug 21 '19 at 23:51
  • 1
    `i === item` isn't going to work with objects. It requires the objects to be the same, not just have equivalent properties. See https://stackoverflow.com/questions/201183/how-to-determine-equality-for-two-javascript-objects – Barmar Aug 21 '19 at 23:52
  • @Barmar Indeed I haven't checked well if this code worked fine. I understood that I need further to remove the duplicates after the `map`. Thanks for your suggestion anyways. – kemakino Aug 22 '19 at 19:29

7 Answers7

4

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);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
2

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;
}, {}));
Jack Bashford
  • 43,180
  • 11
  • 50
  • 79
1

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)
Dacre Denny
  • 29,664
  • 5
  • 45
  • 65
1

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);
wakakak
  • 842
  • 5
  • 13
1

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);
Barmar
  • 741,623
  • 53
  • 500
  • 612
1

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()]);
Code Maniac
  • 37,143
  • 5
  • 39
  • 60
1
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;
}, {});
ugliest
  • 101
  • 6
  • This seemed the most elegant and universal approach for me, so I have given the check to you. But it's nice to see many nice ideas from all you have posted an answer, thank you :) – kemakino Aug 22 '19 at 19:38