/**
* @namespace equals
*/
import { TYPES } from "./types.js";
const { array, iterable, object } = TYPES;
const definedOnly = ([_k, v]) => v !== undefined;
/**
* <p>Equality checking for arbitrary JS data, including nested objects.</p>
*
* <p>As JS can do coerced equality checking, and because in rare cases that's
* essential, the third argument is a "strict" flag that defaults to true, but
* can be explicitly set to "false" to perform coercive equality checking.</p>
*
* @name equals.equals
* @function
* @param {*} v1 - One of the two things part of this equality check.
* @param {*} v2 - The other thing that's part of this equality check.
* @param {boolean} strict - True to check with strict type equality, false for coercive equality
* @returns {boolean} True if v1 equals v2, false if not.
*/
export function equals(v1, v2, strict = true) {
// primitive and alias equality?
if (strict ? v1 === v2 : v1 == v2) return true;
// complex equality?
if (array(v1) && array(v2)) return arrayEquals(v1, v2, strict);
if (iterable(v1) && iterable(v2)) return iterableEquals(v1, v2, strict);
if (object(v1) && object(v2)) return objectEquals(v1, v2, strict);
// no equality.
return false;
}
/**
* Arrays are considered equal if they have the same length, contain
* the same elements, and those elements are in the same order.
*
* @param {array} v1 - One of the two things part of this equality check.
* @param {array} v2 - The other thing that's part of this equality check.
* @param {boolean} strict - True to check with strict type equality, false for coercive equality
* @returns {boolean} True if v1 equals v2, false if not.
* @ignore
*/
function arrayEquals(v1, v2, strict = true) {
if (v1.length !== v2.length) return false;
return v1.every((e, i) => equals(e, v2[i], strict));
}
/**
* Iterables are objects like String, Array, TypedArray, Map and Set.
* Of these, only Array is considered primitive, and all the others
* require casting to array in order to evaluate their equality.
*
* (But note that we want to deal with String objects as string literals)
*
* @param {Iterable} v1 - One of the two things part of this equality check.
* @param {Iterable} v2 - The other thing that's part of this equality check.
* @param {boolean} strict - True to check with strict type equality, false for coercive equality
* @returns {boolean} True if v1 equals v2, false if not.
* @ignore
*/
function iterableEquals(v1, v2, strict = true) {
if (strict && v1.__proto__.constructor !== v2.__proto__.constructor) {
return false;
}
v1 = Array.from(v1);
v2 = Array.from(v2);
return arrayEquals(v1, v2, strict);
}
/**
* Objects are considered the same if they contain the same unordered
* set of enumerable keys (note: read that again, because "enumerable"
* is not the same as "actually defined"), and each key points to the
* same value (as determined through recursion)
*
* @param {object} v1 - One of the two things part of this equality check.
* @param {object} v2 - The other thing that's part of this equality check.
* @param {boolean} strict - True to check with strict type equality, false for coercive equality
* @returns {boolean} True if v1 equals v2, false if not.
* @ignore
*/
function objectEquals(v1, v2, strict = true) {
// Note that properties that are explicitly assigned as `undefined`
// are functionality identical in a JS engine to nonexistent properties.
const m1 = Object.entries(v1).filter(definedOnly);
const m2 = Object.entries(v2).filter(definedOnly);
// Key ordering is irrelevant, but let's not waste more time
// on sorting if we know these are not equal based on length:
if (m1.length !== m2.length) return false;
m1.sort();
m2.sort();
return m1.every((e, i) => arrayEquals(e, m2[i], strict));
}