2024-01-01 21:11:32 +00:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
2024-04-10 02:43:44 +00:00
|
|
|
// This module is browser compatible.
|
2024-09-07 00:11:42 +00:00
|
|
|
|
|
|
|
type KeyedCollection = Set<unknown> | Map<unknown, unknown>;
|
|
|
|
function isKeyedCollection(x: unknown): x is KeyedCollection {
|
|
|
|
return x instanceof Set || x instanceof Map;
|
|
|
|
}
|
|
|
|
|
2024-11-06 10:38:19 +00:00
|
|
|
function prototypesEqual(a: object, b: object) {
|
|
|
|
const pa = Object.getPrototypeOf(a);
|
|
|
|
const pb = Object.getPrototypeOf(b);
|
|
|
|
return pa === pb ||
|
|
|
|
pa === Object.prototype && pb === null ||
|
|
|
|
pa === null && pb === Object.prototype;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isBasicObjectOrArray(obj: object) {
|
|
|
|
const proto = Object.getPrototypeOf(obj);
|
|
|
|
return proto === null || proto === Object.prototype ||
|
|
|
|
proto === Array.prototype;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Slightly faster than Reflect.ownKeys in V8 as of 12.9.202.13-rusty (2024-10-28)
|
|
|
|
function ownKeys(obj: object) {
|
|
|
|
return [
|
|
|
|
...Object.getOwnPropertyNames(obj),
|
|
|
|
...Object.getOwnPropertySymbols(obj),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
function getKeysDeep(obj: object) {
|
|
|
|
const keys = new Set<string | symbol>();
|
|
|
|
|
|
|
|
while (obj !== Object.prototype && obj !== Array.prototype && obj != null) {
|
|
|
|
for (const key of ownKeys(obj)) {
|
|
|
|
keys.add(key);
|
|
|
|
}
|
|
|
|
obj = Object.getPrototypeOf(obj);
|
|
|
|
}
|
|
|
|
|
|
|
|
return keys;
|
|
|
|
}
|
|
|
|
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
const Temporal: any = (globalThis as any).Temporal ??
|
|
|
|
new Proxy({}, { get: () => {} });
|
|
|
|
|
|
|
|
/** A non-exhaustive list of prototypes that can be accurately fast-path compared with `String(instance)` */
|
|
|
|
const stringComparablePrototypes = new Set<unknown>(
|
|
|
|
[
|
|
|
|
Intl.Locale,
|
|
|
|
RegExp,
|
|
|
|
Temporal.Duration,
|
|
|
|
Temporal.Instant,
|
|
|
|
Temporal.PlainDate,
|
|
|
|
Temporal.PlainDateTime,
|
|
|
|
Temporal.PlainTime,
|
|
|
|
Temporal.PlainYearMonth,
|
|
|
|
Temporal.PlainMonthDay,
|
|
|
|
Temporal.ZonedDateTime,
|
|
|
|
URL,
|
|
|
|
URLSearchParams,
|
|
|
|
].filter((x) => x != null).map((x) => x.prototype),
|
|
|
|
);
|
|
|
|
|
|
|
|
function isPrimitive(x: unknown) {
|
|
|
|
return typeof x === "string" ||
|
|
|
|
typeof x === "number" ||
|
|
|
|
typeof x === "boolean" ||
|
|
|
|
typeof x === "bigint" ||
|
|
|
|
typeof x === "symbol" ||
|
|
|
|
x == null;
|
|
|
|
}
|
|
|
|
|
|
|
|
type TypedArray = Pick<Uint8Array | BigUint64Array, "length" | number>;
|
|
|
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
|
|
function compareTypedArrays(a: TypedArray, b: TypedArray) {
|
|
|
|
if (a.length !== b.length) return false;
|
|
|
|
for (let i = 0; i < b.length; i++) {
|
|
|
|
if (!sameValueZero(a[i], b[i])) return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Check both strict equality (`0 == -0`) and `Object.is` (`NaN == NaN`) */
|
|
|
|
function sameValueZero(a: unknown, b: unknown) {
|
|
|
|
return a === b || Object.is(a, b);
|
2023-07-13 07:04:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-09-19 23:29:31 +00:00
|
|
|
* Deep equality comparison used in assertions.
|
2023-12-06 17:13:38 +00:00
|
|
|
*
|
2024-11-06 10:38:19 +00:00
|
|
|
* @param a The actual value
|
|
|
|
* @param b The expected value
|
2024-05-30 02:38:16 +00:00
|
|
|
* @returns `true` if the values are deeply equal, `false` otherwise
|
|
|
|
*
|
|
|
|
* @example Usage
|
2023-12-06 17:13:38 +00:00
|
|
|
* ```ts
|
2024-09-19 23:29:31 +00:00
|
|
|
* import { equal } from "@std/assert/equal";
|
2023-12-06 17:13:38 +00:00
|
|
|
*
|
|
|
|
* equal({ foo: "bar" }, { foo: "bar" }); // Returns `true`
|
2024-11-06 10:38:19 +00:00
|
|
|
* equal({ foo: "bar" }, { foo: "baz" }); // Returns `false`
|
2023-12-06 17:13:38 +00:00
|
|
|
* ```
|
2023-07-13 07:04:30 +00:00
|
|
|
*/
|
2024-11-06 10:38:19 +00:00
|
|
|
export function equal(a: unknown, b: unknown): boolean {
|
|
|
|
const seen = new Map<unknown, unknown>();
|
2023-07-13 07:04:30 +00:00
|
|
|
return (function compare(a: unknown, b: unknown): boolean {
|
2024-11-06 10:38:19 +00:00
|
|
|
if (sameValueZero(a, b)) return true;
|
|
|
|
if (isPrimitive(a) || isPrimitive(b)) return false;
|
|
|
|
|
2023-07-13 07:04:30 +00:00
|
|
|
if (a instanceof Date && b instanceof Date) {
|
2024-11-06 10:38:19 +00:00
|
|
|
return Object.is(a.getTime(), b.getTime());
|
2023-07-13 07:04:30 +00:00
|
|
|
}
|
|
|
|
if (a && typeof a === "object" && b && typeof b === "object") {
|
2024-11-06 10:38:19 +00:00
|
|
|
if (!prototypesEqual(a, b)) {
|
2023-07-13 07:04:30 +00:00
|
|
|
return false;
|
|
|
|
}
|
2024-11-06 10:38:19 +00:00
|
|
|
if (a instanceof TypedArray) {
|
|
|
|
return compareTypedArrays(a as TypedArray, b as TypedArray);
|
|
|
|
}
|
|
|
|
if (a instanceof WeakMap) {
|
2023-07-13 07:04:30 +00:00
|
|
|
throw new TypeError("cannot compare WeakMap instances");
|
|
|
|
}
|
2024-11-06 10:38:19 +00:00
|
|
|
if (a instanceof WeakSet) {
|
2023-07-13 07:04:30 +00:00
|
|
|
throw new TypeError("cannot compare WeakSet instances");
|
|
|
|
}
|
2024-11-06 10:38:19 +00:00
|
|
|
if (a instanceof WeakRef) {
|
|
|
|
return compare(a.deref(), (b as WeakRef<WeakKey>).deref());
|
2024-05-07 00:08:16 +00:00
|
|
|
}
|
2023-07-13 07:04:30 +00:00
|
|
|
if (seen.get(a) === b) {
|
|
|
|
return true;
|
|
|
|
}
|
2024-05-07 00:08:16 +00:00
|
|
|
if (Object.keys(a).length !== Object.keys(b).length) {
|
2023-07-13 07:04:30 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
seen.set(a, b);
|
|
|
|
if (isKeyedCollection(a) && isKeyedCollection(b)) {
|
|
|
|
if (a.size !== b.size) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-09-07 00:11:42 +00:00
|
|
|
const aKeys = [...a.keys()];
|
2024-11-06 10:38:19 +00:00
|
|
|
const primitiveKeysFastPath = aKeys.every(isPrimitive);
|
2024-09-07 00:11:42 +00:00
|
|
|
if (primitiveKeysFastPath) {
|
|
|
|
if (a instanceof Set) {
|
|
|
|
return a.symmetricDifference(b).size === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const key of aKeys) {
|
|
|
|
if (
|
|
|
|
!b.has(key) ||
|
2024-09-09 04:11:27 +00:00
|
|
|
!compare(a.get(key), (b as Map<unknown, unknown>).get(key))
|
2024-09-07 00:11:42 +00:00
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-07-13 07:04:30 +00:00
|
|
|
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 */
|
2024-09-07 00:11:42 +00:00
|
|
|
|
2024-09-09 05:01:36 +00:00
|
|
|
if (!compare(aKey, bKey)) continue;
|
|
|
|
|
2023-07-13 07:04:30 +00:00
|
|
|
if (
|
2024-09-09 05:01:36 +00:00
|
|
|
(aKey === aValue && bKey === bValue) ||
|
|
|
|
(compare(aValue, bValue))
|
2023-07-13 07:04:30 +00:00
|
|
|
) {
|
|
|
|
unmatchedEntries--;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return unmatchedEntries === 0;
|
|
|
|
}
|
2024-11-06 10:38:19 +00:00
|
|
|
|
|
|
|
let keys: Iterable<string | symbol>;
|
|
|
|
|
|
|
|
if (isBasicObjectOrArray(a)) {
|
|
|
|
// fast path
|
|
|
|
keys = ownKeys({ ...a, ...b });
|
|
|
|
} else if (stringComparablePrototypes.has(Object.getPrototypeOf(a))) {
|
|
|
|
// medium path
|
|
|
|
return String(a) === String(b);
|
|
|
|
} else {
|
|
|
|
// slow path
|
|
|
|
keys = getKeysDeep(a).union(getKeysDeep(b));
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const key of keys) {
|
|
|
|
type Key = keyof typeof a;
|
|
|
|
if (!compare(a[key as Key], b[key as Key])) {
|
2023-07-13 07:04:30 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2024-11-06 10:38:19 +00:00
|
|
|
})(a, b);
|
2023-07-13 07:04:30 +00:00
|
|
|
}
|