Las operaciones de conjuntos pueden organizarse como funciones reutilizables para resolver problemas de datos, relaciones, permisos, filtros y combinaciones.
JavaScript permite implementar operaciones de conjuntos de forma clara usando Set, arreglos y funciones. En este tema construiremos utilidades reutilizables y veremos cómo aplicarlas a problemas habituales.
La idea central es transformar definiciones matemáticas en código simple, verificable y fácil de combinar.
Conviene agrupar las operaciones en funciones con nombres claros.
const Conjuntos = {
union(a, b) {
return new Set([...a, ...b]);
},
interseccion(a, b) {
return new Set([...a].filter(x => b.has(x)));
},
diferencia(a, b) {
return new Set([...a].filter(x => !b.has(x)));
}
};
console.log(Conjuntos.union(new Set([1, 2]), new Set([2, 3])));
Así se evita repetir lógica en distintas partes del programa.
Una función puede recibir arreglos o conjuntos y convertirlos a Set antes de operar.
function comoConjunto(coleccion) {
return coleccion instanceof Set ? coleccion : new Set(coleccion);
}
const valores = comoConjunto([1, 2, 2, 3]);
console.log(valores); // Set { 1, 2, 3 }
Esta técnica hace que las funciones sean más flexibles.
La unión puede extenderse a más de dos conjuntos.
function comoConjunto(coleccion) {
return coleccion instanceof Set ? coleccion : new Set(coleccion);
}
function unirTodos(...colecciones) {
return new Set(colecciones.flatMap(c => [...comoConjunto(c)]));
}
const resultado = unirTodos([1, 2], [2, 3], new Set([3, 4]));
console.log(resultado); // Set { 1, 2, 3, 4 }
Esto resulta útil para combinar etiquetas, permisos o resultados de búsquedas.
También se puede calcular la intersección común de muchas colecciones.
function comoConjunto(coleccion) {
return coleccion instanceof Set ? coleccion : new Set(coleccion);
}
function intersecarTodos(...colecciones) {
const [primera, ...resto] = colecciones.map(comoConjunto);
return new Set([...primera].filter(x => resto.every(c => c.has(x))));
}
console.log(intersecarTodos([1, 2, 3], [2, 3, 4], [0, 2, 5])); // Set { 2 }
El resultado contiene solo los elementos presentes en todas las colecciones.
La diferencia puede usarse para quitar de un conjunto todos los elementos presentes en otras colecciones.
function comoConjunto(coleccion) {
return coleccion instanceof Set ? coleccion : new Set(coleccion);
}
function unirTodos(...colecciones) {
return new Set(colecciones.flatMap(c => [...comoConjunto(c)]));
}
function excluir(base, ...colecciones) {
const prohibidos = unirTodos(...colecciones);
return new Set([...comoConjunto(base)].filter(x => !prohibidos.has(x)));
}
console.log(excluir([1, 2, 3, 4, 5], [2, 4], [5])); // Set { 1, 3 }
Este patrón es útil para listas de exclusión.
En programación, el complemento suele calcularse respecto de un universo explícito.
function complemento(universo, subconjunto) {
return new Set([...universo].filter(x => !subconjunto.has(x)));
}
const todos = new Set(["A", "B", "C", "D"]);
const seleccionados = new Set(["A", "C"]);
console.log(complemento(todos, seleccionados)); // Set { "B", "D" }
Sin universo definido, el complemento no puede calcularse en código de manera completa.
El producto cartesiano genera todos los pares ordenados posibles entre dos conjuntos.
function productoCartesiano(a, b) {
const pares = [];
for (const x of a) {
for (const y of b) {
pares.push([x, y]);
}
}
return pares;
}
console.log(productoCartesiano(new Set(["S", "M"]), new Set(["rojo", "azul"])));
El resultado se representa como un arreglo de pares.
Una relación puede implementarse como un conjunto de pares ordenados. Para evitar problemas al comparar arreglos, se puede codificar cada par como texto.
function par(a, b) {
return `${a}|${b}`;
}
const sigueA = new Set([
par("Ana", "Luis"),
par("Luis", "Sofía")
]);
console.log(sigueA.has(par("Ana", "Luis"))); // true
La codificación permite consultar pertenencia de forma directa.
Cuando se necesita mostrar la relación, se puede transformar cada clave en un par nuevamente.
function par(a, b) {
return `${a}|${b}`;
}
function decodificarPar(clave) {
return clave.split("|");
}
const sigueA = new Set([
par("Ana", "Luis"),
par("Luis", "Sofía")
]);
for (const conexion of sigueA) {
console.log(decodificarPar(conexion));
}
En aplicaciones reales conviene elegir un separador que no aparezca en los datos o usar una representación más robusta.
Los conjuntos son eficientes para filtrar registros según una lista de identificadores.
const permitidos = new Set([2, 4]);
const usuarios = [
{ id: 1, nombre: "Ana" },
{ id: 2, nombre: "Luis" },
{ id: 4, nombre: "Sofía" }
];
const visibles = usuarios.filter(usuario => permitidos.has(usuario.id));
console.log(visibles);
La consulta de pertenencia evita recorrer la lista de permitidos para cada elemento.
La diferencia permite detectar qué elementos aparecieron en una versión nueva de una colección.
const Conjuntos = {
diferencia(a, b) {
return new Set([...a].filter(x => !b.has(x)));
}
};
const antes = new Set(["A", "B", "C"]);
const despues = new Set(["B", "C", "D", "E"]);
const nuevos = Conjuntos.diferencia(despues, antes);
console.log(nuevos); // Set { "D", "E" }
Este enfoque sirve para comparar sincronizaciones, datos remotos o estados de una interfaz.
El mismo razonamiento permite detectar valores que ya no están.
const Conjuntos = {
diferencia(a, b) {
return new Set([...a].filter(x => !b.has(x)));
}
};
const antes = new Set(["A", "B", "C"]);
const despues = new Set(["B", "C", "D", "E"]);
const eliminados = Conjuntos.diferencia(antes, despues);
console.log(eliminados); // Set { "A" }
Comparar conjuntos permite describir cambios con claridad.
La igualdad de conjuntos es útil cuando el orden no importa.
function comoConjunto(coleccion) {
return coleccion instanceof Set ? coleccion : new Set(coleccion);
}
function sonIguales(a, b) {
a = comoConjunto(a);
b = comoConjunto(b);
return a.size === b.size && [...a].every(x => b.has(x));
}
console.log(sonIguales(["leer", "editar"], ["editar", "leer"])); // true
Esto es frecuente en permisos, filtros activos y opciones seleccionadas.
Un conjunto puede acumular valores únicos encontrados en una lista de objetos.
const productos = [
{ nombre: "Monitor", categoria: "hardware" },
{ nombre: "Teclado", categoria: "hardware" },
{ nombre: "Editor", categoria: "software" }
];
const categorias = new Set(productos.map(p => p.categoria));
console.log(categorias); // Set { "hardware", "software" }
El resultado resume las categorías presentes sin duplicados.
Set y Map se combinan bien cuando se necesita agrupar valores por clave.
const etiquetasPorUsuario = new Map();
function agregarEtiqueta(usuario, etiqueta) {
if (!etiquetasPorUsuario.has(usuario)) {
etiquetasPorUsuario.set(usuario, new Set());
}
etiquetasPorUsuario.get(usuario).add(etiqueta);
}
agregarEtiqueta("Ana", "admin");
agregarEtiqueta("Ana", "activo");
console.log(etiquetasPorUsuario.get("Ana"));
Cada usuario queda asociado a un conjunto de etiquetas únicas.
En JavaScript, dos objetos con el mismo contenido no son iguales si son referencias distintas.
const conjunto = new Set();
conjunto.add({ id: 1 });
conjunto.add({ id: 1 });
console.log(conjunto.size); // 2
Si se necesita unicidad por identificador, es mejor almacenar el identificador o usar un Map.
Para trabajar con objetos, una estrategia común es deduplicar por una propiedad estable.
function unicosPorId(items) {
const vistos = new Set();
return items.filter(item => {
if (vistos.has(item.id)) {
return false;
}
vistos.add(item.id);
return true;
});
}
const usuarios = [
{ id: 1, nombre: "Ana" },
{ id: 2, nombre: "Luis" },
{ id: 1, nombre: "Ana repetida" }
];
console.log(unicosPorId(usuarios));
El conjunto guarda los identificadores ya encontrados.
Implementar conjuntos en JavaScript permite trasladar conceptos matemáticos a soluciones concretas. Con Set, funciones auxiliares y una representación adecuada, se pueden resolver problemas de datos únicos, comparación de colecciones, relaciones, permisos, filtros y combinaciones.
La clave es elegir una representación clara y mantener las operaciones fieles a su significado matemático.