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?

3
  • 1
    You should use reduce. map always returns an array with the same number of elements, but you want to combine equivalent elements. Commented Aug 21, 2019 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 stackoverflow.com/questions/201183/… Commented Aug 21, 2019 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. Commented Aug 22, 2019 at 19:29

7 Answers 7

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);

Sign up to request clarification or add additional context in comments.

Comments

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;
}, {}));

Comments

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;
}, {});

1 Comment

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 :)
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)

Comments

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);

Comments

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);

Comments

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()]);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.