Cómo obtener la diferencia entre dos matrices de objetos en JavaScript

101

Tengo dos conjuntos de resultados como este:

// Result 1
[
    { value: "0", display: "Jamsheer" },
    { value: "1", display: "Muhammed" },
    { value: "2", display: "Ravi" },
    { value: "3", display: "Ajmal" },
    { value: "4", display: "Ryan" }
]

// Result 2
[
    { value: "0", display: "Jamsheer" },
    { value: "1", display: "Muhammed" },
    { value: "2", display: "Ravi" },
    { value: "3", display: "Ajmal" },
]

El resultado final que necesito es la diferencia entre estas matrices; el resultado final debería ser así:

[{ value: "4", display: "Ryan" }]

¿Es posible hacer algo como esto en JavaScript?

BKM
fuente
Entonces, ¿desea una matriz de todos los elementos que no ocurren en ambas matrices, filtrados por valor y visualización?
Cerbrus
Quiero la diferencia entre las dos matrices. El valor estará presente en cualquiera de la matriz.
BKM
2
Parece la matriz como se muestra cuando se inicia sesión en la consola de Firebug ...
Mike C
2
Lo siento, pero el objeto json está mal ... necesitas cambiar = por:
Walter Zalazar

Respuestas:

138

Usando solo JS nativo, algo como esto funcionará:

a = [{ value:"4a55eff3-1e0d-4a81-9105-3ddd7521d642", display:"Jamsheer"}, { value:"644838b3-604d-4899-8b78-09e4799f586f", display:"Muhammed"}, { value:"b6ee537a-375c-45bd-b9d4-4dd84a75041d", display:"Ravi"}, { value:"e97339e1-939d-47ab-974c-1b68c9cfb536", display:"Ajmal"},  { value:"a63a6f77-c637-454e-abf2-dfb9b543af6c", display:"Ryan"}]
b = [{ value:"4a55eff3-1e0d-4a81-9105-3ddd7521d642", display:"Jamsheer", $$hashKey:"008"}, { value:"644838b3-604d-4899-8b78-09e4799f586f", display:"Muhammed", $$hashKey:"009"}, { value:"b6ee537a-375c-45bd-b9d4-4dd84a75041d", display:"Ravi", $$hashKey:"00A"}, { value:"e97339e1-939d-47ab-974c-1b68c9cfb536", display:"Ajmal", $$hashKey:"00B"}]

function comparer(otherArray){
  return function(current){
    return otherArray.filter(function(other){
      return other.value == current.value && other.display == current.display
    }).length == 0;
  }
}

var onlyInA = a.filter(comparer(b));
var onlyInB = b.filter(comparer(a));

result = onlyInA.concat(onlyInB);

console.log(result);

Cerbrus
fuente
1
Esto funciona, y es quizás la mejor respuesta directa, pero sería bueno convertirlo en algo que aceptara un predicado y dos listas y devolviera la diferencia simétrica de las dos listas aplicando el predicado de manera apropiada. (¡Puntos extra si fuera más eficiente que tener que realizar todas las comparaciones m * n dos veces!)
Scott Sauyet
@ScottSauyet: No estoy familiarizado con el término "predicado" (no soy un hablante nativo de inglés). ¿Eso está return a.value ===...en tu respuesta? (Buena solución, por cierto, +1) Aparte de usar Array.prototype.some(), realmente no puedo encontrar una forma más eficiente / más corta de hacer esto.
Cerbrus
2
@Cerbrus: Sí. Un predicado es una función que devuelve un valor booleano ( trueo falsevalor). En este caso, si separamos la noción de probar la igualdad del resto del código al requerir que el usuario pase la verificación de igualdad como una función, podemos hacer un algoritmo genérico sencillo.
Scott Sauyet
@ScottSauyet: Me ha tomado un tiempo encontrar esta respuesta nuevamente, pero ahora es un poco mejor: D
Cerbrus
ES6 const comparer = (otherArray) => (current) => otherArray.filter ((otro) => other.value == current.value && other.display == current.display) .length == 0;
Shnigi
68

Puede usarlo Array.prototype.filter()en combinación con Array.prototype.some().

Aquí hay un ejemplo (asumiendo que sus matrices están almacenadas en las variables result1y result2):

//Find values that are in result1 but not in result2
var uniqueResultOne = result1.filter(function(obj) {
    return !result2.some(function(obj2) {
        return obj.value == obj2.value;
    });
});

//Find values that are in result2 but not in result1
var uniqueResultTwo = result2.filter(function(obj) {
    return !result1.some(function(obj2) {
        return obj.value == obj2.value;
    });
});

//Combine the two arrays of unique entries
var result = uniqueResultOne.concat(uniqueResultTwo);
Kaspermoerch
fuente
40

Para aquellos a los que les gustan las soluciones de una sola línea en ES6, algo como esto:

const arrayOne = [ 
  { value: "4a55eff3-1e0d-4a81-9105-3ddd7521d642", display: "Jamsheer" },
  { value: "644838b3-604d-4899-8b78-09e4799f586f", display: "Muhammed" },
  { value: "b6ee537a-375c-45bd-b9d4-4dd84a75041d", display: "Ravi" },
  { value: "e97339e1-939d-47ab-974c-1b68c9cfb536", display: "Ajmal" },
  { value: "a63a6f77-c637-454e-abf2-dfb9b543af6c", display: "Ryan" },
];
          
const arrayTwo = [
  { value: "4a55eff3-1e0d-4a81-9105-3ddd7521d642", display: "Jamsheer"},
  { value: "644838b3-604d-4899-8b78-09e4799f586f", display: "Muhammed"},
  { value: "b6ee537a-375c-45bd-b9d4-4dd84a75041d", display: "Ravi"},
  { value: "e97339e1-939d-47ab-974c-1b68c9cfb536", display: "Ajmal"},
];

const results = arrayOne.filter(({ value: id1 }) => !arrayTwo.some(({ value: id2 }) => id2 === id1));

console.log(results);

atheane
fuente
3
¡Explícalo por favor!
Bruno Brito
15

Adopto un enfoque un poco más de propósito general, aunque similar en ideas a los enfoques de @Cerbrus y @Kasper Moerch . Creo una función que acepta un predicado para determinar si dos objetos son iguales (aquí ignoramos la $$hashKeypropiedad, pero podría ser cualquier cosa) y devuelvo una función que calcula la diferencia simétrica de dos listas basadas en ese predicado:

a = [{ value:"4a55eff3-1e0d-4a81-9105-3ddd7521d642", display:"Jamsheer"}, { value:"644838b3-604d-4899-8b78-09e4799f586f", display:"Muhammed"}, { value:"b6ee537a-375c-45bd-b9d4-4dd84a75041d", display:"Ravi"}, { value:"e97339e1-939d-47ab-974c-1b68c9cfb536", display:"Ajmal"},  { value:"a63a6f77-c637-454e-abf2-dfb9b543af6c", display:"Ryan"}]
b = [{ value:"4a55eff3-1e0d-4a81-9105-3ddd7521d642", display:"Jamsheer", $$hashKey:"008"}, { value:"644838b3-604d-4899-8b78-09e4799f586f", display:"Muhammed", $$hashKey:"009"}, { value:"b6ee537a-375c-45bd-b9d4-4dd84a75041d", display:"Ravi", $$hashKey:"00A"}, { value:"e97339e1-939d-47ab-974c-1b68c9cfb536", display:"Ajmal", $$hashKey:"00B"}]

var makeSymmDiffFunc = (function() {
    var contains = function(pred, a, list) {
        var idx = -1, len = list.length;
        while (++idx < len) {if (pred(a, list[idx])) {return true;}}
        return false;
    };
    var complement = function(pred, a, b) {
        return a.filter(function(elem) {return !contains(pred, elem, b);});
    };
    return function(pred) {
        return function(a, b) {
            return complement(pred, a, b).concat(complement(pred, b, a));
        };
    };
}());

var myDiff = makeSymmDiffFunc(function(x, y) {
    return x.value === y.value && x.display === y.display;
});

var result = myDiff(a, b); //=>  {value="a63a6f77-c637-454e-abf2-dfb9b543af6c", display="Ryan"}

Tiene una pequeña ventaja sobre el enfoque de Cerebrus (al igual que el enfoque de Kasper Moerch) en que se escapa temprano; si encuentra una coincidencia, no se molesta en comprobar el resto de la lista. Si tuviera una curryfunción a mano, haría esto de manera un poco diferente, pero funciona bien.

Explicación

Un comentario pidió una explicación más detallada para principiantes. He aquí un intento.

Pasamos la siguiente función a makeSymmDiffFunc:

function(x, y) {
    return x.value === y.value && x.display === y.display;
}

Esta función es la forma en que decidimos que dos objetos son iguales. Como todas las funciones que devuelven trueo false, se puede llamar una "función de predicado", pero eso es solo terminología. El punto principal es que makeSymmDiffFuncestá configurado con una función que acepta dos objetos y devuelve truesi los consideramos iguales, falsesi no.

Usando eso, makeSymmDiffFunc(lea "hacer una función de diferencia simétrica") nos devuelve una nueva función:

        return function(a, b) {
            return complement(pred, a, b).concat(complement(pred, b, a));
        };

Esta es la función que realmente usaremos. Le pasamos dos listas y encuentra los elementos en la primera no en la segunda, luego los de la segunda no en la primera y combina estas dos listas.

Sin embargo, volviendo a mirarlo, definitivamente podría haber tomado una pista de su código y simplificar un poco la función principal usando some:

var makeSymmDiffFunc = (function() {
    var complement = function(pred, a, b) {
        return a.filter(function(x) {
            return !b.some(function(y) {return pred(x, y);});
        });
    };
    return function(pred) {
        return function(a, b) {
            return complement(pred, a, b).concat(complement(pred, b, a));
        };
    };
}());

complementusa el predicado y devuelve los elementos de su primera lista, no de la segunda. Esto es más simple que mi primer paso con una containsfunción separada .

Finalmente, la función principal está envuelta en una expresión de función invocada inmediatamente ( IIFE ) para mantener la complementfunción interna fuera del alcance global.


Actualización, unos años después

Ahora que ES2015 se ha vuelto bastante ubicuo, sugeriría la misma técnica, con mucho menos repetición:

const diffBy = (pred) => (a, b) => a.filter(x => !b.some(y => pred(x, y)))
const makeSymmDiffFunc = (pred) => (a, b) => diffBy(pred)(a, b).concat(diffBy(pred)(b, a))

const myDiff = makeSymmDiffFunc((x, y) => x.value === y.value && x.display === y.display)

const result = myDiff(a, b)
//=>  {value="a63a6f77-c637-454e-abf2-dfb9b543af6c", display="Ryan"}
Scott Sauyet
fuente
1
¿Podría agregar alguna explicación más a su código? No estoy seguro de que un principiante en JavaScript entendería cómo funciona el enfoque de predicado.
kaspermoerch
1
@KasperMoerch: Se agregó una explicación más larga. Espero que eso ayude. (También me hizo reconocer una limpieza seria que este código debería tener.)
Scott Sauyet
8
import differenceBy from 'lodash/differenceBy'

const myDifferences = differenceBy(Result1, Result2, 'value')

Esto devolverá la diferencia entre dos matrices de objetos, usando la clave valuepara compararlos. Tenga en cuenta que no se devolverán dos cosas con el mismo valor, ya que las otras claves se ignoran.

Esto es parte de lodash .

Noé
fuente
El objeto json anterior es incorrecto. Cuando intente de esta manera cambie = por:
Walter Zalazar
6

Puede crear un objeto con claves como el valor único correspondiente a cada objeto en la matriz y luego filtrar cada matriz según la existencia de la clave en el objeto de otro. Reduce la complejidad de la operación.

ES6

let a = [{ value:"4a55eff3-1e0d-4a81-9105-3ddd7521d642", display:"Jamsheer"}, { value:"644838b3-604d-4899-8b78-09e4799f586f", display:"Muhammed"}, { value:"b6ee537a-375c-45bd-b9d4-4dd84a75041d", display:"Ravi"}, { value:"e97339e1-939d-47ab-974c-1b68c9cfb536", display:"Ajmal"},  { value:"a63a6f77-c637-454e-abf2-dfb9b543af6c", display:"Ryan"}];
let b = [{ value:"4a55eff3-1e0d-4a81-9105-3ddd7521d642", display:"Jamsheer", $$hashKey:"008"}, { value:"644838b3-604d-4899-8b78-09e4799f586f", display:"Muhammed", $$hashKey:"009"}, { value:"b6ee537a-375c-45bd-b9d4-4dd84a75041d", display:"Ravi", $$hashKey:"00A"}, { value:"e97339e1-939d-47ab-974c-1b68c9cfb536", display:"Ajmal", $$hashKey:"00B"}];

let valuesA = a.reduce((a,{value}) => Object.assign(a, {[value]:value}), {});
let valuesB = b.reduce((a,{value}) => Object.assign(a, {[value]:value}), {});
let result = [...a.filter(({value}) => !valuesB[value]), ...b.filter(({value}) => !valuesA[value])];
console.log(result);

ES5

var a = [{ value:"4a55eff3-1e0d-4a81-9105-3ddd7521d642", display:"Jamsheer"}, { value:"644838b3-604d-4899-8b78-09e4799f586f", display:"Muhammed"}, { value:"b6ee537a-375c-45bd-b9d4-4dd84a75041d", display:"Ravi"}, { value:"e97339e1-939d-47ab-974c-1b68c9cfb536", display:"Ajmal"},  { value:"a63a6f77-c637-454e-abf2-dfb9b543af6c", display:"Ryan"}];
var b = [{ value:"4a55eff3-1e0d-4a81-9105-3ddd7521d642", display:"Jamsheer", $$hashKey:"008"}, { value:"644838b3-604d-4899-8b78-09e4799f586f", display:"Muhammed", $$hashKey:"009"}, { value:"b6ee537a-375c-45bd-b9d4-4dd84a75041d", display:"Ravi", $$hashKey:"00A"}, { value:"e97339e1-939d-47ab-974c-1b68c9cfb536", display:"Ajmal", $$hashKey:"00B"}];

var valuesA = a.reduce(function(a,c){a[c.value] = c.value; return a; }, {});
var valuesB = b.reduce(function(a,c){a[c.value] = c.value; return a; }, {});
var result = a.filter(function(c){ return !valuesB[c.value]}).concat(b.filter(function(c){ return !valuesA[c.value]}));
console.log(result);

Nikhil Aggarwal
fuente
5

Creo que la solución @Cerbrus es acertada. Implementé la misma solución pero extraje el código repetido en su propia función (DRY).

 function filterByDifference(array1, array2, compareField) {
  var onlyInA = differenceInFirstArray(array1, array2, compareField);
  var onlyInb = differenceInFirstArray(array2, array1, compareField);
  return onlyInA.concat(onlyInb);
}

function differenceInFirstArray(array1, array2, compareField) {
  return array1.filter(function (current) {
    return array2.filter(function (current_b) {
        return current_b[compareField] === current[compareField];
      }).length == 0;
  });
}
Miguel
fuente
2

Encontré esta solución usando filtro y algunos.

resultFilter = (firstArray, secondArray) => {
  return firstArray.filter(firstArrayItem =>
    !secondArray.some(
      secondArrayItem => firstArrayItem._user === secondArrayItem._user
    )
  );
};

Tony Gorez
fuente
1

La mayoría de las respuestas aquí son bastante complejas, pero ¿no es la lógica detrás de esto bastante simple?

  1. comprobar qué matriz es más larga y proporcionarla como primer parámetro (si la longitud es igual, el orden de los parámetros no importa)
  2. Iterar sobre array1.
  3. Para el elemento de iteración actual de array1, compruebe si está presente en array2
  4. Si NO está presente, entonces
  5. Empuje a matriz de 'diferencia'
const getArraysDifference = (longerArray, array2) => {
  const difference = [];

  longerArray.forEach(el1 => {      /*1*/
    el1IsPresentInArr2 = array2.some(el2 => el2.value === el1.value); /*2*/

    if (!el1IsPresentInArr2) { /*3*/
      difference.push(el1);    /*4*/
    }
  });

  return difference;
}

O (n ^ 2) complejidad.

azrahel
fuente
más 1 para la inclusión de la complejidad
delavago1999
1

puede hacer diff a en by diff b en a, luego fusionar ambos resultados

let a = [
    { value: "0", display: "Jamsheer" },
    { value: "1", display: "Muhammed" },
    { value: "2", display: "Ravi" },
    { value: "3", display: "Ajmal" },
    { value: "4", display: "Ryan" }
]

let b = [
    { value: "0", display: "Jamsheer" },
    { value: "1", display: "Muhammed" },
    { value: "2", display: "Ravi" },
    { value: "3", display: "Ajmal" }
]

// b diff a
let resultA = b.filter(elm => !a.map(elm => JSON.stringify(elm)).includes(JSON.stringify(elm)));

// a diff b
let resultB = a.filter(elm => !b.map(elm => JSON.stringify(elm)).includes(JSON.stringify(elm)));  

// show merge 
console.log([...resultA, ...resultB]);

lemospía
fuente
0

Hice una diferencia generalizada que compara 2 objetos de cualquier tipo y puede ejecutar un controlador de modificación gist.github.com/bortunac "diff.js" un ejemplo de usar:

old_obj={a:1,b:2,c:[1,2]}
now_obj={a:2 , c:[1,3,5],d:55}

por lo que la propiedad a se modifica, b se elimina, c se modifica, d se agrega

var handler=function(type,pointer){
console.log(type,pointer,this.old.point(pointer)," | ",this.now.point(pointer)); 

}

ahora usa like

df=new diff();
df.analize(now_obj,old_obj);
df.react(handler);

la consola mostrará

mdf ["a"]  1 | 2 
mdf ["c", "1"]  2 | 3 
add ["c", "2"]  undefined | 5 
add ["d"]  undefined | 55 
del ["b"]  2 | undefined 
Bortunac
fuente
0

Forma más genérica y sencilla:

findObject(listOfObjects, objectToSearch) {
    let found = false, matchingKeys = 0;
    for(let object of listOfObjects) {
        found = false;
        matchingKeys = 0;
        for(let key of Object.keys(object)) {
            if(object[key]==objectToSearch[key]) matchingKeys++;
        }
        if(matchingKeys==Object.keys(object).length) {
            found = true;
            break;
        }
    }
    return found;
}

get_removed_list_of_objects(old_array, new_array) {
    // console.log('old:',old_array);
    // console.log('new:',new_array);
    let foundList = [];
    for(let object of old_array) {
        if(!this.findObject(new_array, object)) foundList.push(object);
    }
    return foundList;
}

get_added_list_of_objects(old_array, new_array) {
    let foundList = [];
    for(let object of new_array) {
        if(!this.findObject(old_array, object)) foundList.push(object);
    }
    return foundList;
}
Pulin Jhaveri
fuente
0

Prefiero el objeto de mapa cuando se trata de arreglos grandes.

// create tow arrays
array1 = Array.from({length: 400},() => ({value:Math.floor(Math.random() * 4000)}))
array2 = Array.from({length: 400},() => ({value:Math.floor(Math.random() * 4000)}))

// calc diff with some function
console.time('diff with some');
results = array2.filter(({ value: id1 }) => array1.some(({ value: id2 }) => id2 === id1));
console.log('diff results ',results.length)
console.timeEnd('diff with some');

// calc diff with map object
console.time('diff with map');
array1Map = {};
for(const item1 of array1){
    array1Map[item1.value] = true;
}
results = array2.filter(({ value: id2 }) => array1Map[id2]);
console.log('map results ',results.length)
console.timeEnd('diff with map');

yoni12ab
fuente
0

JavaScript tiene mapas, que proporcionan tiempo de inserción y búsqueda O (1). Por lo tanto, esto se puede resolver en O (n) (y no en O (n²) como lo hacen todas las demás respuestas). Para eso, es necesario generar una clave primitiva única (cadena / número) para cada objeto. Uno podriaJSON.stringify , pero eso es bastante propenso a errores, ya que el orden de los elementos podría influir en la igualdad:

 JSON.stringify({ a: 1, b: 2 }) !== JSON.stringify({ b: 2, a: 1 })

Por lo tanto, tomaría un delimitador que no aparece en ninguno de los valores y compondría una cadena manualmente:

const toHash = value => value.value + "@" + value.display;

Luego se crea un mapa. Cuando un elemento ya existe en el mapa, se elimina; de lo contrario, se agrega. Por lo tanto, solo quedan los elementos que se incluyen en tiempos impares (es decir, solo una vez). Esto solo funcionará si los elementos son únicos en cada matriz:

const entries = new Map();

for(const el of [...firstArray, ...secondArray]) {
  const key = toHash(el);
  if(entries.has(key)) {
    entries.delete(key);
  } else {
    entries.set(key, el);
  }
}

const result = [...entries.values()];

Jonas Wilms
fuente
0

Me encontré con esta pregunta mientras buscaba una forma de seleccionar el primer elemento en una matriz que no coincide con ninguno de los valores en otra matriz y logré resolverlo eventualmente con array.find () y array.filter () como esta

var carList= ['mercedes', 'lamborghini', 'bmw', 'honda', 'chrysler'];
var declinedOptions = ['mercedes', 'lamborghini'];

const nextOption = carList.find(car=>{
    const duplicate = declinedOptions.filter(declined=> {
      return declined === car
    })
    console.log('duplicate:',duplicate) //should list out each declined option
    if(duplicate.length === 0){//if theres no duplicate, thats the nextOption
      return car
    }
})

console.log('nextOption:', nextOption);
//expected outputs
//duplicate: mercedes
//duplicate: lamborghini
//duplicate: []
//nextOption: bmw

si necesita seguir obteniendo una lista actualizada antes de verificar la siguiente mejor opción, esto debería funcionar lo suficientemente bien :)

Bimpong
fuente
-2

Si está dispuesto a usar bibliotecas externas, puede usar _.difference en underscore.js para lograrlo. _.difference devuelve los valores de la matriz que no están presentes en las otras matrices.

_.difference([1,2,3,4,5][1,4,10])

==>[2,3,5]
Mubashir Kp
fuente
11
Esto solo funciona en matrices con valores primitivos. Si la matriz contiene una lista de objetos, como pide esta pregunta, no funcionará ya que intenta comparar las referencias en lugar de los objetos en sí, lo que casi siempre significará que todo es diferente.
Ross Peoples
1
gracias Ross me salvaste de un dolor de cabeza. Estaba a punto de informar de un error para subrayar.
Etienne