@longlast/equals 0.5.7 → 0.5.9

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/README.md CHANGED
@@ -32,4 +32,4 @@ equals(
32
32
 
33
33
  [Browse the docs on longlast.js.org][docs].
34
34
 
35
- [docs]: https://longlast.js.org/equals/
35
+ [docs]: https://longlast.js.org/reference/equals/
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
@@ -21,9 +53,9 @@ import { type Curried2 } from "@longlast/curry";
21
53
  * - Maps are equal iff they have the same set of keys, and their
22
54
  * corresponding values are deeply equal. Note that map keys are _not_
23
55
  * 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}.
56
+ * - Partially applied functions (via {@link curry} or {@link partial-apply})
57
+ * are equal iff they originate from the same function and their bound
58
+ * arguments are equal according to `equals`.
27
59
  * - Other objects are equal iff they have the same prototype (e.g. the same
28
60
  * class) and the same set of enumerable string-keyed properties, and the
29
61
  * values of their corresponding properties are equal (according to
@@ -34,7 +66,7 @@ import { type Curried2 } from "@longlast/curry";
34
66
  * class. For example:
35
67
  *
36
68
  * ```ts
37
- * import {$equals} from "@longlast/symbols"
69
+ * import {$equals} from "@longlast/symbols";
38
70
  *
39
71
  * class HttpError extends Error {
40
72
  * private statusCode: number;
package/dist/index.js CHANGED
@@ -3,74 +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:
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}.
12
13
  *
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
- * - Maps are equal iff they have the same set of keys, and their
25
- * corresponding values are deeply equal. Note that map keys are _not_
26
- * deeply compared.
27
- * - Partially applied curried functions are equal iff they originate from
28
- * the same curried function and their bound arguments are equal
29
- * according to `equals`. See {@link curry}.
30
- * - Other objects are equal iff they have the same prototype (e.g. the same
31
- * class) and the same set of enumerable string-keyed properties, and the
32
- * values of their corresponding properties are equal (according to
33
- * `equals`).
34
- *
35
- * You can customize how `equals()` compares values of a specific class by
36
- * using the {@link symbols.$equals $equals} symbol to define a method on that
37
- * class. For example:
14
+ * `equalsWith` is curried. See {@link curry}.
38
15
  *
16
+ * @example
39
17
  * ```ts
40
- * import {$equals} from "@longlast/symbols"
41
- *
42
- * class HttpError extends Error {
43
- * private statusCode: number;
44
- * constructor(message: string, statusCode: number) {
45
- * super(message);
46
- * this.statusCode = statusCode;
47
- * }
18
+ * // `mathEquals` treats -0 as equal to 0.
19
+ * const mathEquals = equalsWith({override: trueIfBothZero});
48
20
  *
49
- * [$equals](other: unknown) {
50
- * return other instanceof HttpError &&
51
- * other.statusCode === this.statusCode &&
52
- * other.message === this.message;
21
+ * function trueIfBothZero(a: unknown, b: unknown) {
22
+ * if (a === 0 && b === 0) {
23
+ * return true;
53
24
  * }
54
25
  * }
55
- * ```
56
26
  *
57
- * Note that this makes the comparison asymmetrical: `a` is considered equal to
58
- * `b` iff `a[$equals](b)` returns truthy. The `$equals` method will always be
59
- * called on the *first* argument to `equals()`.
60
- *
61
- * `equals()` is curried. See {@link curry}.
62
- *
63
- * ## Limitations
64
- *
65
- * `equals()` can throw a `RangeError` if one of its arguments contains a
66
- * reference cycle. Avoid passing mutable objects to `equals()` unless you know
67
- * that they do not contain cycles.
27
+ * mathEquals({x: 0}, {x: -0}); // => true
28
+ * ```
68
29
  */
69
- export const equals = curry(_equals);
70
- function _equals(a, b) {
71
- // This is an optimized implementation. There is a simpler, equivalent one
72
- // in pkg/equals/alt/reference.ts.
73
- 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) {
74
37
  return a === b;
75
38
  }
76
39
  // TODO: (pre-1.0.0) decide if we should pass `equals` as the second
@@ -88,7 +51,7 @@ function _equals(a, b) {
88
51
  const bUnapplied = b[$unapplied];
89
52
  return (aUnapplied != null &&
90
53
  aUnapplied === bUnapplied &&
91
- _equals(getBoundArguments(a), getBoundArguments(b)));
54
+ _equalsWith(options, getBoundArguments(a), getBoundArguments(b)));
92
55
  }
93
56
  // If `a` is a primitive at this point, return false, since we already know
94
57
  // it is not identical to `b`.
@@ -116,7 +79,8 @@ function _equals(a, b) {
116
79
  }
117
80
  if (Array.isArray(a)) {
118
81
  unsafeNarrow(b);
119
- 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])));
120
84
  }
121
85
  if (setConstructorString === aConstructorString) {
122
86
  unsafeNarrow(a);
@@ -131,7 +95,7 @@ function _equals(a, b) {
131
95
  return false;
132
96
  }
133
97
  for (const key of a.keys()) {
134
- if (!b.has(key) || !_equals(a.get(key), b.get(key))) {
98
+ if (!b.has(key) || !_equalsWith(options, a.get(key), b.get(key))) {
135
99
  return false;
136
100
  }
137
101
  }
@@ -151,7 +115,7 @@ function _equals(a, b) {
151
115
  if (!bKeySet.has(key)) {
152
116
  return false;
153
117
  }
154
- if (!_equals(a[key], b[key])) {
118
+ if (!_equalsWith(options, a[key], b[key])) {
155
119
  return false;
156
120
  }
157
121
  }
@@ -159,6 +123,69 @@ function _equals(a, b) {
159
123
  }
160
124
  return false;
161
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 functions (via {@link curry} or {@link partial-apply})
146
+ * are equal iff they originate from the same function and their bound
147
+ * arguments are equal according to `equals`.
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({});
162
189
  function getBoundArguments(f) {
163
190
  // TODO: (pre-1.0.0) remove `f[$boundArguments]` fallback.
164
191
  return f[$getBoundArguments]?.() ?? f[$boundArguments];
@@ -170,9 +197,6 @@ function functionString(f) {
170
197
  return Function.prototype.toString.call(f);
171
198
  }
172
199
  function constructorOf(value) {
173
- if (value == null) {
174
- return null;
175
- }
176
200
  return protoOf(value)?.constructor;
177
201
  }
178
202
  function unsafeNarrow(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longlast/equals",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "Deeply compares objects",
5
5
  "homepage": "https://longlast.js.org/",
6
6
  "license": "MIT",