UNPKG

is-equal

Version:

Are these two values conceptually equal?

301 lines (268 loc) 11.8 kB
'use strict'; var ObjectPrototype = Object.prototype; var toStr = ObjectPrototype.toString; var booleanValue = Boolean.prototype.valueOf; var hasOwn = require('hasown'); var isArray = require('isarray'); var isArrowFunction = require('is-arrow-function'); var isBoolean = require('is-boolean-object'); var isDate = require('is-date-object'); var isGenerator = require('is-generator-function'); var isNumber = require('is-number-object'); var isRegex = require('is-regex'); var isString = require('is-string'); var isSymbol = require('is-symbol'); var isCallable = require('is-callable'); var isBigInt = require('is-bigint'); var getIterator = require('es-get-iterator'); var toPrimitive = require('es-to-primitive/es2015'); var whichCollection = require('which-collection'); var whichBoxedPrimitive = require('which-boxed-primitive'); var getPrototypeOf = require('object.getprototypeof/polyfill')(); var hasSymbols = require('has-symbols/shams')(); var hasBigInts = require('has-bigints')(); var objectType = function (v) { return whichCollection(v) || whichBoxedPrimitive(v) || typeof v; }; var isProto = Object.prototype.isPrototypeOf; var functionsHaveNames = require('functions-have-names')(); var symbolValue = hasSymbols ? Symbol.prototype.valueOf : null; var bigIntValue = hasBigInts ? BigInt.prototype.valueOf : null; var normalizeFnWhitespace = function normalizeWhitespace(fnStr) { // this is needed in IE 9, at least, which has inconsistencies here. return fnStr.replace(/^function ?\(/, 'function (').replace('){', ') {'); }; var testToPrim = function testToPrimitive(value, other, hint, hintName) { var valPrimitive = NaN; var valPrimitiveThrows = false; try { valPrimitive = toPrimitive(value, hint); } catch (error) { valPrimitiveThrows = true; } var otherPrimitive = NaN; var otherPrimitiveThrows = false; try { otherPrimitive = toPrimitive(other, hint); } catch (error) { otherPrimitiveThrows = true; } if (valPrimitiveThrows || otherPrimitiveThrows) { if (!valPrimitiveThrows) { return 'second argument toPrimitive (hint ' + hintName + ') throws; first does not'; } if (!otherPrimitiveThrows) { return 'first argument toPrimitive (hint ' + hintName + ') throws; second does not'; } } else if (valPrimitive !== otherPrimitive) { return 'first argument toPrimitive does not match second argument toPrimitive (hint ' + hintName + ')'; } return ''; }; module.exports = function whyNotEqual(value, other) { if (value === other) { return ''; } if (value == null || other == null) { return value === other ? '' : String(value) + ' !== ' + String(other); } var valToStr = toStr.call(value); var otherToStr = toStr.call(other); if (valToStr !== otherToStr) { return 'toStringTag is not the same: ' + valToStr + ' !== ' + otherToStr; } var valIsBool = isBoolean(value); var otherIsBool = isBoolean(other); if (valIsBool || otherIsBool) { if (!valIsBool) { return 'first argument is not a boolean; second argument is'; } if (!otherIsBool) { return 'second argument is not a boolean; first argument is'; } var valBoolVal = booleanValue.call(value); var otherBoolVal = booleanValue.call(other); if (valBoolVal === otherBoolVal) { return ''; } return 'primitive value of boolean arguments do not match: ' + valBoolVal + ' !== ' + otherBoolVal; } var valIsNumber = isNumber(value); var otherIsNumber = isNumber(other); if (valIsNumber || otherIsNumber) { if (!valIsNumber) { return 'first argument is not a number; second argument is'; } if (!otherIsNumber) { return 'second argument is not a number; first argument is'; } var valNum = Number(value); var otherNum = Number(other); if (valNum === otherNum) { return ''; } var valIsNaN = isNaN(value); var otherIsNaN = isNaN(other); if (valIsNaN && !otherIsNaN) { return 'first argument is NaN; second is not'; } else if (!valIsNaN && otherIsNaN) { return 'second argument is NaN; first is not'; } else if (valIsNaN && otherIsNaN) { return ''; } return 'numbers are different: ' + value + ' !== ' + other; } var valIsString = isString(value); var otherIsString = isString(other); if (valIsString || otherIsString) { if (!valIsString) { return 'second argument is string; first is not'; } if (!otherIsString) { return 'first argument is string; second is not'; } var stringVal = String(value); var otherVal = String(other); if (stringVal === otherVal) { return ''; } return 'string values are different: "' + stringVal + '" !== "' + otherVal + '"'; } var valIsDate = isDate(value); var otherIsDate = isDate(other); if (valIsDate || otherIsDate) { if (!valIsDate) { return 'second argument is Date, first is not'; } if (!otherIsDate) { return 'first argument is Date, second is not'; } var valTime = +value; var otherTime = +other; if (valTime !== otherTime) { return 'Dates have different time values: ' + valTime + ' !== ' + otherTime; } } var valIsRegex = isRegex(value); var otherIsRegex = isRegex(other); if (valIsRegex || otherIsRegex) { if (!valIsRegex) { return 'second argument is RegExp, first is not'; } if (!otherIsRegex) { return 'first argument is RegExp, second is not'; } var regexStringVal = String(value); var regexStringOther = String(other); if (regexStringVal !== regexStringOther) { return 'regular expressions differ: ' + regexStringVal + ' !== ' + regexStringOther; } } var valIsArray = isArray(value); var otherIsArray = isArray(other); if (valIsArray || otherIsArray) { if (!valIsArray) { return 'second argument is an Array, first is not'; } if (!otherIsArray) { return 'first argument is an Array, second is not'; } if (value.length !== other.length) { return 'arrays have different length: ' + value.length + ' !== ' + other.length; } var index = value.length - 1; var equal = ''; var valHasIndex, otherHasIndex; while (equal === '' && index >= 0) { valHasIndex = hasOwn(value, index); otherHasIndex = hasOwn(other, index); if (!valHasIndex && otherHasIndex) { return 'second argument has index ' + index + '; first does not'; } if (valHasIndex && !otherHasIndex) { return 'first argument has index ' + index + '; second does not'; } equal = whyNotEqual(value[index], other[index]); index -= 1; } return equal; } var valueIsSym = isSymbol(value); var otherIsSym = isSymbol(other); if (valueIsSym !== otherIsSym) { if (valueIsSym) { return 'first argument is Symbol; second is not'; } return 'second argument is Symbol; first is not'; } if (valueIsSym && otherIsSym) { return symbolValue.call(value) === symbolValue.call(other) ? '' : 'first Symbol value !== second Symbol value'; } var valueIsBigInt = isBigInt(value); var otherIsBigInt = isBigInt(other); if (valueIsBigInt !== otherIsBigInt) { if (valueIsBigInt) { return 'first argument is BigInt; second is not'; } return 'second argument is BigInt; first is not'; } if (valueIsBigInt && otherIsBigInt) { return bigIntValue.call(value) === bigIntValue.call(other) ? '' : 'first BigInt value !== second BigInt value'; } var valueIsGen = isGenerator(value); var otherIsGen = isGenerator(other); if (valueIsGen !== otherIsGen) { if (valueIsGen) { return 'first argument is a Generator function; second is not'; } return 'second argument is a Generator function; first is not'; } var valueIsArrow = isArrowFunction(value); var otherIsArrow = isArrowFunction(other); if (valueIsArrow !== otherIsArrow) { if (valueIsArrow) { return 'first argument is an arrow function; second is not'; } return 'second argument is an arrow function; first is not'; } var valueIsCallable = isCallable(value); var otherIsCallable = isCallable(other); if (valueIsCallable || otherIsCallable) { if (valueIsCallable !== otherIsCallable) { return valueIsCallable ? 'first argument is callable; second is not' : 'second argument is callable; first is not'; } if (functionsHaveNames && whyNotEqual(value.name, other.name) !== '') { return 'Function names differ: "' + value.name + '" !== "' + other.name + '"'; } if (whyNotEqual(value.length, other.length) !== '') { return 'Function lengths differ: ' + value.length + ' !== ' + other.length; } var valueStr = normalizeFnWhitespace(String(value)); var otherStr = normalizeFnWhitespace(String(other)); if ( whyNotEqual(valueStr, otherStr) !== '' && !( !valueIsGen && !valueIsArrow && whyNotEqual(valueStr.replace(/\)\s*\{/, '){'), otherStr.replace(/\)\s*\{/, '){')) === '' ) ) { return 'Function string representations differ'; } } var valueIsObj = valIsDate || valIsRegex || valIsArray || valueIsGen || valueIsArrow || valueIsCallable || Object(value) === value; var otherIsObj = otherIsDate || otherIsRegex || otherIsArray || otherIsGen || otherIsArrow || otherIsCallable || Object(other) === other; if (valueIsObj || otherIsObj) { if (typeof value !== typeof other) { return 'arguments have a different typeof: ' + typeof value + ' !== ' + typeof other; } if (isProto.call(value, other)) { return 'first argument is the [[Prototype]] of the second'; } if (isProto.call(other, value)) { return 'second argument is the [[Prototype]] of the first'; } if (getPrototypeOf(value) !== getPrototypeOf(other)) { return 'arguments have a different [[Prototype]]'; } var valueIsFn = typeof value === 'function'; var otherIsFn = typeof other === 'function'; if (!valueIsFn || !otherIsFn) { var result = testToPrim(value, other, String, 'String') || testToPrim(value, other, Number, 'Number') || testToPrim(value, other, void undefined, 'default'); if (result) { return result; } } var valueIterator = getIterator(value); var otherIterator = getIterator(other); if (!!valueIterator !== !!otherIterator) { if (valueIterator) { return 'first argument is iterable; second is not'; } return 'second argument is iterable; first is not'; } if (valueIterator && otherIterator) { // both should be truthy or falsy at this point var valueNext, otherNext, nextWhy; do { valueNext = valueIterator.next(); otherNext = otherIterator.next(); if (!valueNext.done && !otherNext.done) { nextWhy = whyNotEqual(valueNext, otherNext); if (nextWhy !== '') { return 'iteration results are not equal: ' + nextWhy; } } } while (!valueNext.done && !otherNext.done); if (valueNext.done && !otherNext.done) { return 'first ' + objectType(value) + ' argument finished iterating before second ' + objectType(other); } if (!valueNext.done && otherNext.done) { return 'second ' + objectType(other) + ' argument finished iterating before first ' + objectType(value); } return ''; } var key, valueKeyIsRecursive, otherKeyIsRecursive, keyWhy; for (key in value) { if (hasOwn(value, key)) { if (!hasOwn(other, key)) { return 'first argument has key "' + key + '"; second does not'; } valueKeyIsRecursive = !!value[key] && value[key][key] === value; otherKeyIsRecursive = !!other[key] && other[key][key] === other; if (valueKeyIsRecursive !== otherKeyIsRecursive) { if (valueKeyIsRecursive) { return 'first argument has a circular reference at key "' + key + '"; second does not'; } return 'second argument has a circular reference at key "' + key + '"; first does not'; } if (!valueKeyIsRecursive && !otherKeyIsRecursive) { keyWhy = whyNotEqual(value[key], other[key]); if (keyWhy !== '') { return 'value at key "' + key + '" differs: ' + keyWhy; } } } } for (key in other) { if (hasOwn(other, key) && !hasOwn(value, key)) { return 'second argument has key "' + key + '"; first does not'; } } return ''; } return false; };