456 lines
15 KiB
JavaScript
456 lines
15 KiB
JavaScript
'use strict';
|
|
/* globals Symbol: false, Uint8Array: false, WeakMap: false */
|
|
/*!
|
|
* deep-eql
|
|
* Copyright(c) 2013 Jake Luer <jake@alogicalparadox.com>
|
|
* MIT Licensed
|
|
*/
|
|
|
|
var type = require('type-detect');
|
|
function FakeMap() {
|
|
this._key = 'chai/deep-eql__' + Math.random() + Date.now();
|
|
}
|
|
|
|
FakeMap.prototype = {
|
|
get: function getMap(key) {
|
|
return key[this._key];
|
|
},
|
|
set: function setMap(key, value) {
|
|
if (Object.isExtensible(key)) {
|
|
Object.defineProperty(key, this._key, {
|
|
value: value,
|
|
configurable: true,
|
|
});
|
|
}
|
|
},
|
|
};
|
|
|
|
var MemoizeMap = typeof WeakMap === 'function' ? WeakMap : FakeMap;
|
|
/*!
|
|
* Check to see if the MemoizeMap has recorded a result of the two operands
|
|
*
|
|
* @param {Mixed} leftHandOperand
|
|
* @param {Mixed} rightHandOperand
|
|
* @param {MemoizeMap} memoizeMap
|
|
* @returns {Boolean|null} result
|
|
*/
|
|
function memoizeCompare(leftHandOperand, rightHandOperand, memoizeMap) {
|
|
// Technically, WeakMap keys can *only* be objects, not primitives.
|
|
if (!memoizeMap || isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
|
|
return null;
|
|
}
|
|
var leftHandMap = memoizeMap.get(leftHandOperand);
|
|
if (leftHandMap) {
|
|
var result = leftHandMap.get(rightHandOperand);
|
|
if (typeof result === 'boolean') {
|
|
return result;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/*!
|
|
* Set the result of the equality into the MemoizeMap
|
|
*
|
|
* @param {Mixed} leftHandOperand
|
|
* @param {Mixed} rightHandOperand
|
|
* @param {MemoizeMap} memoizeMap
|
|
* @param {Boolean} result
|
|
*/
|
|
function memoizeSet(leftHandOperand, rightHandOperand, memoizeMap, result) {
|
|
// Technically, WeakMap keys can *only* be objects, not primitives.
|
|
if (!memoizeMap || isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
|
|
return;
|
|
}
|
|
var leftHandMap = memoizeMap.get(leftHandOperand);
|
|
if (leftHandMap) {
|
|
leftHandMap.set(rightHandOperand, result);
|
|
} else {
|
|
leftHandMap = new MemoizeMap();
|
|
leftHandMap.set(rightHandOperand, result);
|
|
memoizeMap.set(leftHandOperand, leftHandMap);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Primary Export
|
|
*/
|
|
|
|
module.exports = deepEqual;
|
|
module.exports.MemoizeMap = MemoizeMap;
|
|
|
|
/**
|
|
* Assert deeply nested sameValue equality between two objects of any type.
|
|
*
|
|
* @param {Mixed} leftHandOperand
|
|
* @param {Mixed} rightHandOperand
|
|
* @param {Object} [options] (optional) Additional options
|
|
* @param {Array} [options.comparator] (optional) Override default algorithm, determining custom equality.
|
|
* @param {Array} [options.memoize] (optional) Provide a custom memoization object which will cache the results of
|
|
complex objects for a speed boost. By passing `false` you can disable memoization, but this will cause circular
|
|
references to blow the stack.
|
|
* @return {Boolean} equal match
|
|
*/
|
|
function deepEqual(leftHandOperand, rightHandOperand, options) {
|
|
// If we have a comparator, we can't assume anything; so bail to its check first.
|
|
if (options && options.comparator) {
|
|
return extensiveDeepEqual(leftHandOperand, rightHandOperand, options);
|
|
}
|
|
|
|
var simpleResult = simpleEqual(leftHandOperand, rightHandOperand);
|
|
if (simpleResult !== null) {
|
|
return simpleResult;
|
|
}
|
|
|
|
// Deeper comparisons are pushed through to a larger function
|
|
return extensiveDeepEqual(leftHandOperand, rightHandOperand, options);
|
|
}
|
|
|
|
/**
|
|
* Many comparisons can be canceled out early via simple equality or primitive checks.
|
|
* @param {Mixed} leftHandOperand
|
|
* @param {Mixed} rightHandOperand
|
|
* @return {Boolean|null} equal match
|
|
*/
|
|
function simpleEqual(leftHandOperand, rightHandOperand) {
|
|
// Equal references (except for Numbers) can be returned early
|
|
if (leftHandOperand === rightHandOperand) {
|
|
// Handle +-0 cases
|
|
return leftHandOperand !== 0 || 1 / leftHandOperand === 1 / rightHandOperand;
|
|
}
|
|
|
|
// handle NaN cases
|
|
if (
|
|
leftHandOperand !== leftHandOperand && // eslint-disable-line no-self-compare
|
|
rightHandOperand !== rightHandOperand // eslint-disable-line no-self-compare
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// Anything that is not an 'object', i.e. symbols, functions, booleans, numbers,
|
|
// strings, and undefined, can be compared by reference.
|
|
if (isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
|
|
// Easy out b/c it would have passed the first equality check
|
|
return false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/*!
|
|
* The main logic of the `deepEqual` function.
|
|
*
|
|
* @param {Mixed} leftHandOperand
|
|
* @param {Mixed} rightHandOperand
|
|
* @param {Object} [options] (optional) Additional options
|
|
* @param {Array} [options.comparator] (optional) Override default algorithm, determining custom equality.
|
|
* @param {Array} [options.memoize] (optional) Provide a custom memoization object which will cache the results of
|
|
complex objects for a speed boost. By passing `false` you can disable memoization, but this will cause circular
|
|
references to blow the stack.
|
|
* @return {Boolean} equal match
|
|
*/
|
|
function extensiveDeepEqual(leftHandOperand, rightHandOperand, options) {
|
|
options = options || {};
|
|
options.memoize = options.memoize === false ? false : options.memoize || new MemoizeMap();
|
|
var comparator = options && options.comparator;
|
|
|
|
// Check if a memoized result exists.
|
|
var memoizeResultLeft = memoizeCompare(leftHandOperand, rightHandOperand, options.memoize);
|
|
if (memoizeResultLeft !== null) {
|
|
return memoizeResultLeft;
|
|
}
|
|
var memoizeResultRight = memoizeCompare(rightHandOperand, leftHandOperand, options.memoize);
|
|
if (memoizeResultRight !== null) {
|
|
return memoizeResultRight;
|
|
}
|
|
|
|
// If a comparator is present, use it.
|
|
if (comparator) {
|
|
var comparatorResult = comparator(leftHandOperand, rightHandOperand);
|
|
// Comparators may return null, in which case we want to go back to default behavior.
|
|
if (comparatorResult === false || comparatorResult === true) {
|
|
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, comparatorResult);
|
|
return comparatorResult;
|
|
}
|
|
// To allow comparators to override *any* behavior, we ran them first. Since it didn't decide
|
|
// what to do, we need to make sure to return the basic tests first before we move on.
|
|
var simpleResult = simpleEqual(leftHandOperand, rightHandOperand);
|
|
if (simpleResult !== null) {
|
|
// Don't memoize this, it takes longer to set/retrieve than to just compare.
|
|
return simpleResult;
|
|
}
|
|
}
|
|
|
|
var leftHandType = type(leftHandOperand);
|
|
if (leftHandType !== type(rightHandOperand)) {
|
|
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, false);
|
|
return false;
|
|
}
|
|
|
|
// Temporarily set the operands in the memoize object to prevent blowing the stack
|
|
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, true);
|
|
|
|
var result = extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandType, options);
|
|
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, result);
|
|
return result;
|
|
}
|
|
|
|
function extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandType, options) {
|
|
switch (leftHandType) {
|
|
case 'String':
|
|
case 'Number':
|
|
case 'Boolean':
|
|
case 'Date':
|
|
// If these types are their instance types (e.g. `new Number`) then re-deepEqual against their values
|
|
return deepEqual(leftHandOperand.valueOf(), rightHandOperand.valueOf());
|
|
case 'Promise':
|
|
case 'Symbol':
|
|
case 'function':
|
|
case 'WeakMap':
|
|
case 'WeakSet':
|
|
case 'Error':
|
|
return leftHandOperand === rightHandOperand;
|
|
case 'Arguments':
|
|
case 'Int8Array':
|
|
case 'Uint8Array':
|
|
case 'Uint8ClampedArray':
|
|
case 'Int16Array':
|
|
case 'Uint16Array':
|
|
case 'Int32Array':
|
|
case 'Uint32Array':
|
|
case 'Float32Array':
|
|
case 'Float64Array':
|
|
case 'Array':
|
|
return iterableEqual(leftHandOperand, rightHandOperand, options);
|
|
case 'RegExp':
|
|
return regexpEqual(leftHandOperand, rightHandOperand);
|
|
case 'Generator':
|
|
return generatorEqual(leftHandOperand, rightHandOperand, options);
|
|
case 'DataView':
|
|
return iterableEqual(new Uint8Array(leftHandOperand.buffer), new Uint8Array(rightHandOperand.buffer), options);
|
|
case 'ArrayBuffer':
|
|
return iterableEqual(new Uint8Array(leftHandOperand), new Uint8Array(rightHandOperand), options);
|
|
case 'Set':
|
|
return entriesEqual(leftHandOperand, rightHandOperand, options);
|
|
case 'Map':
|
|
return entriesEqual(leftHandOperand, rightHandOperand, options);
|
|
default:
|
|
return objectEqual(leftHandOperand, rightHandOperand, options);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Compare two Regular Expressions for equality.
|
|
*
|
|
* @param {RegExp} leftHandOperand
|
|
* @param {RegExp} rightHandOperand
|
|
* @return {Boolean} result
|
|
*/
|
|
|
|
function regexpEqual(leftHandOperand, rightHandOperand) {
|
|
return leftHandOperand.toString() === rightHandOperand.toString();
|
|
}
|
|
|
|
/*!
|
|
* Compare two Sets/Maps for equality. Faster than other equality functions.
|
|
*
|
|
* @param {Set} leftHandOperand
|
|
* @param {Set} rightHandOperand
|
|
* @param {Object} [options] (Optional)
|
|
* @return {Boolean} result
|
|
*/
|
|
|
|
function entriesEqual(leftHandOperand, rightHandOperand, options) {
|
|
// IE11 doesn't support Set#entries or Set#@@iterator, so we need manually populate using Set#forEach
|
|
if (leftHandOperand.size !== rightHandOperand.size) {
|
|
return false;
|
|
}
|
|
if (leftHandOperand.size === 0) {
|
|
return true;
|
|
}
|
|
var leftHandItems = [];
|
|
var rightHandItems = [];
|
|
leftHandOperand.forEach(function gatherEntries(key, value) {
|
|
leftHandItems.push([ key, value ]);
|
|
});
|
|
rightHandOperand.forEach(function gatherEntries(key, value) {
|
|
rightHandItems.push([ key, value ]);
|
|
});
|
|
return iterableEqual(leftHandItems.sort(), rightHandItems.sort(), options);
|
|
}
|
|
|
|
/*!
|
|
* Simple equality for flat iterable objects such as Arrays, TypedArrays or Node.js buffers.
|
|
*
|
|
* @param {Iterable} leftHandOperand
|
|
* @param {Iterable} rightHandOperand
|
|
* @param {Object} [options] (Optional)
|
|
* @return {Boolean} result
|
|
*/
|
|
|
|
function iterableEqual(leftHandOperand, rightHandOperand, options) {
|
|
var length = leftHandOperand.length;
|
|
if (length !== rightHandOperand.length) {
|
|
return false;
|
|
}
|
|
if (length === 0) {
|
|
return true;
|
|
}
|
|
var index = -1;
|
|
while (++index < length) {
|
|
if (deepEqual(leftHandOperand[index], rightHandOperand[index], options) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*!
|
|
* Simple equality for generator objects such as those returned by generator functions.
|
|
*
|
|
* @param {Iterable} leftHandOperand
|
|
* @param {Iterable} rightHandOperand
|
|
* @param {Object} [options] (Optional)
|
|
* @return {Boolean} result
|
|
*/
|
|
|
|
function generatorEqual(leftHandOperand, rightHandOperand, options) {
|
|
return iterableEqual(getGeneratorEntries(leftHandOperand), getGeneratorEntries(rightHandOperand), options);
|
|
}
|
|
|
|
/*!
|
|
* Determine if the given object has an @@iterator function.
|
|
*
|
|
* @param {Object} target
|
|
* @return {Boolean} `true` if the object has an @@iterator function.
|
|
*/
|
|
function hasIteratorFunction(target) {
|
|
return typeof Symbol !== 'undefined' &&
|
|
typeof target === 'object' &&
|
|
typeof Symbol.iterator !== 'undefined' &&
|
|
typeof target[Symbol.iterator] === 'function';
|
|
}
|
|
|
|
/*!
|
|
* Gets all iterator entries from the given Object. If the Object has no @@iterator function, returns an empty array.
|
|
* This will consume the iterator - which could have side effects depending on the @@iterator implementation.
|
|
*
|
|
* @param {Object} target
|
|
* @returns {Array} an array of entries from the @@iterator function
|
|
*/
|
|
function getIteratorEntries(target) {
|
|
if (hasIteratorFunction(target)) {
|
|
try {
|
|
return getGeneratorEntries(target[Symbol.iterator]());
|
|
} catch (iteratorError) {
|
|
return [];
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/*!
|
|
* Gets all entries from a Generator. This will consume the generator - which could have side effects.
|
|
*
|
|
* @param {Generator} target
|
|
* @returns {Array} an array of entries from the Generator.
|
|
*/
|
|
function getGeneratorEntries(generator) {
|
|
var generatorResult = generator.next();
|
|
var accumulator = [ generatorResult.value ];
|
|
while (generatorResult.done === false) {
|
|
generatorResult = generator.next();
|
|
accumulator.push(generatorResult.value);
|
|
}
|
|
return accumulator;
|
|
}
|
|
|
|
/*!
|
|
* Gets all own and inherited enumerable keys from a target.
|
|
*
|
|
* @param {Object} target
|
|
* @returns {Array} an array of own and inherited enumerable keys from the target.
|
|
*/
|
|
function getEnumerableKeys(target) {
|
|
var keys = [];
|
|
for (var key in target) {
|
|
keys.push(key);
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
/*!
|
|
* Determines if two objects have matching values, given a set of keys. Defers to deepEqual for the equality check of
|
|
* each key. If any value of the given key is not equal, the function will return false (early).
|
|
*
|
|
* @param {Mixed} leftHandOperand
|
|
* @param {Mixed} rightHandOperand
|
|
* @param {Array} keys An array of keys to compare the values of leftHandOperand and rightHandOperand against
|
|
* @param {Object} [options] (Optional)
|
|
* @return {Boolean} result
|
|
*/
|
|
function keysEqual(leftHandOperand, rightHandOperand, keys, options) {
|
|
var length = keys.length;
|
|
if (length === 0) {
|
|
return true;
|
|
}
|
|
for (var i = 0; i < length; i += 1) {
|
|
if (deepEqual(leftHandOperand[keys[i]], rightHandOperand[keys[i]], options) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*!
|
|
* Recursively check the equality of two Objects. Once basic sameness has been established it will defer to `deepEqual`
|
|
* for each enumerable key in the object.
|
|
*
|
|
* @param {Mixed} leftHandOperand
|
|
* @param {Mixed} rightHandOperand
|
|
* @param {Object} [options] (Optional)
|
|
* @return {Boolean} result
|
|
*/
|
|
|
|
function objectEqual(leftHandOperand, rightHandOperand, options) {
|
|
var leftHandKeys = getEnumerableKeys(leftHandOperand);
|
|
var rightHandKeys = getEnumerableKeys(rightHandOperand);
|
|
if (leftHandKeys.length && leftHandKeys.length === rightHandKeys.length) {
|
|
leftHandKeys.sort();
|
|
rightHandKeys.sort();
|
|
if (iterableEqual(leftHandKeys, rightHandKeys) === false) {
|
|
return false;
|
|
}
|
|
return keysEqual(leftHandOperand, rightHandOperand, leftHandKeys, options);
|
|
}
|
|
|
|
var leftHandEntries = getIteratorEntries(leftHandOperand);
|
|
var rightHandEntries = getIteratorEntries(rightHandOperand);
|
|
if (leftHandEntries.length && leftHandEntries.length === rightHandEntries.length) {
|
|
leftHandEntries.sort();
|
|
rightHandEntries.sort();
|
|
return iterableEqual(leftHandEntries, rightHandEntries, options);
|
|
}
|
|
|
|
if (leftHandKeys.length === 0 &&
|
|
leftHandEntries.length === 0 &&
|
|
rightHandKeys.length === 0 &&
|
|
rightHandEntries.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
* Returns true if the argument is a primitive.
|
|
*
|
|
* This intentionally returns true for all objects that can be compared by reference,
|
|
* including functions and symbols.
|
|
*
|
|
* @param {Mixed} value
|
|
* @return {Boolean} result
|
|
*/
|
|
function isPrimitive(value) {
|
|
return value === null || typeof value !== 'object';
|
|
}
|