Is there a simple way to merge ES6 Maps together (like Object.assign
)? And while we're at it, what about ES6 Sets (like Array.concat
)?
This question is related to
javascript
set
ecmascript-6
const mergedMaps = (...maps) => {
const dataMap = new Map([])
for (const map of maps) {
for (const [key, value] of map) {
dataMap.set(key, value)
}
}
return dataMap
}
const map = mergedMaps(new Map([[1, false]]), new Map([['foo', 'bar']]), new Map([['lat', 1241.173512]]))
Array.from(map.keys()) // [1, 'foo', 'lat']
No, there are no builtin operations for these, but you can easily create them your own:
Map.prototype.assign = function(...maps) {
for (const m of maps)
for (const kv of m)
this.add(...kv);
return this;
};
Set.prototype.concat = function(...sets) {
const c = this.constructor;
let res = new (c[Symbol.species] || c)();
for (const set of [this, ...sets])
for (const v of set)
res.add(v);
return res;
};
Based off of Asaf Katz's answer, here's a typescript version:
export function union<T> (...iterables: Array<Set<T>>): Set<T> {
const set = new Set<T>()
iterables.forEach(iterable => {
iterable.forEach(item => set.add(item))
})
return set
}
The approved answer is great but that creates a new set every time.
If you want to mutate an existing object instead, use a helper function.
function concatSets(set, ...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
set.add(item);
}
}
}
Usage:
const setA = new Set([1, 2, 3]);
const setB = new Set([4, 5, 6]);
const setC = new Set([7, 8, 9]);
concatSets(setA, setB, setC);
// setA will have items 1, 2, 3, 4, 5, 6, 7, 8, 9
function concatMaps(map, ...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
map.set(...item);
}
}
}
Usage:
const mapA = new Map().set('S', 1).set('P', 2);
const mapB = new Map().set('Q', 3).set('R', 4);
concatMaps(mapA, mapB);
// mapA will have items ['S', 1], ['P', 2], ['Q', 3], ['R', 4]
You can use the spread syntax to merge them together:
const map1 = {a: 1, b: 2}
const map2 = {b: 1, c: 2, a: 5}
const mergedMap = {...a, ...b}
=> {a: 5, b: 1, c: 2}
Edit:
I benchmarked my original solution against other solutions suggests here and found that it is very inefficient.
The benchmark itself is very interesting (link) It compares 3 solutions (higher is better):
- @fregante (formerly called @bfred.it) solution, which adds values one by one (14,955 op/sec)
- @jameslk's solution, which uses a self invoking generator (5,089 op/sec)
- my own, which uses reduce & spread (3,434 op/sec)
As you can see, @fregante's solution is definitely the winner.
Performance + Immutability
With that in mind, here's a slightly modified version which doesn't mutates the original set and excepts a variable number of iterables to combine as arguments:
function union(...iterables) { const set = new Set(); for (const iterable of iterables) { for (const item of iterable) { set.add(item); } } return set; }
Usage:
const a = new Set([1, 2, 3]); const b = new Set([1, 3, 5]); const c = new Set([4, 5, 6]); union(a,b,c) // {1, 2, 3, 4, 5, 6}
I would like to suggest another approach, using reduce
and the spread
operator:
function union (sets) {
return sets.reduce((combined, list) => {
return new Set([...combined, ...list]);
}, new Set());
}
Usage:
const a = new Set([1, 2, 3]);
const b = new Set([1, 3, 5]);
const c = new Set([4, 5, 6]);
union([a, b, c]) // {1, 2, 3, 4, 5, 6}
Tip:
We can also make use of the rest
operator to make the interface a bit nicer:
function union (...sets) {
return sets.reduce((combined, list) => {
return new Set([...combined, ...list]);
}, new Set());
}
Now, instead of passing an array of sets, we can pass an arbitrary number of arguments of sets:
union(a, b, c) // {1, 2, 3, 4, 5, 6}
merge to Map
let merge = {...map1,...map2};
For reasons I do not understand, you cannot directly add the contents of one Set to another with a built-in operation. Operations like union, intersect, merge, etc... are pretty basic set operations, but are not built-in. Fortunately, you can construct these all yourself fairly easily.
So, to implement a merge operation (merging the contents of one Set into another or one Map into another), you can do this with a single .forEach()
line:
var s = new Set([1,2,3]);
var t = new Set([4,5,6]);
t.forEach(s.add, s);
console.log(s); // 1,2,3,4,5,6
And, for a Map
, you could do this:
var s = new Map([["key1", 1], ["key2", 2]]);
var t = new Map([["key3", 3], ["key4", 4]]);
t.forEach(function(value, key) {
s.set(key, value);
});
Or, in ES6 syntax:
t.forEach((value, key) => s.set(key, value));
FYI, if you want a simple subclass of the built-in Set
object that contains a .merge()
method, you can use this:
// subclass of Set that adds new methods
// Except where otherwise noted, arguments to methods
// can be a Set, anything derived from it or an Array
// Any method that returns a new Set returns whatever class the this object is
// allowing SetEx to be subclassed and these methods will return that subclass
// For this to work properly, subclasses must not change behavior of SetEx methods
//
// Note that if the contructor for SetEx is passed one or more iterables,
// it will iterate them and add the individual elements of those iterables to the Set
// If you want a Set itself added to the Set, then use the .add() method
// which remains unchanged from the original Set object. This way you have
// a choice about how you want to add things and can do it either way.
class SetEx extends Set {
// create a new SetEx populated with the contents of one or more iterables
constructor(...iterables) {
super();
this.merge(...iterables);
}
// merge the items from one or more iterables into this set
merge(...iterables) {
for (let iterable of iterables) {
for (let item of iterable) {
this.add(item);
}
}
return this;
}
// return new SetEx object that is union of all sets passed in with the current set
union(...sets) {
let newSet = new this.constructor(...sets);
newSet.merge(this);
return newSet;
}
// return a new SetEx that contains the items that are in both sets
intersect(target) {
let newSet = new this.constructor();
for (let item of this) {
if (target.has(item)) {
newSet.add(item);
}
}
return newSet;
}
// return a new SetEx that contains the items that are in this set, but not in target
// target must be a Set (or something that supports .has(item) such as a Map)
diff(target) {
let newSet = new this.constructor();
for (let item of this) {
if (!target.has(item)) {
newSet.add(item);
}
}
return newSet;
}
// target can be either a Set or an Array
// return boolean which indicates if target set contains exactly same elements as this
// target elements are iterated and checked for this.has(item)
sameItems(target) {
let tsize;
if ("size" in target) {
tsize = target.size;
} else if ("length" in target) {
tsize = target.length;
} else {
throw new TypeError("target must be an iterable like a Set with .size or .length");
}
if (tsize !== this.size) {
return false;
}
for (let item of target) {
if (!this.has(item)) {
return false;
}
}
return true;
}
}
module.exports = SetEx;
This is meant to be in it's own file setex.js that you can then require()
into node.js and use in place of the built-in Set.
To merge the sets in the array Sets, you can do
var Sets = [set1, set2, set3];
var merged = new Set([].concat(...Sets.map(set => Array.from(set))));
It is slightly mysterious to me why the following, which should be equivalent, fails at least in Babel:
var merged = new Set([].concat(...Sets.map(Array.from)));
It does not make any sense to call new Set(...anArrayOrSet)
when adding multiple elements (from either an array or another set) to an existing set.
I use this in a reduce
function, and it is just plain silly. Even if you have the ...array
spread operator available, you should not use it in this case, as it wastes processor, memory, and time resources.
// Add any Map or Set to another
function addAll(target, source) {
if (target instanceof Map) {
Array.from(source.entries()).forEach(it => target.set(it[0], it[1]))
} else if (target instanceof Set) {
source.forEach(it => target.add(it))
}
}
// Add any Map or Set to another_x000D_
function addAll(target, source) {_x000D_
if (target instanceof Map) {_x000D_
Array.from(source.entries()).forEach(it => target.set(it[0], it[1]))_x000D_
} else if (target instanceof Set) {_x000D_
source.forEach(it => target.add(it))_x000D_
}_x000D_
}_x000D_
_x000D_
const items1 = ['a', 'b', 'c']_x000D_
const items2 = ['a', 'b', 'c', 'd']_x000D_
const items3 = ['d', 'e']_x000D_
_x000D_
let set_x000D_
_x000D_
set = new Set(items1)_x000D_
addAll(set, items2)_x000D_
addAll(set, items3)_x000D_
console.log('adding array to set', Array.from(set))_x000D_
_x000D_
set = new Set(items1)_x000D_
addAll(set, new Set(items2))_x000D_
addAll(set, new Set(items3))_x000D_
console.log('adding set to set', Array.from(set))_x000D_
_x000D_
const map1 = [_x000D_
['a', 1],_x000D_
['b', 2],_x000D_
['c', 3]_x000D_
]_x000D_
const map2 = [_x000D_
['a', 1],_x000D_
['b', 2],_x000D_
['c', 3],_x000D_
['d', 4]_x000D_
]_x000D_
const map3 = [_x000D_
['d', 4],_x000D_
['e', 5]_x000D_
]_x000D_
_x000D_
const map = new Map(map1)_x000D_
addAll(map, new Map(map2))_x000D_
addAll(map, new Map(map3))_x000D_
console.log('adding map to map',_x000D_
'keys', Array.from(map.keys()),_x000D_
'values', Array.from(map.values()))
_x000D_
Here's my solution using generators:
For Maps:
let map1 = new Map(), map2 = new Map();
map1.set('a', 'foo');
map1.set('b', 'bar');
map2.set('b', 'baz');
map2.set('c', 'bazz');
let map3 = new Map(function*() { yield* map1; yield* map2; }());
console.log(Array.from(map3)); // Result: [ [ 'a', 'foo' ], [ 'b', 'baz' ], [ 'c', 'bazz' ] ]
For Sets:
let set1 = new Set(['foo', 'bar']), set2 = new Set(['bar', 'baz']);
let set3 = new Set(function*() { yield* set1; yield* set2; }());
console.log(Array.from(set3)); // Result: [ 'foo', 'bar', 'baz' ]
Transform the sets into arrays, flatten them and finally the constructor will uniqify.
const union = (...sets) => new Set(sets.map(s => [...s]).flat());
Source: Stackoverflow.com