I want to be able to get a list of all differences between two JavaScript object graphs, with the property names and values where the deltas occur.
For what it is w
This script has a NPM version as well, if you are using NodeJS. https://github.com/NV/objectDiff.js
Rejoice.
Here is a partial, naïve solution to my problem - I will update this as I further develop it.
function findDifferences(objectA, objectB) {
var propertyChanges = [];
var objectGraphPath = ["this"];
(function(a, b) {
if(a.constructor == Array) {
// BIG assumptions here: That both arrays are same length, that
// the members of those arrays are _essentially_ the same, and
// that those array members are in the same order...
for(var i = 0; i < a.length; i++) {
objectGraphPath.push("[" + i.toString() + "]");
arguments.callee(a[i], b[i]);
objectGraphPath.pop();
}
} else if(a.constructor == Object || (a.constructor != Number &&
a.constructor != String && a.constructor != Date &&
a.constructor != RegExp && a.constructor != Function &&
a.constructor != Boolean)) {
// we can safely assume that the objects have the
// same property lists, else why compare them?
for(var property in a) {
objectGraphPath.push(("." + property));
if(a[property].constructor != Function) {
arguments.callee(a[property], b[property]);
}
objectGraphPath.pop();
}
} else if(a.constructor != Function) { // filter out functions
if(a != b) {
propertyChanges.push({ "Property": objectGraphPath.join(""), "ObjectA": a, "ObjectB": b });
}
}
})(objectA, objectB);
return propertyChanges;
}
And here is a sample of how it would be used and the data it would provide (please excuse the long example, but I want to use something relatively non-trivial):
var person1 = {
FirstName : "John",
LastName : "Doh",
Age : 30,
EMailAddresses : [
"john.doe@gmail.com",
"jd@initials.com"
],
Children : [
{
FirstName : "Sara",
LastName : "Doe",
Age : 2
}, {
FirstName : "Beth",
LastName : "Doe",
Age : 5
}
]
};
var person2 = {
FirstName : "John",
LastName : "Doe",
Age : 33,
EMailAddresses : [
"john.doe@gmail.com",
"jdoe@hotmail.com"
],
Children : [
{
FirstName : "Sara",
LastName : "Doe",
Age : 3
}, {
FirstName : "Bethany",
LastName : "Doe",
Age : 5
}
]
};
var differences = findDifferences(person1, person2);
At this point, here is what the differences
array would look like if you serialized it to JSON:
[
{
"Property":"this.LastName",
"ObjectA":"Doh",
"ObjectB":"Doe"
}, {
"Property":"this.Age",
"ObjectA":30,
"ObjectB":33
}, {
"Property":"this.EMailAddresses[1]",
"ObjectA":"jd@initials.com",
"ObjectB":"jdoe@hotmail.com"
}, {
"Property":"this.Children[0].Age",
"ObjectA":2,
"ObjectB":3
}, {
"Property":"this.Children[1].FirstName",
"ObjectA":"Beth",
"ObjectB":"Bethany"
}
]
The this
in the Property
value refers to the root of the object that was compared. So, this solution is not yet exactly what I need, but it is pretty darn close.
Hope this is useful to someone out there, and if you have any suggestions for improvement, I am all-ears; I wrote this very late last night (i.e. early this morning) and there may be things I am completely overlooking.
Thanks.
Solution 1
Use JSON.stringify(obj) to get a string representation of the objects you want to compare. Save the string to a file. Use any diff viewer to compare the text files.
Note: JSON.stringify will ignore properties that point to function definitions.
Solution 2
This could do what you want with some modification, it is a modified version of the function _.isEqual (http://documentcloud.github.com/underscore/). Please feel free to suggest any modifications! I wrote it to figure out where the first difference between two objects occur.
// Given two objects find the first key or value not matching, algorithm is a
// inspired by of _.isEqual.
function diffObjects(a, b) {
console.info("---> diffObjects", {"a": a, "b": b});
// Check object identity.
if (a === b) return true;
// Different types?
var atype = typeof(a), btype = typeof(b);
if (atype != btype) {
console.info("Type mismatch:", {"a": a, "b": b});
return false;
};
// Basic equality test (watch out for coercions).
if (a == b) return true;
// One is falsy and the other truthy.
if ((!a && b) || (a && !b)) {
console.info("One is falsy and the other truthy:", {"a": a, "b": b});
return false;
}
// Unwrap any wrapped objects.
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// One of them implements an isEqual()?
if (a.isEqual) return a.isEqual(b);
// Check dates' integer values.
if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
// Both are NaN?
if (_.isNaN(a) && _.isNaN(b)) {
console.info("Both are NaN?:", {"a": a, "b": b});
return false;
}
// Compare regular expressions.
if (_.isRegExp(a) && _.isRegExp(b))
return a.source === b.source &&
a.global === b.global &&
a.ignoreCase === b.ignoreCase &&
a.multiline === b.multiline;
// If a is not an object by this point, we can't handle it.
if (atype !== 'object') {
console.info("a is not an object:", {"a": a});
return false;
}
// Check for different array lengths before comparing contents.
if (a.length && (a.length !== b.length)) {
console.info("Arrays are of different length:", {"a": a, "b": b});
return false;
}
// Nothing else worked, deep compare the contents.
var aKeys = _.keys(a), bKeys = _.keys(b);
// Different object sizes?
if (aKeys.length != bKeys.length) {
console.info("Different object sizes:", {"a": a, "b": b});
return false;
}
// Recursive comparison of contents.
for (var key in a) if (!(key in b) || !diffObjects(a[key], b[key])) return false;
return true;
};
You can also try rus-diff https://github.com/mirek/node-rus-diff which generates MongoDB compatible (rename/unset/set) diff.
For your example objects:
var person1 = {
FirstName: "John",
LastName: "Doh",
Age: 30,
EMailAddresses: ["john.doe@gmail.com", "jd@initials.com"],
Children: [
{
FirstName: "Sara",
LastName: "Doe",
Age: 2
}, {
FirstName: "Beth",
LastName: "Doe",
Age: 5
}
]
};
var person2 = {
FirstName: "John",
LastName: "Doe",
Age: 33,
EMailAddresses: ["john.doe@gmail.com", "jdoe@hotmail.com"],
Children: [
{
FirstName: "Sara",
LastName: "Doe",
Age: 3
}, {
FirstName: "Bethany",
LastName: "Doe",
Age: 5
}
]
};
var rusDiff = require('rus-diff').rusDiff
console.log(rusDiff(person1, person2))
It generates a list of sets:
{ '$set':
{ 'Age': 33,
'Children.0.Age': 3,
'Children.1.FirstName': 'Bethany',
'EMailAddresses.1': 'jdoe@hotmail.com',
'LastName': 'Doe' } }