Set: Operaciones de conjuntos

Unión, Intersección, Diferencia y Exclusión

Ahora que ya conocemos la estructura de datos Set y sabemos para que sirve, podría interesarnos hacer operaciones de conjuntos con elementos Set. Javascript no lo soporta de forma nativa, sin embargo, es muy sencillo emularlas si conocemos los métodos funcionales de los array.

Operaciones de conjuntos

Lo primero, repasemos las operaciones de conjuntos para entender lo que vamos a hacer. Observa la siguiente imagen, en ella tenemos 4 operaciones: Unión, Intersección, Diferencia y Exclusión. Cada una de ellas, está formada por un conjunto A y un conjunto B (en azul). Sin embargo, al realizar una operación, la parte en color negro es la resultante de la operación:

Operaciones de conjuntos

Así pues:

  • Unión: Es la suma de todos los elementos del conjunto A y el conjunto B.
  • Intersección: Es la parte común de los elementos del conjunto A y el conjunto B.
  • Diferencia: Son los elementos del conjunto A quitándole los comunes del conjunto B.
  • Exclusión: Son los elementos del conjunto A y el conjunto B que no están en ambos.

Con datos primitivos

Unión de conjuntos

La primera de las operaciones sería realizar la unión, es decir, crear un conjunto Set que tenga los elementos del primer conjunto y los del segundo conjunto, sin repetir:

const firstSet = new Set([1, 2, 3, 4, 5]);
const secondSet = new Set([4, 5, 6, 7, 8]);

const set = new Set([...firstSet, ...secondSet]);
// Set({ 1, 2, 3, 4, 5, 6, 7, 8 })

Como los Set son estructuras de datos que no admiten repetición, no hay que preocuparse de ese detalle. Simplemente, desestructuramos los elementos de los conjuntos en un , y se los pasamos al nuevo conjunto.

Intersección de conjuntos

La segunda operación sería realizar la intersección, es decir, crear un conjunto que tenga los elementos comunes entre el primer y el segundo conjunto:

const firstSet = new Set([1, 2, 3, 4, 5]);
const secondSet = new Set([4, 5, 6, 7, 8]);

const commonElements = [...firstSet].filter(element => secondSet.has(element));
const set = new Set(commonElements);
// Set({ 4, 5 })

En este caso, primero hacemos una desestructuración del primero conjunto para convertirlo en un . Al ser un array, tenemos disponible el método .filter(), que es un método que permite crear un nuevo array, filtrando sus elementos bajo un criterio específico.

Ese criterio, no será más que comprobar los elementos del primer conjunto, que estén incluidos en el segundo conjunto, utilizando el método .has(). Finalmente, creamos un nuevo Set basándonos en el resultado de la operación anterior.

Diferencia de conjuntos

Ahora le tocaría el turno a la diferencia de conjuntos, es decir, los elementos del primer conjunto que no están en el segundo conjunto.

const firstSet = new Set([1, 2, 3, 4, 5]);
const secondSet = new Set([4, 5, 6, 7, 8]);

const diffElements = [...firstSet].filter(element => !secondSet.has(element));
const set = new Set(diffElements);
// Set({ 1, 2, 3 })

Este es similar al anterior, sólo que invertimos la condición, añadiendo un ! antes para negarlo. Es decir, en lugar de establecer la condición del apartado anterior: «filtrar si el elemento está en el segundo conjunto», en este caso hacemos la condición «filtrar si el elemento NO está en el segundo conjunto».

Exclusión de conjuntos

Por último, vamos a realizar la exclusión de conjuntos. Esto es, quedarnos con aquellos elementos que no están en ambos conjuntos, es decir, que están sólo en el primer conjunto, o que están sólo en el segundo conjunto:

const firstSet = new Set([1, 2, 3, 4, 5]);
const secondSet = new Set([4, 5, 6, 7, 8]);

const firstOnlyElements = [...firstSet].filter(element => !secondSet.has(element));
const secondOnlyElements = [...secondSet].filter(element => !firstSet.has(element));
const set = new Set([...firstOnlyElements, ...secondOnlyElements]);
// Set({ 1, 2, 3, 6, 7, 8 })

En este caso, hemos obtenido primero los elementos del primer conjunto que no están en el segundo, y luego los elementos del segundo que no están en el primero. Finalmente, los desestructuramos y los pasamos en un array para crear el conjunto resultante.

OJO: Estas operaciones se pueden realizar con seguridad si estamos trabajando con conjuntos con datos primitivos, es decir, , o . Pero ten en cuenta que si tenemos estructuras más complejas como , habría que establecer un mecanismo para saber si un objeto es igual a otro, o crear algún método similar a .has().

Con datos complejos

Como hemos comentado, si tuvieramos conjuntos algo más complejos, las operaciones se vuelven más complicadas y requieren una implementación más detallada. Vamos a cambiar nuestros conjuntos de , por conjuntos de para entender cuál es el problema.

Ahora tenemos un conjunto firstSet con elementos que se utilizan en el frontend, mientras que en el conjunto secondSet tenemos elementos que se utilizan en el backend:

const firstSet = new Set([
{ name: "Javascript" },
{ name: "CSS" },
{ name: "HTML" },
{ name: "SVG" },
{ name: "JSON" }
]);

const secondSet = new Set([
{ name: "Javascript", type: "Node" },
{ name: "PHP" },
{ name: "Go" },
{ name: "Javascript", type: "Deno" },
{ name: "JSON" }
]);

Si intentamos hacer la intersección que explicamos anteriormente con estas estructuras, comprobaremos que no funciona correctamente, ya que obtenemos un conjunto vacío. No nos ha detectado los objetos { name: "JSON" } como repetidos:

const commonElements = [...firstSet].filter(element => secondSet.has(element));
const set = new Set(commonElements);

set.size // 0

Esto ocurre porque aunque el objeto { name: "JSON" } del conjunto del frontend y el objeto { name: "JSON" } del conjunto del backend parecen tener la misma información, se trata de objetos diferentes: no son el mismo objeto (realmente está comparando sus referencias, el lugar de memoria donde se está guardando).

Posibles soluciones

Teniendo claro el problema anterior, la forma más sencilla de solucionar el problema es crear una lista de elementos únicos, ya sea u , donde se garantizará que se encuentran referenciados de forma única. Así, al crear los conjuntos, se hará referenciándolos y permitiendo que se detecten repetidos:

const elements = [
{ name: "Javascript" },
{ name: "CSS" },
{ name: "HTML" },
{ name: "SVG" },
{ name: "JSON" },
{ name: "Javascript", type: "Node" },
{ name: "PHP" },
{ name: "Go" },
{ name: "Javascript", type: "Deno" }
];

const firstSet = new Set(elements.slice(0, 5)); // Los primeros 5 elementos
const secondSet = new Set(elements.slice(4)); // Los últimos 5 elementos

const commonElements = [...firstSet].filter(element => secondSet.has(element));
const set = new Set(commonElements);

set.size // 1

Observa que ahora, el elemento { name: "JSON" } es exactamente el mismo en ambos conjuntos (misma referencia a memoria), por lo que si lo detecta como elemento común.

Existen otras formas más potentes de solucionar el problema con una implementación algo más profunda, pero se escapa del tema que abordamos en este artículo. Entre algunas soluciones posibles estarían las siguientes:

  1. Crear una clase (wrapper) que implemente las operaciones por debajo
  2. Crear una clase (elemento) que extienda el Set e implemente las diferencias
  3. Delegar el trabajo a una librería de terceros como cset
Tabla de contenidos