Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I properly sort an array and remove duplicates in JavaScript with only one variable declaration?

I have a working code that sorts an array from highest to lowest id, then removes duplicates.

Let's say we have an array of activities:

[
    {
        id: 1,
        contact_id: 2,
        some_field: true
    },
    {
        id: 2,
        contact_id: 3,
        some_field: true
    },
    {
        id: 3,
        contact_id: 4,
        some_field: false
    },
    {
        id: 4,
        contact_id: 3,
        some_field: true
    },
    {
        id: 5,
        contact_id: 4,
        some_field: false
    }
]

So the output with duplicate contact_id removed and arranged from highest id to lowest id must be:

[
    {
        id: 5,
        contact_id: 4,
        some_field: false
    },
    {
        id: 4,
        contact_id: 3,
        some_field: true
    },
    {
        id: 1,
        contact_id: 2,
        some_field: true
    }
]

My code looks like this:

// Sort by latest to oldest activity
arr = arr.sort((a, b) => b.id > a.id);
// Remove duplicates
arr = arr.filter((obj, pos, arr) => {
    return arr.map(mapObj => mapObj['contact_id']).indexOf(obj['contact_id']) === pos;
});

While the code works, is there a better way to do this? like instead of having the arr variable set twice?

like image 245
wobsoriano Avatar asked Dec 08 '22 14:12

wobsoriano


1 Answers

Note, first off, that there are several problems with this code:

  • This is not how sort works. The function passed to sort should return a negative number, positive number or zero, if the first value is, respectively, less than, greater than, or equal to, the second one. While it may seem to work for your specific test case, returning a boolean is a bad idea.

  • This is a very painful way of collecting the unique values. On every filter iteration, you rebuild your entire list of keys. Then for each one, you scan through that list checking to find if your current value is included. IMHO, it would be significantly cleaner to use a Set to store the keys, and test whether that Set has the given key before adding it to the output.


While the answers from Sreekath and HMR are fine, I am a big fan of the functional programming library Ramda. (Disclaimer: I'm one of its authors.) I tend to think in terms of simple, reusable functions.

When I think of how to solve this problem, I think of it through a Ramda viewpoint. And I would probably solve this problem like this:

const {pipe, sortBy, prop, reverse, uniqBy} = R

const convert = pipe(
  sortBy(prop('id')),
  reverse,
  uniqBy(prop('contact_id'))
)

const arr = [{"contact_id": 2, "id": 1, "some_field": true}, {"contact_id": 3, "id": 2, "some_field": true}, {"contact_id": 4, "id": 3, "some_field": false}, {"contact_id": 3, "id": 4, "some_field": true}, {"contact_id": 4, "id": 5, "some_field": false}]


console.log(convert(arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>

I think that is fairly readable, at least once you understand that pipe creates a pipeline of functions, each handing its result to the next one.

Now, there is often not a reason to include a large library like Ramda to solve a fairly simple problem. But all the functions used in that version are easily reusable. So it might make sense to try to create your own versions of these functions and keep them available to the rest of your application. In fact, that's how libraries like Ramda actually get built.

So here is a version that has simple implementations of those functions, ones you might place in a utility library:

const pipe = (...fns) => (arg) => fns.reduce((a, f) => f(a), arg)
const prop = (name) => (obj) => obj[name]
const reverse = (xs) => xs.slice(0).reverse()
const sortBy = (fn) => (xs) => xs.slice(0).sort((a, b) => {  
  const aa = fn(a), bb = fn(b)
  return aa < bb ? -1 : aa > bb ? 1 : 0
})
const uniqBy = (fn) => (xs) => {
  const keys = new Set(), result = [];
  xs.forEach(x => {
    const key = fn(x)
    if (!keys.has(key)) {
      keys.add(key)
      result.push(x)
    }
  })
  return result
}

const convert = pipe(
  sortBy(prop('id')),
  reverse,
  uniqBy(prop('contact_id'))
)

const arr = [{"contact_id": 2, "id": 1, "some_field": true}, {"contact_id": 3, "id": 2, "some_field": true}, {"contact_id": 4, "id": 3, "some_field": false}, {"contact_id": 3, "id": 4, "some_field": true}, {"contact_id": 4, "id": 5, "some_field": false}]


console.log(convert(arr))

Each of these except uniqBy is a nearly trivial implementation. For the most part, these are not the implementations used in Ramda, which often has additional concerns. But they should work mostly the same.

This is a great combination of reusable functions for which you could well find many other uses across your code-base.


Finally, while this does avoid having several reassignments, it does not remove the problem of having several iterations through the data. In fact, because sortBy does not offer a direction parameter, we already have to add one more step: sortBy, then reverse. (It makes sense to do it like this because it's so easy to combine the reverse with sortBy that there is no reason for a function such as reverseSortBy.) This is my preferred style of working. I very rarely run into performance problems because of this, and I choose to deal with such issues only if the system is demonstrating measureable performance problems.

like image 186
Scott Sauyet Avatar answered Dec 11 '22 12:12

Scott Sauyet