UNPKG

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
"use strict" 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);