fast-equals 4.0.3 → 5.0.0-beta.1

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 (71) hide show
  1. package/.babelrc +34 -0
  2. package/.prettierrc +4 -0
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +146 -39
  5. package/build/rollup/config.base.js +49 -0
  6. package/build/rollup/config.cjs.js +10 -0
  7. package/build/rollup/config.esm.js +10 -0
  8. package/build/rollup/config.min.js +13 -0
  9. package/build/rollup/config.umd.js +10 -0
  10. package/build/tsconfig/base.json +32 -0
  11. package/build/tsconfig/cjs.json +8 -0
  12. package/build/tsconfig/declarations.json +9 -0
  13. package/build/tsconfig/esm.json +8 -0
  14. package/build/tsconfig/min.json +8 -0
  15. package/build/tsconfig/umd.json +8 -0
  16. package/build/webpack.config.js +57 -0
  17. package/dist/cjs/index.cjs +510 -0
  18. package/dist/cjs/index.cjs.map +1 -0
  19. package/dist/cjs/types/comparator.d.ts +2 -0
  20. package/dist/cjs/types/equals.d.ts +46 -0
  21. package/dist/cjs/types/index.d.ts +56 -0
  22. package/dist/cjs/types/internalTypes.d.ts +36 -0
  23. package/dist/cjs/types/utils.d.ts +19 -0
  24. package/dist/esm/index.mjs +499 -0
  25. package/dist/esm/index.mjs.map +1 -0
  26. package/dist/esm/types/comparator.d.ts +2 -0
  27. package/dist/esm/types/equals.d.ts +46 -0
  28. package/dist/esm/types/index.d.ts +56 -0
  29. package/dist/esm/types/internalTypes.d.ts +36 -0
  30. package/dist/esm/types/utils.d.ts +19 -0
  31. package/dist/min/index.js +1 -0
  32. package/dist/min/types/comparator.d.ts +2 -0
  33. package/dist/min/types/equals.d.ts +46 -0
  34. package/dist/min/types/index.d.ts +56 -0
  35. package/dist/min/types/internalTypes.d.ts +36 -0
  36. package/dist/min/types/utils.d.ts +19 -0
  37. package/dist/umd/index.js +516 -0
  38. package/dist/umd/index.js.map +1 -0
  39. package/dist/umd/types/comparator.d.ts +2 -0
  40. package/dist/umd/types/equals.d.ts +46 -0
  41. package/dist/umd/types/index.d.ts +56 -0
  42. package/dist/umd/types/internalTypes.d.ts +36 -0
  43. package/dist/umd/types/utils.d.ts +19 -0
  44. package/index.d.ts +62 -55
  45. package/package.json +59 -38
  46. package/recipes/explicit-property-check.md +4 -6
  47. package/recipes/legacy-circular-equal-support.md +28 -20
  48. package/recipes/legacy-regexp-support.md +15 -16
  49. package/recipes/non-standard-properties.md +35 -23
  50. package/recipes/using-meta-in-comparison.md +10 -13
  51. package/src/comparator.ts +43 -40
  52. package/src/equals.ts +263 -0
  53. package/src/index.ts +183 -79
  54. package/src/internalTypes.ts +74 -0
  55. package/src/utils.ts +42 -49
  56. package/dist/fast-equals.cjs.js +0 -432
  57. package/dist/fast-equals.cjs.js.map +0 -1
  58. package/dist/fast-equals.esm.js +0 -422
  59. package/dist/fast-equals.esm.js.map +0 -1
  60. package/dist/fast-equals.js +0 -438
  61. package/dist/fast-equals.js.map +0 -1
  62. package/dist/fast-equals.min.js +0 -1
  63. package/dist/fast-equals.mjs +0 -422
  64. package/dist/fast-equals.mjs.map +0 -1
  65. package/recipes/strict-property-descriptor-check.md +0 -42
  66. package/src/arrays.ts +0 -36
  67. package/src/dates.ts +0 -12
  68. package/src/maps.ts +0 -66
  69. package/src/objects.ts +0 -62
  70. package/src/regexps.ts +0 -11
  71. package/src/sets.ts +0 -61
package/src/comparator.ts CHANGED
@@ -1,37 +1,37 @@
1
- import { isPlainObject, isPromiseLike, sameValueZeroEqual } from './utils';
2
-
3
1
  import type {
4
- CreateComparatorCreatorOptions,
2
+ ComparatorConfig,
5
3
  EqualityComparator,
6
- } from '../index.d';
4
+ State,
5
+ } from './internalTypes';
7
6
 
8
7
  const ARGUMENTS_TAG = '[object Arguments]';
9
8
  const BOOLEAN_TAG = '[object Boolean]';
10
9
  const DATE_TAG = '[object Date]';
11
- const REG_EXP_TAG = '[object RegExp]';
12
10
  const MAP_TAG = '[object Map]';
13
11
  const NUMBER_TAG = '[object Number]';
14
12
  const OBJECT_TAG = '[object Object]';
13
+ const REG_EXP_TAG = '[object RegExp]';
15
14
  const SET_TAG = '[object Set]';
16
15
  const STRING_TAG = '[object String]';
17
16
 
18
- const { toString } = Object.prototype;
17
+ const { isArray } = Array;
18
+ const getTag = Object.prototype.toString.call.bind(
19
+ Object.prototype.toString,
20
+ ) as (a: object) => string;
19
21
 
20
22
  export function createComparator<Meta>({
21
23
  areArraysEqual,
22
24
  areDatesEqual,
23
25
  areMapsEqual,
24
26
  areObjectsEqual,
27
+ arePrimitiveWrappersEqual,
25
28
  areRegExpsEqual,
26
29
  areSetsEqual,
27
- createIsNestedEqual,
28
- }: CreateComparatorCreatorOptions<Meta>): EqualityComparator<Meta> {
29
- const isEqual = createIsNestedEqual(comparator as EqualityComparator<Meta>);
30
-
30
+ }: ComparatorConfig<Meta>): EqualityComparator<Meta> {
31
31
  /**
32
32
  * compare the value of the two objects and return true if they are equivalent in values
33
33
  */
34
- function comparator(a: any, b: any, meta: Meta): boolean {
34
+ return function comparator(a: any, b: any, state: State<Meta>): boolean {
35
35
  // If the items are strictly equal, no need to do a value comparison.
36
36
  if (a === b) {
37
37
  return true;
@@ -57,64 +57,69 @@ export function createComparator<Meta>({
57
57
  // `isPlainObject` only checks against the object's own realm. Cross-realm
58
58
  // comparisons are rare, and will be handled in the ultimate fallback, so
59
59
  // we can avoid the `toString.call()` cost unless necessary.
60
- if (isPlainObject(a) && isPlainObject(b)) {
61
- return areObjectsEqual(a, b, isEqual, meta);
60
+ if (a.constructor === Object && b.constructor === Object) {
61
+ return areObjectsEqual(a, b, state);
62
62
  }
63
63
 
64
64
  // `isArray()` works on subclasses and is cross-realm, so we can again avoid
65
65
  // the `toString.call()` cost unless necessary by just checking if either
66
66
  // and then both are arrays.
67
- const aArray = Array.isArray(a);
68
- const bArray = Array.isArray(b);
67
+ const aArray = isArray(a);
68
+ const bArray = isArray(b);
69
69
 
70
70
  if (aArray || bArray) {
71
- return aArray === bArray && areArraysEqual(a, b, isEqual, meta);
71
+ return aArray === bArray && areArraysEqual(a, b, state);
72
72
  }
73
73
 
74
74
  // Since this is a custom object, use the classic `toString.call()` to get its
75
75
  // type. This is reasonably performant in modern environments like v8 and
76
76
  // SpiderMonkey, and allows for cross-realm comparison when other checks like
77
77
  // `instanceof` do not.
78
- const aTag = toString.call(a);
78
+ const tag = getTag(a);
79
79
 
80
- if (aTag !== toString.call(b)) {
80
+ if (tag !== getTag(b)) {
81
81
  return false;
82
82
  }
83
83
 
84
- if (aTag === DATE_TAG) {
85
- // `getTime()` showed better results compared to alternatives like `valueOf()`
86
- // or the unary `+` operator.
87
- return areDatesEqual(a, b, isEqual, meta);
84
+ if (tag === DATE_TAG) {
85
+ return areDatesEqual(a, b, state);
88
86
  }
89
87
 
90
- if (aTag === REG_EXP_TAG) {
91
- return areRegExpsEqual(a, b, isEqual, meta);
88
+ if (tag === REG_EXP_TAG) {
89
+ return areRegExpsEqual(a, b, state);
92
90
  }
93
91
 
94
- if (aTag === MAP_TAG) {
95
- return areMapsEqual(a, b, isEqual, meta);
92
+ if (tag === MAP_TAG) {
93
+ return areMapsEqual(a, b, state);
96
94
  }
97
95
 
98
- if (aTag === SET_TAG) {
99
- return areSetsEqual(a, b, isEqual, meta);
96
+ if (tag === SET_TAG) {
97
+ return areSetsEqual(a, b, state);
100
98
  }
101
99
 
102
100
  // If a simple object tag, then we can prioritize a simple object comparison because
103
- // it is likely a custom class. If an arguments tag, it should be treated as a standard
104
- // object.
105
- if (aTag === OBJECT_TAG || aTag === ARGUMENTS_TAG) {
101
+ // it is likely a custom class.
102
+ if (tag === OBJECT_TAG) {
106
103
  // The exception for value comparison is `Promise`-like contracts. These should be
107
- // treated the same as standard `Promise` objects, which means strict equality.
108
- return isPromiseLike(a) || isPromiseLike(b)
109
- ? false
110
- : areObjectsEqual(a, b, isEqual, meta);
104
+ // treated the same as standard `Promise` objects, which means strict equality, and if
105
+ // it reaches this point then that strict equality comparison has already failed.
106
+ return (
107
+ typeof a.then !== 'function' &&
108
+ typeof b.then !== 'function' &&
109
+ areObjectsEqual(a, b, state)
110
+ );
111
+ }
112
+
113
+ // If an arguments tag, it should be treated as a standard object.
114
+ if (tag === ARGUMENTS_TAG) {
115
+ return areObjectsEqual(a, b, state);
111
116
  }
112
117
 
113
118
  // As the penultimate fallback, check if the values passed are primitive wrappers. This
114
119
  // is very rare in modern JS, which is why it is deprioritized compared to all other object
115
120
  // types.
116
- if (aTag === BOOLEAN_TAG || aTag === NUMBER_TAG || aTag === STRING_TAG) {
117
- return sameValueZeroEqual(a.valueOf(), b.valueOf());
121
+ if (tag === BOOLEAN_TAG || tag === NUMBER_TAG || tag === STRING_TAG) {
122
+ return arePrimitiveWrappersEqual(a, b, state);
118
123
  }
119
124
 
120
125
  // If not matching any tags that require a specific type of comparison, then we hard-code false because
@@ -129,7 +134,5 @@ export function createComparator<Meta>({
129
134
  // In all cases, these decisions should be reevaluated based on changes to the language and
130
135
  // common development practices.
131
136
  return false;
132
- }
133
-
134
- return comparator as EqualityComparator<Meta>;
137
+ };
135
138
  }
package/src/equals.ts ADDED
@@ -0,0 +1,263 @@
1
+ import { getStrictProperties, hasOwn, sameValueZeroEqual } from './utils';
2
+ import type { Dictionary, State } from './internalTypes';
3
+
4
+ const OWNER = '_owner';
5
+
6
+ const { getOwnPropertyDescriptor, keys } = Object;
7
+
8
+ /**
9
+ * Whether the arrays are equal in value.
10
+ */
11
+ export function areArraysEqual(a: any[], b: any[], state: State<any>) {
12
+ let index = a.length;
13
+
14
+ if (b.length !== index) {
15
+ return false;
16
+ }
17
+
18
+ while (index-- > 0) {
19
+ if (!state.equals(a[index], b[index], index, index, a, b, state)) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ return true;
25
+ }
26
+
27
+ /**
28
+ * Whether the dates passed are equal in value.
29
+ *
30
+ * @NOTE
31
+ * This is a standalone function instead of done inline in the comparator
32
+ * to allow for overrides.
33
+ */
34
+ export function areDatesEqual(a: Date, b: Date): boolean {
35
+ return sameValueZeroEqual(a.getTime(), b.getTime());
36
+ }
37
+
38
+ /**
39
+ * Whether the `Map`s are equal in value.
40
+ */
41
+ export function areMapsEqual(
42
+ a: Map<any, any>,
43
+ b: Map<any, any>,
44
+ state: State<any>,
45
+ ): boolean {
46
+ let isValueEqual = a.size === b.size;
47
+
48
+ if (isValueEqual && a.size) {
49
+ // The use of `forEach()` is to avoid the transpilation cost of `for...of` comparisons, and
50
+ // the inability to control the performance of the resulting code. It also avoids excessive
51
+ // iteration compared to doing comparisons of `keys()` and `values()`. As a result, though,
52
+ // we cannot short-circuit the iterations; bookkeeping must be done to short-circuit the
53
+ // equality checks themselves.
54
+
55
+ const matchedIndices: Record<number, true> = {};
56
+
57
+ let indexA = 0;
58
+
59
+ a.forEach((aValue, aKey) => {
60
+ if (!isValueEqual) {
61
+ return;
62
+ }
63
+
64
+ let hasMatch = false;
65
+ let matchIndexB = 0;
66
+
67
+ b.forEach((bValue, bKey) => {
68
+ if (
69
+ !hasMatch &&
70
+ !matchedIndices[matchIndexB] &&
71
+ (hasMatch =
72
+ state.equals(aKey, bKey, indexA, matchIndexB, a, b, state) &&
73
+ state.equals(aValue, bValue, aKey, bKey, a, b, state))
74
+ ) {
75
+ matchedIndices[matchIndexB] = true;
76
+ }
77
+
78
+ matchIndexB++;
79
+ });
80
+
81
+ indexA++;
82
+ isValueEqual = hasMatch;
83
+ });
84
+ }
85
+
86
+ return isValueEqual;
87
+ }
88
+
89
+ /**
90
+ * Whether the objects are equal in value.
91
+ */
92
+ export function areObjectsEqual(
93
+ a: Dictionary,
94
+ b: Dictionary,
95
+ state: State<any>,
96
+ ): boolean {
97
+ const properties = keys(a);
98
+
99
+ let index = properties.length;
100
+
101
+ if (keys(b).length !== index) {
102
+ return false;
103
+ }
104
+
105
+ let property: string | symbol;
106
+
107
+ // Decrementing `while` showed faster results than either incrementing or
108
+ // decrementing `for` loop and than an incrementing `while` loop. Declarative
109
+ // methods like `some` / `every` were not used to avoid incurring the garbage
110
+ // cost of anonymous callbacks.
111
+ while (index-- > 0) {
112
+ property = properties[index]!;
113
+
114
+ if (
115
+ property === OWNER &&
116
+ (a.$$typeof || b.$$typeof) &&
117
+ a.$$typeof !== b.$$typeof
118
+ ) {
119
+ return false;
120
+ }
121
+
122
+ if (
123
+ !hasOwn(b, property) ||
124
+ !state.equals(a[property], b[property], property, property, a, b, state)
125
+ ) {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Whether the objects are equal in value with strict property checking.
135
+ */
136
+ export function areObjectsEqualStrict(
137
+ a: Dictionary,
138
+ b: Dictionary,
139
+ state: State<any>,
140
+ ): boolean {
141
+ const properties = getStrictProperties(a);
142
+
143
+ let index = properties.length;
144
+
145
+ if (getStrictProperties(b).length !== index) {
146
+ return false;
147
+ }
148
+
149
+ let property: string | symbol;
150
+ let descriptorA: ReturnType<typeof getOwnPropertyDescriptor>;
151
+ let descriptorB: ReturnType<typeof getOwnPropertyDescriptor>;
152
+
153
+ // Decrementing `while` showed faster results than either incrementing or
154
+ // decrementing `for` loop and than an incrementing `while` loop. Declarative
155
+ // methods like `some` / `every` were not used to avoid incurring the garbage
156
+ // cost of anonymous callbacks.
157
+ while (index-- > 0) {
158
+ property = properties[index]!;
159
+
160
+ if (
161
+ property === OWNER &&
162
+ (a.$$typeof || b.$$typeof) &&
163
+ a.$$typeof !== b.$$typeof
164
+ ) {
165
+ return false;
166
+ }
167
+
168
+ if (!hasOwn(b, property)) {
169
+ return false;
170
+ }
171
+
172
+ if (
173
+ !state.equals(a[property], b[property], property, property, a, b, state)
174
+ ) {
175
+ return false;
176
+ }
177
+
178
+ descriptorA = getOwnPropertyDescriptor(a, property);
179
+ descriptorB = getOwnPropertyDescriptor(b, property);
180
+
181
+ if (
182
+ (descriptorA || descriptorB) &&
183
+ (!descriptorA ||
184
+ !descriptorB ||
185
+ descriptorA.configurable !== descriptorB.configurable ||
186
+ descriptorA.enumerable !== descriptorB.enumerable ||
187
+ descriptorA.writable !== descriptorB.writable)
188
+ ) {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Whether the primitive wrappers passed are equal in value.
198
+ *
199
+ * @NOTE
200
+ * This is a standalone function instead of done inline in the comparator
201
+ * to allow for overrides.
202
+ */
203
+ export function arePrimitiveWrappersEqual(a: Date, b: Date): boolean {
204
+ return sameValueZeroEqual(a.valueOf(), b.valueOf());
205
+ }
206
+
207
+ /**
208
+ * Whether the regexps passed are equal in value.
209
+ *
210
+ * @NOTE
211
+ * This is a standalone function instead of done inline in the comparator
212
+ * to allow for overrides. An example of this would be supporting a
213
+ * pre-ES2015 environment where the `flags` property is not available.
214
+ */
215
+ export function areRegExpsEqual(a: RegExp, b: RegExp): boolean {
216
+ return a.source === b.source && a.flags === b.flags;
217
+ }
218
+
219
+ /**
220
+ * Whether the `Set`s are equal in value.
221
+ */
222
+ export function areSetsEqual(
223
+ a: Set<any>,
224
+ b: Set<any>,
225
+ state: State<any>,
226
+ ): boolean {
227
+ let isValueEqual = a.size === b.size;
228
+
229
+ if (isValueEqual && a.size) {
230
+ // The use of `forEach()` is to avoid the transpilation cost of `for...of` comparisons, and
231
+ // the inability to control the performance of the resulting code. It also avoids excessive
232
+ // iteration compared to doing comparisons of `keys()` and `values()`. As a result, though,
233
+ // we cannot short-circuit the iterations; bookkeeping must be done to short-circuit the
234
+ // equality checks themselves.
235
+
236
+ const matchedIndices: Record<number, true> = {};
237
+
238
+ a.forEach((aValue, aKey) => {
239
+ if (!isValueEqual) {
240
+ return;
241
+ }
242
+
243
+ let hasMatch = false;
244
+ let matchIndex = 0;
245
+
246
+ b.forEach((bValue, bKey) => {
247
+ if (
248
+ !hasMatch &&
249
+ !matchedIndices[matchIndex] &&
250
+ (hasMatch = state.equals(aValue, bValue, aKey, bKey, a, b, state))
251
+ ) {
252
+ matchedIndices[matchIndex] = true;
253
+ }
254
+
255
+ matchIndex++;
256
+ });
257
+
258
+ isValueEqual = hasMatch;
259
+ });
260
+ }
261
+
262
+ return isValueEqual;
263
+ }