same-values-as
Version:
A very relaxed 'deep equals' that allows for any order in arrays, even if array values are objects
160 lines (129 loc) • 5.59 kB
JavaScript
var global, exports;
function sortObject(a,b) {
var sa = JSON.stringify(a).split('').sort().join('');
var sb = JSON.stringify(b).split('').sort().join('');
if (sa < sb) return -1;
if (sa > sb) return 1;
return 0;
}
function OK(reason) {
//console.log(reason); // comment in to hunt things down
return true;
}
function primitiveValue(x) {
return (['boolean','number','string','symbol'].indexOf(typeof x) >= 0) ? x : null;
}
function sameValuesAs (actual, expected) {
return recurseObject(actual, expected, '<root>');
}
function recurseObject(actual, expected, message) {
if (actual === expected) return OK('equal values by ===');
if (dateLike(actual) && (dateLike(expected) === dateLike(actual))) return OK('equal dates'); // optimistic date comparison
if (primitiveValue(actual) !== primitiveValue(expected)) throw new Error(message + ' Primitive values are not equal');
// need a special case for arrays of objects here,
// otherwise we can get into nasty loops
if (Array.isArray(actual) && Array.isArray(expected)) {
if (actual.length !== expected.length) {
throw new Error(message + ' Array lengths are different ('+(actual.length)+' vs '+(expected.length)+')');
}
actual.sort(sortObject);
expected.sort(sortObject);
for (var i = 0; i < actual.length; i++) {
recurseObject(actual[i], expected[i], message+'['+i+']');
}
return true
}
return objectSameValues(actual, expected, message);
}
function dateLike(val) {
if (typeof val === 'number' && val < 1E9) return undefined; // date parsing in recent versions of Node has got worse.
if (val instanceof Date) return val.getTime();
var maybeDate = Date.parse(val);
if (Number.isNaN(maybeDate)) return undefined;
return maybeDate;
}
function objectSameValues(a, b, message) {
if (isUndefinedOrNull(a) && isUndefinedOrNull(b)) return OK('both null or undefined');
if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) throw new Error(message + ' Object instances do not match');
if (a.prototype !== b.prototype) throw new Error(message + ' Object prototypes are not equal');
if (isArguments(a) !== isArguments(b)) throw new Error(message + ' One of the objects is not a function');
if (isArguments(a) && isArguments(b)) return recurseObject(pSlice.call(a), pSlice.call(b), message);
if (isBuffer(a)) {
if (!isBuffer(b)) throw new Error(message + ' One of the objects is not a buffer');
if (a.length !== b.length) throw new Error(message + ' The buffer lengths do not match');
for (i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
throw new Error(message + ' The buffer values do not match');
}
}
return OK('equal buffers');
}
// We now either have an array or an object.
// We want to match arrays regardless of order ('cos we're mad)
var ka,kb;
try {
ka = Object.getOwnPropertyNames(a),
kb = Object.getOwnPropertyNames(b);
}
catch (e) {
//happens when one is a string literal and the other isn't
// happens when two primitives don't match, ie. 1 and 2
throw new Error(message + ' Expected ' + '\''+ b + '\'' + ' does not match actual ' + '\'' + a + '\'');;
}
var arrays = Array.isArray(a);
if (arrays) {
if (! Array.isArray(b)) {
throw new Error(message + ' One of the objects is not an array');
}
// sort both arrays for testing below
a.sort(sortObject); // this is very expensive,
b.sort(sortObject); // but does the trick for just about anything.
}
// test object or array deeply
var allKeys = toUnique(ka.concat(kb));
for (var i = allKeys.length - 1; i >= 0; i--) {
var key = allKeys[i];
if (isEmptyOrUndefined(a[key]) && isEmptyOrUndefined(b[key])) {
// this is a weird special case for my own purposes.
// An empty array on an object is considered the same as
// a missing value, but not the same as null
} else {
if (arrays) {
// If we have an array then the key is the position in the array
recurseObject(a[key], b[key], message+'['+key+']');
} else {
recurseObject(a[key], b[key], message+'.'+key);
}
}
}
return OK('no inequalities found');
}
function toUnique(a){
var c,b=a.length;
if (b==0)return a;
while(c=--b)while(c--)a[b]!==a[c]||a.splice(c,1);
return a;
}
function isEmptyOrUndefined(value) {
if (value === null) return false;
return value === undefined || value.length === 0;
}
function isUndefinedOrNull(value) {
return value === null || value === undefined;
}
function isBuffer (x) {
if (!x || typeof x !== 'object' || typeof x.length !== 'number') return false;
if (typeof x.copy !== 'function' || typeof x.slice !== 'function') {
return false;
}
if (x.length > 0 && typeof x[0] !== 'number') return false;
return true;
}
function isArguments(x) {
return typeof x === "object" && ( "callee" in x ) && typeof x.length === "number";
}
(function (provides) {
provides.compare = sameValuesAs;
/* istanbul ignore next */ // `this` branch doesn't get followed
})(global || exports || this);