Repository: marpple/FxTS (ts)

Issue:
Add `intersectionBy` 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 = intersectionBy((o) => o.x, [{ x: 2 }, { x: 1 }], [{ x: 1 }, {x : 3}, {x : 4}]);
iter.next(); // {value:{x: 1}, done:false}
iter.next(); // {value:undefined, done:true}


```


Pull request description:
feat: add `intersectionBy` function
Fixes #82 


Patch:
diff --git a/src/Lazy/difference.ts b/src/Lazy/difference.ts
index 611add88..00cbf97e 100644
--- a/src/Lazy/difference.ts
+++ b/src/Lazy/difference.ts
@@ -4,7 +4,7 @@ import { isAsyncIterable, isIterable } from "../_internal/utils";
 import differenceBy from "./differenceBy";
 
 /**
- * Returns Iterable/AsyncIterable of all elements in the iterable2 not contained in the iterable1.
+ * Returns Iterable/AsyncIterable of all elements in the `iterable2` not contained in the `iterable1`.
  *
  * @example
  * ```ts
diff --git a/src/Lazy/differenceBy.ts b/src/Lazy/differenceBy.ts
index 6c6e9784..066e671b 100644
--- a/src/Lazy/differenceBy.ts
+++ b/src/Lazy/differenceBy.ts
@@ -8,10 +8,6 @@ import pipe from "../pipe";
 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>,
@@ -21,7 +17,7 @@ function* sync<T>(
 
   yield* pipe(
     iterable2,
-    reject((a) => hasValue(set, f(a))),
+    reject((a) => pipe1(f(a), (b) => set.has(b))),
     uniq,
   );
 }
@@ -35,7 +31,7 @@ async function* asyncSequential<T>(
 
   yield* pipe(
     iterable2,
-    reject((a) => hasValue(set, f(a))),
+    reject((a) => pipe1(f(a), (b) => set.has(b))),
     uniq,
   );
 }
diff --git a/src/Lazy/index.ts b/src/Lazy/index.ts
index fe05d270..ac5b14ef 100644
--- a/src/Lazy/index.ts
+++ b/src/Lazy/index.ts
@@ -12,6 +12,7 @@ import dropUntil from "./dropUntil";
 import filter from "./filter";
 import flat from "./flat";
 import flatMap from "./flatMap";
+import intersectionBy from "./intersectionBy";
 import map from "./map";
 import peek from "./peek";
 import pluck from "./pluck";
@@ -45,6 +46,7 @@ export {
   filter,
   flat,
   flatMap,
+  intersectionBy,
   map,
   peek,
   pluck,
diff --git a/src/Lazy/intersectionBy.ts b/src/Lazy/intersectionBy.ts
new file mode 100644
index 00000000..60b32f21
--- /dev/null
+++ b/src/Lazy/intersectionBy.ts
@@ -0,0 +1,123 @@
+import map from "./map";
+import filter from "./filter";
+import toAsync from "./toAsync";
+import toArray from "../toArray";
+import concurrent, { isConcurrent } from "./concurrent";
+import { isAsyncIterable, isIterable } from "../_internal/utils";
+import pipe1 from "../pipe1";
+import pipe from "../pipe";
+import uniq from "./uniq";
+
+function* sync<T>(
+  f: (a: T) => unknown,
+  iterable1: Iterable<T>,
+  iterable2: Iterable<T>,
+) {
+  const set = new Set(map(f, iterable1));
+
+  yield* pipe(
+    iterable2,
+    filter((a) => pipe1(f(a), (b) => set.has(b))),
+    uniq,
+  );
+}
+
+async function* asyncSequential<T>(
+  f: (a: T) => unknown,
+  iterable1: AsyncIterable<T>,
+  iterable2: AsyncIterable<T>,
+) {
+  const set = new Set(await toArray(map(f, iterable1)));
+
+  yield* pipe(
+    iterable2,
+    filter((a) => pipe1(f(a), (b) => set.has(b))),
+    uniq,
+  );
+}
+
+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` contained in the `iterable1`.
+ * Duplication is determined according to the value returned by applying the supplied `f` to `iterable2`.
+ *
+ * @example
+ * ```ts
+ * const iter = intersectionBy(a => a.x, [{ x: 1 }, { x: 4 }], [{ x: 1 },  { x: 2 },  { x: 3 }])
+ * iter.next(); // {value: {x: 1, done: false}
+ * iter.next(); // {value: undefined, done: true}
+ * ```
+ */
+function intersectionBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: Iterable<A>,
+  iterable2: Iterable<A>,
+): IterableIterator<A>;
+
+function intersectionBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: AsyncIterable<A>,
+  iterable2: Iterable<A>,
+): AsyncIterableIterator<A>;
+
+function intersectionBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: Iterable<A>,
+  iterable2: AsyncIterable<A>,
+): AsyncIterableIterator<A>;
+
+function intersectionBy<A, B = unknown>(
+  f: (a: A) => B,
+  iterable1: AsyncIterable<A>,
+  iterable2: AsyncIterable<A>,
+): AsyncIterableIterator<A>;
+
+function intersectionBy<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 intersectionBy;
diff --git a/src/add.ts b/src/add.ts
deleted file mode 100644
index e0e280cc..00000000
--- a/src/add.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import isNumber from "./isNumber";
-import isString from "./isString";
-import Awaited from "./types/Awaited";
-
-// prettier-ignore
-type ReturnAddType<T, A extends T, B extends T> = 
-  | [A] extends [Promise<Awaited<T>>]
-  ? Promise<Awaited<T>>
-  : [B] extends [Promise<Awaited<T>>]
-  ? Promise<Awaited<T>>
-  : Awaited<T>;
-
-function sync(a: number | string, b: number | string) {
-  if (isNumber(a) && isNumber(b)) {
-    return a + b;
-  }
-
-  if (isString(a) && isString(b)) {
-    return a + b;
-  }
-
-  throw new TypeError("'a' or 'b' must be type of number or string");
-}
-
-async function async(a: Promise<number | string>, b: Promise<number | string>) {
-  return sync(await a, await b);
-}
-
-/**
- * Adds two values.
- *
- * @example
- * ```ts
- * add(1, 2); // 3
- * await add(1, Promise.resolve(2)); // 3
- * await add(Promise.resolve(1), 2); // 3
- * await add(Promise.resolve(1), Promise.resolve(2)); // 3
- *
- * add('a', 'b'); // 'ab'
- * await add('a', Promise.resolve('b')); // 'ab'
- * await add(Promise.resolve('a'), 'b'); // 'ab'
- * await add(Promise.resolve('a'), Promise.resolve('b')); // 'ab'
- * ```
- */
-function add<
-  A extends number | Promise<number>,
-  B extends number | Promise<number>,
->(a: A, b: B): ReturnAddType<number | Promise<number>, A, B>;
-function add<
-  A extends string | Promise<string>,
-  B extends string | Promise<string>,
->(a: A, b: B): ReturnAddType<string | Promise<string>, A, B>;
-
-function add<
-  A extends number | Promise<number>,
-  B extends number | Promise<number>,
->(a: A): (b: B) => ReturnAddType<number | Promise<number>, A, B>;
-
-function add<
-  A extends string | Promise<string>,
-  B extends string | Promise<string>,
->(a: A): (b: B) => ReturnAddType<string | Promise<string>, A, B>;
-
-function add<
-  A extends string | number | Promise<string | number>,
-  B extends string | number | Promise<string | number>,
->(a: A, b?: B): number | string | Promise<string | number> | ((b: B) => any) {
-  if (b === undefined) {
-    return (b: B) => {
-      return add(a as any, b as any);
-    };
-  }
-
-  if (a instanceof Promise || b instanceof Promise) {
-    return async(Promise.resolve(a), Promise.resolve(b));
-  }
-
-  return sync(a, b);
-}
-
-export default add;
diff --git a/src/index.ts b/src/index.ts
index ae41e4e4..222cba20 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,3 @@
-import add from "./add";
 import apply from "./apply";
 import consume from "./consume";
 import countBy from "./countBy";
@@ -16,7 +15,6 @@ import indexBy from "./indexBy";
 import isArray from "./isArray";
 import isEmpty from "./isEmpty";
 import isNil from "./isNil";
-import isNumber from "./isNumber";
 import isObject from "./isObject";
 import isString from "./isString";
 import join from "./join";
@@ -41,7 +39,6 @@ import tap from "./tap";
 import toArray from "./toArray";
 
 export {
-  add,
   apply,
   consume,
   countBy,
@@ -61,7 +58,6 @@ export {
   isArray,
   isEmpty,
   isNil,
-  isNumber,
   isObject,
   isString,
   join,
diff --git a/src/isNumber.ts b/src/isNumber.ts
deleted file mode 100644
index a81e1019..00000000
--- a/src/isNumber.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * Returns true if `n` is a Number.
- *
- * @example
- * ```ts
- * isNumber("a"); // true
- * isNumber(2); // false
- * ```
- */
-function isNumber(n: unknown): n is number {
-  return typeof n === "number";
-}
-
-export default isNumber;


Test patch:
diff --git a/test/Lazy/intersectionBy.spec.ts b/test/Lazy/intersectionBy.spec.ts
new file mode 100644
index 00000000..b9375869
--- /dev/null
+++ b/test/Lazy/intersectionBy.spec.ts
@@ -0,0 +1,69 @@
+import { toArray, toAsync, intersectionBy } from "../../src";
+import { Concurrent } from "../../src/Lazy/concurrent";
+import { generatorMock } from "../utils";
+
+describe("intersectionBy", function () {
+  describe("sync", function () {
+    it.each([
+      [
+        [{ x: 1 }, { x: 4 }],
+        [{ x: 1 }, { x: 2 }, { x: 3 }],
+        (a: any) => a.x,
+        [{ x: 1 }],
+      ],
+      ["abcd", "cdefgc", (a: any) => a, ["c", "d"]],
+    ])(
+      "should return all elements in %o contained %o",
+      function (iterable1, iterable2, f, result) {
+        const iter = intersectionBy(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: 1 }],
+      ],
+
+      [
+        [{ x: 1 }, { x: 4 }],
+        toAsync([{ x: 1 }, { x: 2 }, { x: 3 }]),
+        (a: any) => a.x,
+        [{ x: 1 }],
+      ],
+      [
+        toAsync([{ x: 1 }, { x: 4 }]),
+        [{ x: 1 }, { x: 2 }, { x: 3 }],
+        (a: any) => a.x,
+        [{ x: 1 }],
+      ],
+      ["abcd", "cdefgc", (a: any) => a, ["c", "d"]],
+    ])(
+      "should return all elements in iterable1 contained iterable2",
+      async function (iterable1, iterable2, f, result) {
+        const iter = intersectionBy(
+          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 = intersectionBy((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/test/add.spec.ts b/test/add.spec.ts
deleted file mode 100644
index ce3441fe..00000000
--- a/test/add.spec.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { add, pipe } from "../src";
-
-describe("add", function () {
-  describe("sync", function () {
-    it.each([
-      [1, 2, 3],
-      ["a", "b", "ab"],
-    ])("should add two values %s %s", function (a, b, result) {
-      expect(add(a as any, b as any)).toEqual(result);
-    });
-
-    it("should be able to be used as a curried function in the pipeline", function () {
-      const res1 = pipe(1, add(2));
-      expect(res1).toEqual(3);
-      const res2 = pipe("b", add("a"));
-      expect(res2).toEqual("ab");
-    });
-  });
-
-  describe("async", function () {
-    it.each([
-      [Promise.resolve(1), 2, 3],
-      [1, Promise.resolve(2), 3],
-      [Promise.resolve(1), Promise.resolve(2), 3],
-      [Promise.resolve("a"), "b", "ab"],
-      ["a", Promise.resolve("b"), "ab"],
-      [Promise.resolve("a"), Promise.resolve("b"), "ab"],
-    ])("should add two values %s %s", async function (a, b, result) {
-      expect(await add(a as Promise<any>, b as Promise<any>)).toEqual(result);
-    });
-
-    it("should be able to be used as a curried function in the pipeline", async function () {
-      const res1 = await pipe(1, add(Promise.resolve(2)));
-      expect(res1).toEqual(3);
-      const res2 = await pipe("b", add(Promise.resolve("a")));
-      expect(res2).toEqual("ab");
-    });
-  });
-});
diff --git a/test/isNumber.spec.ts b/test/isNumber.spec.ts
deleted file mode 100644
index 706d114f..00000000
--- a/test/isNumber.spec.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { isNumber } from "../src/index";
-
-describe("isNumber", function () {
-  it.each([undefined, null, true, "1", Symbol("a"), () => null])(
-    "given non number then should return false",
-    function (s) {
-      const result = isNumber(s);
-      expect(result).toEqual(false);
-    },
-  );
-
-  it("given number then should return true", function () {
-    const result = isNumber(2);
-    expect(result).toEqual(true);
-  });
-});
diff --git a/type-check/Lazy/intersectionBy.test.ts b/type-check/Lazy/intersectionBy.test.ts
new file mode 100644
index 00000000..d6048574
--- /dev/null
+++ b/type-check/Lazy/intersectionBy.test.ts
@@ -0,0 +1,22 @@
+import * as Test from "../../src/types/Test";
+import { intersectionBy, toAsync } from "../../src";
+
+const { checks, check } = Test;
+
+const res1 = intersectionBy((a) => a, [1, 2, 3, 4], [5, 6, 3, 4]);
+const res2 = intersectionBy((a) => String(a), [1, 2, 3, 4], [5, 6, 3, 4]);
+const res3 = intersectionBy((a) => a, toAsync([1, 2, 3, 4]), [5, 6, 3, 4]);
+const res4 = intersectionBy((a) => a, [1, 2, 3, 4], toAsync([5, 6, 3, 4]));
+const res5 = intersectionBy(
+  (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>(),
+]);
diff --git a/type-check/add.test.ts b/type-check/add.test.ts
deleted file mode 100644
index a931653d..00000000
--- a/type-check/add.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as Test from "../src/types/Test";
-import { add, pipe } from "../src";
-
-const { checks, check } = Test;
-
-const res1 = add(1, 2);
-const res2 = add("a", "b");
-const res3 = add(Promise.resolve(1), 2);
-const res4 = add(1, Promise.resolve(2));
-const res5 = add(Promise.resolve(1), Promise.resolve(2));
-const res6 = add(Promise.resolve("a"), "b");
-const res7 = add("a", Promise.resolve("b"));
-const res8 = add(Promise.resolve("a"), Promise.resolve("b"));
-const res9 = pipe(2, add(1));
-const res10 = pipe("b", add("a"));
-const res11 = pipe(Promise.resolve(2), add(1));
-const res12 = pipe(Promise.resolve("b"), add("a"));
-const res13 = add(2)(3);
-const res14 = add("a")("b");
-
-checks([
-  check<typeof res1, number, Test.Pass>(),
-  check<typeof res2, string, Test.Pass>(),
-  check<typeof res3, Promise<number>, Test.Pass>(),
-  check<typeof res4, Promise<number>, Test.Pass>(),
-  check<typeof res5, Promise<number>, Test.Pass>(),
-  check<typeof res6, Promise<string>, Test.Pass>(),
-  check<typeof res7, Promise<string>, Test.Pass>(),
-  check<typeof res8, Promise<string>, Test.Pass>(),
-  check<typeof res9, number, Test.Pass>(),
-  check<typeof res10, string, Test.Pass>(),
-  check<typeof res11, Promise<number>, Test.Pass>(),
-  check<typeof res12, Promise<string>, Test.Pass>(),
-  check<typeof res13, number, Test.Pass>(),
-  check<typeof res14, string, Test.Pass>(),
-]);
diff --git a/type-check/isNumber.test.ts b/type-check/isNumber.test.ts
deleted file mode 100644
index 751d82e8..00000000
--- a/type-check/isNumber.test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as Test from "../src/types/Test";
-import { isNumber, pipe } from "../src";
-
-const { checks, check } = Test;
-
-const res1 = isNumber(1);
-const res2 = isNumber("a");
-const res3 = pipe(1, isNumber);
-
-checks([
-  check<typeof res1, boolean, Test.Pass>(),
-  check<typeof res2, boolean, Test.Pass>(),
-  check<typeof res3, boolean, Test.Pass>(),
-]);