UNPKG

@tienedev/datype

Version:

Modern TypeScript utility library with pragmatic typing and zero dependencies

177 lines (174 loc) 5.27 kB
'use strict'; /** * Performs a deep equality comparison between two values. * * @template T - The type of the first value * @template U - The type of the second value * @param a - The first value to compare * @param b - The second value to compare * @returns `true` if the values are deeply equal, `false` otherwise * * This function performs deep comparison for: * - Primitive values (using Object.is for NaN/±0 handling) * - Objects (comparing own enumerable properties recursively) * - Arrays (comparing elements recursively) * - Dates (comparing time values) * - RegExp (comparing source and flags) * - Set and Map (comparing contents) * - Functions (reference equality only) * - Circular references (handled safely) * * @example * ```typescript * import { isEqual } from 'datype'; * * // Primitive values * isEqual(1, 1); // true * isEqual('hello', 'hello'); // true * isEqual(NaN, NaN); // true (unlike === comparison) * isEqual(+0, -0); // false (unlike === comparison) * * // Objects * isEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true * isEqual({ a: 1, b: 2 }, { b: 2, a: 1 }); // true (order doesn't matter) * isEqual({ a: { b: 1 } }, { a: { b: 1 } }); // true (deep comparison) * * // Arrays * isEqual([1, 2, 3], [1, 2, 3]); // true * isEqual([1, [2, 3]], [1, [2, 3]]); // true (deep comparison) * * // Dates * isEqual(new Date('2023-01-01'), new Date('2023-01-01')); // true * * // Mixed types * isEqual({ date: new Date('2023-01-01'), arr: [1, 2] }, * { date: new Date('2023-01-01'), arr: [1, 2] }); // true * ``` */ function isEqual(a, b) { return deepEqual(a, b, new Map()); } function deepEqual(a, b, seen) { // Use Object.is for primitive comparison (handles NaN and ±0 correctly) if (Object.is(a, b)) { return true; } // Handle null/undefined cases if (a === null || a === undefined || b === null || b === undefined) { return a === b; } // Different types are not equal if (typeof a !== typeof b) { return false; } // Handle primitive types if (typeof a !== 'object') { return false; } // Circular reference detection if (seen.has(a)) { return seen.get(a) === b; } // Both are objects from this point const objA = a; const objB = b; // Handle Date objects if (a instanceof Date && b instanceof Date) { return Object.is(a.getTime(), b.getTime()); } // Handle RegExp objects if (a instanceof RegExp && b instanceof RegExp) { return a.source === b.source && a.flags === b.flags; } // Handle Set objects if (a instanceof Set && b instanceof Set) { if (a.size !== b.size) { return false; } seen.set(a, b); for (const item of Array.from(a)) { let found = false; for (const otherItem of Array.from(b)) { if (deepEqual(item, otherItem, seen)) { found = true; break; } } if (!found) { seen.delete(a); return false; } } seen.delete(a); return true; } // Handle Map objects if (a instanceof Map && b instanceof Map) { if (a.size !== b.size) { return false; } seen.set(a, b); for (const [key, value] of Array.from(a)) { let found = false; for (const [otherKey, otherValue] of Array.from(b)) { if (deepEqual(key, otherKey, seen) && deepEqual(value, otherValue, seen)) { found = true; break; } } if (!found) { seen.delete(a); return false; } } seen.delete(a); return true; } // Handle Array objects if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) { return false; } seen.set(a, b); for (let i = 0; i < a.length; i++) { if (!deepEqual(a[i], b[i], seen)) { seen.delete(a); return false; } } seen.delete(a); return true; } // One is array, other is not if (Array.isArray(a) || Array.isArray(b)) { return false; } // Handle Functions (reference equality only) if (typeof a === 'function' && typeof b === 'function') { return a === b; } // Handle plain objects seen.set(a, b); const keysA = Object.keys(objA); const keysB = Object.keys(objB); // Different number of properties if (keysA.length !== keysB.length) { seen.delete(a); return false; } // Check all properties for (const key of keysA) { if (!Object.prototype.hasOwnProperty.call(objB, key)) { seen.delete(a); return false; } if (!deepEqual(objA[key], objB[key], seen)) { seen.delete(a); return false; } } seen.delete(a); return true; } exports.isEqual = isEqual;