@mark1russell7/collections 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/collections",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "Collections with no dependencies: lists, maps, queues, a summary tree, channels, lazy operations and dependency graphs.",
6
6
  "keywords": [
7
7
  "collections",
@@ -39,37 +39,135 @@ export const shallowEq: Eq<any> = (a, b) => {
39
39
  return true;
40
40
  };
41
41
 
42
+ /** SameValueZero: the meaning of `===`, and NaN equals NaN. */
43
+ const sameValueZero = (a: unknown, b: unknown): boolean => a === b || (Number.isNaN(a) && Number.isNaN(b));
44
+
45
+ const isObject = (value: unknown): value is object => typeof value === "object" && value !== null;
46
+
47
+ /** The pairs that one comparison holds equal. The log lets a failed try remove its pairs. */
48
+ interface HeldPairs {
49
+ readonly partners: Map<object, Set<object>>;
50
+ readonly log: [object, object][];
51
+ }
52
+
53
+ function releasePairs(held: HeldPairs, mark: number): void {
54
+ while (held.log.length > mark) {
55
+ const [a, b] = held.log.pop()!;
56
+ held.partners.get(a)?.delete(b);
57
+ }
58
+ }
59
+
60
+ function equalElements(a: ArrayLike<unknown>, b: ArrayLike<unknown>): boolean {
61
+ if (a.length !== b.length) return false;
62
+ for (let i = 0; i < a.length; i++) {
63
+ if (!sameValueZero(a[i], b[i])) return false;
64
+ }
65
+ return true;
66
+ }
67
+
68
+ function bytesOf(value: ArrayBufferLike | DataView): Uint8Array {
69
+ return ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : new Uint8Array(value);
70
+ }
71
+
72
+ function enumerableKeys(value: object): (string | symbol)[] {
73
+ return Reflect.ownKeys(value).filter((key) => Object.prototype.propertyIsEnumerable.call(value, key));
74
+ }
75
+
76
+ function equalKeys(a: any, b: any, held: HeldPairs): boolean {
77
+ const keys = enumerableKeys(a);
78
+ if (keys.length !== enumerableKeys(b).length) return false;
79
+ return keys.every((key) => Object.prototype.propertyIsEnumerable.call(b, key) && equalValues(a[key], b[key], held));
80
+ }
81
+
42
82
  /**
43
- * Deep structural equality (recursive comparison).
44
- * This function handles nested objects and arrays.
83
+ * The comparison of the entries of two maps, or of the members of two sets. A primitive key finds
84
+ * its partner by `has`. An object key tries each object key of `b` that is not taken yet. The first
85
+ * partner is correct, because the relation is an equivalence.
45
86
  */
46
- export const deepEq: Eq<any> = (a, b): boolean => {
47
- if (a === b) return true;
48
- if (typeof a !== typeof b) return false;
49
- if (typeof a !== "object" || a === null || b === null) return false;
87
+ function equalEntries(a: Map<unknown, unknown> | Set<unknown>, b: Map<unknown, unknown> | Set<unknown>, held: HeldPairs): boolean {
88
+ if (a.size !== b.size) return false;
89
+ const valueAt = (key: unknown): unknown => (b instanceof Map ? b.get(key) : key);
90
+ const free = [...b.entries()].filter(([key]) => isObject(key));
91
+ for (const [key, value] of a.entries()) {
92
+ if (!isObject(key)) {
93
+ if (!b.has(key) || !equalValues(value, valueAt(key), held)) return false;
94
+ continue;
95
+ }
96
+ const at = free.findIndex(([otherKey, otherValue]) => {
97
+ const mark = held.log.length;
98
+ if (equalValues(key, otherKey, held) && equalValues(value, otherValue, held)) return true;
99
+ releasePairs(held, mark);
100
+ return false;
101
+ });
102
+ if (at < 0) return false;
103
+ free.splice(at, 1);
104
+ }
105
+ return true;
106
+ }
50
107
 
51
- if (Array.isArray(a) && Array.isArray(b)) {
52
- if (a.length !== b.length) return false;
108
+ /** The comparison of two objects with the same prototype. */
109
+ function equalObjects(a: any, b: any, held: HeldPairs): boolean {
110
+ if (Array.isArray(a) || Array.isArray(b)) {
111
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
53
112
  for (let i = 0; i < a.length; i++) {
54
- if (!deepEq(a[i], b[i])) return false;
113
+ if (!equalValues(a[i], b[i], held)) return false;
55
114
  }
56
115
  return true;
57
116
  }
117
+ if (a instanceof DataView || a instanceof ArrayBuffer || (typeof SharedArrayBuffer !== "undefined" && a instanceof SharedArrayBuffer)) {
118
+ return equalElements(bytesOf(a), bytesOf(b));
119
+ }
120
+ if (ArrayBuffer.isView(a)) return equalElements(a as unknown as ArrayLike<unknown>, b);
121
+ if (a instanceof Promise || a instanceof WeakMap || a instanceof WeakSet || a instanceof WeakRef) return false;
122
+ if (a instanceof Date && !sameValueZero(a.getTime(), b.getTime())) return false;
123
+ if (a instanceof RegExp && (a.source !== b.source || a.flags !== b.flags)) return false;
124
+ if (a instanceof Error && !(a.name === b.name && a.message === b.message && equalValues(a.cause, b.cause, held))) return false;
125
+ const boxed = a instanceof Number || a instanceof String || a instanceof Boolean || a instanceof BigInt || a instanceof Symbol;
126
+ if (boxed && !sameValueZero(a.valueOf(), b.valueOf())) return false;
127
+ if ((a instanceof Map || a instanceof Set) && !equalEntries(a, b, held)) return false;
128
+ return equalKeys(a, b, held);
129
+ }
58
130
 
59
- if (Array.isArray(a) !== Array.isArray(b)) return false;
60
-
61
- const keysA = Object.keys(a);
62
- const keysB = Object.keys(b);
63
-
64
- if (keysA.length !== keysB.length) return false;
65
-
66
- for (const key of keysA) {
67
- if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
68
- if (!deepEq(a[key], b[key])) return false;
131
+ /**
132
+ * The comparison of two values. A pair stays held after it passes, so a shared or cyclic part
133
+ * costs one visit. A failure ends the whole comparison, or the try in `equalEntries`, which
134
+ * releases its pairs.
135
+ */
136
+ function equalValues(a: unknown, b: unknown, held: HeldPairs): boolean {
137
+ if (sameValueZero(a, b)) return true;
138
+ if (!isObject(a) || !isObject(b)) return false;
139
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
140
+ let partners = held.partners.get(a);
141
+ if (partners?.has(b)) return true;
142
+ if (partners === undefined) {
143
+ partners = new Set();
144
+ held.partners.set(a, partners);
69
145
  }
146
+ partners.add(b);
147
+ held.log.push([a, b]);
148
+ return equalObjects(a, b, held);
149
+ }
70
150
 
71
- return true;
72
- };
151
+ /**
152
+ * Deep structural equality. This relation is an equivalence: it is reflexive, symmetric and
153
+ * transitive, for each value.
154
+ *
155
+ * - Primitives compare by SameValueZero: NaN equals NaN, and 0 equals -0.
156
+ * - Two objects must have the same prototype. So `{}` differs from `Object.create(null)`, from an
157
+ * instance of a class with the same keys, and from an object of another realm.
158
+ * - Arrays compare by length and by index. A hole equals `undefined`.
159
+ * - Typed arrays compare by element. `ArrayBuffer`, `SharedArrayBuffer` and `DataView` compare by byte.
160
+ * - `Date` compares by time, `RegExp` by source and flags, and `Error` by name, message and cause.
161
+ * - A boxed primitive compares by its value. `Map` and `Set` compare by entries, in any order. An object key
162
+ * matches a deep-equal key.
163
+ * - Each other object compares by its own enumerable keys, strings and symbols, in any order. The objects above do too,
164
+ * except arrays and binary data.
165
+ * - Functions, `Promise`, `WeakMap`, `WeakSet` and `WeakRef` compare by identity.
166
+ * - Cycles are safe: two cyclic values are equal when they unfold to the same infinite tree.
167
+ *
168
+ * The recursion follows the depth of the data, so a very deep value can exhaust the stack.
169
+ */
170
+ export const deepEq: Eq<any> = (a, b): boolean => equalValues(a, b, { partners: new Map(), log: [] });
73
171
 
74
172
  /**
75
173
  * Equality by comparing a specific property.