mirror of
https://github.com/denoland/std.git
synced 2024-11-22 04:59:05 +00:00
0b2497f16e
* fix: update codebase to work with Deno RC * work * fix * fix * fix * fixes * work * update * fixes * fix * revert
153 lines
4.6 KiB
TypeScript
153 lines
4.6 KiB
TypeScript
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
// This module is browser compatible.
|
|
|
|
type KeyedCollection = Set<unknown> | Map<unknown, unknown>;
|
|
function isKeyedCollection(x: unknown): x is KeyedCollection {
|
|
return x instanceof Set || x instanceof Map;
|
|
}
|
|
|
|
function constructorsEqual(a: object, b: object) {
|
|
return a.constructor === b.constructor ||
|
|
a.constructor === Object && !b.constructor ||
|
|
!a.constructor && b.constructor === Object;
|
|
}
|
|
|
|
/**
|
|
* Deep equality comparison used in assertions.
|
|
*
|
|
* @param c The actual value
|
|
* @param d The expected value
|
|
* @returns `true` if the values are deeply equal, `false` otherwise
|
|
*
|
|
* @example Usage
|
|
* ```ts
|
|
* import { equal } from "@std/assert/equal";
|
|
*
|
|
* equal({ foo: "bar" }, { foo: "bar" }); // Returns `true`
|
|
* equal({ foo: "bar" }, { foo: "baz" }); // Returns `false
|
|
* ```
|
|
*/
|
|
export function equal(c: unknown, d: unknown): boolean {
|
|
const seen = new Map();
|
|
return (function compare(a: unknown, b: unknown): boolean {
|
|
// Have to render RegExp & Date for string comparison
|
|
// unless it's mistreated as object
|
|
if (
|
|
a &&
|
|
b &&
|
|
((a instanceof RegExp && b instanceof RegExp) ||
|
|
(a instanceof URL && b instanceof URL))
|
|
) {
|
|
return String(a) === String(b);
|
|
}
|
|
if (a instanceof Date && b instanceof Date) {
|
|
const aTime = a.getTime();
|
|
const bTime = b.getTime();
|
|
// Check for NaN equality manually since NaN is not
|
|
// equal to itself.
|
|
if (Number.isNaN(aTime) && Number.isNaN(bTime)) {
|
|
return true;
|
|
}
|
|
return aTime === bTime;
|
|
}
|
|
if (typeof a === "number" && typeof b === "number") {
|
|
return Number.isNaN(a) && Number.isNaN(b) || a === b;
|
|
}
|
|
if (Object.is(a, b)) {
|
|
return true;
|
|
}
|
|
if (a && typeof a === "object" && b && typeof b === "object") {
|
|
if (a && b && !constructorsEqual(a, b)) {
|
|
return false;
|
|
}
|
|
if (a instanceof WeakMap || b instanceof WeakMap) {
|
|
if (!(a instanceof WeakMap && b instanceof WeakMap)) return false;
|
|
throw new TypeError("cannot compare WeakMap instances");
|
|
}
|
|
if (a instanceof WeakSet || b instanceof WeakSet) {
|
|
if (!(a instanceof WeakSet && b instanceof WeakSet)) return false;
|
|
throw new TypeError("cannot compare WeakSet instances");
|
|
}
|
|
if (a instanceof WeakRef || b instanceof WeakRef) {
|
|
if (!(a instanceof WeakRef && b instanceof WeakRef)) return false;
|
|
return compare(a.deref(), b.deref());
|
|
}
|
|
if (seen.get(a) === b) {
|
|
return true;
|
|
}
|
|
if (Object.keys(a).length !== Object.keys(b).length) {
|
|
return false;
|
|
}
|
|
seen.set(a, b);
|
|
if (isKeyedCollection(a) && isKeyedCollection(b)) {
|
|
if (a.size !== b.size) {
|
|
return false;
|
|
}
|
|
|
|
const aKeys = [...a.keys()];
|
|
const primitiveKeysFastPath = aKeys.every((k) => {
|
|
return typeof k === "string" ||
|
|
typeof k === "number" ||
|
|
typeof k === "boolean" ||
|
|
typeof k === "bigint" ||
|
|
typeof k === "symbol" ||
|
|
k == null;
|
|
});
|
|
if (primitiveKeysFastPath) {
|
|
if (a instanceof Set) {
|
|
return a.symmetricDifference(b).size === 0;
|
|
}
|
|
|
|
for (const key of aKeys) {
|
|
if (
|
|
!b.has(key) ||
|
|
!compare(a.get(key), (b as Map<unknown, unknown>).get(key))
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
let unmatchedEntries = a.size;
|
|
|
|
for (const [aKey, aValue] of a.entries()) {
|
|
for (const [bKey, bValue] of b.entries()) {
|
|
/* Given that Map keys can be references, we need
|
|
* to ensure that they are also deeply equal */
|
|
|
|
if (!compare(aKey, bKey)) continue;
|
|
|
|
if (
|
|
(aKey === aValue && bKey === bValue) ||
|
|
(compare(aValue, bValue))
|
|
) {
|
|
unmatchedEntries--;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return unmatchedEntries === 0;
|
|
}
|
|
const merged = { ...a, ...b };
|
|
for (
|
|
const key of [
|
|
...Object.getOwnPropertyNames(merged),
|
|
...Object.getOwnPropertySymbols(merged),
|
|
]
|
|
) {
|
|
type Key = keyof typeof merged;
|
|
if (!compare(a && a[key as Key], b && b[key as Key])) {
|
|
return false;
|
|
}
|
|
if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
})(c, d);
|
|
}
|