Simplest way to merge ES6 Maps/Sets?

JavascriptSetEcmascript 6

Javascript Problem Overview


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)?

Javascript Solutions


Solution 1 - Javascript

For sets:

var merged = new Set([...set1, ...set2, ...set3])

For maps:

var merged = new Map([...map1, ...map2, ...map3])

Note that if multiple maps have the same key, the value of the merged map will be the value of the last merging map with that key.

Solution 2 - Javascript

For reasons I do not understand, you cannot directly add the contents of one Set to another with a built-in method. 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.

[Added in 2021] - There is now a proposal to add new Set/Map methods for these types of operations, but the timing of implementation is not immediately clear. They appear to be in Stage 2 of the spec process.

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));

[Added in 2021]

Since there is now an official proposal for new Set methods, you could use this polyfill for the proposed .union() method that would work in ES6+ versions of ECMAScript. Note, per the spec, this returns a new Set that is the union of two other sets. It does not merge the contents of one set into another and this implements the type checking that is specified in the proposal.

if (!Set.prototype.union) {
    Set.prototype.union = function(iterable) {
        if (typeof this !== "object") {
            throw new TypeError("Must be of object type");
        }
        const Species = this.constructor[Symbol.species];
        const newSet = new Species(this);
        if (typeof newSet.add !== "function") {
            throw new TypeError("add method on new set species is not callable");
        }
        for (item of iterable) {
            newSet.add(item);
        }
        return newSet;
    }
}

Or, here's a more complete version that models the ECMAScript process for getting the species constructor more completely and has been adapted to run on older versions of Javascript that might not even have Symbol or have Symbol.species set:

if (!Set.prototype.union) {
    Set.prototype.union = function(iterable) {
        if (typeof this !== "object") {
            throw new TypeError("Must be of object type");
        }
        const Species = getSpeciesConstructor(this, Set);
        const newSet = new Species(this);
        if (typeof newSet.add !== "function") {
            throw new TypeError("add method on new set species is not callable");
        }
        for (item of iterable) {
            newSet.add(item);
        }
        return newSet;
    }
}

function isConstructor(C) {
    return typeof C === "function" && typeof C.prototype === "object";
}

function getSpeciesConstructor(obj, defaultConstructor) {
    const C = obj.constructor;
    if (!C) return defaultConstructor;
    if (typeof C !== "function") {
        throw new TypeError("constructor is not a function");
    }

    // use try/catch here to handle backward compatibility when Symbol does not exist
    let S;
    try {
        S = C[Symbol.species];
        if (!S) {
            // no S, so use C
            S = C;
        }
    } catch (e) {
        // No Symbol so use C
        S = C;
    }
    if (!isConstructor(S)) {
        throw new TypeError("constructor function is not a constructor");
    }
    return S;
}

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.

Solution 3 - Javascript

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' ]

Solution 4 - Javascript

> 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}


Original Answer

I would like to suggest another approach, using reduce and the spread operator:

Implementation
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}

Solution 5 - Javascript

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.

Set

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

Map

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]

Solution 6 - Javascript

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)));

Solution 7 - Javascript

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
}

Solution 8 - Javascript

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))
  }
}

Demo Snippet

// 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))
  }
}

const items1 = ['a', 'b', 'c']
const items2 = ['a', 'b', 'c', 'd']
const items3 = ['d', 'e']

let set

set = new Set(items1)
addAll(set, items2)
addAll(set, items3)
console.log('adding array to set', Array.from(set))

set = new Set(items1)
addAll(set, new Set(items2))
addAll(set, new Set(items3))
console.log('adding set to set', Array.from(set))

const map1 = [
  ['a', 1],
  ['b', 2],
  ['c', 3]
]
const map2 = [
  ['a', 1],
  ['b', 2],
  ['c', 3],
  ['d', 4]
]
const map3 = [
  ['d', 4],
  ['e', 5]
]

const map = new Map(map1)
addAll(map, new Map(map2))
addAll(map, new Map(map3))
console.log('adding map to map',
  'keys', Array.from(map.keys()),
  'values', Array.from(map.values()))

Solution 9 - Javascript

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;
};

Solution 10 - Javascript

Example
const mergedMaps = (...maps) => {
    const dataMap = new Map([])

    for (const map of maps) {
        for (const [key, value] of map) {
            dataMap.set(key, value)
        }
    }

    return dataMap
}
Usage
const map = mergedMaps(new Map([[1, false]]), new Map([['foo', 'bar']]), new Map([['lat', 1241.173512]]))
Array.from(map.keys()) // [1, 'foo', 'lat']

Solution 11 - Javascript

Transform the sets into arrays, flatten them and finally the constructor will uniqify.

const union = (...sets) => new Set(sets.map(s => [...s]).flat());

Solution 12 - Javascript

I've created a small snippet to merge any number of Sets using a function in ES6. You can change the "Set" to "Map" get it working with Maps.

const mergeSets = (...args) => {
    return new Set(args.reduce((acc, current) => {
        return [...acc, ...current];
    }, []));
};

const foo = new Set([1, 2, 3]);
const bar = new Set([1, 3, 4, 5]);

mergeSets(foo, bar); // Set(5) {1, 2, 3, 4, 5}
mergeSets(foo, bar, new Set([6])); // Set(6) {1, 2, 3, 4, 5, 6}

Solution 13 - Javascript

A good solution no matter if you have two or more maps to merge is to group them as an Array and use the following:

Array.prototype.merge = function () {
  return this.reduce((p, c) => Object.assign(c, p), {});
};

Solution 14 - Javascript

There are a couple of ways to do it. You can use Map.merge function:

let mergedMap = map1.merge(map2);

Note: If the keys of any of the Maps are the same, the value of the duplicate key in the last Map to be merged in will be used.

Please check here for more information: https://untangled.io/immutable-js-6-ways-to-merge-maps-with-full-live-examples/#:~:text=merge(),merged%20in%20will%20be%20used.

Solution 15 - Javascript

I created a helper method to merge maps and handle the values of duplicate keys in any pair-wise way desired:

const mergeMaps = (map1, map2, combineValuesOfDuplicateKeys) => {
  const mapCopy1 = new Map(map1);
  const mapCopy2 = new Map(map2);

  mapCopy1.forEach((value, key) => {
    if (!mapCopy2.has(key)) {
      mapCopy2.set(key, value);
    } else {
      const newValue = combineValuesOfDuplicateKeys
        ? combineValuesOfDuplicateKeys(value, mapCopy2.get(key))
        : mapCopy2.get(key);
      mapCopy2.set(key, newValue);
      mapCopy1.delete(key);
    }
  });

  return new Map([...mapCopy1, ...mapCopy2]);
};

const mergeMaps = (map1, map2, combineValuesOfDuplicateKeys) => {
  const mapCopy1 = new Map(map1);
  const mapCopy2 = new Map(map2);

  mapCopy1.forEach((value, key) => {
    if (!mapCopy2.has(key)) {
      mapCopy2.set(key, value);
    } else {
      const newValue = combineValuesOfDuplicateKeys
        ? combineValuesOfDuplicateKeys(value, mapCopy2.get(key))
        : mapCopy2.get(key);
      mapCopy2.set(key, newValue);
      mapCopy1.delete(key);
    }
  });

  return new Map([...mapCopy1, ...mapCopy2]);
};

const map1 = new Map([
  ["key1", 1],
  ["key2", 2]
]);

const map2 = new Map([
  ["key2", 3],
  ["key4", 4]
]);

const show = (object) => {
  return JSON.stringify(Array.from(object), null, 2)
}

document.getElementById("app").innerHTML = `
<h1>Maps are awesome!</h1>
<div>map1 = ${show(map1)}</div>
<div>map2 = ${show(map2)}</div><br>
<div>Set value of last duplicate key:<br>merged map = ${show(mergeMaps(map1, map2))}</div><br>
<div>Set value of pair-wise summated duplicate keys:<br>merged map = ${show(mergeMaps(map1, map2, (value1, value2) => value1 + value2))}</div><br>
<div>Set value of pair-wise difference of duplicate keys:<br>merged map = ${show(mergeMaps(map1, map2, (value1, value2) => value1 - value2))}</div><br>
<div>Set value of pair-wise multiplication of duplicate keys:<br>merged map = ${show(mergeMaps(map1, map2, (value1, value2) => value1 * value2))}</div><br>
<div>Set value of pair-wise quotient of duplicate keys:<br>merged map = ${show(mergeMaps(map1, map2, (value1, value2) => value1 / value2))}</div><br>
<div>Set value of pair-wise power of duplicate keys:<br>merged map = ${show(mergeMaps(map1, map2, (value1, value2) => Math.pow(value1, value2)))}</div><br>
`;

<!DOCTYPE html>
<html>

<head>
	<title>Parcel Sandbox</title>
	<meta charset="UTF-8" />
</head>

<body>
	<div id="app"></div>

	<script src="src/index.js">
	</script>
</body>

</html>

Solution 16 - Javascript

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}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionjameslkView Question on Stackoverflow
Solution 1 - JavascriptOriolView Answer on Stackoverflow
Solution 2 - Javascriptjfriend00View Answer on Stackoverflow
Solution 3 - JavascriptjameslkView Answer on Stackoverflow
Solution 4 - JavascriptAsaf KatzView Answer on Stackoverflow
Solution 5 - JavascriptfreganteView Answer on Stackoverflow
Solution 6 - Javascriptuser663031View Answer on Stackoverflow
Solution 7 - JavascriptndpView Answer on Stackoverflow
Solution 8 - JavascriptSteven SpunginView Answer on Stackoverflow
Solution 9 - JavascriptBergiView Answer on Stackoverflow
Solution 10 - JavascriptdimpiaxView Answer on Stackoverflow
Solution 11 - JavascriptNicoAdrianView Answer on Stackoverflow
Solution 12 - Javascriptddobby94View Answer on Stackoverflow
Solution 13 - JavascriptAmaroView Answer on Stackoverflow
Solution 14 - JavascriptOguzView Answer on Stackoverflow
Solution 15 - JavascriptmarvatronView Answer on Stackoverflow
Solution 16 - JavascriptNadiar SyaripulView Answer on Stackoverflow