Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Grouping array of objects by multiple properties

I have an array of notifications that I want to group by certain conditions (like facebook's notifications)

var data = [
    { id: 1, type: 'shop.follower', by: { id: 2, name: 'User B', }, in: null, read_at: '2021-01-03 10:15:43', created_at: '2020-08-02 05:21:20' },
    { id: 2, type: 'product.liked', by: { id: 2, name: 'User B' }, in: { id: 1, ... }, read_at: '2021-01-03 10:15:43', created_at: '2020-08-02 05:24:45' },
    { id: 3, type: 'product.commented', by: { id: 3, name: 'User C' }, in: { id: 1, ... }, read_at: '2021-02-20 20:01:39', created_at: '2021-02-19 16:21:43' },
    { id: 4, type: 'product.liked', by: { id: 4, name: 'User D' }, in: { id: 1, ... }, read_at: '2021-03-29 15:14:21', created_at: '2021-03-28 08:11:50' },
    { id: 5, type: 'product.liked', by: { id: 3, name: 'User C' }, in: { id: 1, ... }, read_at: null, created_at: '2021-03-28 08:12:24' },
    { id: 6, type: 'shop.follower', by: { id: 5, name: 'User E' }, in: null, read_at: null, created_at: '2021-05-23 10:02:21' },
    { id: 7, type: 'shop.follower', by: { id: 3, name: 'User C' }, in: null, read_at: null, created_at: '2021-07-18 10:31:12' },
    { id: 8, type: 'comment.replied', by: { id: 4, name: 'User D' }, in: { id: 6, ... }, read_at: null, created_at: '2021-07-24 08:34:25' },
]

Let's say I want to group by date in descending order

So I have this code:

function sortByDate(array, desc = true) {
    if (desc === false) {
        // Ascending order
        return array.sort((a, b) => {
            if (new Date(a.created_at) > new Date(b.created_at)) {
                return 1
            } else {
                return -1
            }

            return 0
        })
    }

    // Descending order
    return array.sort((a, b) => {
        if (new Date(a.created_at) < new Date(b.created_at)) {
            return 1
        } else {
            return -1
        }

        return 0
    })
}

So now we have array like this:

[
    { id: 8, type: 'comment.replied', by: { id: 4, name: 'User D' }, in: { id: 6, ... }, read_at: null, created_at: '2021-07-24 08:34:25' },
    { id: 7, type: 'shop.follower', by: { id: 3, name: 'User C' }, in: null, read_at: null, created_at: '2021-07-18 10:31:12' },
    { id: 6, type: 'shop.follower', by: { id: 5, name: 'User E' }, in: null, read_at: null, created_at: '2021-05-23 10:02:21' },
    { id: 5, type: 'product.liked', by: { id: 3, name: 'User C' }, in: { id: 1, ... }, read_at: null, created_at: '2021-03-28 08:12:24' },
    { id: 4, type: 'product.liked', by: { id: 4, name: 'User D' }, in: { id: 1, ... }, read_at: '2021-03-29 15:14:21', created_at: '2021-03-28 08:11:50' },
    { id: 3, type: 'product.commented', by: { id: 3, name: 'User C' }, in: { id: 1, ... }, read_at: '2021-02-20 20:01:39', created_at: '2021-02-19 16:21:43' },
    { id: 2, type: 'product.liked', by: { id: 2, name: 'User B' }, in: { id: 1, ... }, read_at: '2021-01-03 10:15:43', created_at: '2020-08-02 05:24:45' },
    { id: 1, type: 'shop.follower', by: { id: 2, name: 'User B', }, in: null, read_at: '2021-01-03 10:15:43', created_at: '2020-08-02 05:21:20' },
]

Now that our array is sorted, I created a function:

// https://www.tutorialspoint.com/most-efficient-method-to-groupby-on-an-array-of-objects-in-javascript
function groupByProperty(array, property) {
    return array.reduce((acc, object) => {
        const key = object[property]

        if (! acc[key]) {
            acc[key] = []
        }

        acc[key].push(object)

        return acc
    }, {})
}

Then, I run this code

Object.values(groupByProperty(data, 'type'))

Which return:

[
    [
        { id: 8, type: 'comment.replied', by: { id: 4, name: 'User D' }, in: { id: 6, ... }, read_at: null, created_at: '2021-07-24 08:34:25' }
    ],
    [
        { id: 7, type: 'shop.follower', by: { id: 3, name: 'User C' }, in: null, read_at: null, created_at: '2021-07-18 10:31:12' },
        { id: 6, type: 'shop.follower', by: { id: 5, name: 'User E' }, in: null, read_at: null, created_at: '2021-05-23 10:02:21' },
        { id: 1, type: 'shop.follower', by: { id: 2, name: 'User B', }, in: null, read_at: '2021-01-03 10:15:43', created_at: '2020-08-02 05:21:20' }
    ],
    [
        { id: 5, type: 'product.liked', by: { id: 3, name: 'User C' }, in: { id: 1, ... }, read_at: null, created_at: '2021-03-28 08:12:24' },
        { id: 4, type: 'product.liked', by: { id: 4, name: 'User D' }, in: { id: 1, ... }, read_at: '2021-03-29 15:14:21', created_at: '2021-03-28 08:11:50' },
        { id: 2, type: 'product.liked', by: { id: 2, name: 'User B' }, in: { id: 1, ... }, read_at: '2021-01-03 10:15:43', created_at: '2020-08-02 05:24:45' }
    ],
    [
        { id: 3, type: 'product.commented', by: { id: 3, name: 'User C' }, in: { id: 1, ... }, read_at: '2021-02-20 20:01:39', created_at: '2021-02-19 16:21:43' }
    ],
]

I want to group these notifications by these categories:

  1. Same type (I already covered this in my function groupByProperty())
  2. Same in: { id: ... } Except for type: shop.follower
  3. If #1 and #2 were true, check for similar objects with created_at: ... date interval between 10 minutes
  4. If we have a case like #3 (multiple), if one among it has read_at = null, then it will be an unread notification, get the latest (newest) date

In id: 4 and id: 5, interval between timestamp are less than 10 minutes, so I want it to group as one

example EXPECTED OUTPUT:

[
    [
        { by: {id: 4, name: "User D"}, created_at: "2021-07-24 08:34:25", id: 8, in: {id: 6}, read_at: null, type: "comment.replied" }
    ],
    [
        { by: {id: 3, name: "User C"}, created_at: "2021-07-18 10:31:12", id: 7, in: null, read_at: null, type: "shop.follower" }
    ],
    [
        { by: {id: 5, name: "User E"}, created_at: "2021-05-23 10:02:21", id: 6, in: null, read_at: null, type: "shop.follower" }
    ],
    [
        { by: {id: 3, name: "User C"}, created_at: "2021-03-28 08:12:24", id: 5, in: {id: 1}, read_at: null, type: "product.liked" },
        { by: {id: 4, name: "User D"}, created_at: "2021-03-28 08:11:50", id: 4, in: {id: 1}, read_at: "2021-03-29 15:14:21", type: "product.liked" }
    ],
    [
        { by: {id: 3, name: "User C"}, created_at: "2021-02-19 16:21:43", id: 3, in: {id: 1}, read_at: "2021-02-20 20:01:39", type: "product.commented" }
    ],
    [
        { by: {id: 2, name: "User B"}, created_at: "2020-08-02 05:24:45", id: 2, in: {id: 1}, read_at: "2021-01-03 10:15:43", type: "product.liked" }
    ],
    [
        { by: {id: 2, name: "User B"}, created_at: "2020-08-02 05:21:20", id: 1, in: null, read_at: "2021-01-03 10:15:43", type: "shop.follower" }
    ],
]

example IN BROWSER:

|------------------------------------------------------------------------------|
| - (UNREAD) User D replied to your comment .....,   2021-07-24 08:34:25       |
| - (UNREAD) User C start follow your shops .....,   2021-07-18 10:31:12       |
| - (UNREAD) User E start follow your shops .....,   2021-05-23 10:02:21       |
| - (UNREAD) User C and D liked your product .....,  2021-03-28 08:12:24       | <= (Please pay attention)
| - (READ) User C commented on your product .....,   2021-02-19 16:21:43       |
| - (READ) User B liked your product .....,          2020-08-02 05:24:45       |
| - (READ) User B start follow your shops .....,     2020-08-02 05:21:20       |

This is the code I tried to find interval between 10 minutes

function inRangeBetween(val, min, max) {
    if (val >= min && val <= max) {
        return true
    }

    return false
}

var startingPoint = { min: 0, max: 0, type: null },
    newData = []

for (var i = 0; i < data.length; i++) {
    if (startingPoint.min < 1
        && startingPoint.max < 1
        && startingPoint.type === null) {
        console.log('Starting point')
        var start = new Date(data[i].created_at)
        startingPoint.min = start.getTime()
        startingPoint.max = start.getTime() + (10 * 60000)
        startingPoint.type = data[i].type
        newData[data[i].type] = []
    } else {

        // startingPoint has values
        if (inRangeBetween(new Date(data[i].created_at).getTime(), startingPoint.min, startingPoint.max
            && data[i].type === startingPoint.type) {
            console.log(`Pushing new object to key ${data[i].type}`)
            newData[data[i].type].push(data[i])
        } else {
            // Set new values for startingPoint, and start again comparing
            console.log('Starting point values changes')
            startingPoint.min = new Date(data[i]).getTime()
            startingPoint.min = new Date(data[i]).getTime() + (10 * 60000)
            startingPoint.type = data[i].type
            newData[data[i].type] = []
            newData[data[i].type].push(data[i])
        }
    }
}

// Not working

How to achieve this? (Stuck in this problem for 5 days)

Thanks in advance

like image 299
Gwein Avatar asked Jun 30 '21 09:06

Gwein


People also ask

How do you group an array?

The group() method executes the callbackFn function once for each index of the array, returning a string (or value that can be coerced to a string) indicating the group of the element. A new property and array is created in the result object for each unique group name that is returned by the callback.

Can an array have multiple objects?

Answer. Items can be added to an array with the push method. Every array has this method build it, together with many other methods, and can be used to push a new value to the end of the array. In the push method create an object with the keys and the values that you want to add.

How do you find a specific value in an array?

Use filter if you want to find all items in an array that meet a specific condition. Use find if you want to check if that at least one item meets a specific condition. Use includes if you want to check if an array contains a particular value. Use indexOf if you want to find the index of a particular item in an array.


2 Answers

Try like this:

var data = [
    { id: 1, type: 'shop.follower', by: { id: 2, name: 'User B', }, in: null, read_at: '2021-01-03 10:15:43', created_at: '2020-08-02 05:21:20' },
    { id: 2, type: 'product.liked', by: { id: 2, name: 'User B' }, in: { id: 1 }, read_at: '2021-01-03 10:15:43', created_at: '2020-08-02 05:24:45' },
    { id: 3, type: 'product.commented', by: { id: 3, name: 'User C' }, in: { id: 1 }, read_at: '2021-02-20 20:01:39', created_at: '2021-02-19 16:21:43' },
    { id: 4, type: 'product.liked', by: { id: 4, name: 'User D' }, in: { id: 1 }, read_at: '2021-01-03 10:15:43', created_at: '2021-03-28 08:11:50' },
    { id: 5, type: 'product.liked', by: { id: 3, name: 'User C' }, in: { id: 1 }, read_at: null, created_at: '2021-03-28 08:12:24' },
    { id: 6, type: 'shop.follower', by: { id: 5, name: 'User E' }, in: null, read_at: null, created_at: '2021-05-23 10:02:21' },
    { id: 7, type: 'shop.follower', by: { id: 3, name: 'User C' }, in: null, read_at: null, created_at: '2021-07-18 10:31:12' },
    { id: 8, type: 'comment.replied', by: { id: 4, name: 'User D' }, in: { id: 6 }, read_at: null, created_at: '2021-07-24 08:34:25' }
]

function sortByDate(array, desc = true) {
    if (desc === false) {
        // Ascending order
        return array.sort((a, b) => {
            if (new Date(a.created_at) > new Date(b.created_at)) {
                return 1
            } else {
                return -1
            }

            return 0
        })
    }

    // Descending order
    return array.sort((a, b) => {
        if (new Date(a.created_at) < new Date(b.created_at)) {
            return 1
        } else {
            return -1
        }

        return 0
    })
}

function groupByProperties(array, properties) {
    return Object.values(array.reduce((acc, object) => {
        const key = properties.reduce((acc, property) => {
          return acc + (object[property] ? JSON.stringify(object[property]) : '')
        }, '')

        if (! acc[key]) {
          acc[key] = []
        }

        acc[key].push(object)

        return acc
    }, {}))
}

function groupByInterval(data, interval) {
  var group;
  
  for(var i = 0; i < data.length; i++) {
    if(1 < data[i].length) {
      
      var max_date = new Date(data[i][0].created_at);
      
      for(var j = data[i].length - 1; 0 < j; j--) {
        var next_date = new Date(data[i][j].created_at)
        
        if(interval < max_date - next_date) {
           if(!group) {
             group = i + 1;
             data.splice(group, 0, [])
           }
           
           data[group].splice(0, 0, data[i][j])
           data[i].splice(j, 1)
        };
      };
      
      if(group) {
         return groupByInterval(data, interval)
      };
    }
    
    data[i].sort((a, b) => {
      if(!a.read_at) {
        return -1
      }
      
      if(!b.read_at) {
        return 1
      }
      
      return 0
    })
  }
  
  data.sort((a, b) => new Date(b[0].created_at) - new Date(a[0].created_at))

  return data
}


sortByDate(data)

//1. Same type
//2. Same in: { id: ... } (Except for type: shop.follower)
data = groupByProperties(data, ['type', 'in'])

//3. If #1 and #2 true, check for similar objects with created_at: ... date gap between 10 minutes
//4. If we have a case like #3 (multiple), if one among it has read_at = null, then it unread notification, then get the latest (newest) date
data = groupByInterval(data, 1000 * 60 * 10) //10min

console.log(data)
  • groupByProperties() is based on groupByProperty(), but accepts multiple properties for grouping (categories 1 and 2). It checks whether the value of the property is falsy (such as null), excluding it from grouping criteria if so.
  • groupByInterval() added to separate groups according to a specified interval in milliseconds (categories 3 and 4). It then sorts the groups according to read_at being falsy, so that objects with read_at == null appear first in each group. It then sorts across groups to achieve the order in the expected result.
like image 151
sbgib Avatar answered Oct 22 '22 02:10

sbgib


Mr.sbgib is absolutely correct, but i just modified sortByDate function to a little short as follows to reduce duplicate codes,

function sortByDate(array, desc = true) {
  return array.sort((a, b) => {
     var compare = new Date(a.created_at) < new Date(b.created_at);
     return (desc == true) ? ((compare == true) ? 1 : -1) : ((compare == false) ? 1 : -1);
  })
}
like image 43
Rinshan Kolayil Avatar answered Oct 22 '22 02:10

Rinshan Kolayil