126

I have this kind of array:

var foo = [ { "a" : "1" }, { "b" : "2" }, { "a" : "1" } ];

I'd like to filter it to have:

var bar = [ { "a" : "1" }, { "b" : "2" }];

I tried using _.uniq, but I guess because { "a" : "1" } is not equal to itself, it doesn't work. Is there any way to provide underscore uniq with an overriden equals function?

John Slegers
  • 45,213
  • 22
  • 199
  • 169
plus-
  • 45,453
  • 15
  • 60
  • 73

13 Answers13

234

.uniq/.unique accepts a callback

var list = [{a:1,b:5},{a:1,c:5},{a:2},{a:3},{a:4},{a:3},{a:2}];

var uniqueList = _.uniq(list, function(item, key, a) { 
    return item.a;
});

// uniqueList = [Object {a=1, b=5}, Object {a=2}, Object {a=3}, Object {a=4}]

Notes:

  1. Callback return value used for comparison
  2. First comparison object with unique return value used as unique
  3. underscorejs.org demonstrates no callback usage
  4. lodash.com shows usage

Another example : using the callback to extract car makes, colors from a list

tronman
  • 9,862
  • 10
  • 46
  • 61
Shanimal
  • 11,517
  • 7
  • 63
  • 76
  • `false` is not required for `_.uniq()`. Also in lodash you could have wrote it like this `_.uniq(a, 'a');`, since it'll pluck the property `a` on the objects. – Larry Battle Mar 14 '13 at 16:07
  • The "'_.pluck' callback shorthand" only works if you pass a value for isSorted (e.g. `_.uniq(a, false, 'a')`) I pinged github/bestiejs/lodash and they said the issue was fixed on edge. So if you're not using a function, make sure you have the latest. This may not be an issue for underscore. – Shanimal Mar 16 '13 at 15:08
  • 2
    Iterator doesn't sound like a good name, it's a hash like function that will be to determine identity of each object – Ruan Mendes Jul 04 '13 at 17:03
  • Edited to use callback to be more consistent with lodash docs :) – Shanimal Aug 12 '13 at 18:38
  • 1
    You example at [jsbin](http://jsbin.com/evodub/13/edit) can have a update. (1) Makes : _(cars).uniq('make').map('make').valueOf() AND (2) Colors: _(cars).uniq('color').map('color').valueOf(). You can ripe off the color and make closures. (All that if you upgrade de lodash used) – Vitor Tyburski Apr 17 '14 at 12:52
  • Yes! I totally encourage you to post said update in an edit. :) – Shanimal May 30 '14 at 07:20
  • The issue with this, the iteratee only transforms each element to another value which will be checked for uniqueness. If you don't know (or don't want to know) the actual format of the objects, then you will need to stringify the objects. I recommend you look at [my solution](http://stackoverflow.com/a/17479527/1104107) which is more flexible and compares objects based on their properties. – Joshua Bambrick Nov 01 '14 at 15:26
  • No. Stringify won't solve that problem because stringify is non-deterministic, furthermore uniq uses a where style callback to compare two object with a deep comparison for equality. maybe it didn't work for some edge case, but for most situations this is totally sufficient. – Shanimal Jan 21 '15 at 19:42
  • how dooes one know order and no of parameters passed in callback function "function(item, key, a)" ? – StackUP Jul 09 '16 at 06:11
  • The [lodash docs](https://lodash.com/docs) are pretty good with explanation of the callback. Most of the lodash functions use the same values for the callback. I remember *VIL* or `Value`, `Index`, `List`. With exceptions, like `reduce` which precedes that with `Result`. – Shanimal Jul 11 '16 at 05:48
  • what about little more complicated example: {day: 15, month: 8, year: 2017} 1 : {day: 14, month: 8, year: 2017} 2 : {day: 14, month: 8, year: 2017} 3 : {day: 14, month: 8, year: 2017} 4 : {day: 14, month: 8, year: 2017} 5 : {day: 14, month: 8, year: 2017} 6 : {day: 14, month: 8, year: 2017} – aleXela Aug 15 '17 at 13:00
  • What did you have in mind? Did you see the car make/color link. You can apply that code to dates as well. :) http://jsbin.com/evodub/13/edit?js,output – Shanimal Aug 15 '17 at 14:43
38

If you're looking to remove duplicates based on an id you could do something like this:

var res = [
  {id: 1, content: 'heeey'},
  {id: 2, content: 'woah'}, 
  {id: 1, content:'foo'},
  {id: 1, content: 'heeey'},
];
var uniques = _.map(_.groupBy(res,function(doc){
  return doc.id;
}),function(grouped){
  return grouped[0];
});

//uniques
//[{id: 1, content: 'heeey'},{id: 2, content: 'woah'}]
Petter
  • 381
  • 3
  • 3
  • The accepted answer doesn't work when the unique identifier is a `Date`. However this does. – gunwin Mar 05 '17 at 22:39
17

Implementation of Shiplu's answer.

var foo = [ { "a" : "1" }, { "b" : "2" }, { "a" : "1" } ];

var x = _.uniq( _.collect( foo, function( x ){
    return JSON.stringify( x );
}));

console.log( x ); // returns [ { "a" : "1" }, { "b" : "2" } ]
Larry Battle
  • 9,008
  • 4
  • 41
  • 55
  • btw, how did you get 4 upvotes? To get at the properties of your result you'd have to revert each array value back into an object. Something like `JSON.parse(x[0]).a` because x isn't an array of objects it's an array of strings. Also, if you add b values to the uniques and invert the order of a/b they are no longer considered unique by your function. (e.g. `"{\"a\":\"1\",\"b\":2}" != "{\"b\":2,\"a\":\"1\"}") Maybe im missing something, but shouldn't the result at least be useful? Here is a jsbin to illustrate http://jsbin.com/utoruz/2/edit – Shanimal Mar 16 '13 at 16:14
  • 1
    You're right the condition of having the same keys but in different order breaks the implementation. But I'm unsure why you're only checking the key `a` for each object, when there could be duplicate objects not containing the key `a`. However, it would make sense if `a` was a unique id. – Larry Battle Mar 17 '13 at 07:30
  • When I was answering the question it seemed to me that the focus of the question was to override `(a ==(=) b when a = b = {a:1})`. The point of my answer was the iterator. I tried to answer without worrying about the motive, which could be anything, right? (e.g. maybe they wanted to extract a list of makes, colors from a list of cars in a show. http://jsbin.com/evodub/2/edit) Cheers! – Shanimal Mar 17 '13 at 18:46
  • Also I think it helps us give concise answers when someone asking a question provides motive. This is a race, so I prefer to be first and clarify if necessary. Happy St. Patricks Day. – Shanimal Mar 17 '13 at 19:03
  • Well I just gave another upvote because this answered my question on comparing nested arrays. Was only looking for how to override the `iterator` – nevi_me Apr 12 '13 at 22:42
  • This actually returns `["{"a":"1"}", "{"b":"2"}"]`, the objects will be returned as strings. – RonnyKnoxville Aug 27 '15 at 08:48
  • Anyone who has issues with the array of strings can convert them back to objects using: .map( json_str => JSON.parse( json_str ) ) – Bruce Lim Jan 25 '17 at 09:08
15

When I have an attribute id, this is my preffered way in underscore:

var x = [{i:2}, {i:2, x:42}, {i:4}, {i:3}];
_.chain(x).indexBy("i").values().value();
// > [{i:2, x:42}, {i:4}, {i:3}]
tuxbear
  • 551
  • 4
  • 13
12

Using underscore unique lib following is working for me, I m making list unique on the based of _id then returning String value of _id:

var uniqueEntities = _.uniq(entities, function (item, key, a) {
                                    return item._id.toString();
                                });
Aqib Mumtaz
  • 4,936
  • 1
  • 36
  • 33
10

Here is a simple solution, which uses a deep object comparison to check for duplicates (without resorting to converting to JSON, which is inefficient and hacky)

var newArr = _.filter(oldArr, function (element, index) {
    // tests if the element has a duplicate in the rest of the array
    for(index += 1; index < oldArr.length; index += 1) {
        if (_.isEqual(element, oldArr[index])) {
            return false;
        }
    }
    return true;
});

It filters out all elements if they have a duplicate later in the array - such that the last duplicate element is kept.

The testing for a duplicate uses _.isEqual which performs an optimised deep comparison between the two objects see the underscore isEqual documentation for more info.

edit: updated to use _.filter which is a cleaner approach

Joshua Bambrick
  • 2,669
  • 5
  • 27
  • 35
10

The lodash 4.6.1 docs have this as an example for object key equality:

_.uniqWith(objects, _.isEqual);

https://lodash.com/docs#uniqWith

Ryan Quinn
  • 1,175
  • 1
  • 15
  • 28
7

Try iterator function

For example you can return first element

x = [['a',1],['b',2],['a',1]]

_.uniq(x,false,function(i){  

   return i[0]   //'a','b'

})

=> [['a',1],['b',2]]

IvanM
  • 2,913
  • 2
  • 30
  • 30
3

here's my solution (coffeescript) :

_.mixin
  deepUniq: (coll) ->
    result = []
    remove_first_el_duplicates = (coll2) ->

      rest = _.rest(coll2)
      first = _.first(coll2)
      result.push first
      equalsFirst = (el) -> _.isEqual(el,first)

      newColl = _.reject rest, equalsFirst

      unless _.isEmpty newColl
        remove_first_el_duplicates newColl

    remove_first_el_duplicates(coll)
    result

example:

_.deepUniq([ {a:1,b:12}, [ 2, 1, 2, 1 ], [ 1, 2, 1, 2 ],[ 2, 1, 2, 1 ], {a:1,b:12} ]) 
//=> [ { a: 1, b: 12 }, [ 2, 1, 2, 1 ], [ 1, 2, 1, 2 ] ]
szymanowski
  • 1,359
  • 1
  • 14
  • 25
3

with underscore i had to use String() in the iteratee function

function isUniq(item) {
    return String(item.user);
}
var myUniqArray = _.uniq(myArray, isUniq);
dam1
  • 3,536
  • 3
  • 22
  • 18
0

I wanted to solve this simple solution in a straightforward way of writing, with a little bit of a pain of computational expenses... but isn't it a trivial solution with a minimum variable definition, is it?

function uniq(ArrayObjects){
  var out = []
  ArrayObjects.map(obj => {
    if(_.every(out, outobj => !_.isEqual(obj, outobj))) out.push(obj)
  })
  return out
}
Junji Shimagaki
  • 286
  • 2
  • 9
0
var foo = [ { "a" : "1" }, { "b" : "2" }, { "a" : "1" } ];
var bar = _.map(_.groupBy(foo, function (f) { 
        return JSON.stringify(f); 
    }), function (gr) { 
        return gr[0]; 
    }
);

Lets break this down. First lets group the array items by their stringified value

var grouped = _.groupBy(foo, function (f) { 
    return JSON.stringify(f); 
});

grouped looks like:

{
    '{ "a" : "1" }' = [ { "a" : "1" } { "a" : "1" } ],
    '{ "b" : "2" }' = [ { "b" : "2" } ]
}

Then lets grab the first element from each group

var bar = _.map(grouped, function(gr)
    return gr[0]; 
});

bar looks like: [ { "a" : "1" }, { "b" : "2" } ]

Put it all together:

var foo = [ { "a" : "1" }, { "b" : "2" }, { "a" : "1" } ];
var bar = _.map(_.groupBy(foo, function (f) { 
        return JSON.stringify(f); 
    }), function (gr) { 
        return gr[0]; 
    }
);
-5

You can do it in a shorthand as:

_.uniq(foo, 'a')

nnattawat
  • 686
  • 5
  • 11