comparando conjuntos ECMA6 para la igualdad

102

¿Cómo se comparan dos conjuntos de JavaScript? Intenté usar ==y, ===pero ambos devuelven falso.

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

Estos dos conjuntos son equivalentes porque, por definición, los conjuntos no tienen orden (al menos no habitualmente). Revisé la documentación de Set on MDN y no encontré nada útil. Alguien sabe cómo hacer esto?

williamcodes
fuente
Dos conjuntos son dos objetos diferentes. ===es para igualdad de valor, no igualdad de objeto.
elclanrs
3
iterar y comparar el valor de cada miembro, si todos son iguales, el conjunto es "igual"
dandavis
1
@dandavis Con conjuntos, los miembros son los valores.
2
Los conjuntos y mapas tienen un orden, que es el orden de inserción, por el motivo que sea: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
CodeManX
8
Lo peor de todo, incluso new Set([1,2,3]) != new Set([1,2,3]). Esto hace que Javascript Set sea inútil para conjuntos de conjuntos porque el superconjunto contendrá subconjuntos duplicados. La única solución alternativa que viene a la mente es convertir todos los subconjuntos en matrices, ordenar cada matriz y luego codificar cada matriz como una cadena (por ejemplo, JSON).
7vujy0f0hy

Respuestas:

73

Prueba esto:

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

Un enfoque más funcional sería:

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

La allfunción funciona para todos los objetos iterables (por ejemplo, Sety Map).

Si Array.fromtuviera un apoyo más amplio, podríamos haber implementado la allfunción como:

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

Espero que ayude.

Aadit M Shah
fuente
2
Creo que debería cambiar el nombre de hasa isPartOfo isInoelem
Bergi
@Bergi Cambié el nombre de la función hasa isIn.
Aadit M Shah
1
@DavidGiven Sí, los conjuntos en JavaScript se repiten en orden de inserción: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Aadit M Shah
48
Cada día estoy más seguro de que JavaScript es el lenguaje más pésimo jamás creado. Todos tienen que inventar sus propias funciones básicas para hacer frente a su limitación, y esto está en ES6, ¡y estamos en 2017! ¿Por qué no podían agregar funciones tan frecuentes a la especificación del objeto Set?
Ghasan
2
@ GhasanAl-Sakkaf, estoy de acuerdo, es quizás que TC39 consiste en científicos, pero no pragmáticos ...
Marecky
59

También puedes probar:

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
fuente
Sin duda un mejor solución, ya que encaja dentro de una condición if
Hugodby
¡Me encanta lo idiomática que es esta solución y lo legible que es! Gracias @Max
daydreamer
29

lodash proporciona _.isEqual(), que hace comparaciones profundas. Esto es muy útil si no quieres escribir el tuyo propio. A partir de lodash 4, _.isEqual()compara correctamente Sets.

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
anthonyserious
fuente
7

La otra respuesta funcionará bien; aquí hay otra 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); }

Sin embargo, tenga en cuenta que esto no hace una comparación de igualdad profunda. Entonces

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

devolverá falso. Si los dos conjuntos anteriores deben considerarse iguales, debemos recorrer ambos conjuntos haciendo comparaciones profundas de calidad en cada elemento. Estipulamos la existencia de una deepEqualrutina. Entonces la lógica sería

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

Qué hace esto: para cada miembro de s1, busque un miembro profundamente igual de s2. Si lo encuentra, elimínelo para que no se pueda volver a utilizar. Los dos conjuntos son profundamente iguales si todos los elementos de s1 se encuentran en s2 y s2 se agota. No probado.

Puede encontrar esto útil: http://www.2ality.com/2015/01/es6-set-operations.html .


fuente
6

Ninguna de estas soluciones "devuelve" la funcionalidad esperada a una estructura de datos como un conjunto de conjuntos. En su estado actual, el conjunto de Javascript es inútil para este propósito porque el superconjunto contendrá subconjuntos duplicados, que Javascript ve erróneamente como distintos. La única solución en la que puedo pensar es convertir cada subconjunto a Array , ordenarlo y luego codificarlo como String (por ejemplo, JSON).

Solución

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>

Prueba definitiva: 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
fuente
1
¡Gran solución! Y si sabe que acaba de tener un conjunto de cadenas o números, simplemente se convierte en[...set1].sort().toString() === [...set2].sort().toString()
3

La razón por la que su enfoque devuelve falso es porque está comparando dos objetos diferentes (incluso si tienen el mismo contenido), por lo que comparar dos objetos diferentes (no referencias, sino objetos) siempre devuelve falso.

El siguiente enfoque fusiona dos conjuntos en uno y simplemente compara estúpidamente el tamaño. Si es lo mismo, es lo mismo:

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

Al revés : es muy simple y corto. Sin biblioteca externa solo vanilla JS

Desventaja : Probablemente será más lento que simplemente iterar sobre los valores y necesitará más espacio.

thadeuszlay
fuente
1

Comparando dos objetos con ==, ===

Cuando use el operador ==u ===para comparar dos objetos, siempre obtendrá a false menos que esos objetos hagan referencia al mismo objeto . Por ejemplo:

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

De lo contrario, == equivale a falso aunque el objeto contenga los mismos 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

Es posible que deba considerar la comparación manual

En ECMAScript 6, puede convertir conjuntos en matrices de antemano para que pueda detectar la diferencia entre ellos:

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 es una de las características estándar de ECMAScript 6 pero no es ampliamente compatible con los navegadores modernos. Consulte la tabla de compatibilidad aquí: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Browser_compatibility

TaoPR
fuente
1
¿No dejará de identificar a los miembros de los bque no están a?
1
@torazaburo De hecho. La mejor forma de omitir la comprobación de si los miembros de bno están incluidos aes comprobar si a.size === b.size.
Aadit M Shah
1
¿Poner a.size === b.sizeprimero en cortocircuito la comparación de elementos individuales si no es necesario?
2
Si el tamaño es diferente, por definición los conjuntos no son iguales, por lo que es mejor verificar esa condición primero.
1
Bueno, el otro problema aquí es que, por la naturaleza de los conjuntos, la hasoperación en conjuntos está diseñada para ser muy eficiente, a diferencia de la indexOfoperación en matrices. Por lo tanto, tendría sentido cambiar la función de filtro para que sea return !b.has(i). Eso también eliminaría la necesidad de convertir ben una matriz.
1

Creé un 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
fuente
1

Según la respuesta aceptada, asumiendo el apoyo de Array.from, aquí hay una frase:

function eqSet(a, b) {
    return a.size === b.size && Array.from(a).every(b.has.bind(b));
}
John Hoffer
fuente
O un verdadero de una sola línea asumiendo funciones de flecha y el operador de propagación: eqSet = (a,b) => a.size === b.size && [...a].every(b.has.bind(b))
John Hoffer
1

Si los conjuntos contienen solo tipos de datos primitivos o los objetos dentro de los conjuntos tienen igualdad de referencia, entonces hay una forma más sencilla

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

Mikhail Unenov
fuente
0

Sigo este enfoque en las pruebas:

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
fuente
5
Espera un segundo ... ¿esto solo verifica si A tiene elementos que B no tiene? No comprueba si B tiene elementos que A no tiene. Si intenta a=[1,2,3]y b=[1,2,3,4], entonces dice que son lo mismo. Así que supongo que necesita un cheque adicional comosetA.size === setB.size
0

Modificación muy leve basada en la respuesta 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
);

Si alguien más tiene un problema como yo debido a alguna peculiaridad del último babel, tenía que agregar un condicional explícito aquí.

(También para el plural, creo que arees un poco más intuitivo de leer en voz alta)

rob2d
fuente
-1

1) Compruebe si los tamaños son iguales. Si no, entonces no son iguales.

2) iterar sobre cada elemento de A y verificar que exista en B. Si uno falla, regrese unequal

3) Si las 2 condiciones anteriores fallan, significa que son iguales.

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

sapy
fuente
Esta respuesta es completamente incorrecta. La declaración de retorno en el forEachmétodo NO hará que la función principal regrese.
xaviert