Como obter a diferença entre duas matrizes de objetos em JavaScript

100

Tenho dois 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" },
]

O resultado final de que preciso é a diferença entre essas matrizes - o resultado final deve ser assim:

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

É possível fazer algo assim em JavaScript?

BKM
fonte
Então, você quer um array de todos os elementos que não ocorrem em ambos os arrays, filtrados por valor e display?
Cerbrus
Eu quero a diferença entre as duas matrizes. O valor estará presente em qualquer um da matriz.
BKM
2
Parece a matriz exibida quando conectado ao console do firebug ...
Mike C
2
Desculpe, mas o objeto json está errado ... você precisa alterar = por:
Walter Zalazar

Respostas:

136

Usando apenas JS nativo, algo assim 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
fonte
1
Isso funciona, e talvez seja a melhor resposta direta, mas seria bom transformá-la em algo que aceitasse um predicado e duas listas e retornasse a diferença simétrica das duas listas aplicando o predicado apropriadamente. (Pontos extras se fosse mais eficiente do que ter que passar por todas as comparações m * n duas vezes!)
Scott Sauyet
@ScottSauyet: Não estou familiarizado com o termo "predicado" (não sou falante nativo de inglês). Essa é a return a.value ===...sua resposta? (Boa solução, a propósito, +1) Além de usar Array.prototype.some(), não consigo encontrar uma maneira mais eficiente / mais curta de fazer isso.
Cerbrus
2
@Cerbrus: Sim. Um predicado é uma função que retorna um booleano ( trueou falsevalor.) Neste caso, se separarmos a noção de teste de igualdade do resto do código exigindo que o usuário passe na verificação de igualdade como uma função, podemos fazer um algoritmo genérico direto.
Scott Sauyet
@ScottSauyet: Demorou um pouco para encontrar esta resposta novamente, mas agora está um pouco melhor: D
Cerbrus
ES6 const comparer = (otherArray) => (current) => otherArray.filter ((other) => other.value == current.value && other.display == current.display) .length == 0;
Shnigi
68

Você pode usar Array.prototype.filter()em combinação com Array.prototype.some().

Aqui está um exemplo (assumindo que seus arrays estão armazenados nas variáveis result1e 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
fonte
40

Para quem gosta de soluções de uma linha no ES6, algo assim:

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

Atenas
fonte
3
Explique por favor!
Bruno Brito
15

Eu adoto uma abordagem um pouco mais geral, embora semelhante em ideias às abordagens de @Cerbrus e @Kasper Moerch . Eu crio uma função que aceita um predicado para determinar se dois objetos são iguais (aqui nós ignoramos a $$hashKeypropriedade, mas pode ser qualquer coisa) e retorno uma função que calcula a diferença simétrica de duas listas com base nesse 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"}

Tem uma pequena vantagem sobre a abordagem de Cerebrus (assim como a abordagem de Kasper Moerch) por escapar mais cedo; se encontrar uma correspondência, não se preocupa em verificar o resto da lista. Se eu tivesse uma curryfunção útil, faria isso de maneira um pouco diferente, mas funciona bem.

Explicação

Um comentário pedia uma explicação mais detalhada para iniciantes. Aqui está uma tentativa.

Passamos a seguinte função para makeSymmDiffFunc:

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

Esta função é como decidimos que dois objetos são iguais. Como todas as funções que retornam trueou false, pode ser chamada de "função de predicado", mas isso é apenas terminologia. O ponto principal é que makeSymmDiffFuncse configure com uma função que aceita dois objetos e retorna truese os considerarmos iguais, falsese não o fizermos.

Usando isso, makeSymmDiffFunc(leia "fazer função de diferença simétrica") retorna uma nova função:

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

Esta é a função que realmente usaremos. Passamos duas listas e ele encontra os elementos da primeira, não da segunda, então os da segunda, que não estão na primeira, e combina essas duas listas.

No entanto, olhando novamente, eu poderia definitivamente ter entendido seu código e simplificado a função principal um pouco 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 o predicado e retorna os elementos da primeira lista, não da segunda. Isso é mais simples do que minha primeira passagem com uma containsfunção separada .

Finalmente, a função principal é envolvida em uma expressão de função imediatamente chamada ( IIFE ) para manter a complementfunção interna fora do escopo global.


Atualização, alguns anos depois

Agora que o ES2015 se tornou bastante onipresente, eu sugeriria a mesma técnica, com muito menos clichê:

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
fonte
1
Você poderia adicionar mais alguma explicação ao seu código? Não tenho certeza se um iniciante em JavaScript entenderia como funciona a abordagem de predicado.
Kaspermoerch
1
@KasperMoerch: Adicionada uma explicação longa. Espero que ajude. (Isso também me fez reconhecer uma limpeza séria que este código deveria ter.)
Scott Sauyet
8
import differenceBy from 'lodash/differenceBy'

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

Isso retornará a diferença entre duas matrizes de objetos, usando a chave valuepara compará-los. Observe que duas coisas com o mesmo valor não serão retornadas, pois as outras chaves são ignoradas.

Isso faz parte do lodash .

Noé
fonte
O objeto json acima está errado. Ao tentar desta forma, altere = para:
Walter Zalazar
6

Você pode criar um objeto com chaves como o valor exclusivo correspondente para cada objeto na matriz e, em seguida, filtrar cada matriz com base na existência da chave no objeto do outro. Reduz a complexidade da operação.

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
fonte
5

Acho que a solução @Cerbrus está certa. Eu implementei a mesma solução, mas extraí o código repetido em sua própria função (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;
  });
}
Michael
fonte
2

Eu encontrei essa solução usando filtro e alguns.

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

Gorez Tony
fonte
1

A maioria das respostas aqui são bastante complexas, mas a lógica por trás disso não é muito simples?

  1. verifique qual array é mais longo e forneça-o como primeiro parâmetro (se o comprimento for igual, a ordem dos parâmetros não importa)
  2. Itere sobre array1.
  3. Para o elemento de iteração atual da matriz1, verifique se ele está presente na matriz2
  4. Se NÃO estiver presente, então
  5. Empurre-o para a matriz 'diferença'
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;
}

Complexidade de O (n ^ 2).

azrahel
fonte
mais 1 para inclusão de complexidade
delavago1999
1

você pode fazer diff a em be diff b em a, em seguida, mesclar os dois 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]);

lemospy
fonte
0

Eu fiz um diff generalizado que compara 2 objetos de qualquer tipo e pode executar um manipulador de modificação gist.github.com/bortunac "diff.js" uma ex de usar:

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

então a propriedade a é modificada, b é excluída, c modificada, d é adicionada

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

}

agora use como

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

o console irá mostrar

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

Maneira mais genérica e simples:

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
fonte
0

Eu prefiro objeto de mapa quando se trata de grandes arrays.

// 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
fonte
0

JavaScript possui mapas, que fornecem tempo de inserção e pesquisa O (1). Portanto, isso pode ser resolvido em O (n) (e não em O (n²) como todas as outras respostas fazem). Para isso, é necessário gerar uma chave primitiva única (string / número) para cada objeto. Pode-se JSON.stringify, mas isso é bastante sujeito a erros, pois a ordem dos elementos pode influenciar a igualdade:

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

Portanto, eu pegaria um delimitador que não aparece em nenhum dos valores e compor uma string manualmente:

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

Em seguida, um mapa é criado. Quando um elemento já existe no Mapa, ele é removido, caso contrário, é adicionado. Portanto, apenas os elementos que são incluídos tempos ímpares (ou seja, apenas uma vez) permanecem. Isso só funcionará se os elementos forem únicos em cada array:

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
fonte
0

Eu me deparei com essa pergunta enquanto procurava uma maneira de escolher o primeiro item em um array que não corresponde a nenhum dos valores em outro array e consegui classificá-lo eventualmente com array.find () e array.filter () como isto

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

se você precisar buscar uma lista atualizada antes de verificar a próxima melhor opção, isso deve funcionar bem o suficiente :)

Bimpong
fonte
-2

Se você deseja usar bibliotecas externas, pode usar _.difference em underscore.js para fazer isso. _.difference retorna os valores da matriz que não estão presentes nas outras matrizes.

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

==>[2,3,5]
Mubashir Kp
fonte
11
Isso só funciona em arrays com valores primitivos. Se o array contiver uma lista de objetos, como esta pergunta pede, não funcionará, pois tenta comparar as referências em vez dos próprios objetos, o que quase sempre significa que tudo é diferente.
Ross Peoples
1
obrigado Ross você me salvou de uma dor de cabeça. Eu estava prestes a relatar um bug para sublinhar.
Etienne