Skip to content

Instantly share code, notes, and snippets.

@petsel
Created January 15, 2024 17:32
Show Gist options
  • Save petsel/1ebb53d98b411776255e87bbab315154 to your computer and use it in GitHub Desktop.
Save petsel/1ebb53d98b411776255e87bbab315154 to your computer and use it in GitHub Desktop.
modularized approach/implementation for comparing/detecting "Deep Structural Equality".
// 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