3

Is it possible to use [...new Set()] to return an array of unique objects based on the inner id value? If this isn't possible, is there any other clever ES6 ways to achieve this output?

Reference: Unique Values in an Array

var arr = [
  {email: 'matthew@gmail.com', id: 10}
]

var arr2 = [
  {email: 'matthew@gmail.com', id: 10},
  {email: 'matthew@gmail.com', id: 13}
]
mergedArray = arr.concat(arr2);

console.log(
  [...new Set(mergedArray)]
);

// output would be:
//  [
//    {email:'matthew@gmail.com', id: 10},
//    {email:'matthew@gmail.com', id: 13}
//  ]
Community
  • 1
  • 1
Armeen Moon
  • 18,061
  • 35
  • 120
  • 233

4 Answers4

3

To get the unique objects based on ID, you could create a Map instead of a Set, pass it a 2-element Array as iterator, and it will have unique keys, and then get it's values

var arr = [
  {email: 'matthew@gmail.com', id: 10}
]

var arr2 = [
  {email: 'matthew@gmail.com', id: 10},
  {email: 'matthew@gmail.com', id: 13}
]

var mergedArray = arr.concat(arr2);
var map         = new Map(mergedArray.map(o => [o.id,o]));
var unique      = [...map.values()];

console.log(unique);
adeneo
  • 312,895
  • 29
  • 395
  • 388
  • @adeneo this is a fine succinct solution, but it actually iterates thru the inputs twice as much as is necessary – once to create the map, and then once to expand into the output array. I only remark about this because you seemed to believe my code does "a lot of iteration" – Mulan Nov 08 '16 at 21:03
  • @naomik - I'm not used to functional programming, so it took me about ten times reading it before I got closed to understanding it. You're creating a map of the unique ID's from the first array, and then you're using that map to filter the second array, and then you join the arrays when the second array is filtered. And you're right, there's nothing really clever about it, it's just written in an unbelievably complicated way, at least for me who isn't used to reading code like that. And sure, the spread above iterates a second time, it's not a problem, spreads are fast in most engines. – adeneo Nov 08 '16 at 21:30
  • FYI, if the first array had multiple objects with the same ID, your answer wouldn't catch it, not that it's an issue, as the OP didn't post data like that anyway, just that it doesn't neccessarely return *"unique objects based on the inner id"* in all cases -> https://jsfiddle.net/6oy2z676/ – adeneo Nov 08 '16 at 21:32
  • @adeneo it is by design that my procedure makes no assumption about the initial input. It is not a commutative procedure – `unionBy (f) (x) (y)` will return a different result than `unionBy (f) (y) (x)`. If you need have potential duplicates in both (or more) of the inputs, `unionByAll (f) ([], x, y)` will give you the desired result. I will make a note in my answer tho. Thanks for calling it to my attention. – Mulan Nov 08 '16 at 23:04
3

Note: this is the fastest solution so far, see test case on jsperf.com.

I think the best solution would be to create a map object with ids as keys, and array elements as values. Since it's not possible to have two different elements with the same key in an object, duplicate elements would be automatically removed. You could then convert the map object back to array using the Object.values() function (note that this is a part of ES 2017, not ES 6).

const arr = [
  { email: 'matthew@gmail.com', id: 10 },
];

const arr2 = [
  { email: 'matthew@gmail.com', id: 10 },
  { email: 'matthew@gmail.com', id: 13 },
];
const mergedArray = [...arr, ...arr2];

const map = {};
for (const element of mergedArray) {
  map[element.id] = element;
}
const newArray = Object.values(map);
console.log(newArray);

Also, instead of doing arr.concat(arr2) you can use the spread operator: [...arr, ...arr2]. IMO it's more readable.

Michał Perłakowski
  • 88,409
  • 26
  • 156
  • 177
1

Since this is tagged with functional programming I'm going to offer a more functional approach that uses generic procedures

My implementation of unionBy uses an underlying Set but this implementation detail is not leaked to the functions that use it. Instead, the filtering predicate control is inverted by being passed to your "unioning" procedure as a higher-order function. This is important because the user of your function should not care that a Set is being used. Perhaps the Set is most ideal, but in other situations it might not be. Either way, it's best if the user of the unionBy procedure declares what the grouping value is, not anything else.

To see what I mean, let's first look at unionById – it accepts two arrays of Object type, and returns an array of Object type.

// unionById :: [Object] -> [Object] -> [Object]
const unionById = unionBy (p=> x=> p (x.id));

Here, the first argument to unionBy is a user-defined procedure, but the first parameter of your procedure will itself expect another procedure, p in this case. The second parameter of your procedure, x in this case, will be the individual elements being examined. You simply pass whatever the "grouping" value is back to p. Since we want to group by each x's id field, we simply call p(x.id)


Here's a runnable code snippet

// id :: a -> a
const id = x=> x

// unionBy :: ((a -> b) -> a -> b) -> [c] -> [c] -> [c] 
const unionBy = p=> xs=> ys=> {
  let s = new Set (xs.map (p (id)));
  let zs = ys.filter (p (z => s.has (z) ? false : s.add (z)));
  return xs.concat (zs);
};

// unionById :: [Object] -> [Object] -> [Object]
const unionById = unionBy (p=> x=> p (x.id));

// your data
var arr = [
  {email: 'matthew@gmail.com', id: 10}
]

var arr2 = [
  {email: 'matthew@gmail.com', id: 10},
  {email: 'matthew@gmail.com', id: 13}
]

// check it out
console.log(unionById(arr)(arr2))

This might be most clear if you look at the very generic union

// apply :: (a -> b) -> a -> b
const apply = f=> x=> f(x)

// union :: [a] -> [a] -> [a]
const union = unionBy (apply);

union ([1,2,3]) ([2,3,4]);
// => [ 1, 2, 3, 4 ]

unionBy is powerful because it makes no assumptions about what the inputs will be. Before we were using it on an array of Objects – Here you'll see it working on an array of Strings.

let xs = ['A', 'B', 'C'];
let ys = ['a', 'b', 'x', 'y'];
unionBy (p=> x=> p(x.toLowerCase())) (xs) (ys);
// => [ 'A', 'B', 'C', 'x', 'y']

You'll probably want unionByAll which accepts 2 or more input arrays

// uncurry :: (a -> b -> c) -> (a,b) -> c  
const uncurry = f=> (x,y)=> f (x) (y);

// unionByAll :: ((a -> b) -> a -> b) -> [[c]] -> [c]
const unionByAll = p=> (x,...xs)=> {
  return xs.reduce (uncurry (unionBy (p)), x);
};

unionByAll (apply) ([1,2,3], [2,3,4], [3,4,5]);
// => [ 1, 2, 3, 4, 5 ]

Care: @adeneo points out that unionBy makes no assumption about the initial input. It is by design that unionBy does not check for duplicates in the initial input – it is not a commutative procedure.

// "duplicates" in xs will not be removed
unionBy (f) (xs) (ys)

// "duplicates" in ys will not be removed
unionBy (f) (ys) (xs)

If you have potential duplicates in one or more of your inputs, utilizing unionByAll will help you

// "duplicates" in both xs and ys will now be removed
unionByAll (f) ([], xs, ys)
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    Could you add some explanation? I find it hard to understand. – Michał Perłakowski Nov 08 '16 at 20:30
  • I'm going to need time to digest these answers. This answer does look like the most robust! – Armeen Moon Nov 08 '16 at 20:36
  • @MatthewHarwood take your time. I made an update demonstrating how `unionBy` makes no assumption about what the inputs will be. Let me know if you have any questions. – Mulan Nov 08 '16 at 20:45
  • 1
    It seems like this solution is the slowest of all the answers here, and it's over 10 times slower than my solution (at least in Chrome). See [test case on jsperf.com](https://jsperf.com/use-new-set-to-get). – Michał Perłakowski Nov 08 '16 at 20:45
  • It's also, by far, the least readable. Just looking at it, it looks like Klingon. I like it though, it's actually clever as it accepts just about anything, but it does a lot of iteration and uses a lot of built in methods, many, many times, which is why it's slower. – adeneo Nov 08 '16 at 20:49
  • @Gothdo, speed is not the only metric used to measure the quality of code. This question was tagged functional programming and I've provided an answer in that spectrum. If I was solely concerned with speed, I might just use a `while` loop, some mutations, and maybe not even use a `Set`. – Mulan Nov 08 '16 at 20:50
  • 1
    @adaneo, strange, I find almost nothing clever about it. It iterates through each input array *once* – or in other words, each value from each array is only touched a single time. – Mulan Nov 08 '16 at 20:52
  • @naomik Yes, and I think it would be fine if your code was 1.5, or even 2 times slower, but 10 times is a significant difference. Even if it's better in some other ways, I don't think it's better overall. – Michał Perłakowski Nov 08 '16 at 20:55
  • @adeneo if you're having trouble reading it, I can understand it takes time getting used to writing higher-order procedures. – Mulan Nov 08 '16 at 20:58
  • 1
    @Gothdo the performance doesn't concern me at all. If something takes 1 thousandth of a millisecond and another takes 1 hundredth of a millisecond, I just don't care. If this is called in a tight loop and getting repeated millions of times where it might start to make a difference, then sure, it could benefit from a refactor. – Mulan Nov 08 '16 at 21:00
  • 1
    @naomik It kind of bother me that the predicate have to be applied to both arrays. I guess that's the only way to implement `groupBy`. The generalization comes at a price. Anyway very nice solution: +1. –  Nov 08 '16 at 22:04
  • @ftor can you think of another way to initialize the Set with the first input, `xs` ? – Mulan Nov 08 '16 at 22:22
  • @naomik, you are right, I shouldn't have removed the `functional-programming` tag. Thank your very much for the correction! –  Nov 08 '16 at 22:57
0

Here would be my approach, not to use Set but a Map:

const uniques = mergedArray
  .reduce((map, item) => map.set(item.id, item), new Map())
  .values();

This gives you an iterable, which may or may not work for what you need.

Jacob
  • 77,566
  • 24
  • 149
  • 228
  • `Map.prototype.values()` returns an iterator, not an array. – Michał Perłakowski Nov 08 '16 at 20:29
  • Yep, I pointed that out. I wouldn't convert to an `Array` unless it was required. – Jacob Nov 08 '16 at 20:31
  • 1
    The question asks specifically about arrays ("Is it possible to use `[...new Set()]` to return an array"). In most cases it's necessary to convert an iterator to an array, because otherwise you can't use array methods on it. – Michał Perłakowski Nov 08 '16 at 20:35
  • To convert an iterator to a JavaScript `Array`, you can use the spread operator: `const uniquesArray = [...uniques]`. Sometimes someone says "array" thinking that's the only option for a collection or using it in a generic sense, not meaning `Array` specifically. – Jacob Nov 09 '16 at 22:45