davefogo
davefogo

Reputation: 113

Summarize count of occurrences in an array of objects with Array#reduce

I want to summarize an array of objects and return the number of object occurrences in another array of objects. What is the best way to do this?

From this

var arrayOfSongs = [
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Green","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"}
];

To this

var newArrayOfSongs = [
  {"title": "Blue", "playCount": 3 },
  {"title": "Green", "playCount": 1}
]

I have tried

 arrayOfSongs.reduce(function(acc, cv) {
   acc[cv.title] = (acc[cv.title] || 0) + 1;
     return acc;
   }, {});
 }

But it returns an object:

 { "Blue": 3, "Green": 1};

Upvotes: 3

Views: 169

Answers (4)

000
000

Reputation: 27247

I recommend doing this in two stages. First, chunk the array by title, then map the chunks into the output you want. This will really help you in future changes. Doing this all in one pass is highly complex and will increase the chance of messing up in the future.

var arrayOfSongs = [
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Green","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"}
];

function chunkByAttribute(arr, attr) {
  return arr.reduce(function(acc, e) {
   acc[e[attr]] = acc[e[attr]] || [];
   acc[e[attr]].push(e);
   return acc;
  }, {});
}

var songsByTitle = chunkByAttribute(arrayOfSongs, 'title');

var formattedOutput = Object.keys(songsByTitle).map(function (title) {
  return {
    title: title,
    playCount: songsByTitle[title].length
  };
});

There, now everything is named according to what it does, everything does just one thing, and is a bit easier to follow.

Upvotes: 1

Jaromanda X
Jaromanda X

Reputation: 1

To build on what you already have done, the next step is to "convert" the object to an array

    var arrayOfSongs = [
        {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
        {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
        {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
        {"title":"Green","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"}
    ];

    var obj = arrayOfSongs.reduce(function(acc, cv) {
       acc[cv.title] = (acc[cv.title] || 0) + 1;
       return acc;
    }, {});

    // *** added code starts here ***
    var newArrayOfSongs = Object.keys(obj).map(function(title) { 
        return {
            title: title, 
            playCount:obj[title]
        };
    });

    console.log(newArrayOfSongs);

Upvotes: 1

zfrisch
zfrisch

Reputation: 8660

https://jsfiddle.net/93e35wcq/

I used a set object to get the unique track titles, then used Array.map to splice those and return a song object that contains play count inside the track title.

The Data:

var arrayOfSongs = [{
  "title": "Blue",
  "duration": 161.71,
  "audioUrl": "/assets/music/blue",
  "playing": false,
  "playedAt": "2016-12-21T22:58:55.203Z"
}, {
  "title": "Blue",
  "duration": 161.71,
  "audioUrl": "/assets/music/blue",
  "playing": false,
  "playedAt": "2016-12-21T22:58:55.203Z"
}, {
  "title": "Blue",
  "duration": 161.71,
  "audioUrl": "/assets/music/blue",
  "playing": false,
  "playedAt": "2016-12-21T22:58:55.203Z"
}, {
  "title": "Green",
  "duration": 161.71,
  "audioUrl": "/assets/music/blue",
  "playing": false,
  "playedAt": "2016-12-21T22:58:55.203Z"
}];

The Function:

function getPlayCount(arrayOfSongs) {
  let songObj = {};
  let SongSet = new Set();
  arrayOfSongs.map(obj => (SongSet.has(obj.title)) ? true : SongSet.add(obj.title));
  for (let songTitle of SongSet.values()) {
    songObj[songTitle] = {
      playCount: 0
    };
    arrayOfSongs.map(obj => (obj.title === songTitle) ? songObj[songTitle].playCount++ : false)
  }
  return songObj;
}

console.log(getPlayCount(arrayOfSongs));

Which isn't exactly what you wanted formatting wise, but if you're married to it, this will do the trick:

    function getPlayCount(arrayOfSongs) {
  let songObj = {};
  let SongSet = new Set();
  arrayOfSongs.map(obj => (SongSet.has(obj.title)) ? true : SongSet.add(obj.title));
  for (let songTitle of SongSet.values()) {
    songObj[songTitle] = 0;
    arrayOfSongs.map(obj => (obj.title === songTitle) ? songObj[songTitle]++ : false)
  }
  return songObj;
}

console.log(getPlayCount(arrayOfSongs));

https://jsfiddle.net/93e35wcq/1/

Upvotes: 0

Aruna
Aruna

Reputation: 12022

You should pass the initial argument to the reduce function as an array instead of object and filter array for the existing value as below,

Working snippet:

var arrayOfSongs = [
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Blue","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"},
  {"title":"Green","duration":161.71,"audioUrl":"/assets/music/blue","playing":false,"playedAt":"2016-12-21T22:58:55.203Z"}
];


var newArrayOfSongs = arrayOfSongs.reduce(function(acc, cv) {
    var arr = acc.filter(function(obj) {
      return obj.title === cv.title;
    });
   
    if(arr.length === 0) {
      acc.push({title: cv.title, playCount: 1});
    } else {
      arr[0].playCount += 1;
    }
    
    return acc;
   }, []);

console.log(newArrayOfSongs);

Upvotes: 1

Related Questions