fast-equals 3.0.2 → 4.0.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.
@@ -0,0 +1,146 @@
1
+ import { isPlainObject, isPromiseLike, sameValueZeroEqual } from './utils';
2
+
3
+ import type {
4
+ EqualityComparator,
5
+ EqualityComparatorCreator,
6
+ TypeEqualityComparator,
7
+ } from './utils';
8
+
9
+ export interface CreateComparatorCreatorOptions<Meta> {
10
+ areArraysEqual: TypeEqualityComparator<any[], Meta>;
11
+ areDatesEqual: TypeEqualityComparator<Date, Meta>;
12
+ areMapsEqual: TypeEqualityComparator<Map<any, any>, Meta>;
13
+ areObjectsEqual: TypeEqualityComparator<Record<string, any>, Meta>;
14
+ areRegExpsEqual: TypeEqualityComparator<RegExp, Meta>;
15
+ areSetsEqual: TypeEqualityComparator<Set<any>, Meta>;
16
+ createIsNestedEqual: EqualityComparatorCreator<Meta>;
17
+ }
18
+
19
+ const ARGUMENTS_TAG = '[object Arguments]';
20
+ const BOOLEAN_TAG = '[object Boolean]';
21
+ const DATE_TAG = '[object Date]';
22
+ const REG_EXP_TAG = '[object RegExp]';
23
+ const MAP_TAG = '[object Map]';
24
+ const NUMBER_TAG = '[object Number]';
25
+ const OBJECT_TAG = '[object Object]';
26
+ const SET_TAG = '[object Set]';
27
+ const STRING_TAG = '[object String]';
28
+
29
+ const { toString } = Object.prototype;
30
+
31
+ export function createComparator<Meta>({
32
+ areArraysEqual,
33
+ areDatesEqual,
34
+ areMapsEqual,
35
+ areObjectsEqual,
36
+ areRegExpsEqual,
37
+ areSetsEqual,
38
+ createIsNestedEqual,
39
+ }: CreateComparatorCreatorOptions<Meta>): EqualityComparator<Meta> {
40
+ const isEqual = createIsNestedEqual(comparator as EqualityComparator<Meta>);
41
+
42
+ /**
43
+ * compare the value of the two objects and return true if they are equivalent in values
44
+ */
45
+ function comparator(a: any, b: any, meta: Meta): boolean {
46
+ // If the items are strictly equal, no need to do a value comparison.
47
+ if (a === b) {
48
+ return true;
49
+ }
50
+
51
+ // If the items are not non-nullish objects, then the only possibility
52
+ // of them being equal but not strictly is if they are both `NaN`. Since
53
+ // `NaN` is uniquely not equal to itself, we can use self-comparison of
54
+ // both objects, which is faster than `isNaN()`.
55
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') {
56
+ return a !== a && b !== b;
57
+ }
58
+
59
+ // Checks are listed in order of commonality of use-case:
60
+ // 1. Common complex object types (plain object, array)
61
+ // 2. Common data values (date, regexp)
62
+ // 3. Less-common complex object types (map, set)
63
+ // 4. Less-common data values (promise, primitive wrappers)
64
+ // Inherently this is both subjective and assumptive, however
65
+ // when reviewing comparable libraries in the wild this order
66
+ // appears to be generally consistent.
67
+
68
+ // `isPlainObject` only checks against the object's own realm. Cross-realm
69
+ // comparisons are rare, and will be handled in the ultimate fallback, so
70
+ // we can avoid the `toString.call()` cost unless necessary.
71
+ if (isPlainObject(a) && isPlainObject(b)) {
72
+ return areObjectsEqual(a, b, isEqual, meta);
73
+ }
74
+
75
+ // `isArray()` works on subclasses and is cross-realm, so we can again avoid
76
+ // the `toString.call()` cost unless necessary by just checking if either
77
+ // and then both are arrays.
78
+ const aArray = Array.isArray(a);
79
+ const bArray = Array.isArray(b);
80
+
81
+ if (aArray || bArray) {
82
+ return aArray === bArray && areArraysEqual(a, b, isEqual, meta);
83
+ }
84
+
85
+ // Since this is a custom object, use the classic `toString.call()` to get its
86
+ // type. This is reasonably performant in modern environments like v8 and
87
+ // SpiderMonkey, and allows for cross-realm comparison when other checks like
88
+ // `instanceof` do not.
89
+ const aTag = toString.call(a);
90
+
91
+ if (aTag !== toString.call(b)) {
92
+ return false;
93
+ }
94
+
95
+ if (aTag === DATE_TAG) {
96
+ // `getTime()` showed better results compared to alternatives like `valueOf()`
97
+ // or the unary `+` operator.
98
+ return areDatesEqual(a, b, isEqual, meta);
99
+ }
100
+
101
+ if (aTag === REG_EXP_TAG) {
102
+ return areRegExpsEqual(a, b, isEqual, meta);
103
+ }
104
+
105
+ if (aTag === MAP_TAG) {
106
+ return areMapsEqual(a, b, isEqual, meta);
107
+ }
108
+
109
+ if (aTag === SET_TAG) {
110
+ return areSetsEqual(a, b, isEqual, meta);
111
+ }
112
+
113
+ // If a simple object tag, then we can prioritize a simple object comparison because
114
+ // it is likely a custom class. If an arguments tag, it should be treated as a standard
115
+ // object.
116
+ if (aTag === OBJECT_TAG || aTag === ARGUMENTS_TAG) {
117
+ // The exception for value comparison is `Promise`-like contracts. These should be
118
+ // treated the same as standard `Promise` objects, which means strict equality.
119
+ return isPromiseLike(a) || isPromiseLike(b)
120
+ ? a === b
121
+ : areObjectsEqual(a, b, isEqual, meta);
122
+ }
123
+
124
+ // As the penultimate fallback, check if the values passed are primitive wrappers. This
125
+ // is very rare in modern JS, which is why it is deprioritized compared to all other object
126
+ // types.
127
+ if (aTag === BOOLEAN_TAG || aTag === NUMBER_TAG || aTag === STRING_TAG) {
128
+ return sameValueZeroEqual(a.valueOf(), b.valueOf());
129
+ }
130
+
131
+ // If not matching any tags that require a specific type of comparison, then use strict
132
+ // equality. This is for a few reasons:
133
+ // - For types that cannot be introspected (`Promise`, `WeakMap`, etc.), this is the only
134
+ // comparison that can be made.
135
+ // - For types that can be introspected, but rarely have requirements to be compared
136
+ // (`ArrayBuffer`, `DataView`, etc.), the cost is avoided to prioritize the common
137
+ // use-cases.
138
+ // - For types that can be introspected, but do not have an objective definition of what
139
+ // equality is (`Error`, etc.), the subjective decision was to be conservative.
140
+ // In all cases, these decisions should be reevaluated based on changes to the language and
141
+ // common development practices.
142
+ return a === b;
143
+ }
144
+
145
+ return comparator as EqualityComparator<Meta>;
146
+ }
package/src/dates.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { sameValueZeroEqual } from './utils';
2
+
3
+ /**
4
+ * Whether the dates passed are equal in value.
5
+ *
6
+ * @NOTE
7
+ * This is a standalone function instead of done inline in the comparator
8
+ * to allow for overrides.
9
+ */
10
+ export function areDatesEqual(a: Date, b: Date): boolean {
11
+ return sameValueZeroEqual(a.valueOf(), b.valueOf());
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { createComparator } from './comparator';
2
+ import { areArraysEqual, areArraysEqualCircular } from './arrays';
3
+ import { areDatesEqual } from './dates';
4
+ import { areMapsEqual, areMapsEqualCircular } from './maps';
5
+ import { areObjectsEqual, areObjectsEqualCircular } from './objects';
6
+ import { areRegExpsEqual } from './regexps';
7
+ import { areSetsEqual, areSetsEqualCircular } from './sets';
8
+ import {
9
+ createDefaultIsNestedEqual,
10
+ EqualityComparator,
11
+ merge,
12
+ sameValueZeroEqual,
13
+ } from './utils';
14
+
15
+ import type { CreateComparatorCreatorOptions } from './comparator';
16
+
17
+ export { sameValueZeroEqual };
18
+
19
+ export type { CreateComparatorCreatorOptions } from './comparator';
20
+ export type {
21
+ EqualityComparator,
22
+ EqualityComparatorCreator,
23
+ InternalEqualityComparator,
24
+ NativeEqualityComparator,
25
+ TypeEqualityComparator,
26
+ } from './utils';
27
+
28
+ export type BaseCircularMeta = Pick<
29
+ WeakMap<any, any>,
30
+ 'delete' | 'get' | 'set'
31
+ >;
32
+
33
+ export type GetComparatorOptions<Meta> = (
34
+ defaultOptions: CreateComparatorCreatorOptions<Meta>,
35
+ ) => Partial<CreateComparatorCreatorOptions<Meta>>;
36
+
37
+ const DEFAULT_CONFIG: CreateComparatorCreatorOptions<undefined> = Object.freeze(
38
+ {
39
+ areArraysEqual,
40
+ areDatesEqual,
41
+ areMapsEqual,
42
+ areObjectsEqual,
43
+ areRegExpsEqual,
44
+ areSetsEqual,
45
+ createIsNestedEqual: createDefaultIsNestedEqual,
46
+ },
47
+ );
48
+ const DEFAULT_CIRCULAR_CONFIG: CreateComparatorCreatorOptions<
49
+ WeakMap<object, any>
50
+ > = Object.freeze(
51
+ merge(DEFAULT_CONFIG, {
52
+ areArraysEqual: areArraysEqualCircular,
53
+ areMapsEqual: areMapsEqualCircular,
54
+ areObjectsEqual: areObjectsEqualCircular,
55
+ areSetsEqual: areSetsEqualCircular,
56
+ }),
57
+ );
58
+
59
+ const isDeepEqual = createComparator(DEFAULT_CONFIG);
60
+
61
+ /**
62
+ * Whether the items passed are deeply-equal in value.
63
+ */
64
+ export function deepEqual<A, B>(a: A, b: B): boolean {
65
+ return isDeepEqual(a, b, undefined);
66
+ }
67
+
68
+ const isShallowEqual = createComparator(
69
+ merge(DEFAULT_CONFIG, { createIsNestedEqual: () => sameValueZeroEqual }),
70
+ );
71
+
72
+ /**
73
+ * Whether the items passed are shallowly-equal in value.
74
+ */
75
+ export function shallowEqual<A, B>(a: A, b: B): boolean {
76
+ return isShallowEqual(a, b, undefined);
77
+ }
78
+
79
+ const isCircularDeepEqual = createComparator(DEFAULT_CIRCULAR_CONFIG);
80
+
81
+ /**
82
+ * Whether the items passed are deeply-equal in value, including circular references.
83
+ */
84
+ export function circularDeepEqual<A, B>(a: A, b: B): boolean {
85
+ return isCircularDeepEqual(a, b, new WeakMap());
86
+ }
87
+
88
+ const isCircularShallowEqual = createComparator(
89
+ merge(DEFAULT_CIRCULAR_CONFIG, {
90
+ createIsNestedEqual: () => sameValueZeroEqual,
91
+ }),
92
+ );
93
+
94
+ /**
95
+ * Whether the items passed are shallowly-equal in value, including circular references.
96
+ */
97
+ export function circularShallowEqual<A, B>(a: A, b: B): boolean {
98
+ return isCircularShallowEqual(a, b, new WeakMap());
99
+ }
100
+
101
+ /**
102
+ * Create a custom equality comparison method.
103
+ *
104
+ * This can be done to create very targeted comparisons in extreme hot-path scenarios
105
+ * where the standard methods are not performant enough, but can also be used to provide
106
+ * support for legacy environments that do not support expected features like
107
+ * `RegExp.prototype.flags` out of the box.
108
+ */
109
+ export function createCustomEqual<Meta = undefined>(
110
+ getComparatorOptions: GetComparatorOptions<Meta>,
111
+ ): EqualityComparator<Meta> {
112
+ return createComparator<Meta>(
113
+ merge(DEFAULT_CONFIG, getComparatorOptions(DEFAULT_CONFIG)),
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Create a custom equality comparison method that handles circular references. This is very
119
+ * similar to `createCustomEqual`, with the only difference being that `meta` expects to be
120
+ * populated with a `WeakMap`-like contract.
121
+ *
122
+ * This can be done to create very targeted comparisons in extreme hot-path scenarios
123
+ * where the standard methods are not performant enough, but can also be used to provide
124
+ * support for legacy environments that do not support expected features like
125
+ * `WeakMap` out of the box.
126
+ */
127
+ export function createCustomCircularEqual<
128
+ Meta extends BaseCircularMeta = WeakMap<any, any>,
129
+ >(getComparatorOptions: GetComparatorOptions<Meta>): EqualityComparator<Meta> {
130
+ const comparator = createComparator<Meta>(
131
+ merge(
132
+ DEFAULT_CIRCULAR_CONFIG,
133
+ getComparatorOptions(DEFAULT_CIRCULAR_CONFIG as any),
134
+ ),
135
+ );
136
+
137
+ return ((a: any, b: any, meta: any = new WeakMap()) =>
138
+ comparator(a, b, meta)) as EqualityComparator<Meta>;
139
+ }
package/src/maps.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { createIsCircular } from './utils';
2
+
3
+ import type { InternalEqualityComparator } from './utils';
4
+
5
+ /**
6
+ * Whether the `Map`s are equal in value.
7
+ */
8
+ export function areMapsEqual(
9
+ a: Map<any, any>,
10
+ b: Map<any, any>,
11
+ isEqual: InternalEqualityComparator<any>,
12
+ meta: any,
13
+ ): boolean {
14
+ let isValueEqual = a.size === b.size;
15
+
16
+ if (!isValueEqual) {
17
+ return false;
18
+ }
19
+
20
+ if (!a.size) {
21
+ return true;
22
+ }
23
+
24
+ // The use of `forEach()` is to avoid the transpilation cost of `for...of` comparisons, and
25
+ // the inability to control the performance of the resulting code. It also avoids excessive
26
+ // iteration compared to doing comparisons of `keys()` and `values()`. As a result, though,
27
+ // we cannot short-circuit the iterations; bookkeeping must be done to short-circuit the
28
+ // equality checks themselves.
29
+
30
+ const matchedIndices: Record<number, true> = {};
31
+
32
+ let indexA = 0;
33
+
34
+ a.forEach((aValue, aKey) => {
35
+ if (!isValueEqual) {
36
+ return;
37
+ }
38
+
39
+ let hasMatch = false;
40
+ let matchIndexB = 0;
41
+
42
+ b.forEach((bValue, bKey) => {
43
+ if (
44
+ !hasMatch &&
45
+ !matchedIndices[matchIndexB] &&
46
+ (hasMatch =
47
+ isEqual(aKey, bKey, indexA, matchIndexB, a, b, meta) &&
48
+ isEqual(aValue, bValue, aKey, bKey, a, b, meta))
49
+ ) {
50
+ matchedIndices[matchIndexB] = true;
51
+ }
52
+
53
+ matchIndexB++;
54
+ });
55
+
56
+ indexA++;
57
+ isValueEqual = hasMatch;
58
+ });
59
+
60
+ return isValueEqual;
61
+ }
62
+
63
+ /**
64
+ * Whether the `Map`s are equal in value, including circular references.
65
+ */
66
+ export const areMapsEqualCircular = createIsCircular(areMapsEqual);
package/src/objects.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { createIsCircular } from './utils';
2
+
3
+ import type { InternalEqualityComparator } from './utils';
4
+
5
+ interface Dictionary<Value> {
6
+ [key: string]: Value;
7
+ }
8
+
9
+ const OWNER = '_owner';
10
+ const { hasOwnProperty } = Object.prototype;
11
+
12
+ /**
13
+ * Whether the objects are equal in value.
14
+ */
15
+ export function areObjectsEqual(
16
+ a: Dictionary<any>,
17
+ b: Dictionary<any>,
18
+ isEqual: InternalEqualityComparator<any>,
19
+ meta: any,
20
+ ): boolean {
21
+ const keysA = Object.keys(a);
22
+
23
+ let index = keysA.length;
24
+
25
+ if (Object.keys(b).length !== index) {
26
+ return false;
27
+ }
28
+
29
+ let key: string;
30
+
31
+ // Decrementing `while` showed faster results than either incrementing or
32
+ // decrementing `for` loop and than an incrementing `while` loop. Declarative
33
+ // methods like `some` / `every` were not used to avoid incurring the garbage
34
+ // cost of anonymous callbacks.
35
+ while (index-- > 0) {
36
+ key = keysA[index];
37
+
38
+ if (key === OWNER) {
39
+ const reactElementA = !!a.$$typeof;
40
+ const reactElementB = !!b.$$typeof;
41
+
42
+ if ((reactElementA || reactElementB) && reactElementA !== reactElementB) {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ if (
48
+ !hasOwnProperty.call(b, key) ||
49
+ !isEqual(a[key], b[key], key, key, a, b, meta)
50
+ ) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Whether the objects are equal in value, including circular references.
60
+ */
61
+ export const areObjectsEqualCircular = createIsCircular(areObjectsEqual);
package/src/regexps.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whether the regexps passed are equal in value.
3
+ *
4
+ * @NOTE
5
+ * This is a standalone function instead of done inline in the comparator
6
+ * to allow for overrides. An example of this would be supporting a
7
+ * pre-ES2015 environment where the `flags` property is not available.
8
+ */
9
+ export function areRegExpsEqual(a: RegExp, b: RegExp): boolean {
10
+ return a.source === b.source && a.flags === b.flags;
11
+ }
package/src/sets.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { createIsCircular } from './utils';
2
+
3
+ import type { InternalEqualityComparator } from './utils';
4
+
5
+ /**
6
+ * Whether the `Set`s are equal in value.
7
+ */
8
+ export function areSetsEqual(
9
+ a: Set<any>,
10
+ b: Set<any>,
11
+ isEqual: InternalEqualityComparator<any>,
12
+ meta: any,
13
+ ): boolean {
14
+ let isValueEqual = a.size === b.size;
15
+
16
+ if (!isValueEqual) {
17
+ return false;
18
+ }
19
+
20
+ if (!a.size) {
21
+ return true;
22
+ }
23
+
24
+ // The use of `forEach()` is to avoid the transpilation cost of `for...of` comparisons, and
25
+ // the inability to control the performance of the resulting code. It also avoids excessive
26
+ // iteration compared to doing comparisons of `keys()` and `values()`. As a result, though,
27
+ // we cannot short-circuit the iterations; bookkeeping must be done to short-circuit the
28
+ // equality checks themselves.
29
+
30
+ const matchedIndices: Record<number, true> = {};
31
+
32
+ a.forEach((aValue, aKey) => {
33
+ if (!isValueEqual) {
34
+ return;
35
+ }
36
+
37
+ let hasMatch = false;
38
+ let matchIndex = 0;
39
+
40
+ b.forEach((bValue, bKey) => {
41
+ if (
42
+ !hasMatch &&
43
+ !matchedIndices[matchIndex] &&
44
+ (hasMatch = isEqual(aValue, bValue, aKey, bKey, a, b, meta))
45
+ ) {
46
+ matchedIndices[matchIndex] = true;
47
+ }
48
+
49
+ matchIndex++;
50
+ });
51
+
52
+ isValueEqual = hasMatch;
53
+ });
54
+
55
+ return isValueEqual;
56
+ }
57
+
58
+ /**
59
+ * Whether the `Set`s are equal in value, including circular references.
60
+ */
61
+ export const areSetsEqualCircular = createIsCircular(areSetsEqual);
package/src/utils.ts ADDED
@@ -0,0 +1,128 @@
1
+ export type InternalEqualityComparator<Meta> = (
2
+ a: any,
3
+ b: any,
4
+ indexOrKeyA: any,
5
+ indexOrKeyB: any,
6
+ parentA: any,
7
+ parentB: any,
8
+ meta: Meta,
9
+ ) => boolean;
10
+
11
+ export type EqualityComparator<Meta> = Meta extends undefined
12
+ ? <A, B>(a: A, b: B, meta?: Meta) => boolean
13
+ : <A, B>(a: A, b: B, meta: Meta) => boolean;
14
+
15
+ export type EqualityComparatorCreator<Meta> = (
16
+ fn: EqualityComparator<Meta>,
17
+ ) => InternalEqualityComparator<Meta>;
18
+
19
+ export type NativeEqualityComparator = <A, B>(a: A, b: B) => boolean;
20
+
21
+ export type TypeEqualityComparator<Type, Meta> = (
22
+ a: Type,
23
+ b: Type,
24
+ isEqual: InternalEqualityComparator<Meta>,
25
+ meta: Meta,
26
+ ) => boolean;
27
+
28
+ /**
29
+ * Default equality comparator pass-through, used as the standard `isEqual` creator for
30
+ * use inside the built comparator.
31
+ */
32
+ export function createDefaultIsNestedEqual<Meta>(
33
+ comparator: EqualityComparator<Meta>,
34
+ ): InternalEqualityComparator<Meta> {
35
+ return function isEqual<A, B>(
36
+ a: A,
37
+ b: B,
38
+ indexOrKeyA: any,
39
+ indexOrKeyB: any,
40
+ parentA: any,
41
+ parentB: any,
42
+ meta: Meta,
43
+ ) {
44
+ return comparator(a, b, meta);
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Wrap the provided `areItemsEqual` method to manage the circular cache, allowing
50
+ * for circular references to be safely included in the comparison without creating
51
+ * stack overflows.
52
+ */
53
+ export function createIsCircular<
54
+ AreItemsEqual extends TypeEqualityComparator<any, any>,
55
+ >(areItemsEqual: AreItemsEqual): AreItemsEqual {
56
+ return function isCircular(
57
+ a: any,
58
+ b: any,
59
+ isEqual: InternalEqualityComparator<WeakMap<any, any>>,
60
+ cache: WeakMap<any, any>,
61
+ ) {
62
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') {
63
+ return areItemsEqual(a, b, isEqual, cache);
64
+ }
65
+
66
+ const cachedA = cache.get(a);
67
+ const cachedB = cache.get(b);
68
+
69
+ if (cachedA && cachedB) {
70
+ return cachedA === b && cachedB === a;
71
+ }
72
+
73
+ cache.set(a, b);
74
+ cache.set(b, a);
75
+
76
+ const result = areItemsEqual(a, b, isEqual, cache);
77
+
78
+ cache.delete(a);
79
+ cache.delete(b);
80
+
81
+ return result;
82
+ } as AreItemsEqual;
83
+ }
84
+
85
+ /**
86
+ * Targeted shallow merge of two objects.
87
+ *
88
+ * @NOTE
89
+ * This exists as a tinier compiled version of the `__assign` helper that
90
+ * `tsc` injects in case of `Object.assign` not being present.
91
+ */
92
+ export function merge<A extends object, B extends object>(a: A, b: B): A & B {
93
+ const merged: Record<string, any> = {};
94
+
95
+ for (const key in a) {
96
+ merged[key] = a[key];
97
+ }
98
+
99
+ for (const key in b) {
100
+ merged[key] = b[key];
101
+ }
102
+
103
+ return merged as A & B;
104
+ }
105
+
106
+ /**
107
+ * Whether the value is a plain object.
108
+ *
109
+ * @NOTE
110
+ * This is a same-realm compariosn only.
111
+ */
112
+ export function isPlainObject(value: any): boolean {
113
+ return value.constructor === Object || value.constructor == null;
114
+ }
115
+
116
+ /**
117
+ * When the value is `Promise`-like, aka "then-able".
118
+ */
119
+ export function isPromiseLike(value: any): boolean {
120
+ return typeof value.then === 'function';
121
+ }
122
+
123
+ /**
124
+ * Whether the values passed are strictly equal or both NaN.
125
+ */
126
+ export function sameValueZeroEqual(a: any, b: any): boolean {
127
+ return a === b || (a !== a && b !== b);
128
+ }