Repository: remeda/remeda
Pull request #650 (2024-04-22 08:41:31)

feat(isDeepEqual): cherry-pick #640
Cherry picking #640 for v2

codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/remeda/remeda/pr/650/builds/496895) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 7de091f49450029abab45e50f621bd6e0756601e:

|Sandbox| Source |
|--|--|
|[remeda-example-vanilla](https://codesandbox.io/s/remeda-example-vanilla-rhzr8v)| Configuration |


Patch:
diff --git a/eslint.config.js b/eslint.config.js
index 51b6c56..c325d8b 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -348,6 +348,7 @@ export default config(
                 "Promise",
                 "ReadonlyMap",
                 "RegExp",
+                "ReadonlySet",
               ],
             },
           ],
diff --git a/src/isDeepEqual.ts b/src/isDeepEqual.ts
index 80c29a9..44fe00a 100644
--- a/src/isDeepEqual.ts
+++ b/src/isDeepEqual.ts
@@ -7,9 +7,9 @@ import { purry } from "./purry";
  * objects all props will be compared recursively. The built-in Date and RegExp
  * are special-cased and will be compared by their values.
  *
- * !IMPORTANT: Sets, TypedArrays, and symbol properties of objects are not
- * supported right now and might result in unexpected behavior. Please open an
- * issue in the Remeda github project if you need support for these types.
+ * !IMPORTANT: TypedArrays and symbol properties of objects are not supported
+ * right now and might result in unexpected behavior. Please open an issue in
+ * the Remeda github project if you need support for these types.
  *
  * The result would be narrowed to the second value so that the function can be
  * used as a type guard.
@@ -38,9 +38,9 @@ export function isDeepEqual<T, S extends T = T>(data: T, other: S): boolean;
  * objects all props will be compared recursively. The built-in Date and RegExp
  * are special-cased and will be compared by their values.
  *
- * !IMPORTANT: Sets, TypedArrays, and symbol properties of objects are not
- * supported right now and might result in unexpected behavior. Please open an
- * issue in the Remeda github project if you need support for these types.
+ * !IMPORTANT: TypedArrays and symbol properties of objects are not supported
+ * right now and might result in unexpected behavior. Please open an issue in
+ * the Remeda github project if you need support for these types.
  *
  * The result would be narrowed to the second value so that the function can be
  * used as a type guard.
@@ -95,22 +95,15 @@ function isDeepEqualImplementation<T, S>(data: S | T, other: S): data is S {
   }
 
   if (Array.isArray(data)) {
-    if (data.length !== (other as ReadonlyArray<unknown>).length) {
-      return false;
-    }
+    return isDeepEqualArrays(data, other as unknown as ReadonlyArray<unknown>);
+  }
 
-    for (const [index, item] of data.entries()) {
-      if (
-        !isDeepEqualImplementation(
-          item,
-          (other as ReadonlyArray<unknown>)[index],
-        )
-      ) {
-        return false;
-      }
-    }
+  if (data instanceof Map) {
+    return isDeepEqualMaps(data, other as unknown as Map<unknown, unknown>);
+  }
 
-    return true;
+  if (data instanceof Set) {
+    return isDeepEqualSets(data, other as unknown as Set<unknown>);
   }
 
   if (data instanceof Date) {
@@ -121,10 +114,6 @@ function isDeepEqualImplementation<T, S>(data: S | T, other: S): data is S {
     return data.toString() === (other as unknown as RegExp).toString();
   }
 
-  if (data instanceof Map) {
-    return isDeepEqualMaps(data, other as unknown as Map<unknown, unknown>);
-  }
-
   // At this point we only know that the 2 objects share a prototype and are not
   // any of the previous types. They could be plain objects (Object.prototype),
   // they could be classes, they could be other built-ins, or they could be
@@ -154,6 +143,23 @@ function isDeepEqualImplementation<T, S>(data: S | T, other: S): data is S {
   return true;
 }
 
+function isDeepEqualArrays(
+  data: ReadonlyArray<unknown>,
+  other: ReadonlyArray<unknown>,
+): boolean {
+  if (data.length !== other.length) {
+    return false;
+  }
+
+  for (const [index, item] of data.entries()) {
+    if (!isDeepEqualImplementation(item, other[index])) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
 function isDeepEqualMaps(
   data: ReadonlyMap<unknown, unknown>,
   other: ReadonlyMap<unknown, unknown>,
@@ -174,3 +180,36 @@ function isDeepEqualMaps(
 
   return true;
 }
+
+function isDeepEqualSets(
+  data: ReadonlySet<unknown>,
+  other: ReadonlySet<unknown>,
+): boolean {
+  if (data.size !== other.size) {
+    return false;
+  }
+
+  // To ensure we only count each item once we need to "remember" which items of
+  // the other set we've already matched against. We do this by creating a copy
+  // of the other set and removing items from it as we find them in the data
+  // set.
+  const otherCopy = [...other];
+
+  for (const dataItem of data) {
+    let isFound = false;
+
+    for (const [index, otherItem] of otherCopy.entries()) {
+      if (isDeepEqualImplementation(dataItem, otherItem)) {
+        isFound = true;
+        otherCopy.splice(index, 1);
+        break;
+      }
+    }
+
+    if (!isFound) {
+      return false;
+    }
+  }
+
+  return true;
+}


Test patch:
diff --git a/src/isDeepEqual.test.ts b/src/isDeepEqual.test.ts
index f4bc743..bb4b538 100644
--- a/src/isDeepEqual.test.ts
+++ b/src/isDeepEqual.test.ts
@@ -128,6 +128,53 @@ describe("objects", () => {
   });
 });
 
+describe("Sets", () => {
+  test("two empty sets should be equal", () => {
+    expect(isDeepEqual(new Set(), new Set())).toBe(true);
+  });
+
+  test("two sets of the same lenght should not be equal", () => {
+    expect(isDeepEqual(new Set([1]), new Set([1, 2]))).toBe(false);
+  });
+
+  test("two sets of with different primitive content should not be equal", () => {
+    expect(isDeepEqual(new Set([1, 2, 4]), new Set([1, 2, 3]))).toBe(false);
+  });
+
+  test("two sets of with the same primitive content should equal", () => {
+    expect(
+      isDeepEqual(new Set([{ a: 1 }, { b: 3 }]), new Set([{ b: 3 }, { a: 1 }])),
+    ).toBe(true);
+  });
+
+  test("two sets with duplicated non primitive content should not be equal", () => {
+    expect(
+      isDeepEqual(
+        new Set([{ a: 1 }, { b: 3 }, { a: 1 }]),
+        new Set([{ b: 3 }, { a: 1 }, { b: 3 }]),
+      ),
+    ).toBe(false);
+  });
+
+  test("two sets of Maps with the same values should be equal", () => {
+    expect(
+      isDeepEqual(
+        new Set([new Map([["a", 123]]), new Map([["b", 456]])]),
+        new Set([new Map([["b", 456]]), new Map([["a", 123]])]),
+      ),
+    ).toBe(true);
+  });
+
+  test("two sets with more than two items that are all equal should be equal", () => {
+    expect(
+      isDeepEqual(
+        new Set([{ a: 1 }, { b: 3 }, { c: 4 }, { a: 1 }]),
+        new Set([{ b: 3 }, { c: 4 }, { a: 1 }, { a: 1 }]),
+      ),
+    ).toBe(true);
+  });
+});
+
 describe("arrays", () => {
   test("two empty arrays are equal", () => {
     expect(isDeepEqual([], [])).toBe(true);