@longlast/equals 0.5.3 → 0.5.5

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.
Files changed (2) hide show
  1. package/dist/index.js +83 -18
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  import { curry } from "@longlast/curry";
5
5
  import { $boundArguments, $equals, $getBoundArguments, $unapplied, } from "@longlast/symbols";
6
+ // TODO: export an `Equatable` interface that classes with the [$equals] method
7
+ // can implement.
6
8
  /**
7
9
  * @function
8
10
  * Deeply compares two values, returning true if they're equal and false
@@ -65,39 +67,72 @@ export const equals = curry(_equals);
65
67
  function _equals(a, b) {
66
68
  // This is an optimized implementation. There is a simpler, equivalent one
67
69
  // in pkg/equals/alt/reference.ts.
68
- if (a != null && typeof a[$equals] === "function") {
70
+ if (a == null) {
71
+ return a === b;
72
+ }
73
+ // TODO: (pre-1.0.0) decide if we should pass `equals` as the second
74
+ // argument to [$equals]. This would open the "protocol" up a bit more,
75
+ // since people could then define their own implementations of `equals`
76
+ // that work consistently through custom equality comparisons.
77
+ if (typeof a[$equals] === "function") {
69
78
  return Boolean(a[$equals](b));
70
79
  }
71
80
  if (Object.is(a, b)) {
72
81
  return true;
73
82
  }
74
- if (a instanceof Date && b instanceof Date) {
83
+ if (typeof a === "function" && typeof b === "function") {
84
+ const aUnapplied = a[$unapplied];
85
+ const bUnapplied = b[$unapplied];
86
+ return (aUnapplied != null &&
87
+ aUnapplied === bUnapplied &&
88
+ _equals(getBoundArguments(a), getBoundArguments(b)));
89
+ }
90
+ // If `a` is a primitive at this point, return false, since we already know
91
+ // it is not identical to `b`.
92
+ if (typeof a !== "object") {
93
+ return false;
94
+ }
95
+ const aConstructorString = functionString(constructorOf(a));
96
+ const bConstructorString = functionString(constructorOf(b));
97
+ if (aConstructorString !== bConstructorString) {
98
+ return false;
99
+ }
100
+ if (dateConstructorString === aConstructorString) {
101
+ unsafeNarrow(a);
102
+ unsafeNarrow(b);
75
103
  return Object.is(+a, +b);
76
104
  }
77
- if (a instanceof RegExp && b instanceof RegExp) {
105
+ if (regexConstructorString === aConstructorString) {
78
106
  return String(a) === String(b);
79
107
  }
80
- if (a instanceof Error && b instanceof Error) {
81
- return (a.message === b.message &&
82
- Object.getPrototypeOf(a) === Object.getPrototypeOf(b));
108
+ if ((a instanceof Error && b instanceof Error) ||
109
+ nativeErrorConstructorStrings.includes(aConstructorString)) {
110
+ unsafeNarrow(a);
111
+ unsafeNarrow(b);
112
+ return a.message === b.message;
83
113
  }
84
114
  if (Array.isArray(a) && Array.isArray(b)) {
85
115
  return a.length === b.length && a.every((_, i) => _equals(a[i], b[i]));
86
116
  }
87
- if (a instanceof Set && b instanceof Set) {
117
+ if (setConstructorString === aConstructorString) {
118
+ unsafeNarrow(a);
119
+ unsafeNarrow(b);
88
120
  return a.size === b.size && [...a].every((v) => b.has(v));
89
121
  }
90
- if (typeof a === "function" && typeof b === "function") {
91
- const aUnapplied = a[$unapplied];
92
- const bUnapplied = b[$unapplied];
93
- return (aUnapplied != null &&
94
- aUnapplied === bUnapplied &&
95
- _equals(getBoundArguments(a), getBoundArguments(b)));
96
- }
97
- if (a && b && typeof a === "object" && protoOf(a) === protoOf(b)) {
98
- const bKeys = new Set(Object.keys(b));
99
- for (const key of Object.keys(a)) {
100
- if (!bKeys.has(key)) {
122
+ // TODO: typed arrays
123
+ // TODO: Map
124
+ if (objectConstructorString === aConstructorString ||
125
+ protoOf(a) === protoOf(b)) {
126
+ unsafeNarrow(a);
127
+ unsafeNarrow(b);
128
+ const aKeys = Object.keys(a);
129
+ const bKeys = Object.keys(b);
130
+ if (aKeys.length !== bKeys.length) {
131
+ return false;
132
+ }
133
+ const bKeySet = new Set(bKeys);
134
+ for (const key of aKeys) {
135
+ if (!bKeySet.has(key)) {
101
136
  return false;
102
137
  }
103
138
  if (!_equals(a[key], b[key])) {
@@ -109,6 +144,36 @@ function _equals(a, b) {
109
144
  return false;
110
145
  }
111
146
  function getBoundArguments(f) {
147
+ // TODO: (pre-1.0.0) remove `f[$boundArguments]` fallback.
112
148
  return f[$getBoundArguments]?.() ?? f[$boundArguments];
113
149
  }
150
+ function functionString(f) {
151
+ if (typeof f !== "function") {
152
+ return "";
153
+ }
154
+ return Function.prototype.toString.call(f);
155
+ }
156
+ function constructorOf(value) {
157
+ if (value == null) {
158
+ return null;
159
+ }
160
+ return Object.getPrototypeOf(value)?.constructor;
161
+ }
162
+ function unsafeNarrow(value) {
163
+ value;
164
+ }
114
165
  const protoOf = Object.getPrototypeOf;
166
+ const objectConstructorString = functionString(Object);
167
+ const dateConstructorString = functionString(Date);
168
+ const regexConstructorString = functionString(RegExp);
169
+ const setConstructorString = functionString(Set);
170
+ const nativeErrorConstructorStrings = [
171
+ functionString(Error),
172
+ // TODO: add DOMException? Be sure to check the `name` property.
173
+ functionString(EvalError),
174
+ functionString(RangeError),
175
+ functionString(ReferenceError),
176
+ functionString(SyntaxError),
177
+ functionString(TypeError),
178
+ functionString(URIError),
179
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longlast/equals",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Deeply compares objects",
5
5
  "homepage": "https://longlast.js.org/",
6
6
  "license": "MIT",