Repository: marpple/FxTS (ts)

Issue:
Add `cycle` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->
ref https://docs.python.org/3/library/itertools.html#itertools.cycle

## 💻 Use Cases

<!-- What do you want to use this for? -->

```ts
const iter = cycle("abc")
iter.next(); // {value:"a", done: false}
iter.next(); // {value:"b", done: false}
iter.next(); // {value:"c", done: false}
iter.next(); // {value:"a", done: false}
iter.next(); // {value:"b", done: false}

// with pipe
pipe(
  cycle([1,2,3,4]),
  take(5),
  toArray,
); // [1,2,3,4,1]


```


Pull request description:
feat: add `cycle` function
Fixes #78 


Patch:
diff --git a/src/Lazy/cycle.ts b/src/Lazy/cycle.ts
new file mode 100644
index 00000000..e6546cf9
--- /dev/null
+++ b/src/Lazy/cycle.ts
@@ -0,0 +1,92 @@
+import IterableInfer from "../types/IterableInfer";
+import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
+import { isAsyncIterable, isIterable } from "../_internal/utils";
+import concurrent, { isConcurrent } from "./concurrent";
+
+function* sync<T>(iterable: Iterable<T>): IterableIterator<T> {
+  const arr = [];
+  for (const a of iterable) {
+    yield a;
+    arr.push(a);
+  }
+
+  while (arr.length) {
+    for (const a of arr) {
+      yield a;
+    }
+  }
+}
+
+async function* asyncSequential<T>(
+  iterable: AsyncIterable<T>,
+): AsyncIterableIterator<T> {
+  const arr = [];
+  for await (const a of iterable) {
+    yield a;
+    arr.push(a);
+  }
+
+  while (arr.length) {
+    for (const a of arr) {
+      yield a;
+    }
+  }
+}
+
+function async<T>(iterable: AsyncIterable<T>): AsyncIterableIterator<T> {
+  let _iterator: AsyncIterator<T>;
+  return {
+    async next(_concurrent: any) {
+      if (_iterator === undefined) {
+        _iterator = isConcurrent(_concurrent)
+          ? asyncSequential(concurrent(_concurrent.length, iterable))
+          : asyncSequential(iterable);
+      }
+      return _iterator.next(_concurrent);
+    },
+    [Symbol.asyncIterator]() {
+      return this;
+    },
+  };
+}
+
+/**
+ * Returns Iterable/AsyncIterable that infinitely repeats the given Iterable/AsyncIterable
+ *
+ * @example
+ * ```ts
+ * const iter = cycle("abc")
+ * iter.next(); // {value:"a", done: false}
+ * iter.next(); // {value:"b", done: false}
+ * iter.next(); // {value:"c", done: false}
+ * iter.next(); // {value:"a", done: false}
+ * iter.next(); // {value:"b", done: false}
+ *
+ * // with pipe
+ * pipe(
+ *   cycle([1,2,3,4]),
+ *   take(5),
+ *   toArray,
+ * ); // [1,2,3,4,1]
+ * ```
+ */
+function cycle<A extends Iterable<unknown> | AsyncIterable<unknown>>(
+  iter: A,
+): ReturnIterableIteratorType<A>;
+
+function cycle<T extends Iterable<unknown> | AsyncIterable<unknown>>(
+  iterable: T,
+):
+  | IterableIterator<IterableInfer<T>>
+  | AsyncIterableIterator<IterableInfer<T>> {
+  if (isIterable(iterable)) {
+    return sync(iterable as Iterable<IterableInfer<T>>);
+  }
+  if (isAsyncIterable(iterable)) {
+    return async(iterable as AsyncIterable<IterableInfer<T>>);
+  }
+
+  throw new TypeError("'iterable' must be type of Iterable or AsyncIterable");
+}
+
+export default cycle;
diff --git a/src/Lazy/index.ts b/src/Lazy/index.ts
index 994b5738..82b8f7d6 100644
--- a/src/Lazy/index.ts
+++ b/src/Lazy/index.ts
@@ -3,6 +3,7 @@ import chunk from "./chunk";
 import compact from "./compact";
 import concat from "./concat";
 import concurrent from "./concurrent";
+import cycle from "./cycle";
 import drop from "./drop";
 import dropWhile from "./dropWhile";
 import dropUntil from "./dropUntil";
@@ -32,6 +33,7 @@ export {
   compact,
   concat,
   concurrent,
+  cycle,
   drop,
   dropWhile,
   dropUntil,


Test patch:
diff --git a/test/Lazy/cycle.spec.ts b/test/Lazy/cycle.spec.ts
new file mode 100644
index 00000000..af397d9e
--- /dev/null
+++ b/test/Lazy/cycle.spec.ts
@@ -0,0 +1,113 @@
+import {
+  concurrent,
+  delay,
+  map,
+  pipe,
+  take,
+  toArray,
+  toAsync,
+} from "../../src";
+import { Concurrent } from "../../src/Lazy/concurrent";
+import cycle from "../../src/Lazy/cycle";
+import { callFuncAfterTime, generatorMock } from "../utils";
+
+describe("cycle", function () {
+  describe("sync", function () {
+    it.each([
+      [
+        [1, 2, 3, 4, 5],
+        [1, 2, 3, 4, 5, 1, 2, 3],
+      ],
+      ["abcde", ["a", "b", "c", "d", "e", "a", "b", "c"]],
+    ])("should repeat given elements %s %s", function (input, result) {
+      const iter = cycle(input);
+
+      expect(iter.next().value).toEqual(result[0]);
+      expect(iter.next().value).toEqual(result[1]);
+      expect(iter.next().value).toEqual(result[2]);
+      expect(iter.next().value).toEqual(result[3]);
+      expect(iter.next().value).toEqual(result[4]);
+      expect(iter.next().value).toEqual(result[0]);
+      expect(iter.next().value).toEqual(result[1]);
+      expect(iter.next().value).toEqual(result[2]);
+    });
+
+    it("should be able to be used as a curried function in the pipeline", function () {
+      const res = pipe(
+        cycle([1, 2, 3, 4]),
+        map((a) => String(a)),
+        take(5),
+        toArray,
+      );
+
+      expect(res).toEqual(["1", "2", "3", "4", "1"]);
+    });
+  });
+
+  describe("async", function () {
+    it.each([
+      [
+        [1, 2, 3, 4, 5],
+        [1, 2, 3, 4, 5, 1, 2, 3],
+      ],
+      ["abcde", ["a", "b", "c", "d", "e", "a", "b", "c"]],
+    ])("should repeat given elements %s %s", async function (input, result) {
+      const iter = cycle(toAsync(input as any));
+
+      expect((await iter.next()).value).toEqual(result[0]);
+      expect((await iter.next()).value).toEqual(result[1]);
+      expect((await iter.next()).value).toEqual(result[2]);
+      expect((await iter.next()).value).toEqual(result[3]);
+      expect((await iter.next()).value).toEqual(result[4]);
+      expect((await iter.next()).value).toEqual(result[0]);
+      expect((await iter.next()).value).toEqual(result[1]);
+      expect((await iter.next()).value).toEqual(result[2]);
+    });
+
+    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]),
+        cycle,
+        map((a) => String(a)),
+        take(5),
+        toArray,
+      );
+
+      expect(res).toEqual(["1", "2", "3", "4", "1"]);
+    });
+
+    it("should be repeated concurrently", async function () {
+      const fn = jest.fn();
+      callFuncAfterTime(fn, 900);
+
+      const res = await pipe(
+        toAsync(
+          (function* () {
+            yield delay(300, 1);
+            yield delay(300, 2);
+            yield delay(300, 3);
+            yield delay(300, 4);
+            yield delay(300, 5);
+            yield delay(300, 6);
+          })(),
+        ),
+        cycle,
+        concurrent(2),
+        take(6),
+        toArray,
+      );
+
+      expect(fn).toBeCalled();
+      expect(res).toEqual([1, 2, 3, 4, 5, 6]);
+    }, 950);
+
+    it("should be passed concurrent object when job works concurrently", async function () {
+      const mock = generatorMock();
+      const iter = cycle(mock);
+      const concurrent = Concurrent.of(2) as any;
+
+      await iter.next(concurrent);
+      expect((mock as any).getConcurrent()).toEqual(concurrent);
+    });
+  });
+});
diff --git a/type-check/Lazy/cycle.test.ts b/type-check/Lazy/cycle.test.ts
new file mode 100644
index 00000000..9fd92005
--- /dev/null
+++ b/type-check/Lazy/cycle.test.ts
@@ -0,0 +1,22 @@
+import * as Test from "../../src/types/Test";
+import { pipe, cycle, toAsync, range } from "../../src";
+
+const { checks, check } = Test;
+
+const res1 = cycle([1, 2, 3, 4]);
+const res2 = cycle("abc");
+
+const res3 = cycle(toAsync(range(3)));
+const res4 = cycle(toAsync("abc"));
+
+const res5 = pipe(range(3), cycle);
+const res6 = pipe(range(3), toAsync, cycle);
+
+checks([
+  check<typeof res1, IterableIterator<number>, Test.Pass>(),
+  check<typeof res2, IterableIterator<string>, Test.Pass>(),
+  check<typeof res3, AsyncIterableIterator<number>, Test.Pass>(),
+  check<typeof res4, AsyncIterableIterator<string>, Test.Pass>(),
+  check<typeof res5, IterableIterator<number>, Test.Pass>(),
+  check<typeof res6, AsyncIterableIterator<number>, Test.Pass>(),
+]);