Created
January 15, 2024 17:32
-
-
Save petsel/1ebb53d98b411776255e87bbab315154 to your computer and use it in GitHub Desktop.
modularized approach/implementation for comparing/detecting "Deep Structural Equality".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// utility/helper functions. | |
function getFunctionName(value) { | |
return Object.getOwnPropertyDescriptor(value, 'name').value; | |
// return value.name; | |
} | |
function getFunctionSignature(value) { | |
return Function.prototype.toString.call(value).trim(); | |
} | |
function getInternalTypeSignature(value) { | |
return Object.prototype.toString.call(value).trim(); | |
} | |
function getInternalTypeName(value) { | |
const regXInternalTypeName = /^\[object\s+(?<name>.*)]$/; | |
let { name } = regXInternalTypeName.exec( | |
getInternalTypeSignature(value), | |
)?.groups; | |
if (name === 'Object') { | |
const { constructor } = Object.getPrototypeOf(value); | |
if ( | |
typeof constructor === 'function' && | |
getFunctionSignature(constructor).startsWith('class ') | |
) { | |
name = getFunctionName(constructor); | |
} | |
} else if (name === 'Error') { | |
name = getFunctionName(Object.getPrototypeOf(value).constructor); | |
} | |
return name; | |
} | |
// core-comparison functionality. | |
function isStructurallyEqualObjectObjects(a, b, customComparisonLookup) { | |
const aKeys = Object.keys(a); | |
const bKeys = Object.keys(b); | |
return ( | |
aKeys.length === bKeys.length && | |
aKeys.every((key, idx) => | |
isDeepStructuralEquality(a[key], b[key], customComparisonLookup) | |
) | |
); | |
} | |
function isStructurallyEqualArrays(a, b, customComparisonLookup) { | |
return ( | |
a.length === b.length && | |
a.every((item, idx) => | |
isDeepStructuralEquality(item, b[idx], customComparisonLookup) | |
) | |
); | |
} | |
function isDeepEqualMaps(a, b, customComparisonLookup) { | |
return ( | |
(a.size === b.size) && | |
[...a.entries()] | |
.every(([key, value]) => | |
b.has(key) && | |
isDeepStructuralEquality(value, b.get(key), customComparisonLookup) | |
) | |
); | |
} | |
function isDeepEqualSets(a, b) { | |
return ( | |
(a.size === b.size) && | |
[...a.keys()] | |
.every(key => b.has(key)) | |
); | |
} | |
function isEqualDates(a, b) { | |
return a.getTime() === b.getTime(); | |
} | |
function isEqualRegExps(a, b) { | |
return ( | |
(a.source === b.source) && | |
(a.flags.split('').sort().join('') === b.flags.split('').sort().join('')) | |
); | |
} | |
function isStructurallyEqualBooleans(a, b) { | |
const isPrimitiveA = (typeof a === 'boolean'); | |
const isPrimitiveB = (typeof b === 'boolean'); | |
return ( | |
// - does not equal when comparing an | |
// object against a primitive value. | |
isPrimitiveA === isPrimitiveB && | |
Boolean(a) === Boolean(b) | |
); | |
} | |
function isStructurallyEqualStrings(a, b) { | |
const isPrimitiveA = (typeof a === 'string'); | |
const isPrimitiveB = (typeof b === 'string'); | |
return ( | |
// - does not equal when comparing an | |
// object against a primitive value. | |
isPrimitiveA === isPrimitiveB && | |
String(a) === String(b) | |
); | |
} | |
function isStructurallyEqualNumbers(a, b) { | |
const isPrimitiveA = (typeof a === 'number'); | |
const isPrimitiveB = (typeof b === 'number'); | |
return ( | |
// - does not equal when comparing an | |
// object against a primitive value. | |
isPrimitiveA === isPrimitiveB && | |
Number(a) === Number(b) | |
); | |
} | |
// - the arguments precedence of functions which compare two | |
// values of different types, follows the alphabetical order | |
// of the participating values' types ... e.g. `BigInt`, `Number` | |
function isBigIntEqualToNumberValue(bigInt, number) { | |
const isNumberValue = (typeof number === 'number'); | |
// - does not equal when comparing a | |
// bigint value against a number object. | |
return isNumberValue && (Number(bigInt) === number); | |
} | |
// core-comparison lookup-table and main-function. | |
const comparisonLookup = new Map([ | |
['Object_Object', isStructurallyEqualObjectObjects], | |
['Array_Array', isStructurallyEqualArrays], | |
['Map_Map', isDeepEqualMaps], | |
['Set_Set', isDeepEqualSets], | |
['Date_Date', isEqualDates], | |
['RegExp_RegExp', isEqualRegExps], | |
['Boolean_Boolean', isStructurallyEqualBooleans], | |
['String_String', isStructurallyEqualStrings], | |
['Number_Number', isStructurallyEqualNumbers], | |
// ['BigInt_Number', isBigIntEqualToNumberValue], | |
]); | |
function isDeepStructuralEquality(a, b, customComparisonLookup = new Map) { | |
let isEqual = Object.is(a, b); | |
if (!isEqual) { | |
const typeA = getInternalTypeName(a); | |
const typeB = getInternalTypeName(b); | |
const comparisonKey = [typeA, typeB].sort().join('_'); | |
const equalityComparison = (comparisonLookup | |
.get(comparisonKey) ?? customComparisonLookup | |
.get(comparisonKey)) ?? (() => false); | |
const argsPrecedence = comparisonKey.startsWith([typeA, ''].join('_')) | |
&& [a, b] | |
|| [b, a]; | |
isEqual = equalityComparison(...argsPrecedence, customComparisonLookup); | |
} | |
return isEqual; | |
} | |
// minimum test. | |
const m1 = new Map([['foo', 'foo'], ['bar', 'bar']]); | |
const m2 = new Map([['foo', 'foo'], ['bar', 'bar']]); | |
const s1 = new Set(['foo', 'bar', m1, m2]); | |
const s2 = new Set(['foo', 'bar', m1, m2]); | |
console.log( | |
'Map instance comparison ... expected: true ... is:', | |
isDeepStructuralEquality(m1, m2) | |
); | |
console.log( | |
'Set instance comparison ... expected: true ... is:', | |
isDeepStructuralEquality(s1, s2) | |
); | |
console.log( | |
'\nSet instance comparison ... expected: false ... is:', | |
isDeepStructuralEquality(s1, new Map([['foo', 'foo'], ['bar', 'bar']])) | |
); | |
console.log( | |
'\nisDeepStructuralEquality({ num: BigInt(0) }, { num: 0 }) ...', | |
isDeepStructuralEquality({ num: BigInt(0) }, { num: 0 }) | |
); | |
console.log( | |
`\nisDeepStructuralEquality( | |
{ num: BigInt(0) }, { num: 0 }, new Map([ | |
['BigInt_Number', isBigIntEqualToNumberValue], | |
]) | |
) ...`, | |
isDeepStructuralEquality( | |
{ num: BigInt(0) }, { num: 0 }, new Map([ | |
['BigInt_Number', isBigIntEqualToNumberValue], | |
]) | |
) | |
); | |
console.log( | |
`\nisDeepStructuralEquality( | |
{ num: BigInt(0) }, { num: new Number(0) }, new Map([ | |
['BigInt_Number', isBigIntEqualToNumberValue], | |
]) | |
) ...`, | |
isDeepStructuralEquality( | |
{ num: BigInt(0) }, { num: new Number(0) }, new Map([ | |
['BigInt_Number', isBigIntEqualToNumberValue], | |
]) | |
) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment