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)