4

Using flat JavaScript or by making use of lodash, what is the simplest way (hoping lodash has a function) which I compare the following arrays and return the value which has changed:

Before

[
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 32},
  {id: 2, name: 'Joe', age: 38}
]

After

[
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 33},
  {id: 2, name: 'Joe', age: 38}
]

So between before and after, Frank is now age 33, so how can I simply return:

{id: 1, name: 'Frank', age: 33}

Or a more desired result:

{id: 1, age: 33}

EDIT:

As I got such a nice variation of answers to my question, I decided to test them all on server and client side. Here is what I got after using json-generator to generate a json file of 10 000 records:

Node 7.1.0:

David Domain. (Flat JS filter & some): 3.396
Result: { id: 1, name: 'Frank', age: 33 }
Ben Aston (Flat JS nested itteration): 4.359
Result: { age: 33, id: 1 }
Gille Q. (Lodash reduce): 21.335
Result: { id: 1, age: 33 }
Stasovlas. (Lodash differenceBy): 1.442
Result: []  
Vignesh Murugan. (Lodash findWhere): 0
Result: _.findWhere is not a function

Firefox 50.0.2:

David Domain. (Flat JS filter & some): 6.695
Result: { id: 1, name: 'Frank', age: 33 }
Ben Aston (Flat JS nested itteration): 10.594
Result: { age: 33, id: 1 }
Gille Q. (Lodash reduce): 40.085
Result: { id: 1, age: 33 }
Stasovlas. (Lodash differenceBy): 6.499
Result: []

The interesting thing to note here is that Lodash differenceBy does not seem to work when you're dealing with larger amounts of data, at best i could get this to work with only 3 records before i gave up.

@Vignesh must have worked at one point with Underscore but i'm not going to cover this as things have changed and we now use Lodash.

Here is the code I used to test, used timely to track the amount of time taken to execute a function, then looped 1000 times to get the total time to execute the function 1000 times then divided by 1000 to get the average amount of time taken (in ms) to execute the function:

var fs = require('fs');
var timely = require('timely');
var _ = require('lodash');

// Ben Aston
var ben_aston = function (a, b) {
  return a.reduce((p,c,i)=>{
    var diff = objDiff(c, b[i]);
    diff && p.push(diff);
    return p;
  }, [])
}
function objDiff(a, b) {
  var diff = Object.keys(a).reduce((p,c,i)=>{
    if (a[c] === b[c]) {
      return p;
    }
    p[c] = b[c];
    return p;
  }, {});
  if (!Object.keys(diff).length) {
    return;
  }
  diff.id = a.id;
  return diff;
}
var ben_astonT = timely(ben_aston);


// Gille Q.
var gille_q = function (before, after) {
  return _.reduce(before, function(result, value, key) {
    return _.isEqual(value, after[key]) ?
    result : result.concat({id: after[key].id, age: after[key].age});
  }, []);
}
var gille_qT = timely(gille_q);


// David Domain
var david_domain = function (before, after) {
  return after.filter( function( p, idx ) {
    return Object.keys(p).some( function( prop ) {
      return p[prop] !== before[idx][prop];
    })
  })
}
var david_domainT = timely(david_domain);


// Stasovlas
var stasovlas = function (before, after) {
  return _.differenceBy(after, before, 'age');
}
var stasovlasT = timely(stasovlas);


// Vignesh Murugan
var vignesh_murugan = function (before, after) {
  before.forEach((current) => {
    var after = _.findWhere(after,{id : current.id});
    if(!_.isEqual(after , current)) {
      return _.pick(after,"id","name");
    }
  });
}
var vignesh_muruganT = timely(vignesh_murugan);


// Load the data
var before = JSON.parse(fs.readFileSync('./before.json', 'utf8'));
var after = JSON.parse(fs.readFileSync('./after.json', 'utf8'));

// Open average tracking
var ben_aston_ave = 0,
    gille_q_ave = 0,
    david_domain_ave = 0,
    stasovlas_ave = 0,
    vignesh_murugan_ave = 0;

// Do test
for (i = 0; i < 1000; i++) {
  // Ben Aston
  ben_astonT(before, after);
  ben_aston_ave += ben_astonT.time;

  // Gille Q.
  gille_qT(before, after);
  gille_q_ave += gille_qT.time;

  // David Domain
  david_domainT(before, after);
  david_domain_ave += david_domainT.time;

  // Stasovlas
  stasovlasT(before, after);
  stasovlas_ave += stasovlasT.time;

  // Vignesh Murugan
  // vignesh_muruganT(before, after);
  // vignesh_murugan_ave += vignesh_muruganT.time;
}

// Calc averages
ben_aston_ave = ben_aston_ave / 1000;
gille_q_ave = gille_q_ave / 1000;
david_domain_ave = david_domain_ave / 1000;
stasovlas_ave = stasovlas_ave / 1000;
vignesh_murugan_ave = vignesh_murugan_ave / 1000;


console.log('David Domain. (Flat JS filter & some): '+david_domain_ave);
console.log('Result: { id: 1, name: \'Frank\', age: 33 }');
console.log('Ben Aston (Flat JS nested itteration): '+ben_aston_ave);
console.log('Result: { age: 33, id: 1 }');
console.log('Gille Q. (Lodash reduce): '+gille_q_ave);
console.log('Result: { id: 1, age: 33 }');
console.log('Stasovlas. (Lodash differenceBy): '+stasovlas_ave);
console.log('Result: []');
console.log('Vignesh Murugan. (Lodash findWhere): '+vignesh_murugan_ave);
console.log('Result: _.findWhere is not a function');
Craig van Tonder
  • 7,497
  • 18
  • 64
  • 109
  • can you post what you've tried? – Sankar Dec 13 '16 at 13:15
  • @SankarRaj I tried to foreach the results and compare against the before array but it's not a good idea as theres a fair amount of data. Didn't want to push any possible answers in any one direction by providing code for this as i'd like to see what options are available. Maybe it's just down to missing the right terminology for this, I am not sure? – Craig van Tonder Dec 13 '16 at 13:17
  • 1
    http://stackoverflow.com/questions/13147278/using-underscores-difference-method-on-objects or one of the other similar questions – epascarello Dec 13 '16 at 13:18
  • Could the objects be deeper than that? If not you could attempt a string comparison after stringifying them – GMaiolo Dec 13 '16 at 13:18
  • @Goliadkin No, they are always like this but that is a smart idea. I assume that it would be quite quick too. I'll give it some thought and a try, thanks! – Craig van Tonder Dec 13 '16 at 13:21
  • @epascarello Oh and then there is difference -_- – Craig van Tonder Dec 13 '16 at 13:22
  • @Goliadkin - not a good idea in general, stringifying objects doesn't guarantee the order of the keys in the string - it depends on how the object is created (which isn't at all clear, nor need it be, in the question) – Jaromanda X Dec 13 '16 at 13:22
  • @JaromandaX So you learn something every day here on SO, just googled this and found you to be absolutely correct: http://stackoverflow.com/a/24242730/2110294 – Craig van Tonder Dec 13 '16 at 13:26
  • @CraigvanTonder - I don't make it a habit of being wrong (kidding, I'm wrong all the time, just not in this case :p ) – Jaromanda X Dec 13 '16 at 13:27
  • @JaromandaX Thinking about it though, even if the order was wrong, the id value which is required is within the object so regardless of the array index this might still work out in my case. Just wondering which would be faster, lodash differenceBy vs stringify and compare. Lodash certainly is quicker to type out :) – Craig van Tonder Dec 13 '16 at 13:30
  • Who can resist a single line answer!! and, as the question is tagged with lodash, there's no denying it's a good answer (despite needing a 23kByte library to do it :p ) – Jaromanda X Dec 13 '16 at 13:31

5 Answers5

4

use _.differenceBy

var res = _.differenceBy(after, before, 'age');
stasovlas
  • 7,136
  • 2
  • 28
  • 29
  • Thanks so much, this looks like a winner but i shall bench test the examples provided to see which one is the quickest :) – Craig van Tonder Dec 13 '16 at 13:43
  • Hi there, please see the edit to my question, posted some benchmark results which you may find interesting. Your solution did not place in my bench mark test because it did not work for a larger dataset. Weird thing but true, unless you can prove otherwise I believe that David has the best answer to this question? – Craig van Tonder Dec 13 '16 at 17:52
  • @CraigvanTonder nice work! Hmm it really strange, I think is better to ask David. And will share his answer to me too, please – stasovlas Dec 13 '16 at 18:05
1

You could use Array.filter with Array.some, which will give you a new Array with the changed items.

Maybe something like so:

var before = [
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 32},
  {id: 2, name: 'Joe', age: 38}
]

var after = [
  {id: 0, name: 'Bobb', age: 27},
  {id: 1, name: 'Frank', age: 33},
  {id: 2, name: 'Joe', age: 38}
]

var changed = after.filter( function( p, idx ) {
  return Object.keys(p).some( function( prop ) {
    return p[prop] !== before[idx][prop];
  })
})

console.log(changed)
.as-console-wrapper {
  max-height: 100% !important;
}
DavidDomain
  • 14,976
  • 4
  • 42
  • 50
  • Thank you, this is pretty much what I was trying to do myself but couldn't get it right. I appreciate the answer as it's helped me to learn! Am going to bench test the examples provided to see which one is the quickest :) – Craig van Tonder Dec 13 '16 at 13:44
  • Sure, no problem. Be aware that this is just plain JavaScript, lodash is not needed. – DavidDomain Dec 13 '16 at 13:47
  • I appreciate that yes, makes it a better solution in my mind. Theres only a few places where I use lodash and it's because the methods in reaching the conclusions are a bit out of my league at this point. It's always helpful to see simple ways of doing otherwise complicated things and this is indeed one that I can make sense of and use, -1 use case for lodash, maybe one day i'll not need it at all :) – Craig van Tonder Dec 13 '16 at 13:50
  • Hi there, please see the edit to my question, posted some benchmark results which you may find interesting. Your solution placed 1st. – Craig van Tonder Dec 13 '16 at 17:51
1

You can also use lodash reduce method for comparison, here is the code I made for your example wich return what you want in this jsfiddle:

https://jsfiddle.net/7rf9bphL/1/

var a = [
    {id: 0, name: 'Bob', age: 27},
    {id: 1, name: 'Frank', age: 32},
    {id: 2, name: 'Joe', age: 38}];

var b = [
    {id: 0, name: 'Bob', age: 27},
    {id: 1, name: 'Frank', age: 33},
    {id: 2, name: 'Joe', age: 38}];

var result = _.reduce(a, function(result, value, key) {
    return _.isEqual(value, b[key]) ?
        result : result.concat({id: b[key].id, age: b[key].age});
}, []);
console.log("result", result);
Dipiks
  • 3,818
  • 2
  • 23
  • 39
  • Hehe, i actually tried to do this too with reduce but was not coming right and can see now why. Thank you, appreciate the time and effort, will bench test the examples to see which is the quickest! – Craig van Tonder Dec 13 '16 at 13:45
  • @CraigvanTonder always a pleasure to help, you're welcome ! – Dipiks Dec 13 '16 at 13:47
  • Hi there, please see the edit to my question, posted some benchmark results which you may find interesting. Your solution placed 3rd. – Craig van Tonder Dec 13 '16 at 17:51
  • Yep that was interesting, reduce is not really powerful for large amount of data, thanks for the test :) – Dipiks Dec 14 '16 at 09:08
1

Assumes array indexing remains the same:

function diff(a, b) {
    return a.reduce((p,c,i)=>{
        var diff = objDiff(c, b[i]);
        diff && p.push(diff);
        return p;
    }, [])
}
function objDiff(a, b) {
    var diff = Object.keys(a).reduce((p,c,i)=>{
        if (a[c] === b[c]) {
            return p;
        }
        p[c] = b[c];
        return p;
    }, {});
    if (!Object.keys(diff).length) {
        return;
    }
    diff.id = a.id;
    return diff;
}

const before = [{
    id: 0, name: 'Bob', age: 27 }, {
    id: 1, name: 'Frank', age: 32 }, {
    id: 2, name: 'Joe', age: 38 }]
const after = [{
    id: 0, name: 'Bob', age: 27 }, {
    id: 1, name: 'Frank', age: 33 }, {
    id: 2, name: 'Joe', age: 38 }];

console.log(diff(before, after));
Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • Thanks Ben, really appreciate your time and effort here, i'm just wondering too is there any name for this method? I would imagine that this is what lodash is doing underneath. Going to bench test all of these examples and see which is the quickest just for fun :) – Craig van Tonder Dec 13 '16 at 13:48
  • This is effectively a nested iteration. I don't know if there is a better descriptive term. – Ben Aston Dec 13 '16 at 13:50
  • Note that this solution assumes that the position in the array is important. – Ben Aston Dec 13 '16 at 13:51
  • 1
    Hi there, please see the edit to my question, posted some benchmark results which you may find interesting. Your solution placed 2nd. – Craig van Tonder Dec 13 '16 at 17:51
0

I tried using underscore and it works fine

  var x = [
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 32},
  {id: 2, name: 'Joe', age: 38}
]

var y = [
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 33},
  {id: 2, name: 'Joe', age: 38}
]

  x.forEach((current) => {
    var after = _.findWhere(y,{id : current.id})
    if(!_.isEqual(after , current)){
      console.log(_.pick(after,"id","name"))
    }
  })

Here is the Lodash Solution

var difference = [];

x.forEach((current) => {
  var after = _.find(y,{id : current.id})
  if(!_.isEqual(after , current)){
    difference.push(_.pick(after,"id","name"))
  }
})

console.log(difference)
Vignesh Murugan
  • 575
  • 5
  • 18
  • Hi there, please see the edit to my question, posted some benchmark results which you may find interesting. Your solution did not place in my bench mark test because it featured Underscore.js and the function in question no longer works in Lodash. – Craig van Tonder Dec 13 '16 at 17:53