comparar conjuntos ECMA6 para igualdade

102

Como você compara dois conjuntos de javascript? Tentei usar ==e ===mas ambos retornam falso.

a = new Set([1,2,3]);
b = new Set([1,3,2]);
a == b; //=> false
a === b; //=> false

Esses dois conjuntos são equivalentes, porque por definição, os conjuntos não têm ordem (pelo menos não normalmente). Eu olhei a documentação para Set on MDN e não encontrei nada útil. Alguém sabe como fazer isto?

williamcodes
fonte
Dois conjuntos são dois objetos diferentes. ===é para igualdade de valor, não igualdade de objeto.
elclanrs
3
iterar e comparar o valor de cada membro, se todos iguais, definir é "o mesmo"
dandavis
1
@dandavis Com conjuntos, os membros são os valores.
2
Sets e Maps têm um pedido, que é o pedido de inserção - por qualquer motivo: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
CodeManX
8
Pior ainda new Set([1,2,3]) != new Set([1,2,3]). Isso torna o conjunto de Javascript inútil para conjuntos de conjuntos porque o superconjunto conterá subconjuntos duplicados. A única solução alternativa que vem à mente é converter todos os subconjuntos em arrays, classificando cada array e, em seguida, codificando cada array como string (por exemplo JSON).
7vujy0f0hy

Respostas:

73

Experimente isto:

var a = new Set([1,2,3]);
var b = new Set([1,3,2]);

alert(eqSet(a, b)); // true

function eqSet(as, bs) {
    if (as.size !== bs.size) return false;
    for (var a of as) if (!bs.has(a)) return false;
    return true;
}

Uma abordagem mais funcional seria:

var a = new Set([1,2,3]);
var b = new Set([1,3,2]);

alert(eqSet(a, b)); // true

function eqSet(as, bs) {
    return as.size === bs.size && all(isIn(bs), as);
}

function all(pred, as) {
    for (var a of as) if (!pred(a)) return false;
    return true;
}

function isIn(as) {
    return function (a) {
        return as.has(a);
    };
}

A allfunção funciona para todos os objetos iteráveis ​​(por exemplo, Sete Map).

Se Array.fromfosse mais amplamente suportado, poderíamos ter implementado a allfunção como:

function all(pred, as) {
    return Array.from(as).every(pred);
}

Espero que ajude.

Aadit M Shah
fonte
2
Acho que você deve alterar o nome de haspara isPartOfou isInouelem
Bergi 01 de
@Bergi mudei o nome da função haspara isIn.
Aadit M Shah
1
@DavidGiven Sim, os conjuntos em JavaScript são iterados na ordem de inserção: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Aadit M Shah
48
A cada dia tenho mais certeza de que o JavaScript é a linguagem mais ruim já criada. Todo mundo tem que inventar suas próprias funções básicas para lidar com sua limitação, e isso está no ES6, e estamos em 2017! Por que eles não poderiam adicionar funções tão frequentes à especificação do objeto Set!
Ghasan
2
@ GhasanAl-Sakkaf, concordo, talvez o TC39 consista em cientistas, mas não pragmáticos ...
Marecky
59

Você também pode tentar:

var a = new Set([1,2,3]);
var b = new Set([1,3,2]);

isSetsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value));

console.log(isSetsEqual(a,b)) 

Max Leizerovich
fonte
Definitivamente, uma solução melhor, pois se encaixa dentro de uma condição if
Hugodby
Eu amo como essa solução é idiomática e muito legível! Obrigado @Max
daydreamer
29

Lodash fornece _.isEqual(), que faz comparações profundas. Isso é muito útil se você não quiser escrever o seu próprio. A partir do lodash 4, _.isEqual()compara corretamente os Conjuntos.

const _ = require("lodash");

let s1 = new Set([1,2,3]);
let s2 = new Set([1,2,3]);
let s3 = new Set([2,3,4]);

console.log(_.isEqual(s1, s2)); // true
console.log(_.isEqual(s1, s3)); // false
antinissério
fonte
7

A outra resposta funcionará bem; aqui está outra alternativa.

// Create function to check if an element is in a specified set.
function isIn(s)          { return elt => s.has(elt); }

// Check if one set contains another (all members of s2 are in s1).
function contains(s1, s2) { return [...s2] . every(isIn(s1)); }

// Set equality: a contains b, and b contains a
function eqSet(a, b)      { return contains(a, b) && contains(b, a); }

// Alternative, check size first
function eqSet(a, b)      { return a.size === b.size && contains(a, b); }

No entanto, esteja ciente de que isso não faz uma comparação de igualdade profunda. assim

eqSet(Set([{ a: 1 }], Set([{ a: 1 }])

retornará falso. Se os dois conjuntos acima forem considerados iguais, precisamos iterar por ambos os conjuntos, fazendo comparações profundas de qualidade em cada elemento. Estipulamos a existência de uma deepEqualrotina. Então a lógica seria

// Find a member in "s" deeply equal to some value
function findDeepEqual(s, v) { return [...s] . find(m => deepEqual(v, m)); }

// See if sets s1 and s1 are deeply equal. DESTROYS s2.
function eqSetDeep(s1, s2) {
  return [...s1] . every(a1 => {
    var m1 = findDeepEqual(s2, a1);
    if (m1) { s2.delete(m1); return true; }
  }) && !s2.size;
}

O que isso faz: para cada membro de s1, procure um membro profundamente igual de s2. Se encontrado, exclua-o para que não possa ser usado novamente. Os dois conjuntos são profundamente iguais se todos os elementos em s1 forem encontrados em s2, e s2 se esgotar. Não testado.

Você pode achar isso útil: http://www.2ality.com/2015/01/es6-set-operations.html .


fonte
6

Nenhuma dessas soluções traz “de volta” a funcionalidade esperada para uma estrutura de dados, como um conjunto de conjuntos. Em seu estado atual, o conjunto de Javascript é inútil para esse propósito porque o superconjunto conterá subconjuntos duplicados, que o Javascript erroneamente considera distintos. A única solução que consigo pensar é converter cada subconjunto em Array , classificá-lo e, em seguida, codificar como String (por exemplo JSON).

Solução

var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var fromJsonSet = jset => new Set(JSON.parse(jset));

Uso básico

var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var fromJsonSet = jset => new Set(JSON.parse(jset));

var [s1,s2] = [new Set([1,2,3]), new Set([3,2,1])];
var [js1,js2] = [toJsonSet([1,2,3]), toJsonSet([3,2,1])]; // even better

var r = document.querySelectorAll("td:nth-child(2)");
r[0].innerHTML = (toJsonSet(s1) === toJsonSet(s2)); // true
r[1].innerHTML = (toJsonSet(s1) == toJsonSet(s2)); // true, too
r[2].innerHTML = (js1 === js2); // true
r[3].innerHTML = (js1 == js2); // true, too

// Make it normal Set:
console.log(fromJsonSet(js1), fromJsonSet(js2)); // type is Set
<style>td:nth-child(2) {color: red;}</style>

<table>
<tr><td>toJsonSet(s1) === toJsonSet(s2)</td><td>...</td></tr>
<tr><td>toJsonSet(s1) == toJsonSet(s2)</td><td>...</td></tr>
<tr><td>js1 === js2</td><td>...</td></tr>
<tr><td>js1 == js2</td><td>...</td></tr>
</table>

Teste final: conjunto de conjuntos

var toSet = arr => new Set(arr);
var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var toJsonSet_WRONG = set => JSON.stringify([...set]); // no sorting!

var output = document.getElementsByTagName("code"); 
var superarray = [[1,2,3],[1,2,3],[3,2,1],[3,6,2],[4,5,6]];
var superset;

Experiment1:
    superset = toSet(superarray.map(toSet));
    output[0].innerHTML = superset.size; // incorrect: 5 unique subsets
Experiment2:
    superset = toSet([...superset].map(toJsonSet_WRONG));
    output[1].innerHTML = superset.size; // incorrect: 4 unique subsets
Experiment3:
    superset = toSet([...superset].map(toJsonSet));
    output[2].innerHTML = superset.size; // 3 unique subsets
Experiment4:
    superset = toSet(superarray.map(toJsonSet));
    output[3].innerHTML = superset.size; // 3 unique subsets
code {border: 1px solid #88f; background-color: #ddf; padding: 0 0.5em;}
<h3>Experiment 1</h3><p>Superset contains 3 unique subsets but Javascript sees <code>...</code>.<br>Let’s fix this... I’ll encode each subset as a string.</p>
<h3>Experiment 2</h3><p>Now Javascript sees <code>...</code> unique subsets.<br>Better! But still not perfect.<br>That’s because we didn’t sort each subset.<br>Let’s sort it out...</p>
<h3>Experiment 3</h3><p>Now Javascript sees <code>...</code> unique subsets. At long last!<br>Let’s try everything again from the beginning.</p>
<h3>Experiment 4</h3><p>Superset contains 3 unique subsets and Javascript sees <code>...</code>.<br><b>Bravo!</b></p>

7vujy0f0hy
fonte
1
Ótima solução! E se você sabe que acabou de obter um conjunto de strings ou números, ele se torna[...set1].sort().toString() === [...set2].sort().toString()
3

A razão pela qual sua abordagem retorna falso é porque você está comparando dois objetos diferentes (mesmo se eles tiverem o mesmo conteúdo), portanto, comparar dois objetos diferentes (não referências, mas objetos) sempre retorna falso.

A abordagem a seguir mescla dois conjuntos em um e simplesmente compara o tamanho. Se for o mesmo, é o mesmo:

const a1 = [1,2,3];
const a2 = [1,3,2];
const set1 = new Set(a1);
const set2 = new Set(a2);

const compareSet = new Set([...a1, ...a2]);
const isSetEqual = compareSet.size === set2.size && compareSet.size === set1.size;
console.log(isSetEqual);

Positivo : é muito simples e curto. Sem biblioteca externa, apenas vanilla JS

Desvantagem : provavelmente será mais lento do que apenas iterar os valores e você precisará de mais espaço.

thadeuszlay
fonte
1

Comparando dois objetos com ==, ===

Ao usar o operador ==ou ===para comparar dois objetos, você sempre obterá, a false menos que esses objetos façam referência ao mesmo objeto . Por exemplo:

var a = b = new Set([1,2,3]); // NOTE: b will become a global variable
a == b; // <-- true: a and b share the same object reference

Caso contrário, == equivale a falso, embora o objeto contenha os mesmos valores:

var a = new Set([1,2,3]);
var b = new Set([1,2,3]);
a == b; // <-- false: a and b are not referencing the same object

Você pode precisar considerar a comparação manual

No ECMAScript 6, você pode converter conjuntos em matrizes de antemão para que possa detectar a diferença entre eles:

function setsEqual(a,b){
    if (a.size !== b.size)
        return false;
    let aa = Array.from(a); 
    let bb = Array.from(b);
    return aa.filter(function(i){return bb.indexOf(i)<0}).length==0;
}

NOTA: Array.from é um dos recursos padrão do ECMAScript 6, mas não é amplamente compatível com os navegadores modernos. Verifique a tabela de compatibilidade aqui: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Browser_compatibility

TaoPR
fonte
1
Isso não deixará de identificar os membros dos bquais não estão a?
1
@torazaburo Certamente. A melhor maneira de pular a verificação se os membros de bnão estão aé verificando se a.size === b.size.
Aadit M Shah
1
Colocar a.size === b.sizeprimeiro em curto-circuito a comparação de elementos individuais, se não for necessário?
2
Se o tamanho for diferente, por definição os conjuntos não são iguais, então é melhor verificar primeiro essa condição.
1
Bem, o outro problema aqui é que, pela natureza dos conjuntos, a hasoperação em conjuntos é projetada para ser muito eficiente, ao contrário da indexOfoperação em arrays. Portanto, faria sentido alterar sua função de filtro para ser return !b.has(i). Isso também eliminaria a necessidade de converter bem um array.
1

Eu criei um polyfill rápido para Set.prototype.isEqual ()

Set.prototype.isEqual = function(otherSet) {
    if(this.size !== otherSet.size) return false;
    for(let item of this) if(!otherSet.has(item)) return false;
    return true;
}

Github Gist - Set.prototype.isEqual

Lenny
fonte
1

Com base na resposta aceita, assumindo o suporte de Array.from, aqui está uma linha:

function eqSet(a, b) {
    return a.size === b.size && Array.from(a).every(b.has.bind(b));
}
John Hoffer
fonte
Ou um verdadeiro one-liner assumindo funções de seta e o operador de propagação: eqSet = (a,b) => a.size === b.size && [...a].every(b.has.bind(b))
John Hoffer
1

Se os conjuntos contêm apenas tipos de dados primitivos ou objetos dentro dos conjuntos têm igualdade de referência, então há uma maneira mais simples

const isEqualSets = (set1, set2) => (set1.size === set2.size) && (set1.size === new Set([...set1, ...set2]).size);

Mikhail Unenov
fonte
0

Eu sigo esta abordagem nos testes:

let setA = new Set(arrayA);
let setB = new Set(arrayB);
let diff = new Set([...setA].filter(x => !setB.has(x)));
expect([...diff].length).toBe(0);
Francisco
fonte
5
Espere um segundo ... isso só verifica se A tem elementos que B não tem? Não verifica se B possui elementos que A não possui. Se você tentar a=[1,2,3]e b=[1,2,3,4], ele dirá que eles são iguais. Então, acho que você precisa de uma verificação extra comosetA.size === setB.size
0

Modificação muito leve com base na resposta de @Aadit M Shah:

/**
 * check if two sets are equal in the sense that
 * they have a matching set of values.
 *
 * @param {Set} a 
 * @param {Set} b
 * @returns {Boolean} 
 */
const areSetsEqual = (a, b) => (
        (a.size === b.size) ? 
        [...a].every( value => b.has(value) ) : false
);

Se alguém mais está tendo problemas como eu devido a alguma peculiaridade da última babel, tinha que adicionar uma condicional explícita aqui.

(Também para o plural eu acho areum pouco mais intuitivo de ler em voz alta 🙃)

rob2d
fonte
-1

1) Verifique se os tamanhos são iguais. Se não, eles não são iguais.

2) itere sobre cada elemento de A e verifique se existe em B. Se um falhar, retornar unequal

3) Se as 2 condições acima falharem, significa que são iguais.

let isEql = (setA, setB) => {
  if (setA.size !== setB.size)
    return false;
  
  setA.forEach((val) => {
    if (!setB.has(val))
      return false;
  });
  return true;
}

let setA = new Set([1, 2, {
  3: 4
}]);
let setB = new Set([2, {
    3: 4
  },
  1
]);

console.log(isEql(setA, setB));

2) Método 2

let isEql = (A, B) => {
  return JSON.stringify([...A].sort()) == JSON.stringify([...B].sort());
}

let res = isEql(new Set([1, 2, {3:4}]), new Set([{3:4},1, 2]));
console.log(res);

ensopado
fonte
Esta resposta está completamente incorreta. A instrução return no forEachmétodo NÃO fará com que a função pai retorne.
xaviert de