Last active
March 28, 2017 18:38
-
-
Save Edward-Lombe/30c1102fd319b71468b1 to your computer and use it in GitHub Desktop.
Strict Deep Javascript equality
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
beforeEach(function() { | |
jasmine.addMatchers({ | |
toStrictlyEqual : function() { | |
return { | |
compare : function(actual, expected) { | |
var result = {}; | |
result.pass = equals(actual, expected, function (a, b, sPath) { | |
result.message = 'Expected ' + sPath + ' = ' + b + ' to equal ' + a; | |
}); | |
return result; | |
} | |
}; | |
} | |
}); | |
}); | |
function equals(a, b, fnDiff, sPath) { | |
if (typeof fnDiff === 'undefined') { | |
fnDiff = function (a, b, sPath) {}; | |
} | |
if (typeof sPath === 'undefined') { | |
sPath = ''; | |
} | |
// short cut out | |
if (a === b) { | |
return true; | |
} | |
if (typeof a === typeof b) { | |
// object or array | |
if (typeof a === 'object') { | |
// if they are both arrays | |
if (Array.isArray(a) && Array.isArray(b)) { | |
// check that the lengths are the same | |
if (a.length !== b.length) { | |
fnDiff(a, b, sPath); | |
return false; | |
} | |
// recursively call the equals function on each element of the array | |
for (var i = a.length - 1; i >= 0; i--) { | |
var sNewPath = ''; | |
if (sPath === '') { | |
sNewPath += '[' + i + ']'; | |
} else { | |
sNewPath += sPath + '[' + i + ']'; | |
} | |
if (!equals(a[i], b[i], fnDiff, sNewPath)) { | |
return false; | |
} | |
} | |
// if neither object is an array | |
} else if (a === null || b === null) { | |
if (a !== b) { | |
fnDiff(a, b, sPath); | |
return false; | |
} | |
} else if (!Array.isArray(a) && !Array.isArray(b)) { | |
// check that they have the same number of keys | |
if (Object.keys(a).length !== Object.keys(b).length) { | |
fnDiff(a, b, sPath); | |
return false; | |
} | |
for (var key in a) { | |
var sNewPath = ''; | |
if (sPath === '') { | |
sNewPath += key; | |
} else { | |
sNewPath += sPath + '.' + key; | |
} | |
if (!b.hasOwnProperty(key)) { | |
fnDiff(a, b, sPath); | |
return false; | |
} else if (!equals(a[key], b[key], fnDiff, sNewPath)) { | |
return false; | |
} | |
} | |
} else { | |
return false; | |
} | |
} else if (typeof a === 'function') { | |
if (a.toString() !== b.toString()) { | |
fnDiff(a, b, sPath); | |
return false; | |
} | |
} else { | |
if (a !== b) { | |
fnDiff(a, b, sPath); | |
return false; | |
} | |
} | |
// type mismatch | |
} else { | |
fnDiff(a, b, sPath); | |
return false; | |
} | |
return true; | |
} | |
// may as well test it while it's in local scope | |
describe('the equals function', function () { | |
it('should act as "===" for simple literals', function () { | |
expect(equals(10, '10')).toBe(false); | |
expect(equals([1, 2, 3], [1, 2, 3])).toBe(true); | |
expect(equals('foo', 'Foo')).toBe(false); | |
expect(equals(undefined, null)).toBe(false); | |
expect(equals(undefined, undefined)).toBe(true); | |
expect(equals(null, null)).toBe(true); | |
}); | |
it('should deep compare for objects', function () { | |
expect(equals( | |
{ foo : 'bar' }, | |
{ bar : 'bar' } | |
)).toBe(false); | |
var a = { foo : { bar : { foo : { bar : { foo : 'bar ' } } } } }; | |
var b = { foo : { bar : { foo : { bar : { foo : 'foo ' } } } } }; | |
expect(equals(a, b)).toBe(false); | |
a = { foo : 'bar', bar : 'foo' }; | |
a = { foo : 'bar', bar : 'foo', foobar : 'foobar' }; | |
expect(equals(a, b)).toBe(false); | |
a = { | |
foo : 'bar', | |
array : [ | |
{ foo : 'bar' }, | |
{ foo : 'bar' }, | |
{ foo : 'bar' } | |
] | |
}; | |
b = { | |
foo : 'bar', | |
array : [ | |
{ foo : 'bar' }, | |
{ foo : 'bar' }, | |
{ bar : 'bar' } | |
] | |
}; | |
expect(equals(a, b, function (a, b, sPath) { | |
expect(a).toEqual({ foo: 'bar' }); | |
expect(b).toEqual({ bar: 'bar' }); | |
expect(sPath).toBe('array[2]'); | |
})).toBe(false); | |
a = function () { | |
var foo = []; | |
for (var i = 0; i < 10; i++) { | |
foo.push(i); | |
} | |
}; | |
b = function () { | |
var foo = []; | |
for (var j = 0; j < 10; j++) { | |
foo.push(j); | |
} | |
}; | |
expect(equals(a, b)).toBe(false); | |
}); | |
it('should show where the difference is for complex objects', function () { | |
var a = { foo : { bar : { foo : { bar : { foo : 3 } } } } }; | |
var b = { foo : { bar : { foo : { bar : { foo : 2 } } } } }; | |
equals(a, b, function (a, b, sPath) { | |
expect(sPath).toBe('foo.bar.foo.bar.foo'); | |
expect(a).toBe(3); | |
expect(b).toBe(2); | |
}); | |
a = [[[[[[[[[[[[[1]]]]]]]]]]]]]; | |
b = [[[[[[[[[[[[[[2]]]]]]]]]]]]]]; | |
expect(equals(a, b, function (a, b, sPath) { | |
expect(sPath).toBe('[0][0][0][0][0][0][0][0][0][0][0][0][0]'); | |
expect(a).toBe(1); | |
expect(b).toEqual([2]); | |
})).toBe(false); | |
a = [1, 2, { | |
name : { | |
prop : ['eight', 'nine', { | |
foo : { | |
bar : 'foobar' | |
} | |
}] | |
} | |
}]; | |
b = [1, 2, { | |
name : { | |
prop : ['eight', 'nine', { | |
foo : { | |
bar : 'Foobar' | |
} | |
}] | |
} | |
}]; | |
expect(equals(a, b, function (a, b, sPath) { | |
expect(a).toBe('foobar'); | |
expect(b).toBe('Foobar'); | |
expect(sPath).toBe('[2].name.prop[2].foo.bar'); | |
})); | |
}); | |
it('should not use the call back if the objects are equal', function () { | |
var a = { foo : { bar : { foo : { bar : { foo : 'bar' } } } } }; | |
var b = { foo : { bar : { foo : { bar : { foo : 'bar' } } } } }; | |
var fnDiff = jasmine.createSpy('fnDiff'); | |
equals(a, b, fnDiff); | |
expect(fnDiff).not.toHaveBeenCalled(); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment