// Copyright 2019 Ron Buckton. All rights reserved. | |
// This code is governed by the BSD license found in the LICENSE file. | |
/*--- | |
description: > | |
Compare two values structurally | |
defines: [assert.deepEqual] | |
---*/ | |
assert.deepEqual = function(actual, expected, message) { | |
var format = assert.deepEqual.format; | |
assert( | |
assert.deepEqual._compare(actual, expected), | |
`Expected ${format(actual)} to be structurally equal to ${format(expected)}. ${(message || '')}` | |
); | |
}; | |
assert.deepEqual.format = function(value, seen) { | |
switch (typeof value) { | |
case 'string': | |
return typeof JSON !== "undefined" ? JSON.stringify(value) : `"${value}"`; | |
case 'number': | |
case 'boolean': | |
case 'symbol': | |
case 'bigint': | |
return value.toString(); | |
case 'undefined': | |
return 'undefined'; | |
case 'function': | |
return `[Function${value.name ? `: ${value.name}` : ''}]`; | |
case 'object': | |
if (value === null) return 'null'; | |
if (value instanceof Date) return `Date "${value.toISOString()}"`; | |
if (value instanceof RegExp) return value.toString(); | |
if (!seen) { | |
seen = { | |
counter: 0, | |
map: new Map() | |
}; | |
} | |
let usage = seen.map.get(value); | |
if (usage) { | |
usage.used = true; | |
return `[Ref: #${usage.id}]`; | |
} | |
usage = { id: ++seen.counter, used: false }; | |
seen.map.set(value, usage); | |
if (typeof Set !== "undefined" && value instanceof Set) { | |
return `Set {${Array.from(value).map(value => assert.deepEqual.format(value, seen)).join(', ')}}${usage.used ? ` as #${usage.id}` : ''}`; | |
} | |
if (typeof Map !== "undefined" && value instanceof Map) { | |
return `Map {${Array.from(value).map(pair => `${assert.deepEqual.format(pair[0], seen)} => ${assert.deepEqual.format(pair[1], seen)}}`).join(', ')}}${usage.used ? ` as #${usage.id}` : ''}`; | |
} | |
if (Array.isArray ? Array.isArray(value) : value instanceof Array) { | |
return `[${value.map(value => assert.deepEqual.format(value, seen)).join(', ')}]${usage.used ? ` as #${usage.id}` : ''}`; | |
} | |
let tag = Symbol.toStringTag in value ? value[Symbol.toStringTag] : 'Object'; | |
if (tag === 'Object' && Object.getPrototypeOf(value) === null) { | |
tag = '[Object: null prototype]'; | |
} | |
return `${tag ? `${tag} ` : ''}{ ${Object.keys(value).map(key => `${key.toString()}: ${assert.deepEqual.format(value[key], seen)}`).join(', ')} }${usage.used ? ` as #${usage.id}` : ''}`; | |
default: | |
return typeof value; | |
} | |
}; | |
assert.deepEqual._compare = (function () { | |
var EQUAL = 1; | |
var NOT_EQUAL = -1; | |
var UNKNOWN = 0; | |
function deepEqual(a, b) { | |
return compareEquality(a, b) === EQUAL; | |
} | |
function compareEquality(a, b, cache) { | |
return compareIf(a, b, isOptional, compareOptionality) | |
|| compareIf(a, b, isPrimitiveEquatable, comparePrimitiveEquality) | |
|| compareIf(a, b, isObjectEquatable, compareObjectEquality, cache) | |
|| NOT_EQUAL; | |
} | |
function compareIf(a, b, test, compare, cache) { | |
return !test(a) | |
? !test(b) ? UNKNOWN : NOT_EQUAL | |
: !test(b) ? NOT_EQUAL : cacheComparison(a, b, compare, cache); | |
} | |
function tryCompareStrictEquality(a, b) { | |
return a === b ? EQUAL : UNKNOWN; | |
} | |
function tryCompareTypeOfEquality(a, b) { | |
return typeof a !== typeof b ? NOT_EQUAL : UNKNOWN; | |
} | |
function tryCompareToStringTagEquality(a, b) { | |
var aTag = Symbol.toStringTag in a ? a[Symbol.toStringTag] : undefined; | |
var bTag = Symbol.toStringTag in b ? b[Symbol.toStringTag] : undefined; | |
return aTag !== bTag ? NOT_EQUAL : UNKNOWN; | |
} | |
function isOptional(value) { | |
return value === undefined | |
|| value === null; | |
} | |
function compareOptionality(a, b) { | |
return tryCompareStrictEquality(a, b) | |
|| NOT_EQUAL; | |
} | |
function isPrimitiveEquatable(value) { | |
switch (typeof value) { | |
case 'string': | |
case 'number': | |
case 'bigint': | |
case 'boolean': | |
case 'symbol': | |
return true; | |
default: | |
return isBoxed(value); | |
} | |
} | |
function comparePrimitiveEquality(a, b) { | |
if (isBoxed(a)) a = a.valueOf(); | |
if (isBoxed(b)) b = b.valueOf(); | |
return tryCompareStrictEquality(a, b) | |
|| tryCompareTypeOfEquality(a, b) | |
|| compareIf(a, b, isNaNEquatable, compareNaNEquality) | |
|| NOT_EQUAL; | |
} | |
function isNaNEquatable(value) { | |
return typeof value === 'number'; | |
} | |
function compareNaNEquality(a, b) { | |
return isNaN(a) && isNaN(b) ? EQUAL : NOT_EQUAL; | |
} | |
function isObjectEquatable(value) { | |
return typeof value === 'object'; | |
} | |
function compareObjectEquality(a, b, cache) { | |
if (!cache) cache = new Map(); | |
return getCache(cache, a, b) | |
|| setCache(cache, a, b, EQUAL) // consider equal for now | |
|| cacheComparison(a, b, tryCompareStrictEquality, cache) | |
|| cacheComparison(a, b, tryCompareToStringTagEquality, cache) | |
|| compareIf(a, b, isValueOfEquatable, compareValueOfEquality) | |
|| compareIf(a, b, isToStringEquatable, compareToStringEquality) | |
|| compareIf(a, b, isArrayLikeEquatable, compareArrayLikeEquality, cache) | |
|| compareIf(a, b, isStructurallyEquatable, compareStructuralEquality, cache) | |
|| compareIf(a, b, isIterableEquatable, compareIterableEquality, cache) | |
|| cacheComparison(a, b, fail, cache); | |
} | |
function isBoxed(value) { | |
return value instanceof String | |
|| value instanceof Number | |
|| value instanceof Boolean | |
|| typeof Symbol === 'function' && value instanceof Symbol | |
|| typeof BigInt === 'function' && value instanceof BigInt; | |
} | |
function isValueOfEquatable(value) { | |
return value instanceof Date; | |
} | |
function compareValueOfEquality(a, b) { | |
return compareIf(a.valueOf(), b.valueOf(), isPrimitiveEquatable, comparePrimitiveEquality) | |
|| NOT_EQUAL; | |
} | |
function isToStringEquatable(value) { | |
return value instanceof RegExp; | |
} | |
function compareToStringEquality(a, b) { | |
return compareIf(a.toString(), b.toString(), isPrimitiveEquatable, comparePrimitiveEquality) | |
|| NOT_EQUAL; | |
} | |
function isArrayLikeEquatable(value) { | |
return (Array.isArray ? Array.isArray(value) : value instanceof Array) | |
|| (typeof Uint8Array === 'function' && value instanceof Uint8Array) | |
|| (typeof Uint8ClampedArray === 'function' && value instanceof Uint8ClampedArray) | |
|| (typeof Uint16Array === 'function' && value instanceof Uint16Array) | |
|| (typeof Uint32Array === 'function' && value instanceof Uint32Array) | |
|| (typeof Int8Array === 'function' && value instanceof Int8Array) | |
|| (typeof Int16Array === 'function' && value instanceof Int16Array) | |
|| (typeof Int32Array === 'function' && value instanceof Int32Array) | |
|| (typeof Float32Array === 'function' && value instanceof Float32Array) | |
|| (typeof Float64Array === 'function' && value instanceof Float64Array) | |
|| (typeof BigUint64Array === 'function' && value instanceof BigUint64Array) | |
|| (typeof BigInt64Array === 'function' && value instanceof BigInt64Array); | |
} | |
function compareArrayLikeEquality(a, b, cache) { | |
if (a.length !== b.length) return NOT_EQUAL; | |
for (var i = 0; i < a.length; i++) { | |
if (compareEquality(a[i], b[i], cache) === NOT_EQUAL) { | |
return NOT_EQUAL; | |
} | |
} | |
return EQUAL; | |
} | |
function isStructurallyEquatable(value) { | |
return !(typeof Promise === 'function' && value instanceof Promise // only comparable by reference | |
|| typeof WeakMap === 'function' && value instanceof WeakMap // only comparable by reference | |
|| typeof WeakSet === 'function' && value instanceof WeakSet // only comparable by reference | |
|| typeof Map === 'function' && value instanceof Map // comparable via @@iterator | |
|| typeof Set === 'function' && value instanceof Set); // comparable via @@iterator | |
} | |
function compareStructuralEquality(a, b, cache) { | |
var aKeys = []; | |
for (var key in a) aKeys.push(key); | |
var bKeys = []; | |
for (var key in b) bKeys.push(key); | |
if (aKeys.length !== bKeys.length) { | |
return NOT_EQUAL; | |
} | |
aKeys.sort(); | |
bKeys.sort(); | |
for (var i = 0; i < aKeys.length; i++) { | |
var aKey = aKeys[i]; | |
var bKey = bKeys[i]; | |
if (compareEquality(aKey, bKey, cache) === NOT_EQUAL) { | |
return NOT_EQUAL; | |
} | |
if (compareEquality(a[aKey], b[bKey], cache) === NOT_EQUAL) { | |
return NOT_EQUAL; | |
} | |
} | |
return compareIf(a, b, isIterableEquatable, compareIterableEquality, cache) | |
|| EQUAL; | |
} | |
function isIterableEquatable(value) { | |
return typeof Symbol === 'function' | |
&& typeof value[Symbol.iterator] === 'function'; | |
} | |
function compareIteratorEquality(a, b, cache) { | |
if (typeof Map === 'function' && a instanceof Map && b instanceof Map || | |
typeof Set === 'function' && a instanceof Set && b instanceof Set) { | |
if (a.size !== b.size) return NOT_EQUAL; // exit early if we detect a difference in size | |
} | |
var ar, br; | |
while (true) { | |
ar = a.next(); | |
br = b.next(); | |
if (ar.done) { | |
if (br.done) return EQUAL; | |
if (b.return) b.return(); | |
return NOT_EQUAL; | |
} | |
if (br.done) { | |
if (a.return) a.return(); | |
return NOT_EQUAL; | |
} | |
if (compareEquality(ar.value, br.value, cache) === NOT_EQUAL) { | |
if (a.return) a.return(); | |
if (b.return) b.return(); | |
return NOT_EQUAL; | |
} | |
} | |
} | |
function compareIterableEquality(a, b, cache) { | |
return compareIteratorEquality(a[Symbol.iterator](), b[Symbol.iterator](), cache); | |
} | |
function cacheComparison(a, b, compare, cache) { | |
var result = compare(a, b, cache); | |
if (cache && (result === EQUAL || result === NOT_EQUAL)) { | |
setCache(cache, a, b, /** @type {EQUAL | NOT_EQUAL} */(result)); | |
} | |
return result; | |
} | |
function fail() { | |
return NOT_EQUAL; | |
} | |
function setCache(cache, left, right, result) { | |
var otherCache; | |
otherCache = cache.get(left); | |
if (!otherCache) cache.set(left, otherCache = new Map()); | |
otherCache.set(right, result); | |
otherCache = cache.get(right); | |
if (!otherCache) cache.set(right, otherCache = new Map()); | |
otherCache.set(left, result); | |
} | |
function getCache(cache, left, right) { | |
var otherCache; | |
var result; | |
otherCache = cache.get(left); | |
result = otherCache && otherCache.get(right); | |
if (result) return result; | |
otherCache = cache.get(right); | |
result = otherCache && otherCache.get(left); | |
if (result) return result; | |
return UNKNOWN; | |
} | |
return deepEqual; | |
})(); |