Repository: marpple/FxTS (ts)

Issue:
Add `differenceBy` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

## 💻 Use Cases

<!-- What do you want to use this for? -->

```ts

const iter = differenceBy(a => a.x, [{ x: 1 }, { x: 4 }], [{ x: 1 },  { x: 2 },  { x: 3 } ])
iter.next(); // {value: {x: 2}, done: false}
iter.next(); // {value: {x: 3}, done: false}
iter.next(); // {value: undefined, done: true}

```



Pull request description:
feat: add `differenceBy` function
Fixes #86 

Unlike `ramda`, we put the item of the second iterable(`iterable2`) as the first function argument.

Also,`ramda` filter by the first iterable, while FxTS filters by the second iterable.

ramda
```ts
const cmp = (x, y) => x.a === y.a;
const l1 = [{a: 1}, {a: 2}, {a: 3}];
const l2 = [{a: 3}, {a: 4}];
R.differenceWith(cmp, l1, l2); //=> [{a: 1}, {a: 2}]
```

FxTS
 ```ts
const iter = differenceBy(a => a.x, [{ x: 1 }, { x: 4 }], [{ x: 1 },  { x: 2 },  { x: 3 }])
 iter.next(); // {value: {x: 2}, done: false}
 iter.next(); // {value: {x: 3}, done: false}
 iter.next(); // {value: undefined, done: true}
```

Patch:
diff --git a/src/Lazy/differenceBy.ts b/src/Lazy/differenceBy.ts
new file mode 100644
index 00000000..0c4480ba
--- /dev/null
+++ b/src/Lazy/differenceBy.ts
@@ -0,0 +1,116 @@
+import map from "./map";
+import reject from "./reject";
+import pipe1 from "../pipe1";
+import toAsync from "./toAsync";
+import toArray from "../toArray";
+import concurrent, { isConcurrent } from "./concurrent";
+import { isAsyncIterable, isIterable } from "../_internal/utils";
+
+const hasValue = (set: Set<unknown>, a: unknown) => {
+  return pipe1(a, (b) => set.has(b));
+};
+
+function* sync<T>(
+  f: (a: T) => unknown,
+  iterable1: Iterable<T>,
+  iterable2: Iterable<T>,
+) {
+  const set = new Set(map(f, iterable1));
+  yield* reject((a) => hasValue(set, f(a)), iterable2);
+}
+
+async function* asyncSequential<T>(
+  f: (a: T) => unknown,
+  iterable1: AsyncIterable<T>,
+  iterable2: AsyncIterable<T>,
+) {
+  const set = new Set(await toArray(map(f, iterable1)));
+  yield* reject((a) => hasValue(set, f(a)), iterable2);
+}
+
+function async<T>(
+  f: (a: T) => unknown,
+  iterable1: AsyncIterable<T>,
+  iterable2: AsyncIterable<T>,
+): AsyncIterableIterator<T> {
+  let _iterator: AsyncIterator<T>;
+  return {
+    async next(_concurrent: any) {
+      if (_iterator === undefined) {
+        _iterator = isConcurrent(_concurrent)
+          ? asyncSequential(
+              f,
+              iterable1,
+              concurrent(_concurrent.length, iterable2),
+            )
+          : asyncSequential(f, iterable1, iterable2);
+      }
+
+      return _iterator.next(_concurrent);
+    },
+    [Symbol.asyncIterator]() {
+      return this;
+    },
+  };
+}
+
+/**
+ * Returns Iterable/AsyncIterable(i.e no duplicate) of all elements in the `iterable2` not contained in the `iterable1`.
+ * Duplication is determined according to the value returned by applying the supplied `f` to `iterable2`.
+ *
+ * @example
+ * ```ts
+ * const iter = differenceBy(a => a.x, [{ x: 1 }, { x: 4 }], [{ x: 1 },  { x: 2 },  { x: 3 }])
+ * iter.next(); // {value: {x: 2}, done: false}
+ * iter.next(); // {value: {x: 3}, done: false}
+ * iter.next(); // {value: undefined, done: true}
+ * ```
+ */
+function differenceBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: Iterable<A>,
+  iterable2: Iterable<A>,
+): IterableIterator<A>;
+
+function differenceBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: AsyncIterable<A>,
+  iterable2: Iterable<A>,
+): AsyncIterableIterator<A>;
+
+function differenceBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: Iterable<A>,
+  iterable2: AsyncIterable<A>,
+): AsyncIterableIterator<A>;
+
+function differenceBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: AsyncIterable<A>,
+  iterable2: AsyncIterable<A>,
+): AsyncIterableIterator<A>;
+
+function differenceBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: Iterable<A> | AsyncIterable<A>,
+  iterable2: Iterable<A> | AsyncIterable<A>,
+): IterableIterator<A> | AsyncIterableIterator<A> {
+  if (isIterable(iterable1) && isIterable(iterable2)) {
+    return sync(f, iterable1, iterable2);
+  }
+  if (isIterable(iterable1) && isAsyncIterable(iterable2)) {
+    return async(f, toAsync(iterable1), iterable2);
+  }
+  if (isAsyncIterable(iterable1) && isIterable(iterable2)) {
+    return async(f, iterable1, toAsync(iterable2));
+  }
+  if (isAsyncIterable(iterable1) && isAsyncIterable(iterable2)) {
+    return async(f, iterable1, iterable2);
+  }
+
+  throw new TypeError(
+    "'iterable1' and 'iterable2' must be type of Iterable or AsyncIterable",
+  );
+}
+
+export default differenceBy;
diff --git a/src/Lazy/index.ts b/src/Lazy/index.ts
index 82b8f7d6..86852b4f 100644
--- a/src/Lazy/index.ts
+++ b/src/Lazy/index.ts
@@ -4,6 +4,7 @@ import compact from "./compact";
 import concat from "./concat";
 import concurrent from "./concurrent";
 import cycle from "./cycle";
+import differenceBy from "./differenceBy";
 import drop from "./drop";
 import dropWhile from "./dropWhile";
 import dropUntil from "./dropUntil";
@@ -34,6 +35,7 @@ export {
   concat,
   concurrent,
   cycle,
+  differenceBy,
   drop,
   dropWhile,
   dropUntil,


Test patch:
diff --git a/test/Lazy/differenceBy.spec.ts b/test/Lazy/differenceBy.spec.ts
new file mode 100644
index 00000000..043cd2fc
--- /dev/null
+++ b/test/Lazy/differenceBy.spec.ts
@@ -0,0 +1,70 @@
+import { toArray, toAsync } from "../../src";
+import { Concurrent } from "../../src/Lazy/concurrent";
+import differenceBy from "../../src/Lazy/differenceBy";
+import { generatorMock } from "../utils";
+
+describe("differenceBy", function () {
+  describe("sync", function () {
+    it.each([
+      [
+        [{ x: 1 }, { x: 4 }],
+        [{ x: 1 }, { x: 2 }, { x: 3 }],
+        (a: any) => a.x,
+        [{ x: 2 }, { x: 3 }],
+      ],
+      ["abcd", "cdefg", (a: any) => a, ["e", "f", "g"]],
+    ])(
+      "should return all elements in %o not contained %o",
+      function (iterable1, iterable2, f, result) {
+        const iter = differenceBy(f, iterable1 as any, iterable2 as any);
+        expect(toArray(iter)).toEqual(result);
+      },
+    );
+  });
+
+  describe("async", function () {
+    it.each([
+      [
+        toAsync([{ x: 1 }, { x: 4 }]),
+        toAsync([{ x: 1 }, { x: 2 }, { x: 3 }]),
+        (a: any) => a.x,
+        [{ x: 2 }, { x: 3 }],
+      ],
+
+      [
+        [{ x: 1 }, { x: 4 }],
+        toAsync([{ x: 1 }, { x: 2 }, { x: 3 }]),
+        (a: any) => a.x,
+        [{ x: 2 }, { x: 3 }],
+      ],
+      [
+        toAsync([{ x: 1 }, { x: 4 }]),
+        [{ x: 1 }, { x: 2 }, { x: 3 }],
+        (a: any) => a.x,
+        [{ x: 2 }, { x: 3 }],
+      ],
+      ["abcd", "cdefg", (a: any) => a, ["e", "f", "g"]],
+    ])(
+      "should return all elements in iterable1 not contained iterable2",
+      async function (iterable1, iterable2, f, result) {
+        const iter = differenceBy(
+          f,
+          iterable1 as AsyncIterable<any>,
+          iterable2 as AsyncIterable<any>,
+        );
+        expect(await toArray(iter)).toEqual(result);
+      },
+    );
+
+    it("should be passed concurrent object when job works concurrently", async function () {
+      const mock1 = generatorMock();
+      const mock2 = generatorMock();
+      const iter = differenceBy((a) => a, mock1, mock2);
+      const concurrent = Concurrent.of(2) as any;
+
+      await iter.next(concurrent);
+      // mock2 only concurrent
+      expect((mock2 as any).getConcurrent()).toEqual(concurrent);
+    });
+  });
+});
diff --git a/type-check/Lazy/differenceBy.test.ts b/type-check/Lazy/differenceBy.test.ts
new file mode 100644
index 00000000..712341f1
--- /dev/null
+++ b/type-check/Lazy/differenceBy.test.ts
@@ -0,0 +1,22 @@
+import * as Test from "../../src/types/Test";
+import { differenceBy, toAsync } from "../../src";
+
+const { checks, check } = Test;
+
+const res1 = differenceBy((a) => a, [1, 2, 3, 4], [5, 6, 3, 4]);
+const res2 = differenceBy((a) => String(a), [1, 2, 3, 4], [5, 6, 3, 4]);
+const res3 = differenceBy((a) => a, toAsync([1, 2, 3, 4]), [5, 6, 3, 4]);
+const res4 = differenceBy((a) => a, [1, 2, 3, 4], toAsync([5, 6, 3, 4]));
+const res5 = differenceBy(
+  (a) => a,
+  toAsync([1, 2, 3, 4]),
+  toAsync([5, 6, 3, 4]),
+);
+
+checks([
+  check<typeof res1, IterableIterator<number>, Test.Pass>(),
+  check<typeof res2, IterableIterator<number>, Test.Pass>(),
+  check<typeof res3, AsyncIterableIterator<number>, Test.Pass>(),
+  check<typeof res4, AsyncIterableIterator<number>, Test.Pass>(),
+  check<typeof res5, AsyncIterableIterator<number>, Test.Pass>(),
+]);