2022-03-12 02:46:32 +01:00
|
|
|
/**
|
|
|
|
* https://gist.github.com/DLiblik/96801665f9b6c935f12c1071d37eae95
|
|
|
|
Compares two items (values or references) for nested equivalency, meaning that
|
|
|
|
at root and at each key or index they are equivalent as follows:
|
|
|
|
- If a value type, values are either hard equal (===) or are both NaN
|
|
|
|
(different than JS where NaN !== NaN)
|
|
|
|
- If functions, they are the same function instance or have the same value
|
|
|
|
when converted to string via `toString()`
|
|
|
|
- If Date objects, both have the same getTime() or are both NaN (invalid)
|
|
|
|
- If arrays, both are same length, and all contained values areEquivalent
|
|
|
|
recursively - only contents by numeric key are checked
|
|
|
|
- If other object types, enumerable keys are the same (the keys themselves)
|
|
|
|
and values at every key areEquivalent recursively
|
|
|
|
Author: Dathan Liblik
|
|
|
|
License: Free to use anywhere by anyone, as-is, no guarantees of any kind.
|
|
|
|
@param value1 First item to compare
|
|
|
|
@param value2 Other item to compare
|
|
|
|
@param stack Used internally to track circular refs - don't set it
|
|
|
|
*/
|
2023-07-05 01:14:44 +02:00
|
|
|
module.exports = function areEquivalent(value1, value2, numToString = false, stack = []) {
|
|
|
|
if (numToString) {
|
|
|
|
if (value1 !== null && !isNaN(value1)) value1 = String(value1)
|
|
|
|
if (value2 !== null && !isNaN(value2)) value2 = String(value2)
|
|
|
|
}
|
|
|
|
|
2022-03-12 02:46:32 +01:00
|
|
|
// Numbers, strings, null, undefined, symbols, functions, booleans.
|
|
|
|
// Also: objects (incl. arrays) that are actually the same instance
|
|
|
|
if (value1 === value2) {
|
|
|
|
// Fast and done
|
2023-07-05 01:14:44 +02:00
|
|
|
return true
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
2023-04-22 00:49:25 +02:00
|
|
|
// Truthy check to handle value1=null, value2=Object
|
|
|
|
if ((value1 && !value2) || (!value1 && value2)) {
|
2023-07-05 01:14:44 +02:00
|
|
|
console.log('value1/value2 falsy mismatch', value1, value2)
|
2023-04-22 00:49:25 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-07-05 01:14:44 +02:00
|
|
|
const type1 = typeof value1
|
2022-03-12 02:46:32 +01:00
|
|
|
|
|
|
|
// Ensure types match
|
|
|
|
if (type1 !== typeof value2) {
|
2023-07-05 01:14:44 +02:00
|
|
|
console.log('type diff', type1, typeof value2)
|
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Special case for number: check for NaN on both sides
|
|
|
|
// (only way they can still be equivalent but not equal)
|
|
|
|
if (type1 === 'number') {
|
|
|
|
// Failed initial equals test, but could still both be NaN
|
|
|
|
return (isNaN(value1) && isNaN(value2));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Special case for function: check for toString() equivalence
|
|
|
|
if (type1 === 'function') {
|
|
|
|
// Failed initial equals test, but could still have equivalent
|
|
|
|
// implementations - note, will match on functions that have same name
|
|
|
|
// and are native code: `function abc() { [native code] }`
|
2023-07-05 01:14:44 +02:00
|
|
|
return value1.toString() === value2.toString()
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// For these types, cannot still be equal at this point, so fast-fail
|
|
|
|
if (type1 === 'bigint' || type1 === 'boolean' ||
|
|
|
|
type1 === 'function' || type1 === 'string' ||
|
|
|
|
type1 === 'symbol') {
|
2023-07-05 01:14:44 +02:00
|
|
|
console.log('no match for values', value1, value2)
|
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// For dates, cast to number and ensure equal or both NaN (note, if same
|
|
|
|
// exact instance then we're not here - that was checked above)
|
|
|
|
if (value1 instanceof Date) {
|
|
|
|
if (!(value2 instanceof Date)) {
|
2023-07-05 01:14:44 +02:00
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
// Convert to number to compare
|
2023-07-05 01:14:44 +02:00
|
|
|
const asNum1 = +value1, asNum2 = +value2
|
2022-03-12 02:46:32 +01:00
|
|
|
// Check if both invalid (NaN) or are same value
|
2023-07-05 01:14:44 +02:00
|
|
|
return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2))
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, it's a reference type and could be circular, so
|
|
|
|
// make sure we haven't been here before... note we only need to track value1
|
|
|
|
// since value1 being un-circular means value2 will either be equal (and not
|
|
|
|
// circular too) or unequal whether circular or not.
|
|
|
|
if (stack.includes(value1)) {
|
|
|
|
throw new Error(`areEquivalent value1 is circular`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// breadcrumb
|
2023-07-05 01:14:44 +02:00
|
|
|
stack.push(value1)
|
2022-03-12 02:46:32 +01:00
|
|
|
|
|
|
|
// Handle arrays
|
|
|
|
if (Array.isArray(value1)) {
|
|
|
|
if (!Array.isArray(value2)) {
|
2023-07-05 01:14:44 +02:00
|
|
|
console.log('value2 is not array but value1 is', value1, value2)
|
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
2023-07-05 01:14:44 +02:00
|
|
|
const length = value1.length
|
2022-03-12 02:46:32 +01:00
|
|
|
|
|
|
|
if (length !== value2.length) {
|
2023-07-05 01:14:44 +02:00
|
|
|
console.log('array length diff', length)
|
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
2023-07-05 01:14:44 +02:00
|
|
|
if (!areEquivalent(value1[i], value2[i], numToString, stack)) {
|
|
|
|
console.log('2 array items are not equiv', value1[i], value2[i])
|
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
}
|
2023-07-05 01:14:44 +02:00
|
|
|
return true
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Final case: object
|
|
|
|
|
|
|
|
// get both key lists and check length
|
2023-07-05 01:14:44 +02:00
|
|
|
const keys1 = Object.keys(value1)
|
|
|
|
const keys2 = Object.keys(value2)
|
|
|
|
const numKeys = keys1.length
|
2022-03-12 02:46:32 +01:00
|
|
|
|
|
|
|
if (keys2.length !== numKeys) {
|
2023-07-05 01:14:44 +02:00
|
|
|
console.log('Key length is diff', keys2.length, numKeys)
|
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Empty object on both sides?
|
|
|
|
if (numKeys === 0) {
|
2023-07-05 01:14:44 +02:00
|
|
|
return true
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// sort is a native call so it's very fast - much faster than comparing the
|
|
|
|
// values at each key if it can be avoided, so do the sort and then
|
|
|
|
// ensure every key matches at every index
|
2023-07-05 01:14:44 +02:00
|
|
|
keys1.sort()
|
|
|
|
keys2.sort()
|
2022-03-12 02:46:32 +01:00
|
|
|
|
|
|
|
// Ensure perfect match across all keys
|
|
|
|
for (let i = 0; i < numKeys; i++) {
|
|
|
|
if (keys1[i] !== keys2[i]) {
|
2023-07-05 01:14:44 +02:00
|
|
|
console.log('object key is not equiv', keys1[i], keys2[i])
|
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure perfect match across all values
|
|
|
|
for (let i = 0; i < numKeys; i++) {
|
2023-07-05 01:14:44 +02:00
|
|
|
if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], numToString, stack)) {
|
|
|
|
console.log('2 subobjects not equiv', keys1[i], value1[keys1[i]], value2[keys1[i]])
|
|
|
|
return false
|
2022-03-12 02:46:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// back up
|
|
|
|
stack.pop();
|
|
|
|
|
|
|
|
// Walk the same, talk the same - matching ducks. Quack.
|
|
|
|
// 🦆🦆
|
|
|
|
return true;
|
|
|
|
}
|