Najprostszy sposób na połączenie map / zestawów ES6?

170

Czy istnieje prosty sposób na scalenie map ES6 razem (np. Object.assign)? A skoro już o tym mowa, co z zestawami ES6 (takimi jak Array.concat)?

jameslk
źródło
4
Ten wpis na blogu zawiera pewne informacje. 2ality.com/2015/01/es6-set-operations.html
lemieuxster
AFAIK dla mapy, której będziesz musiał użyć, for..ofponieważ keymoże być dowolnego typu
Paul S.

Odpowiedzi:

265

Do zestawów:

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

W przypadku map:

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

Zauważ, że jeśli wiele map ma ten sam klucz, wartość scalonej mapy będzie wartością ostatniej scalonej mapy z tym kluczem.

Oriol
źródło
4
Dokumentacja dotycząca Map: „Konstruktor: new Map([iterable])”, „ iterableto tablica lub inny iterowalny obiekt, którego elementy są parami klucz-wartość (tablice 2-elementowe). Każda para klucz-wartość jest dodawana do nowej mapy ”. - tylko jako odniesienie.
user4642212
34
W przypadku dużych zestawów należy po prostu ostrzec, że iteruje zawartość obu zestawów dwukrotnie, raz, aby utworzyć tymczasową tablicę zawierającą połączenie dwóch zestawów, a następnie przekazuje tę tymczasową tablicę do konstruktora Set, gdzie jest powtarzana ponownie, aby utworzyć nowy zestaw .
jfriend00
2
@ jfriend00: zobacz odpowiedź jameslk poniżej, aby poznać lepszą metodę
Bergi
2
@torazaburo: jak powiedział jfriend00, rozwiązanie Oriols tworzy niepotrzebne tablice pośrednie. Przekazanie iteratora do Mapkonstruktora pozwala uniknąć zużycia pamięci.
Bergi
2
Jestem zszokowany, że ES6 Set / Map nie zapewnia wydajnych metod scalania.
Andy
47

Oto moje rozwiązanie wykorzystujące generatory:

Mapy:

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

Zestawy:

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
źródło
35
(IIGFE = Wyrażenie funkcji generatora natychmiastowo wywołanego)
Oriol
3
miło również, m2.forEach((k,v)=>m1.set(k,v))jeśli chcesz mieć łatwą obsługę przeglądarki
caub
9
@caub ładne rozwiązanie, ale pamiętaj, że pierwszym parametrem forEach jest wartość, więc twoja funkcja powinna mieć wartość m2.forEach ((v, k) => m1.set (k, v));
David Noreña,
44

Z powodów, których nie rozumiem, nie można bezpośrednio dodawać zawartości jednego zestawu do drugiego za pomocą wbudowanej operacji. Operacje takie jak sumowanie, przecinanie, scalanie itp. Są dość podstawowymi operacjami na zbiorach, ale nie są wbudowane. Na szczęście możesz dość łatwo zbudować je samodzielnie.

Tak więc, aby zaimplementować operację scalania (scalanie zawartości jednego zestawu w inny lub jednej mapy w inną), możesz to zrobić za pomocą jednej .forEach()linii:

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

I na przykład Mapmożesz to zrobić:

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

Lub w składni ES6:

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

FYI, jeśli chcesz mieć prostą podklasę wbudowanego Setobiektu, który zawiera .merge()metodę, możesz użyć tego:

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

Ma to być w swoim własnym pliku setex.js, który możesz następnie require()umieścić w node.js i użyć zamiast wbudowanego zestawu.

jfriend00
źródło
3
Nie myślę new Set(s, t). Pracuje. tParametr jest ignorowany. Ponadto oczywiście nie jest rozsądnym zachowaniem addwykrywanie typu swojego parametru i dodanie do zestawu elementów zestawu, ponieważ wtedy nie byłoby sposobu, aby dodać sam zestaw do zestawu.
@torazaburo - co do .add()metody zrobienia zestawu, rozumiem twój punkt widzenia. Po prostu uważam, że jest to o wiele mniej przydatne niż możliwość łączenia zestawów za pomocą, .add()ponieważ nigdy nie potrzebowałem zestawu ani zestawów, ale wielokrotnie musiałem łączyć zestawy. Tylko kwestia opinii o przydatności jednego zachowania do drugiego.
jfriend00
Ach, nienawidzę tego, że to nie działa na mapach: n.forEach(m.add, m)- odwraca pary klucz / wartość!
Bergi
@Bergi - tak, to dziwne Map.prototype.forEach()i Map.prototype.set()mam odwrócone argumenty. Wygląda na to, że ktoś przeoczył. Wymusza więcej kodu, gdy próbuje się ich używać razem.
jfriend00
@ jfriend00: OTOH, to ma sens. setkolejność parametrów jest naturalna dla par klucz / wartość, forEachjest dopasowana do metody Arrays forEach(i rzeczy takie jak $.eachlub, _.eachktóre również wyliczają obiekty).
Bergi
20

Edycja :

Porównałem moje oryginalne rozwiązanie z innymi sugerowanymi tutaj rozwiązaniami i stwierdziłem, że jest bardzo nieefektywne.

Sam benchmark jest bardzo ciekawy ( link ) Porównuje 3 rozwiązania (wyższe tym lepsze):

  • Rozwiązanie @ bfred.it, które dodaje wartości pojedynczo (14955 op / s)
  • Rozwiązanie @ jameslk, które wykorzystuje samowywołujący się generator (5,089 op / s)
  • mój własny, który używa redukcji i rozrzutu (3,434 op / s)

Jak widać, rozwiązanie @ bfred.it jest zdecydowanie zwycięzcą.

Wydajność + niezmienność

Mając to na uwadze, oto nieco zmodyfikowana wersja, która nie modyfikuje oryginalnego zestawu i wyklucza zmienną liczbę elementów iteracyjnych, które można połączyć jako argumenty:

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

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

  return set;
}

Stosowanie:

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}

Oryginalna odpowiedź

Chciałbym zasugerować inne podejście, użycie reducei spreadoperatora:

Realizacja

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

Stosowanie:

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}

Wskazówka:

Możemy również skorzystać z restoperatora, aby uczynić interfejs nieco ładniejszym:

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

Teraz, zamiast przekazywać tablicę zbiorów, możemy przekazać dowolną liczbę argumentów zbiorów:

union(a, b, c) // {1, 2, 3, 4, 5, 6}
Asaf Katz
źródło
To jest strasznie nieefektywne.
Bergi
1
Cześć @Bergi, masz rację. Dziękuję za podniesienie mojej świadomości (: Przetestowałem moje rozwiązania z innymi sugerowanymi tutaj i udowodniłem to na sobie. Ponadto zredagowałem swoją odpowiedź, aby to odzwierciedlić. Rozważ usunięcie swojego głosu przeciw.
Asaf Katz
1
Świetnie, dzięki za porównanie wydajności. Zabawne, że „nieeleganckie” rozwiązanie jest najszybsze;) Przyszedłem tutaj, aby poszukać poprawy w stosunku do forofi add, które wydaje się po prostu bardzo nieefektywne. Naprawdę chciałbym mieć addAll(iterable)metodę na
setach
1
Wersja maszynopisu: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
4
Wydaje się, że link jsperf u góry Twojej odpowiedzi jest uszkodzony. Odnosisz się również do rozwiązania firmy bfred, którego nigdzie tu nie widzę.
jfriend00
12

Zatwierdzona odpowiedź jest świetna, ale za każdym razem tworzy nowy zestaw.

Jeśli chcesz mutować zamiast tego istniejący obiekt, użyj funkcji pomocniczej.

Zestaw

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

Stosowanie:

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

Stosowanie:

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
źródło
5

Aby scalić zestawy w zestawach szyku, możesz to zrobić

var Sets = [set1, set2, set3];

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

Jest dla mnie nieco tajemnicze, dlaczego poniższe, które powinny być równoważne, zawodzą przynajmniej w Babel:

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

źródło
Array.fromprzyjmuje dodatkowe parametry, z których drugi to funkcja mapująca. Array.prototype.mapprzekazuje trzy argumenty do swojego wywołania zwrotnego:, (value, index, array)więc skutecznie wywołuje Sets.map((set, index, array) => Array.from(set, index, array). Oczywiście indexjest liczbą, a nie funkcją mapującą, więc zawodzi.
nickf
1

Na podstawie odpowiedzi Asaf Katza, oto wersja maszynopisu:

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
źródło
1

Nie ma sensu wywoływać new Set(...anArrayOrSet)podczas dodawania wielu elementów (z tablicy lub innego zestawu) do istniejącego zestawu .

Używam tego w reducefunkcji i jest to po prostu głupie. Nawet jeśli masz ...arraydostępny operator rozproszenia, nie powinieneś go używać w tym przypadku, ponieważ powoduje to marnowanie zasobów procesora, pamięci i czasu.

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

Fragment demo

// 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
źródło
1

Przekształć zestawy w tablice, spłaszcz je, a na koniec konstruktor ujednolici.

const union = (...sets) => new Set(sets.map(s => [...s]).flat());
NicoAdrian
źródło
Nie wysyłaj tylko kodu jako odpowiedzi, ale także wyjaśnij, co robi twój kod i jak rozwiązuje problem. Odpowiedzi z wyjaśnieniem są zwykle wyższej jakości i częściej przyciągają głosy za.
Mark Rotteveel
0

Nie, nie ma dla nich wbudowanych operacji, ale możesz łatwo utworzyć własne:

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
źródło
2
Niech robi, co chce.
pishpish
1
Jeśli wszyscy zrobią to, co chcą, skończymy z kolejną porażką
dwelle
0

Przykład

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

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

    return dataMap
}

Stosowanie

const map = mergedMaps(new Map([[1, false]]), new Map([['foo', 'bar']]), new Map([['lat', 1241.173512]]))
Array.from(map.keys()) // [1, 'foo', 'lat']
dimpiax
źródło
0

Możesz użyć składni rozkładów, aby połączyć je ze sobą:

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
źródło
-1

połącz z Mapą

 let merge = {...map1,...map2};
Danilo Santos
źródło