Repository: marpple/FxTS (ts)

Issue:
Feature ` dropWhile` `dropUntil` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->


## ⭐ Suggestion

<!-- What would you like to be added? -->


- [x] dropWhile(Lazy):
Returns Iterable/AsyncIterable excluding elements dropped from the beginning. Elements are dropped until the value applied to `f` returns falsey.

- [ ] dropUntil(Lazy):
Returns Iterable/AsyncIterable excluding elements dropped from the beginning. Elements are deleted until the value applied to `f` returns truly. (It is deleted including the value applied as true.)


## 💻 Use Cases

<!-- What do you want to use this for? -->


```ts
const dropWhileIter = dropWhile(a => a < 5, [1,2,3,4,5,6,7,8,9,10]);
dropWhileIter.next(); // 5
dropWhileIter.next(); // 6
dropWhileIter.next(); // 7
dropWhileIter.next(); // 8
dropWhileIter.next(); // 9
dropWhileIter.next(); // 10

const dropUntilIter = dropUntil(a => a > 5, [1,2,3,4,5,6,7,8,9,10]);
dropUntilIter.next(); // 7
dropUntilIter.next(); // 8
dropUntilIter.next(); // 9
dropUntilIter.next(); // 10
```

Pull request:
feat: add `dropWhile` function 
Fixes #22


Patch:
diff --git a/src/Lazy/dropWhile.ts b/src/Lazy/dropWhile.ts
new file mode 100644
index 00000000..c5806a38
--- /dev/null
+++ b/src/Lazy/dropWhile.ts
@@ -0,0 +1,153 @@
+import IterableInfer from "../types/IterableInfer";
+import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
+import { AsyncFunctionException } from "../_internal/error";
+import { isAsyncIterable, isIterable } from "../_internal/utils";
+import concurrent, { isConcurrent } from "./concurrent";
+
+function* sync<A, B>(f: (a: A) => B, iterable: Iterable<A>) {
+  const iterator = iterable[Symbol.iterator]();
+  const iterableIterator = {
+    [Symbol.iterator]() {
+      return iterator;
+    },
+  };
+
+  for (const a of iterableIterator) {
+    const res = f(a);
+    if (res instanceof Promise) {
+      throw new AsyncFunctionException();
+    }
+
+    if (res) {
+      continue;
+    }
+    yield a;
+    yield* iterableIterator;
+  }
+}
+
+async function* asyncSequential<A, B>(
+  f: (a: A) => B,
+  iterable: AsyncIterable<A>,
+): AsyncIterableIterator<A> {
+  const iterator = iterable[Symbol.asyncIterator]();
+  const iterableIterator = {
+    [Symbol.asyncIterator]() {
+      return iterator;
+    },
+  };
+
+  for await (const a of iterableIterator) {
+    if (await f(a)) {
+      continue;
+    }
+
+    yield a;
+    yield* iterableIterator;
+  }
+}
+
+function async<A, B>(
+  f: (a: A) => B,
+  iterable: AsyncIterable<A>,
+): AsyncIterableIterator<A> {
+  let iterator: AsyncIterator<A>;
+  return {
+    [Symbol.asyncIterator]() {
+      return this;
+    },
+
+    async next(_concurrent: any) {
+      if (iterator === undefined) {
+        iterator = isConcurrent(_concurrent)
+          ? asyncSequential(f, concurrent(_concurrent.length, iterable))
+          : asyncSequential(f, iterable);
+      }
+
+      return iterator.next(_concurrent);
+    },
+  };
+}
+
+/**
+ * Returns Iterable/AsyncIterable excluding elements dropped from the beginning. Elements are dropped until the value applied to `f` returns falsey.
+ *
+ * @example
+ * ```ts
+ * const iter = dropWhile((a) => a < 3, [1, 2, 3, 4, 5]);
+ * iter.next(); // {done:false, value: 3}
+ * iter.next(); // {done:false, value: 4}
+ * iter.next(); // {done:false, value: 5}
+ *
+ * // with pipe
+ * pipe(
+ *  [1, 2, 3, 4, 5],
+ *  dropWhile((a) => a < 3),
+ *  toArray,
+ * ); // [3, 4, 5]
+ *
+ * await pipe(
+ *  Promise.resolve([1, 2, 3, 4, 5]),
+ *  dropWhile((a) => a < 3),
+ *  toArray,
+ * ); // [3, 4, 5]
+ *
+ * // if you want to use asynchronous callback
+ * await pipe(
+ *  Promise.resolve([1, 2, 3, 4, 5]),
+ *  toAsync,
+ *  dropWhile(async (a) => a < 3),
+ *  toArray,
+ * ); // [3, 4, 5]
+ *
+ * // with toAsync
+ * await pipe(
+ *  [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3), Promise.resolve(4), Promise.resolve(5)],
+ *  toAsync,
+ *  dropWhile((a) => a < 3),
+ *  toArray,
+ * ); // [3, 4, 5]
+ *
+ * see {@link https://fxts.dev/docs/pipe | pipe}, {@link https://fxts.dev/docs/toAsync | toAsync},
+ * {@link https://fxts.dev/docs/toArray | toArray}
+ * ```
+ */
+function dropWhile<A, B = unknown>(
+  f: (a: A) => B,
+  iterable: Iterable<A>,
+): IterableIterator<A>;
+
+function dropWhile<A, B = unknown>(
+  f: (a: A) => B,
+  iterable: AsyncIterable<A>,
+): AsyncIterableIterator<A>;
+
+function dropWhile<A extends Iterable<unknown> | AsyncIterable<unknown>, B>(
+  f: (a: IterableInfer<A>) => B,
+): (iterable: A) => ReturnIterableIteratorType<A>;
+
+function dropWhile<A extends Iterable<unknown> | AsyncIterable<unknown>, B>(
+  f: (a: IterableInfer<A>) => B,
+  iterable?: A,
+):
+  | IterableIterator<IterableInfer<A>>
+  | AsyncIterableIterator<IterableInfer<A>>
+  | ((iterable: A) => ReturnIterableIteratorType<A>) {
+  if (iterable === undefined) {
+    return (iterable: A) => {
+      return dropWhile(f, iterable as any) as ReturnIterableIteratorType<A>;
+    };
+  }
+
+  if (isIterable(iterable)) {
+    return sync(f, iterable as Iterable<IterableInfer<A>>);
+  }
+
+  if (isAsyncIterable(iterable)) {
+    return async(f, iterable as AsyncIterable<IterableInfer<A>>);
+  }
+
+  throw new TypeError("iterable must be type of Iterable or AsyncIterable");
+}
+
+export default dropWhile;
diff --git a/src/Lazy/index.ts b/src/Lazy/index.ts
index 5d2a369b..78f88b83 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 drop from "./drop";
+import dropWhile from "./dropWhile";
 import filter from "./filter";
 import flat from "./flat";
 import flatMap from "./flatMap";
@@ -28,6 +29,7 @@ export {
   concat,
   concurrent,
   drop,
+  dropWhile,
   filter,
   flat,
   flatMap,
diff --git a/src/index.ts b/src/index.ts
index 07424ef7..b0230e02 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,7 +9,6 @@ import identity from "./identity";
 import indexBy from "./indexBy";
 import isArray from "./isArray";
 import isString from "./isString";
-import join from "./join";
 import last from "./last";
 import not from "./not";
 import partition from "./partition";
@@ -33,7 +32,6 @@ export {
   indexBy,
   isArray,
   isString,
-  join,
   last,
   not,
   partition,
diff --git a/src/join.ts b/src/join.ts
deleted file mode 100644
index 36561ee3..00000000
--- a/src/join.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import reduce from "./reduce";
-import { isAsyncIterable, isIterable } from "./_internal/utils";
-
-type ReturnJoinType<T extends Iterable<unknown> | AsyncIterable<unknown>> =
-  T extends Iterable<unknown>
-    ? string
-    : T extends AsyncIterable<unknown>
-    ? Promise<string>
-    : never;
-
-function sync<A>(sep: string, iterable: Iterable<A>) {
-  return reduce((a: string, b) => `${a}${sep}${b}`, iterable);
-}
-
-function async<A>(sep: string, iterable: AsyncIterable<A>) {
-  return reduce((a: string, b) => `${a}${sep}${b}`, iterable);
-}
-
-/**
- * Returns all elements in the given iterable into a string separated by separator.
- *
- * @example
- * ```ts
- * const joined = join('~', ['a', 'b', 'c']); // 'a~b~c'
- *
- * // with pipe
- * pipe(
- *  [1, 2, 3, 4],
- *  map(a => a + 10),
- *  filter(a => a % 2 === 0)
- *  join('-'),
- * ); // '12-14'
- *
- * await pipe(
- *  Promise.resolve([1, 2, 3, 4]),
- *  join('-'),
- * ); // '1-2-3-4'
- *
- * // with toAsync
- * await pipe(
- *  [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3), Promise.resolve(4)],
- *  toAsync,
- *  join('-'),
- * ); // '1-2-3-4'
- * ```
- */
-function join<A extends readonly []>(sep: string, iterable: A): "";
-
-function join<A>(sep: string, iterable: Iterable<A>): string;
-
-function join<A>(sep: string, iterable: AsyncIterable<A>): Promise<string>;
-
-function join<A extends Iterable<unknown> | AsyncIterable<unknown>>(
-  sep: string,
-): (iterable: A) => ReturnJoinType<A>;
-
-function join<A extends Iterable<unknown> | AsyncIterable<unknown>>(
-  sep: string,
-  iterable?: A,
-): string | Promise<string> | ((iterable: A) => ReturnJoinType<A>) {
-  if (iterable === undefined) {
-    return (iterable: A): ReturnJoinType<A> => {
-      return join(sep, iterable as any) as ReturnJoinType<A>;
-    };
-  }
-
-  if (Array.isArray(iterable) && iterable.length === 0) return "";
-
-  if (isIterable(iterable)) {
-    return sync(sep, iterable) as string;
-  }
-
-  if (isAsyncIterable(iterable)) {
-    return async(sep, iterable) as Promise<string>;
-  }
-
-  throw new TypeError("iterable must be type of Iterable or AsyncIterable");
-}
-
-export default join;


Test patch:
diff --git a/test/Lazy/dropWhile.spec.ts b/test/Lazy/dropWhile.spec.ts
new file mode 100644
index 00000000..a878bdd4
--- /dev/null
+++ b/test/Lazy/dropWhile.spec.ts
@@ -0,0 +1,162 @@
+import {
+  concurrent,
+  delay,
+  dropWhile,
+  filter,
+  map,
+  pipe,
+  toArray,
+  toAsync,
+} from "../../src/index";
+import { Concurrent } from "../../src/Lazy/concurrent";
+import { AsyncFunctionException } from "../../src/_internal/error";
+import { callFuncAfterTime, generatorMock } from "../utils";
+
+describe("dropWhile", function () {
+  describe("sync", function () {
+    it("should be dropped elements until the value applied to callback returns falsey", function () {
+      const acc = [];
+      for (const a of dropWhile((a) => a < 3, [1, 2, 3, 1, 5])) {
+        acc.push(a);
+      }
+      expect(acc).toEqual([3, 1, 5]);
+    });
+
+    it("should be able to be used as a curried function in the pipeline", function () {
+      const res = pipe(
+        [1, 2, 3, 4, 5, 6, 7, 8],
+        map((a) => a + 10),
+        filter((a) => a % 2 === 0),
+        dropWhile((a) => a < 16),
+        toArray,
+      );
+
+      expect(res).toEqual([16, 18]);
+    });
+
+    it("should throw an error when the callback is asynchronous", function () {
+      const res = () => [...dropWhile(async (a) => a > 5, [1, 2, 3, 4, 5])];
+      expect(res).toThrowError(new AsyncFunctionException());
+    });
+  });
+
+  describe("async", function () {
+    it("should be dropped elements until the value applied to callback returns falsey", async function () {
+      const acc = [];
+      for await (const a of dropWhile((a) => a < 3, toAsync([1, 2, 3, 1, 5]))) {
+        acc.push(a);
+      }
+      expect(acc).toEqual([3, 1, 5]);
+    });
+
+    it("should be able to be used as a curried function in the pipeline", async function () {
+      const res = await pipe(
+        toAsync([1, 2, 3, 4, 5, 6, 7, 8]),
+        map((a) => a + 10),
+        filter((a) => a % 2 === 0),
+        dropWhile((a) => a < 16),
+        toArray,
+      );
+
+      expect(res).toEqual([16, 18]);
+    });
+
+    it("should be able to handle an error when asynchronous", async function () {
+      await expect(
+        pipe(
+          toAsync([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
+          dropWhile((a) => {
+            if (a > 5) {
+              throw new Error("err");
+            }
+            return true;
+          }),
+          toArray,
+        ),
+      ).rejects.toThrow("err");
+    });
+
+    it("should be dropped elements concurrently", async function () {
+      const fn = jest.fn();
+      callFuncAfterTime(fn, 400);
+      const res = await pipe(
+        toAsync([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
+        map((a) => delay(100, a)),
+        filter((a) => a % 2 === 0),
+        dropWhile((a) => a < 6),
+        concurrent(3),
+        toArray,
+      );
+      expect(fn).toBeCalled();
+      expect(res).toEqual([6, 8, 10]);
+    }, 450);
+
+    it("should be controlled the order when concurrency", async function () {
+      const fn = jest.fn();
+      callFuncAfterTime(fn, 1000);
+
+      const res = await pipe(
+        toAsync(
+          (function* () {
+            yield delay(500, 1);
+            yield delay(400, 2);
+            yield delay(300, 3);
+            yield delay(200, 4);
+            yield delay(100, 5);
+            yield delay(500, 6);
+            yield delay(400, 7);
+            yield delay(300, 8);
+            yield delay(200, 1);
+            yield delay(100, 10);
+          })(),
+        ),
+        dropWhile((a) => a < 7),
+        concurrent(5),
+        toArray,
+      );
+      expect(fn).toBeCalled();
+      expect(res).toEqual([7, 8, 1, 10]);
+    }, 1050);
+
+    it("should be able to handle an error when working concurrent", async function () {
+      await expect(
+        pipe(
+          toAsync([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
+          dropWhile((a) => {
+            if (a > 5) {
+              throw new Error("err");
+            }
+            return true;
+          }),
+          concurrent(3),
+          toArray,
+        ),
+      ).rejects.toThrow("err");
+    });
+
+    it("should be able to handle an error when working concurrent - Promise.reject", async function () {
+      await expect(
+        pipe(
+          toAsync([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
+          dropWhile((a) => {
+            if (a > 5) {
+              return Promise.reject(new Error("err"));
+            }
+            return true;
+          }),
+          concurrent(3),
+          toArray,
+        ),
+      ).rejects.toThrow("err");
+    });
+
+    it("should be passed concurrent object when job works concurrently", async function () {
+      const mock = generatorMock();
+      const iter = dropWhile((a) => a, mock);
+      const concurrent = Concurrent.of(2) as any;
+
+      await iter.next(concurrent);
+      expect((mock as any).getConcurrent()).toEqual(concurrent);
+    });
+  });
+});
diff --git a/test/join.spec.ts b/test/join.spec.ts
deleted file mode 100644
index 11243d12..00000000
--- a/test/join.spec.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { filter, join, map, pipe, toAsync } from "../src/index";
-
-describe("join", function () {
-  describe("sync", function () {
-    it("should be joined with separator", function () {
-      expect(join(", ", "hello")).toEqual("h, e, l, l, o");
-    });
-
-    it("should work given it is initial value", function () {
-      expect(join("~", [1, 2, 3, 4, 5])).toEqual("1~2~3~4~5");
-    });
-
-    it("should be able to be used as a curried function in the pipeline", function () {
-      const res = pipe(
-        [1, 2, 3, 4, 5, 6, 7],
-        map((a) => a + 10),
-        filter((a) => a % 2 === 0),
-        join("-"),
-      );
-      expect(res).toEqual("12-14-16");
-    });
-
-    it("should return an empty string when it is an empty array", function () {
-      expect(join("~", [])).toEqual("");
-    });
-  });
-
-  describe("async", function () {
-    it("should be discarded elements by length", async function () {
-      const res = await join("-", toAsync([1, 2, 3, 4, 5]));
-      expect(res).toEqual("1-2-3-4-5");
-    });
-
-    it("should be able to be used as a curried function in the pipeline", async function () {
-      const res = await pipe(
-        toAsync([1, 2, 3, 4, 5, 6, 7]),
-        map((a) => a + 10),
-        filter((a) => a % 2 === 0),
-        join("-"),
-      );
-
-      expect(res).toEqual("12-14-16");
-    });
-
-    it("should be able to handle an error when asynchronous", async function () {
-      await expect(
-        pipe(
-          toAsync("marpple"),
-          filter(() => {
-            throw new Error("err");
-          }),
-          join("!"),
-        ),
-      ).rejects.toThrow("err");
-    });
-  });
-});
diff --git a/type-check/Lazy/dropWhile.test.ts b/type-check/Lazy/dropWhile.test.ts
new file mode 100644
index 00000000..c5a4db05
--- /dev/null
+++ b/type-check/Lazy/dropWhile.test.ts
@@ -0,0 +1,24 @@
+import * as Test from "../../src/types/Test";
+import { toAsync, dropWhile, pipe } from "../../src";
+
+const { checks, check } = Test;
+
+const res1 = dropWhile((a) => a, []);
+const res2 = dropWhile((a) => a, [1, 2, 3, 4]);
+const res3 = dropWhile((a) => a, toAsync([1, 2, 3, 4]));
+const res4 = pipe(
+  [1, 2, 3, 4],
+  dropWhile((a) => a),
+);
+const res5 = pipe(
+  toAsync([1, 2, 3, 4]),
+  dropWhile((a) => a),
+);
+
+checks([
+  check<typeof res1, IterableIterator<never>, Test.Pass>(),
+  check<typeof res2, IterableIterator<number>, Test.Pass>(),
+  check<typeof res3, AsyncIterableIterator<number>, Test.Pass>(),
+  check<typeof res4, IterableIterator<number>, Test.Pass>(),
+  check<typeof res5, AsyncIterableIterator<number>, Test.Pass>(),
+]);
diff --git a/type-check/join.test.ts b/type-check/join.test.ts
deleted file mode 100644
index fb0dbde7..00000000
--- a/type-check/join.test.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as Test from "../src/types/Test";
-import { join, pipe, toAsync } from "../src";
-
-const { checks, check } = Test;
-
-const res1 = join("-", []);
-const res2 = join("!", [1, 2, 3, 4]);
-const res3 = join("*", toAsync([1, 2, 3, 4]));
-const res4 = pipe([1, 2, 3, 4], join("-"));
-const res5 = pipe(toAsync([1, 2, 3, 4]), join("#"));
-
-checks([
-  check<typeof res1, "", Test.Pass>(),
-  check<typeof res2, string, Test.Pass>(),
-  check<typeof res3, Promise<string>, Test.Pass>(),
-  check<typeof res4, string, Test.Pass>(),
-  check<typeof res5, Promise<string>, Test.Pass>(),
-]);