Skip to content

Commit

Permalink
use iterator-based methods for Map / Set comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
planttheidea committed Feb 28, 2023
1 parent bb57801 commit 48e6960
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 61 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

To align with other implementations common in the community, but also to be more functionally correct, the two objects being compared now must have equal `constructor`s.

#### `Map` / `Set` comparisons no longer support IE11

In previous verisons, `.forEach()` was used to ensure that support for `Symbol` was not required, as IE11 did not have `Symbol` and therefore both `Map` and `Set` did not have iterator-based methods such as `.values()` or `.entries()`. Since IE11 is no longer a supported browser, and support for those methods is present in all browsers and Node for quite a while, the comparison has moved to use these methods. This results in a ~20% performance increase.

#### `createCustomEqual` contract has changed

To better facilitate strict comparisons, but also to allow for `meta` use separate from caching, the contract for `createCustomEqual` has changed. See the [README documentation](./README.md#createcustomequal) for more details, but froma high-level:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/coverage-100%25-brightgreen.svg"/>
<img src="https://img.shields.io/badge/license-MIT-blue.svg"/>

Perform [blazing fast](#benchmarks) equality comparisons (either deep or shallow) on two objects passed, while also maintaining a high degree of flexibility for various implementation use-cases. It has no dependencies, and is ~1.74kB when minified and gzipped.
Perform [blazing fast](#benchmarks) equality comparisons (either deep or shallow) on two objects passed, while also maintaining a high degree of flexibility for various implementation use-cases. It has no dependencies, and is ~1.8kB when minified and gzipped.

The following types are handled out-of-the-box:

Expand Down
143 changes: 83 additions & 60 deletions src/equals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,47 +44,56 @@ export function areMapsEqual(
b: Map<any, any>,
state: State<any>,
): boolean {
let isValueEqual = a.size === b.size;
if (a.size !== b.size) {
return false;
}

if (isValueEqual && a.size) {
// The use of `forEach()` is to avoid the transpilation cost of `for...of` comparisons, and
// the inability to control the performance of the resulting code. It also avoids excessive
// iteration compared to doing comparisons of `keys()` and `values()`. As a result, though,
// we cannot short-circuit the iterations; bookkeeping must be done to short-circuit the
// equality checks themselves.
const matchedIndices: Record<number, true> = {};
const aIterable = a.entries();

const matchedIndices: Record<number, true> = {};
let index = 0;
let aResult: IteratorResult<[any, any]>;
let bResult: IteratorResult<[any, any]>;

while ((aResult = aIterable.next())) {
if (aResult.done) {
break;
}

let indexA = 0;
const bIterable = b.entries();

let hasMatch = false;
let matchIndex = 0;

while ((bResult = bIterable.next())) {
if (bResult.done) {
break;
}

a.forEach((aValue, aKey) => {
if (!isValueEqual) {
return;
const [aKey, aValue] = aResult.value;
const [bKey, bValue] = bResult.value;

if (
!hasMatch &&
!matchedIndices[matchIndex] &&
(hasMatch =
state.equals(aKey, bKey, index, matchIndex, a, b, state) &&
state.equals(aValue, bValue, aKey, bKey, a, b, state))
) {
matchedIndices[matchIndex] = true;
}

let hasMatch = false;
let matchIndexB = 0;

b.forEach((bValue, bKey) => {
if (
!hasMatch &&
!matchedIndices[matchIndexB] &&
(hasMatch =
state.equals(aKey, bKey, indexA, matchIndexB, a, b, state) &&
state.equals(aValue, bValue, aKey, bKey, a, b, state))
) {
matchedIndices[matchIndexB] = true;
}

matchIndexB++;
});

indexA++;
isValueEqual = hasMatch;
});
matchIndex++;
}

if (!hasMatch) {
return false;
}

index++;
}

return isValueEqual;
return true;
}

/**
Expand Down Expand Up @@ -219,42 +228,56 @@ export function areSetsEqual(
b: Set<any>,
state: State<any>,
): boolean {
let isValueEqual = a.size === b.size;
if (a.size !== b.size) {
return false;
}

if (isValueEqual && a.size) {
// The use of `forEach()` is to avoid the transpilation cost of `for...of` comparisons, and
// the inability to control the performance of the resulting code. It also avoids excessive
// iteration compared to doing comparisons of `keys()` and `values()`. As a result, though,
// we cannot short-circuit the iterations; bookkeeping must be done to short-circuit the
// equality checks themselves.
const matchedIndices: Record<number, true> = {};
const aIterable = a.values();

const matchedIndices: Record<number, true> = {};
let aResult: IteratorResult<any>;
let bResult: IteratorResult<any>;

a.forEach((aValue, aKey) => {
if (!isValueEqual) {
return;
}
while ((aResult = aIterable.next())) {
if (aResult.done) {
break;
}

const bIterable = b.values();

let hasMatch = false;
let matchIndex = 0;

let hasMatch = false;
let matchIndex = 0;
while ((bResult = bIterable.next())) {
if (bResult.done) {
break;
}

b.forEach((bValue, bKey) => {
if (
!hasMatch &&
!matchedIndices[matchIndex] &&
(hasMatch = state.equals(aValue, bValue, aKey, bKey, a, b, state))
) {
matchedIndices[matchIndex] = true;
}
if (
!hasMatch &&
!matchedIndices[matchIndex] &&
(hasMatch = state.equals(
aResult.value,
bResult.value,
aResult.value,
bResult.value,
a,
b,
state,
))
) {
matchedIndices[matchIndex] = true;
}

matchIndex++;
});
matchIndex++;
}

isValueEqual = hasMatch;
});
if (!hasMatch) {
return false;
}
}

return isValueEqual;
return true;
}

/**
Expand Down

0 comments on commit 48e6960

Please sign in to comment.