-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathequal.ts
134 lines (118 loc) Β· 3.49 KB
/
equal.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Copyright 2021-Present the Unitest authors. All rights reserved. MIT license.
import { isObject } from "../deps.ts";
/** Symbol for equality */
const equality = Symbol.for("equality");
interface Equality {
[equality]: (actual: unknown) => boolean;
toString: () => string;
}
/** Whatever argument is `Equality` or not */
function isEquality(value: object): value is Equality {
return equality in value;
}
/** safe stringify for `Equality` */
function stringifyEquality<T>(value: T): T | string {
if (isObject(value) && isEquality(value)) {
return value.toString();
}
return value;
}
/** equal to `Object` constructor */
function equalConstructor(a: object, b: object): boolean {
return a.constructor === b.constructor ||
a.constructor === Object && !b.constructor ||
!a.constructor && b.constructor === Object;
}
/** is set or not */
function isKeyedCollection(x: unknown): x is Set<unknown> {
return [Symbol.iterator, "size"].every((k) => k in (x as Set<unknown>));
}
/** equal to `Date` */
function equalDate(a: Date, b: Date): boolean {
const aTime = a.getTime();
const bTime = b.getTime();
if (Number.isNaN(aTime) && Number.isNaN(bTime)) {
return true;
}
return aTime === bTime;
}
/** Deep equality comparison used in assertions */
function equal(c: unknown, d: unknown): boolean {
const seen = new Map();
return (function compare(a: unknown, b: unknown): boolean {
if (isObject(a) && isEquality(a)) {
return a[equality](b);
}
if (isObject(b) && isEquality(b)) {
return b[equality](a);
}
if (
((a instanceof RegExp && b instanceof RegExp) ||
(a instanceof URL && b instanceof URL))
) {
return String(a) === String(b);
}
if (a instanceof Date && b instanceof Date) {
return equalDate(a, b);
}
if (Object.is(a, b)) {
return true;
}
if (!isObject(a) || !isObject(b)) return false;
if (!equalConstructor(a, b)) {
return false;
}
if (a instanceof WeakMap || b instanceof WeakMap) {
return false;
}
if (a instanceof WeakSet || b instanceof WeakSet) {
return false;
}
if (seen.get(a) === b) {
return true;
}
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
if (isKeyedCollection(a) && isKeyedCollection(b)) {
if (a.size !== b.size) {
return false;
}
let unmatchedEntries = a.size;
for (const [aKey, aValue] of a.entries()) {
for (const [bKey, bValue] of b.entries()) {
if (
(aKey === aValue && bKey === bValue && compare(aKey, bKey)) ||
(compare(aKey, bKey) && compare(aValue, bValue))
) {
unmatchedEntries--;
}
}
}
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;
}
}
seen.set(a, b);
if (a instanceof WeakRef || b instanceof WeakRef) {
if (!(a instanceof WeakRef && b instanceof WeakRef)) return false;
return compare(a.deref(), b.deref());
}
return true;
})(c, d);
}
export { equal, equalDate, equality, isEquality, stringifyEquality };
export type { Equality };