¿La forma más sencilla de combinar mapas / conjuntos ES6?

170

¿Hay una manera simple de fusionar ES6 Maps (como Object.assign)? Y mientras estamos en eso, ¿qué pasa con los conjuntos ES6 (como Array.concat)?

jameslk
fuente
44
Esta publicación de blog tiene alguna idea. 2ality.com/2015/01/es6-set-operations.html
lemieuxster
AFAIK para el Mapa que necesitarás usar for..ofporque keypuede ser de cualquier tipo
Paul S.

Respuestas:

265

Para conjuntos:

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

Para mapas:

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

Tenga en cuenta que si varios mapas tienen la misma clave, el valor del mapa fusionado será el valor del último mapa fusionado con esa clave.

Oriol
fuente
44
Documentación sobre Map: "Constructor: new Map([iterable])", " iterablees una matriz u otro objeto iterable cuyos elementos son pares clave-valor (matrices de 2 elementos). Cada par clave-valor se agrega al nuevo Mapa ". - Solo como referencia.
user4642212
34
Para conjuntos grandes, solo tenga en cuenta que esto itera el contenido de ambos conjuntos dos veces, una vez para crear una matriz temporal que contenga la unión de los dos conjuntos, luego pasa esa matriz temporal al constructor de conjuntos donde se repite para crear el nuevo conjunto .
jfriend00
2
@ jfriend00: vea la respuesta de jameslk a continuación para un mejor método
Bergi
2
@torazaburo: como dijo jfriend00, la solución de Oriols crea matrices intermedias innecesarias. Pasar un iterador al Mapconstructor evita su consumo de memoria.
Bergi
2
Me molesta que ES6 Set / Map no proporcione métodos de fusión eficientes.
Andy
47

Aquí está mi solución usando generadores:

Para mapas:

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

Para conjuntos:

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' ]
jameslk
fuente
35
(IIGFE = Expresión de función del generador invocada inmediatamente)
Oriol
3
bueno también m2.forEach((k,v)=>m1.set(k,v))si quieres un soporte de navegador fácil
caub
9
@caub buena solución, pero recuerde que el primer parámetro de forEach es el valor, por lo que su función debe ser m2.forEach ((v, k) => m1.set (k, v));
David Noreña
44

Por razones que no entiendo, no puede agregar directamente el contenido de un conjunto a otro con una operación integrada. Operaciones como unión, intersección, fusión, etc. son operaciones de conjunto bastante básicas, pero no están integradas. Afortunadamente, puede construir todo esto usted mismo con bastante facilidad.

Entonces, para implementar una operación de fusión (fusionar el contenido de un Conjunto en otro o un Mapa en otro), puede hacer esto con una sola .forEach()línea:

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

Y, por un lado Map, podrías hacer esto:

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

O, en la sintaxis de ES6:

t.forEach((value, key) => s.set(key, value));

Para su información, si desea una subclase simple del Setobjeto incorporado que contiene un .merge()método, puede usar esto:

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

Está destinado a estar en su propio archivo setex.js que luego puede require()ingresar a node.js y usar en lugar del conjunto incorporado.

jfriend00
fuente
3
No creo new Set(s, t). trabajos. El tparámetro se ignora. Además, obviamente no es un comportamiento razonable tener que adddetectar el tipo de su parámetro y si un conjunto agrega los elementos del conjunto, porque entonces no habría forma de agregar un conjunto a un conjunto.
@torazaburo: en cuanto al .add()método que toma un Set, entiendo tu punto. Simplemente encuentro que es mucho menos útil que poder combinar conjuntos usando .add()ya que nunca he necesitado un Conjunto o Conjuntos, pero he tenido la necesidad de fusionar conjuntos muchas veces. Solo es una cuestión de opinión sobre la utilidad de un comportamiento frente al otro.
jfriend00
Argh, odio que esto no funcione para los mapas: ¡ n.forEach(m.add, m)invierte pares clave / valor!
Bergi
@ Bergi: sí, es extraño eso Map.prototype.forEach()y Map.prototype.set()han invertido argumentos. Parece un descuido de alguien. Fuerza más código cuando intenta usarlos juntos.
jfriend00
@ jfriend00: OTOH, tiene sentido. setEl orden de los parámetros es natural para los pares clave / valor, forEachestá alineado con Arrayel forEachmétodo s (y cosas como $.eacho _.eachque también enumeran objetos).
Bergi
20

Editar :

Comparé mi solución original con otras soluciones sugeridas aquí y descubrí que es muy ineficiente.

El punto de referencia en sí es muy interesante ( enlace ) Compara 3 soluciones (más alto es mejor):

  • La solución de @ bfred.it, que agrega valores uno por uno (14,955 op / seg)
  • La solución de @jameslk, que utiliza un generador de auto invocación (5.089 op / seg)
  • el mío, que usa reducción y propagación (3,434 op / seg)

Como puede ver, la solución de @ bfred.it es definitivamente la ganadora.

Rendimiento + Inmutabilidad

Con eso en mente, aquí hay una versión ligeramente modificada que no muta el conjunto original y exceptúa un número variable de iterables para combinar como argumentos:

function union(...iterables) {
  const set = new Set();

  for (let iterable of iterables) {
    for (let item of iterable) {
      set.add(item);
    }
  }

  return set;
}

Uso:

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}

Respuesta original

Me gustaría sugerir otro enfoque, el uso reducey el spreadoperador:

Implementación

function union (sets) {
  return sets.reduce((combined, list) => {
    return new Set([...combined, ...list]);
  }, new Set());
}

Uso:

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}

Propina:

También podemos hacer uso del restoperador para que la interfaz sea un poco más agradable:

function union (...sets) {
  return sets.reduce((combined, list) => {
    return new Set([...combined, ...list]);
  }, new Set());
}

Ahora, en lugar de pasar una serie de conjuntos, podemos pasar un número arbitrario de argumentos de conjuntos:

union(a, b, c) // {1, 2, 3, 4, 5, 6}
Asaf Katz
fuente
Esto es terriblemente ineficiente.
Bergi
1
Hola @ Bergi, tienes razón. Gracias por aumentar mi conciencia (: he probado mis soluciones contra otras sugeridas aquí y lo probé por mí mismo. Además, he editado mi respuesta para reflejar eso. Por favor considere eliminar su voto negativo.
Asaf Katz
1
Genial, gracias por la comparación de rendimiento. Es curioso cómo la solución "poco elegante" es más rápida;) Vine aquí para buscar una mejora forofy add, lo que parece muy ineficiente. Realmente deseo un addAll(iterable)método en sets
Bruno Schäpper
1
Versión 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 }
mecanografiada
44
El enlace jsperf cerca de la parte superior de su respuesta parece estar roto. Además, se refiere a la solución de bfred que no veo en ningún lado aquí.
jfriend00
12

La respuesta aprobada es excelente, pero eso crea un nuevo conjunto cada vez.

Si en su lugar desea mutar un objeto existente, use una función auxiliar.

Conjunto

function concatSets(set, ...iterables) {
    for (const iterable of iterables) {
        for (const item of iterable) {
            set.add(item);
        }
    }
}

Uso:

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

Mapa

function concatMaps(map, ...iterables) {
    for (const iterable of iterables) {
        for (const item of iterable) {
            map.set(...item);
        }
    }
}

Uso:

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]
fregante
fuente
5

Para fusionar los conjuntos en la matriz Conjuntos, puede hacer

var Sets = [set1, set2, set3];

var merged = new Set([].concat(...Sets.map(set => Array.from(set))));

Es un poco misterioso para mí por qué lo siguiente, que debería ser equivalente, falla al menos en Babel:

var merged = new Set([].concat(...Sets.map(Array.from)));

fuente
Array.fromtoma parámetros adicionales, el segundo es una función de mapeo. Array.prototype.mappasa tres argumentos a su devolución de llamada: (value, index, array)por lo que está llamando efectivamente Sets.map((set, index, array) => Array.from(set, index, array). Obviamente, indexes un número y no una función de mapeo, por lo que falla.
nickf
1

Basado en la respuesta de Asaf Katz, aquí hay una versión mecanografiada:

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
}
ndp
fuente
1

No tiene ningún sentido llamar new Set(...anArrayOrSet)cuando se agregan múltiples elementos (desde una matriz u otro conjunto) a un conjunto existente .

Lo uso en una reducefunción, y es simplemente tonto. Incluso si tiene el ...arrayoperador de propagación disponible, no debe usarlo en este caso, ya que desperdicia recursos de procesador, memoria y tiempo.

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

Fragmento de demostración

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

Steven Spungin
fuente
1

Transforme los conjuntos en matrices, aplánelos y finalmente el constructor se unificará.

const union = (...sets) => new Set(sets.map(s => [...s]).flat());
NicoAdrian
fuente
No publique solo el código como respuesta, sino que también proporcione una explicación de lo que hace su código y cómo resuelve el problema de la pregunta. Las respuestas con una explicación son generalmente de mayor calidad y es más probable que atraigan votos positivos.
Mark Rotteveel
0

No, no hay operaciones integradas para estas, pero puede crearlas fácilmente:

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;
};
Bergi
fuente
2
Deja que haga lo que quiera.
pishpish
1
Si todos hacen lo que quieren, terminaremos con otra debacle
quédate
0

Ejemplo

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

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

    return dataMap
}

Uso

const map = mergedMaps(new Map([[1, false]]), new Map([['foo', 'bar']]), new Map([['lat', 1241.173512]]))
Array.from(map.keys()) // [1, 'foo', 'lat']
dimpiax
fuente
0

Puede usar la sintaxis de propagación para fusionarlos:

const map1 = {a: 1, b: 2}
const map2 = {b: 1, c: 2, a: 5}

const mergedMap = {...a, ...b}

=> {a: 5, b: 1, c: 2}
Nadiar AS
fuente
-1

fusionar al mapa

 let merge = {...map1,...map2};
Danilo Santos
fuente