Repository: remeda/remeda (ts)

Issue:
`uniqueWith` doesn't return items that are not equal to themselves
```ts
const errors = [
  { id: 1, reason: 'No name' },
  { id: 1, reason: 'No name' },
  { reason: 'No name' },
  { reason: 'No name' }
]

R.uniqueWith(errors, (errorA, errorB) => {
  if(errorA.id === undefined || errorB.id === undefined){
    return false
  }
  return errorA.id === errorB.id
})
```

expected
```ts
[
  { id: 1, reason: 'No name' },
  { reason: 'No name' },
  { reason: 'No name' }
]
```

actual
```ts
[
  { id: 1, reason: 'No name' },
]
```

It was unexpected for me that items not equal to themselves were removed from the array

Pull request description:
fix(uniqueWith): return items that are not equal to themselves
Fixes #999 

- Previous implementation checked each item for a match that existed at a different index, skipping those. It checks the item's own equivalency as part of the implementation, but this can unexpectedly remove items due to interactions with the comparator's logic as shown in the issue.
- In the new implementation, the index equality check simultaneously handles this edge case and reduces unnecessary calls of the comparator.

---

Make sure that you:

- [x] Typedoc added for new methods and updated for changed
- [x] Tests added for new methods and updated for changed
- [x] New methods added to `src/index.ts`
- [x] New methods added to `docs/src/content/mapping` for both Lodash and Ramda.

---

<details><summary>We use semantic PR titles to automate the release process!</summary>

https://conventionalcommits.org

PRs should be titled following using the format: `< TYPE >(< scope >)?: description`

### Available Types:

- `feat`: new functions, and changes to a function's type that would impact users.
- `fix`: changes to the runtime behavior of an existing function, or refinements to it's type that shouldn't impact most users.
- `test`: tests-only changes (transparent to users of the function).
- `docs`: changes to the documentation of a function **or the documentation site**.
- `build`, `ci`, `chore`, and `revert`: are only relevant for the internals of the library.

For scope put the name of the function you are working on (either new or
existing).

</details>


Patch:
diff --git a/src/uniqueWith.ts b/src/uniqueWith.ts
index 065a980..45ed366 100644
--- a/src/uniqueWith.ts
+++ b/src/uniqueWith.ts
@@ -57,7 +57,14 @@ export function uniqueWith(...args: ReadonlyArray<unknown>): unknown {
 
 const lazyImplementation =
   <T>(isEquals: IsEquals<T>): LazyEvaluator<T> =>
-  (value, index, data) =>
-    data.findIndex((otherValue) => isEquals(value, otherValue)) === index
+  (value, index, data) => {
+    const firstEqualIndex = data.findIndex(
+      (otherValue, otherIndex) =>
+        index === otherIndex || isEquals(value, otherValue),
+    );
+
+    // skip items that aren't at the first equal index.
+    return firstEqualIndex === index
       ? { done: false, hasNext: true, next: value }
       : SKIP_ITEM;
+  };


Test patch:
diff --git a/src/uniqueWith.test.ts b/src/uniqueWith.test.ts
index faf8364..8d76bd3 100644
--- a/src/uniqueWith.test.ts
+++ b/src/uniqueWith.test.ts
@@ -19,6 +19,31 @@ describe("data_first", () => {
   test("should return uniq", () => {
     expect(uniqueWith(source, isDeepEqual)).toStrictEqual(expected);
   });
+
+  it("should return items that are not equal to themselves", () => {
+    // test case based on https://github.com/remeda/remeda/issues/999
+    const data = [
+      { id: 1, reason: "No name" },
+      { id: 1, reason: "No name" },
+      { reason: "No name" },
+      { reason: "No name" },
+    ];
+    const expectedResult = [
+      { id: 1, reason: "No name" },
+      { reason: "No name" },
+      { reason: "No name" },
+    ];
+
+    const result = uniqueWith(data, (errorA, errorB) => {
+      // the objects with no ids should effectively be ignored from removal of duplicates
+      if (errorA.id === undefined || errorB.id === undefined) {
+        return false;
+      }
+      return errorA.id === errorB.id;
+    });
+
+    expect(result).toStrictEqual(expectedResult);
+  });
 });
 
 describe("data_last", () => {