@longlast/equals 0.5.6 → 0.5.8

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/dist/index.d.ts CHANGED
@@ -1,7 +1,39 @@
1
1
  /**
2
2
  * @module equals
3
3
  */
4
- import { type Curried2 } from "@longlast/curry";
4
+ import { type Curried2, type Curried3 } from "@longlast/curry";
5
+ export interface EqualsOptions {
6
+ /**
7
+ * Custom logic to use in preference to the default {@link equals}
8
+ * comparison.
9
+ */
10
+ readonly override?: undefined | ((a: unknown, b: unknown) => boolean | undefined);
11
+ }
12
+ /**
13
+ * @function
14
+ * A version of {@link equals} that allows callers to override the default
15
+ * comparison algorithm. If the provided override function returns a boolean,
16
+ * it is used as the result of the comparison. If the override returns
17
+ * `undefined`, the behavior of `equalsWith` defaults to that of
18
+ * {@link equals}.
19
+ *
20
+ * `equalsWith` is curried. See {@link curry}.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // `mathEquals` treats -0 as equal to 0.
25
+ * const mathEquals = equalsWith({override: trueIfBothZero});
26
+ *
27
+ * function trueIfBothZero(a: unknown, b: unknown) {
28
+ * if (a === 0 && b === 0) {
29
+ * return true;
30
+ * }
31
+ * }
32
+ *
33
+ * mathEquals({x: 0}, {x: -0}); // => true
34
+ * ```
35
+ */
36
+ export declare const equalsWith: Curried3<EqualsOptions, unknown, unknown, boolean>;
5
37
  /**
6
38
  * @function
7
39
  * Deeply compares two values, returning true if they're equal and false
@@ -18,6 +50,9 @@ import { type Curried2 } from "@longlast/curry";
18
50
  * elements are equal (according to `equals`).
19
51
  * - Sets are equal iff they contain the same elements. Note that set
20
52
  * elements are _not_ deeply compared.
53
+ * - Maps are equal iff they have the same set of keys, and their
54
+ * corresponding values are deeply equal. Note that map keys are _not_
55
+ * deeply compared.
21
56
  * - Partially applied curried functions are equal iff they originate from
22
57
  * the same curried function and their bound arguments are equal
23
58
  * according to `equals`. See {@link curry}.
package/dist/index.js CHANGED
@@ -3,71 +3,37 @@
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.
8
6
  /**
9
7
  * @function
10
- * Deeply compares two values, returning true if they're equal and false
11
- * otherwise. The following criteria are used to determine equality:
12
- *
13
- * - All values are equal to themselves.
14
- * - Primitives `a` and `b` are equal iff `Object.is(a, b)`. This is similar
15
- * to `===` comparison, but treats `NaN` as equal to `NaN` and `0` as
16
- * different from `-0`.
17
- * - Dates are equal iff they have the same millisecond-precision timestamp.
18
- * - RegExps are equal iff they have the same pattern and flags.
19
- * - Errors are equal iff they have the same class and message.
20
- * - Arrays are equal iff they have the same length and their corresponding
21
- * elements are equal (according to `equals`).
22
- * - Sets are equal iff they contain the same elements. Note that set
23
- * elements are _not_ deeply compared.
24
- * - Partially applied curried functions are equal iff they originate from
25
- * the same curried function and their bound arguments are equal
26
- * according to `equals`. See {@link curry}.
27
- * - Other objects are equal iff they have the same prototype (e.g. the same
28
- * class) and the same set of enumerable string-keyed properties, and the
29
- * values of their corresponding properties are equal (according to
30
- * `equals`).
8
+ * A version of {@link equals} that allows callers to override the default
9
+ * comparison algorithm. If the provided override function returns a boolean,
10
+ * it is used as the result of the comparison. If the override returns
11
+ * `undefined`, the behavior of `equalsWith` defaults to that of
12
+ * {@link equals}.
31
13
  *
32
- * You can customize how `equals()` compares values of a specific class by
33
- * using the {@link symbols.$equals $equals} symbol to define a method on that
34
- * class. For example:
14
+ * `equalsWith` is curried. See {@link curry}.
35
15
  *
16
+ * @example
36
17
  * ```ts
37
- * import {$equals} from "@longlast/symbols"
18
+ * // `mathEquals` treats -0 as equal to 0.
19
+ * const mathEquals = equalsWith({override: trueIfBothZero});
38
20
  *
39
- * class HttpError extends Error {
40
- * private statusCode: number;
41
- * constructor(message: string, statusCode: number) {
42
- * super(message);
43
- * this.statusCode = statusCode;
44
- * }
45
- *
46
- * [$equals](other: unknown) {
47
- * return other instanceof HttpError &&
48
- * other.statusCode === this.statusCode &&
49
- * other.message === this.message;
21
+ * function trueIfBothZero(a: unknown, b: unknown) {
22
+ * if (a === 0 && b === 0) {
23
+ * return true;
50
24
  * }
51
25
  * }
52
- * ```
53
- *
54
- * Note that this makes the comparison asymmetrical: `a` is considered equal to
55
- * `b` iff `a[$equals](b)` returns truthy. The `$equals` method will always be
56
- * called on the *first* argument to `equals()`.
57
- *
58
- * `equals()` is curried. See {@link curry}.
59
- *
60
- * ## Limitations
61
26
  *
62
- * `equals()` can throw a `RangeError` if one of its arguments contains a
63
- * reference cycle. Avoid passing mutable objects to `equals()` unless you know
64
- * that they do not contain cycles.
27
+ * mathEquals({x: 0}, {x: -0}); // => true
28
+ * ```
65
29
  */
66
- export const equals = curry(_equals);
67
- function _equals(a, b) {
68
- // This is an optimized implementation. There is a simpler, equivalent one
69
- // in pkg/equals/alt/reference.ts.
70
- if (a == null) {
30
+ export const equalsWith = curry(_equalsWith);
31
+ function _equalsWith(options, a, b) {
32
+ const override = options.override?.(a, b);
33
+ if (override !== undefined) {
34
+ return override;
35
+ }
36
+ if (a == null || b == null) {
71
37
  return a === b;
72
38
  }
73
39
  // TODO: (pre-1.0.0) decide if we should pass `equals` as the second
@@ -85,7 +51,7 @@ function _equals(a, b) {
85
51
  const bUnapplied = b[$unapplied];
86
52
  return (aUnapplied != null &&
87
53
  aUnapplied === bUnapplied &&
88
- _equals(getBoundArguments(a), getBoundArguments(b)));
54
+ _equalsWith(options, getBoundArguments(a), getBoundArguments(b)));
89
55
  }
90
56
  // If `a` is a primitive at this point, return false, since we already know
91
57
  // it is not identical to `b`.
@@ -113,7 +79,8 @@ function _equals(a, b) {
113
79
  }
114
80
  if (Array.isArray(a)) {
115
81
  unsafeNarrow(b);
116
- return a.length === b.length && a.every((_, i) => _equals(a[i], b[i]));
82
+ return (a.length === b.length &&
83
+ a.every((_, i) => _equalsWith(options, a[i], b[i])));
117
84
  }
118
85
  if (setConstructorString === aConstructorString) {
119
86
  unsafeNarrow(a);
@@ -121,7 +88,19 @@ function _equals(a, b) {
121
88
  return a.size === b.size && [...a].every((v) => b.has(v));
122
89
  }
123
90
  // TODO: typed arrays
124
- // TODO: Map
91
+ if (mapConstructorString === aConstructorString) {
92
+ unsafeNarrow(a);
93
+ unsafeNarrow(b);
94
+ if (a.size !== b.size) {
95
+ return false;
96
+ }
97
+ for (const key of a.keys()) {
98
+ if (!b.has(key) || !_equalsWith(options, a.get(key), b.get(key))) {
99
+ return false;
100
+ }
101
+ }
102
+ return true;
103
+ }
125
104
  if (objectConstructorString === aConstructorString ||
126
105
  protoOf(a) === protoOf(b)) {
127
106
  unsafeNarrow(a);
@@ -136,7 +115,7 @@ function _equals(a, b) {
136
115
  if (!bKeySet.has(key)) {
137
116
  return false;
138
117
  }
139
- if (!_equals(a[key], b[key])) {
118
+ if (!_equalsWith(options, a[key], b[key])) {
140
119
  return false;
141
120
  }
142
121
  }
@@ -144,6 +123,69 @@ function _equals(a, b) {
144
123
  }
145
124
  return false;
146
125
  }
126
+ /**
127
+ * @function
128
+ * Deeply compares two values, returning true if they're equal and false
129
+ * otherwise. The following criteria are used to determine equality:
130
+ *
131
+ * - All values are equal to themselves.
132
+ * - Primitives `a` and `b` are equal iff `Object.is(a, b)`. This is similar
133
+ * to `===` comparison, but treats `NaN` as equal to `NaN` and `0` as
134
+ * different from `-0`.
135
+ * - Dates are equal iff they have the same millisecond-precision timestamp.
136
+ * - RegExps are equal iff they have the same pattern and flags.
137
+ * - Errors are equal iff they have the same class and message.
138
+ * - Arrays are equal iff they have the same length and their corresponding
139
+ * elements are equal (according to `equals`).
140
+ * - Sets are equal iff they contain the same elements. Note that set
141
+ * elements are _not_ deeply compared.
142
+ * - Maps are equal iff they have the same set of keys, and their
143
+ * corresponding values are deeply equal. Note that map keys are _not_
144
+ * deeply compared.
145
+ * - Partially applied curried functions are equal iff they originate from
146
+ * the same curried function and their bound arguments are equal
147
+ * according to `equals`. See {@link curry}.
148
+ * - Other objects are equal iff they have the same prototype (e.g. the same
149
+ * class) and the same set of enumerable string-keyed properties, and the
150
+ * values of their corresponding properties are equal (according to
151
+ * `equals`).
152
+ *
153
+ * You can customize how `equals()` compares values of a specific class by
154
+ * using the {@link symbols.$equals $equals} symbol to define a method on that
155
+ * class. For example:
156
+ *
157
+ * ```ts
158
+ * import {$equals} from "@longlast/symbols"
159
+ *
160
+ * class HttpError extends Error {
161
+ * private statusCode: number;
162
+ * constructor(message: string, statusCode: number) {
163
+ * super(message);
164
+ * this.statusCode = statusCode;
165
+ * }
166
+ *
167
+ * [$equals](other: unknown) {
168
+ * return other instanceof HttpError &&
169
+ * other.statusCode === this.statusCode &&
170
+ * other.message === this.message;
171
+ * }
172
+ * }
173
+ * ```
174
+ *
175
+ * Note that this makes the comparison asymmetrical: `a` is considered equal to
176
+ * `b` iff `a[$equals](b)` returns truthy. The `$equals` method will always be
177
+ * called on the *first* argument to `equals()`.
178
+ *
179
+ * `equals()` is curried. See {@link curry}.
180
+ *
181
+ * ## Limitations
182
+ *
183
+ * `equals()` can throw a `RangeError` if one of its arguments contains a
184
+ * reference cycle. Avoid passing mutable objects to `equals()` unless you know
185
+ * that they do not contain cycles.
186
+ */
187
+ // TODO: clear function provenance on `equals`.
188
+ export const equals = equalsWith({});
147
189
  function getBoundArguments(f) {
148
190
  // TODO: (pre-1.0.0) remove `f[$boundArguments]` fallback.
149
191
  return f[$getBoundArguments]?.() ?? f[$boundArguments];
@@ -155,9 +197,6 @@ function functionString(f) {
155
197
  return Function.prototype.toString.call(f);
156
198
  }
157
199
  function constructorOf(value) {
158
- if (value == null) {
159
- return null;
160
- }
161
200
  return protoOf(value)?.constructor;
162
201
  }
163
202
  function unsafeNarrow(value) {
@@ -168,6 +207,7 @@ const objectConstructorString = functionString(Object);
168
207
  const dateConstructorString = functionString(Date);
169
208
  const regexConstructorString = functionString(RegExp);
170
209
  const setConstructorString = functionString(Set);
210
+ const mapConstructorString = functionString(Map);
171
211
  const nativeErrorConstructorStrings = [
172
212
  functionString(Error),
173
213
  // TODO: add DOMException? Be sure to check the `name` property.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longlast/equals",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Deeply compares objects",
5
5
  "homepage": "https://longlast.js.org/",
6
6
  "license": "MIT",