Source

equality.ts

/**
 * @description Enhanced algorithm for deep comparison of any two values with optional ignored properties
 * @summary Performs a deep equality check between two values, handling various types including primitives, objects, arrays, dates, and more
 * @param {unknown} a - First value to compare
 * @param {unknown} b - Second value to compare
 * @param {string[]} propsToIgnore - A list of property names to ignore during comparison
 * @return {boolean} Returns true if the values are deeply equal, false otherwise
 * @function isEqual
 * @mermaid
 * sequenceDiagram
 *   participant Caller
 *   participant isEqual
 *   participant Recursion
 *   
 *   Caller->>isEqual: isEqual(a, b, propsToIgnore)
 *   Note over isEqual: Check simple cases (identity, null, primitives)
 *   
 *   alt a === b
 *     isEqual-->>Caller: true (with special case for +0/-0)
 *   else a or b is null
 *     isEqual-->>Caller: a === b
 *   else different types
 *     isEqual-->>Caller: false
 *   else both NaN
 *     isEqual-->>Caller: true
 *   else primitive types
 *     isEqual-->>Caller: a === b
 *   else both Date objects
 *     isEqual-->>Caller: Compare timestamps
 *   else both RegExp objects
 *     isEqual-->>Caller: Compare string representations
 *   else both Error objects
 *     isEqual-->>Caller: Compare name and message
 *   else both Arrays
 *     Note over isEqual: Check length
 *     loop For each element
 *       isEqual->>Recursion: isEqual(a[i], b[i], propsToIgnore)
 *     end
 *   else both Maps or Sets
 *     Note over isEqual: Compare size and entries
 *   else both TypedArrays
 *     Note over isEqual: Compare byte by byte
 *   else both Objects
 *     Note over isEqual: Filter keys by propsToIgnore
 *     Note over isEqual: Compare key counts
 *     loop For each key
 *       isEqual->>Recursion: isEqual(a[key], b[key], propsToIgnore)
 *     end
 *     Note over isEqual: Check Symbol properties
 *     Note over isEqual: Compare prototypes
 *   end
 *   
 *   isEqual-->>Caller: Comparison result
 * @memberOf module:reflection
 */
export function isEqual(
  a: unknown,
  b: unknown,
  ...propsToIgnore: string[]
): boolean {
  // Handle simple cases
  if (a === b) {
    // Special case for +0 and -0
    return a !== 0 || 1 / (a as number) === 1 / (b as number);
  }
  if (a == null || b == null) return a === b;
  if (typeof a !== typeof b) return false;

  // Handle NaN
  if (Number.isNaN(a) && Number.isNaN(b)) return true;

  // Handle primitive types
  if (typeof a !== "object") return a === b;

  // Handle Date objects
  if (a instanceof Date && b instanceof Date) {
    // Check if both dates are invalid
    if (isNaN(a.getTime()) && isNaN(b.getTime())) return true;
    return a.getTime() === b.getTime();
  }

  // Handle RegExp objects
  if (a instanceof RegExp && b instanceof RegExp)
    return a.toString() === b.toString();

  // Handle Error objects
  if (a instanceof Error && b instanceof Error) {
    return a.name === b.name && a.message === b.message;
  }

  // Handle Array objects
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (!isEqual(a[i], b[i], ...propsToIgnore)) return false;
    }
    return true;
  }

  // Handle Map objects
  if (a instanceof Map && b instanceof Map) {
    if (a.size !== b.size) return false;
    for (const [key, value] of a) {
      if (!b.has(key) || !isEqual(value, b.get(key), ...propsToIgnore))
        return false;
    }
    return true;
  }

  // Handle Set objects
  if (a instanceof Set && b instanceof Set) {
    if (a.size !== b.size) return false;
    for (const item of a) {
      if (!b.has(item)) return false;
    }
    return true;
  }

  // Handle TypedArray objects
  if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
    if (a.byteLength !== b.byteLength) return false;
    if (a.byteOffset !== b.byteOffset) return false;
    if (a.buffer.byteLength !== b.buffer.byteLength) return false;

    const uint8A = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
    const uint8B = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);

    for (let i = 0; i < uint8A.length; i++) {
      if (uint8A[i] !== uint8B[i]) return false;
    }
    return true;
  }

  // Handle other objects
  const aKeys = Object.keys(a).filter((k) => !propsToIgnore.includes(k));
  const bKeys = Object.keys(b).filter((k) => !propsToIgnore.includes(k));

  if (aKeys.length !== bKeys.length) return false;

  for (const key of aKeys) {
    if (!bKeys.includes(key)) return false;
    if (
      !isEqual(
        (a as Record<string, unknown>)[key],
        (b as Record<string, unknown>)[key],
        ...propsToIgnore
      )
    )
      return false;
  }

  // Handle Symbol properties
  const aSymbols = Object.getOwnPropertySymbols(a).filter(
    (s) => !propsToIgnore.includes(s.toString())
  );
  const bSymbols = Object.getOwnPropertySymbols(b).filter(
    (s) => !propsToIgnore.includes(s.toString())
  );

  if (aSymbols.length !== bSymbols.length) return false;

  for (const sym of aSymbols) {
    if (!bSymbols.includes(sym)) return false;
    if (
      !isEqual(
        (a as Record<symbol, unknown>)[sym],
        (b as Record<symbol, unknown>)[sym],
        ...propsToIgnore
      )
    )
      return false;
  }

  // Add this check right before the final return statement
  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
    return false;
  }

  return true;
}