Repository: marpple/FxTS
Pull request #15 (2021-11-22 07:25:22)

feat: add `join` function
#8



Patch:
diff --git a/src/index.ts b/src/index.ts
index b0230e02..07424ef7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,6 +9,7 @@ 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";
@@ -32,6 +33,7 @@ export {
   indexBy,
   isArray,
   isString,
+  join,
   last,
   not,
   partition,
diff --git a/src/join.ts b/src/join.ts
new file mode 100644
index 00000000..36561ee3
--- /dev/null
+++ b/src/join.ts
@@ -0,0 +1,80 @@
+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;
diff --git a/website/docs_md/function-composition.md b/website/docs_md/function-composition.md
index de341aec..f6d5ad39 100644
--- a/website/docs_md/function-composition.md
+++ b/website/docs_md/function-composition.md
@@ -13,7 +13,8 @@ A function named `pipe` is already provided by several libraries, so you may be
 - pipe in [rxjs](https://rxjs.dev/).
 
 `pipe` is a function that connects functions by passing the output of a function to the arguments of another function.
-In FxTS, `pipe` is used as a more important function.
+
+<br/>
 
 Let's take a moment to see why we need `pipe`.
 
@@ -39,8 +40,7 @@ reduce(
 )
 ```
 
-We are providing `pipe` to solve the above problem. Make the above code in human-readable order as shown below.
-Check out [this article](https://fxts.dev/docs/lazy-evaluation) for a comparison with `Array.prototype.[Function]`
+We are providing `pipe` to solve the above problem.
 
 ```typescript
 import { pipe, filter, map } from "@fxts/core";
@@ -53,6 +53,11 @@ pipe(
 );
 ```
 
+It looks easy to read when used with `pipe`.
+
+> Check out [this article](https://fxts.dev/docs/lazy-evaluation) for a comparison with `Array.prototype.[Function]`
+
+<br/>
 Also, you don't have to deal with `Promise` values directly.
 
 ```typescript
diff --git a/website/docs_md/how-to-debug.md b/website/docs_md/how-to-debug.md
index 5c629978..55fae8a8 100644
--- a/website/docs_md/how-to-debug.md
+++ b/website/docs_md/how-to-debug.md
@@ -38,6 +38,7 @@ const is13thOfFriday = (date: Date) =>
 const formatYYYYMMDD = (date: Date) => {
   return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
 };
+
 pipe(
   range(1, Infinity),
   map(addDateFrom(new Date(2000, 0, 1))),
@@ -82,4 +83,4 @@ pipe(
 
 [The entire code is here.](https://codesandbox.io/s/how-to-debug-t2tmb?file=/src/index.ts)
 
-For more information, see the [Lazy evaluation](https://fxts.dev/docs/lazy-evaluation)
+To check for lazy evaluation in FxTS, see the [Lazy evaluation](https://fxts.dev/docs/lazy-evaluation)


Test patch:
diff --git a/test/join.spec.ts b/test/join.spec.ts
new file mode 100644
index 00000000..11243d12
--- /dev/null
+++ b/test/join.spec.ts
@@ -0,0 +1,57 @@
+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/join.test.ts b/type-check/join.test.ts
new file mode 100644
index 00000000..fb0dbde7
--- /dev/null
+++ b/type-check/join.test.ts
@@ -0,0 +1,18 @@
+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>(),
+]);