Repository: marpple/FxTS (ts)

Issue:
Add `split` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

Splits string by separator. ~(not support unicode)~

## 💻 Use Cases

<!-- What do you want to use this for? -->


```ts
const iter = split(',', '1,2,3,4,5,6');
iter.next(); // 1
iter.next(); // 2
iter.next(); // 3
iter.next(); // 4
iter.next(); // 5
iter.next(); // 6
iter.next(); // undefined


```


Pull request description:
feat: add split function 
Fixes #26 


Patch:
diff --git a/src/Lazy/index.ts b/src/Lazy/index.ts
index 78f88b83..fbbaf842 100644
--- a/src/Lazy/index.ts
+++ b/src/Lazy/index.ts
@@ -13,6 +13,7 @@ import peek from "./peek";
 import prepend from "./prepend";
 import range from "./range";
 import reject from "./reject";
+import split from "./split";
 import take from "./take";
 import takeUntil from "./takeUntil";
 import takeWhile from "./takeWhile";
@@ -38,6 +39,7 @@ export {
   prepend,
   range,
   reject,
+  split,
   take,
   takeUntil,
   takeWhile,
diff --git a/src/Lazy/split.ts b/src/Lazy/split.ts
new file mode 100644
index 00000000..907442df
--- /dev/null
+++ b/src/Lazy/split.ts
@@ -0,0 +1,113 @@
+import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
+import { isAsyncIterable, isIterable } from "../_internal/utils";
+import concurrent, { isConcurrent } from "./concurrent";
+
+function* sync(sep: string, iterable: Iterable<string>) {
+  if (sep === "") {
+    return yield* iterable;
+  }
+
+  let acc = "";
+  let chr = "";
+  for (chr of iterable) {
+    if (chr === sep) {
+      yield acc;
+      acc = "";
+    } else {
+      acc += chr;
+    }
+  }
+
+  if (chr === sep) {
+    yield "";
+  } else if (acc.length > 0) {
+    yield acc;
+  }
+}
+
+async function* asyncSequential(sep: string, iterable: AsyncIterable<string>) {
+  if (sep === "") {
+    return yield* iterable;
+  }
+
+  let acc = "";
+  let chr = "";
+
+  for await (chr of iterable) {
+    if (chr === sep) {
+      yield acc;
+      acc = "";
+    } else {
+      acc += chr;
+    }
+  }
+
+  if (chr === sep) {
+    yield "";
+  } else if (acc.length > 0) {
+    yield acc;
+  }
+}
+
+function async(
+  sep: string,
+  iterable: AsyncIterable<string>,
+): AsyncIterableIterator<string> {
+  let _iterator: AsyncIterator<string>;
+  return {
+    async next(_concurrent: any) {
+      if (_iterator === undefined) {
+        _iterator = isConcurrent(_concurrent)
+          ? asyncSequential(sep, concurrent(_concurrent.length, iterable))
+          : asyncSequential(sep, iterable);
+      }
+      return _iterator.next(_concurrent);
+    },
+    [Symbol.asyncIterator]() {
+      return this;
+    },
+  };
+}
+
+function split(
+  sep: string,
+  iterable: Iterable<string>,
+): IterableIterator<string>;
+
+function split(
+  sep: string,
+  iterable: AsyncIterable<string>,
+): AsyncIterableIterator<string>;
+
+function split<A extends Iterable<string> | AsyncIterable<string>>(
+  sep: string,
+): (iterable: A) => ReturnIterableIteratorType<A, string>;
+
+function split<A extends Iterable<string> | AsyncIterable<string>>(
+  sep: string,
+  iterable?: A,
+):
+  | IterableIterator<string>
+  | AsyncIterableIterator<string>
+  | ((iterable: A) => ReturnIterableIteratorType<A, string>) {
+  if (iterable === undefined) {
+    return (iterable: A): ReturnIterableIteratorType<A, string> => {
+      return split(sep, iterable as any) as ReturnIterableIteratorType<
+        A,
+        string
+      >;
+    };
+  }
+
+  if (isIterable(iterable)) {
+    return sync(sep, iterable as Iterable<string>);
+  }
+
+  if (isAsyncIterable(iterable)) {
+    return async(sep, iterable as AsyncIterable<string>);
+  }
+
+  throw new TypeError("iterable must be type of Iterable or AsyncIterable");
+}
+
+export default split;
diff --git a/src/types/Tail.ts b/src/types/Tail.ts
index 2f5aa5f9..af8b8740 100644
--- a/src/types/Tail.ts
+++ b/src/types/Tail.ts
@@ -1,3 +1,9 @@
-type Tail<T extends unknown[]> = T extends [any, ...infer U] ? U : [];
+/* eslint-disable @typescript-eslint/no-explicit-any */
+type Tail<T extends unknown[]> = ((...args: T) => any) extends (
+  a: any,
+  ...args: infer U
+) => any
+  ? U
+  : [];
 
 export default Tail;


Test patch:
diff --git a/test/Lazy/split.spec.ts b/test/Lazy/split.spec.ts
new file mode 100644
index 00000000..710a599f
--- /dev/null
+++ b/test/Lazy/split.spec.ts
@@ -0,0 +1,161 @@
+import {
+  pipe,
+  split,
+  toAsync,
+  map,
+  toArray,
+  filter,
+  delay,
+  concurrent,
+} from "../../src";
+import { Concurrent } from "../../src/Lazy/concurrent";
+import { callFuncAfterTime, generatorMock } from "../utils";
+
+describe("split", function () {
+  describe("sync", function () {
+    it("should return an empty array", function () {
+      const iter = split("", "");
+      expect([...iter]).toEqual([]);
+    });
+
+    it("should be splited by empty string", function () {
+      const iter = split("", "abcdefg");
+      expect([...iter]).toEqual(["a", "b", "c", "d", "e", "f", "g"]);
+    });
+
+    it("should be splited by separator", function () {
+      const iter = split(",", "a,b,c,d,e,f,g");
+      expect([...iter]).toEqual(["a", "b", "c", "d", "e", "f", "g"]);
+    });
+
+    it("should be appended empty string if there is a separator at the end", function () {
+      const iter = split(",", "a,b,c,d,e,f,g,");
+      expect([...iter]).toEqual(["a", "b", "c", "d", "e", "f", "g", ""]);
+    });
+
+    it("should be splited by separator(unicode)", function () {
+      const iter = split(",", "👍,😀,🙇‍♂️,🤩,🎉");
+      expect([...iter]).toEqual(["👍", "😀", "🙇‍♂️", "🤩", "🎉"]);
+    });
+
+    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,9,10",
+        split(","),
+        map((a) => Number(a)),
+        filter((a) => a % 2 === 0),
+        toArray,
+      );
+
+      expect(res).toEqual([2, 4, 6, 8, 10]);
+    });
+  });
+
+  describe("async", function () {
+    it("should return an empty array", async function () {
+      const acc = [];
+      for await (const a of split("", toAsync(""))) {
+        acc.push(a);
+      }
+      expect(acc).toEqual([]);
+    });
+
+    it("should be splited by empty string", async function () {
+      const acc = [];
+      for await (const a of split("", "abcdefg")) {
+        acc.push(a);
+      }
+      expect(acc).toEqual(["a", "b", "c", "d", "e", "f", "g"]);
+    });
+
+    it("should be splited by separator", async function () {
+      const acc = [];
+      for await (const a of split(",", "a,b,c,d,e,f,g")) {
+        acc.push(a);
+      }
+      expect(acc).toEqual(["a", "b", "c", "d", "e", "f", "g"]);
+    });
+
+    it("should be appended empty string if there is a separator at the end", async function () {
+      const acc = [];
+      for await (const a of split(",", "a,b,c,d,e,f,g,")) {
+        acc.push(a);
+      }
+      expect(acc).toEqual(["a", "b", "c", "d", "e", "f", "g", ""]);
+    });
+
+    it("should be splited by separator(unicode)", async function () {
+      const acc = [];
+      for await (const a of split(",", "👍,😀,🙇‍♂️,🤩,🎉")) {
+        acc.push(a);
+      }
+      expect(acc).toEqual(["👍", "😀", "🙇‍♂️", "🤩", "🎉"]);
+    });
+
+    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,9,10"),
+        split(","),
+        map((a) => Number(a)),
+        filter((a) => a % 2 === 0),
+        toArray,
+      );
+
+      expect(res).toEqual([2, 4, 6, 8, 10]);
+    });
+
+    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, ",");
+            yield delay(300, "2");
+            yield delay(200, ",");
+            yield delay(100, "3");
+            yield delay(500, ",");
+            yield delay(400, "4");
+            yield delay(300, ",");
+            yield delay(200, "5");
+          })(),
+        ),
+        split(","),
+        concurrent(5),
+        toArray,
+      );
+
+      expect(fn).toBeCalled();
+      expect(res).toEqual(["1", "2", "3", "4", "5"]);
+    }, 1050);
+
+    it("should be consumed concurrently", async function () {
+      const fn = jest.fn();
+      callFuncAfterTime(fn, 1000);
+
+      const res = await pipe(
+        toAsync("1,2,3,4,5,6,7,8,9,10"),
+        split(","),
+        map((a) => delay(500, a)),
+        map((a) => Number(a)),
+        filter((a) => a % 2 === 0),
+        concurrent(5),
+        toArray,
+      );
+
+      expect(fn).toBeCalled();
+      expect(res).toEqual([2, 4, 6, 8, 10]);
+    }, 1050);
+  });
+
+  it("should be passed concurrent object when job works concurrently", async function () {
+    const mock = generatorMock();
+    const iter = split(",", mock as AsyncIterable<string>);
+    const concurrent = Concurrent.of(2) as any;
+
+    await iter.next(concurrent);
+    expect((mock as any).getConcurrent()).toEqual(concurrent);
+  });
+});
diff --git a/type-check/Lazy/split.test.ts b/type-check/Lazy/split.test.ts
new file mode 100644
index 00000000..3a011b1e
--- /dev/null
+++ b/type-check/Lazy/split.test.ts
@@ -0,0 +1,17 @@
+import * as Test from "../../src/types/Test";
+import { split, toAsync, pipe } from "../../src";
+
+const { checks, check } = Test;
+
+const res1 = split(",", "1,2,3");
+const res2 = split(",", toAsync("1,2,3"));
+
+const res3 = pipe("1,2,3,4", split(","));
+const res4 = pipe(toAsync("1,2,3,4"), split(","));
+
+checks([
+  check<typeof res1, IterableIterator<string>, Test.Pass>(),
+  check<typeof res2, AsyncIterableIterator<string>, Test.Pass>(),
+  check<typeof res3, IterableIterator<string>, Test.Pass>(),
+  check<typeof res4, AsyncIterableIterator<string>, Test.Pass>(),
+]);
diff --git a/type-check/types/Tail.test.ts b/type-check/types/Tail.test.ts
index 28eedbef..a0f15f59 100644
--- a/type-check/types/Tail.test.ts
+++ b/type-check/types/Tail.test.ts
@@ -8,6 +8,4 @@ checks([
   check<Tail<[1]>, [], Test.Pass>(),
   check<Tail<[1, 2]>, [2], Test.Pass>(),
   check<Tail<[1, 2, 3]>, [2, 3], Test.Pass>(),
-  check<Tail<[1, 2, "abc"]>, [2, "abc"], Test.Pass>(),
-  check<Tail<[string, number, boolean]>, [number, boolean], Test.Pass>(),
 ]);