Repository: marpple/FxTS
Pull request #335 (2025-07-19 03:56:59)

feat: add suffle
modified to fit fxts.

https://github.com/marpple/FxTS/pull/332


Patch:
diff --git a/src/Util/index.ts b/src/Util/index.ts
index 6e0b3583..072d8c67 100644
--- a/src/Util/index.ts
+++ b/src/Util/index.ts
@@ -1,3 +1,4 @@
 import debounce from "./debounce";
+import shuffle from "./shuffle";
 
-export { debounce };
+export { debounce, shuffle };
diff --git a/src/Util/shuffle.ts b/src/Util/shuffle.ts
new file mode 100644
index 00000000..3a9a9456
--- /dev/null
+++ b/src/Util/shuffle.ts
@@ -0,0 +1,55 @@
+import { isAsyncIterable, isIterable } from "../_internal/utils";
+import isArray from "../isArray";
+import pipe1 from "../pipe1";
+import toArray from "../toArray";
+import type ReturnArrayType from "../types/ReturnArrayType";
+
+/**
+ * Returns a new array with the elements of the input array shuffled randomly using Fisher-Yates algorithm.
+ *
+ * @example
+ * ```ts
+ * shuffle([1, 2, 3, 4, 5]); // [3, 1, 5, 2, 4] (random order)
+ * shuffle("hello"); // ["o", "e", "l", "h", "l"] (random order) Only ASCII codes are shuffled in the case of a string.
+ * ```
+ */
+function shuffle<T extends Iterable<unknown> | AsyncIterable<unknown>>(
+  iterable: T,
+): ReturnArrayType<T>;
+
+function shuffle<T>(iterable: Iterable<T> | AsyncIterable<T>) {
+  if (isArray(iterable)) {
+    const result = toArray(iterable) as T[];
+    for (let i = result.length - 1; i > 0; i--) {
+      const j = Math.floor(Math.random() * (i + 1));
+      [result[i], result[j]] = [result[j], result[i]];
+    }
+    return result;
+  }
+
+  if (isIterable(iterable)) {
+    return pipe1(toArray(iterable), (arr) => {
+      const result = arr;
+      for (let i = result.length - 1; i > 0; i--) {
+        const j = Math.floor(Math.random() * (i + 1));
+        [result[i], result[j]] = [result[j], result[i]];
+      }
+      return result;
+    });
+  }
+
+  if (isAsyncIterable(iterable)) {
+    return pipe1(toArray(iterable), (arr) => {
+      const result = arr;
+      for (let i = result.length - 1; i > 0; i--) {
+        const j = Math.floor(Math.random() * (i + 1));
+        [result[i], result[j]] = [result[j], result[i]];
+      }
+      return result;
+    });
+  }
+
+  throw new TypeError("'iterable' must be type of Iterable or AsyncIterable");
+}
+
+export default shuffle;
diff --git a/website/function.json b/website/function.json
index fbe81116..a036e1ea 100644
--- a/website/function.json
+++ b/website/function.json
@@ -114,5 +114,5 @@
     "unless",
     "when"
   ],
-  "Util": ["debounce"]
+  "Util": ["debounce", "shuffle"]
 }
diff --git a/website/markdown-plugin/src/function.json b/website/markdown-plugin/src/function.json
index fbe81116..a036e1ea 100644
--- a/website/markdown-plugin/src/function.json
+++ b/website/markdown-plugin/src/function.json
@@ -114,5 +114,5 @@
     "unless",
     "when"
   ],
-  "Util": ["debounce"]
+  "Util": ["debounce", "shuffle"]
 }


Test patch:
diff --git a/test/Util/shuffle.spec.ts b/test/Util/shuffle.spec.ts
new file mode 100644
index 00000000..24ce1e24
--- /dev/null
+++ b/test/Util/shuffle.spec.ts
@@ -0,0 +1,111 @@
+import { pipe, shuffle, toArray, toAsync } from "../../src";
+
+describe("shuffle", function () {
+  describe("sync", function () {
+    it.each([
+      [[1, 2, 3, 4], 4],
+      ["abcd", 4],
+    ])(
+      "should return the given elements %s in shuffled order with length %s",
+      function (input, expectedLength) {
+        const result = shuffle(input);
+        expect(result).toHaveLength(expectedLength);
+
+        // Check all elements are present
+        if (Array.isArray(input)) {
+          expect(result.sort()).toEqual(input.sort());
+        } else {
+          expect(result.sort()).toEqual([...input].sort());
+        }
+      },
+    );
+
+    it("should handle empty array", function () {
+      const result = shuffle([]);
+      expect(result).toEqual([]);
+    });
+
+    it("should be handle immutable", function () {
+      const obj = [1, 2, 3, 4];
+      const result = shuffle(obj);
+      expect(obj !== result).toEqual(true);
+      expect(obj).toEqual([1, 2, 3, 4]);
+    });
+
+    it("should handle single element", function () {
+      const result = shuffle([42]);
+      expect(result).toEqual([42]);
+    });
+
+    it("should be able to be used as a curried function in the pipeline", function () {
+      const res = pipe([1, 2, 3, 4, 5, 6], shuffle, toArray);
+      expect(res).toHaveLength(6);
+      expect(res.sort()).toEqual([1, 2, 3, 4, 5, 6]);
+    });
+
+    it("should produce different results on multiple calls (statistical test)", function () {
+      const input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+      const results = [];
+
+      // Generate multiple shuffles
+      for (let i = 0; i < 50; i++) {
+        results.push(shuffle(input));
+      }
+
+      // Check that not all results are identical (very unlikely with proper shuffling)
+      const firstResult = JSON.stringify(results[0]);
+      const allIdentical = results.every(
+        (result) => JSON.stringify(result) === firstResult,
+      );
+      expect(allIdentical).toBe(false);
+    });
+  });
+
+  describe("async", function () {
+    it.each([
+      [[1, 2, 3, 4], 4],
+      ["abcd", 4],
+    ])(
+      "should return the given elements %s in shuffled order with length %s",
+      async function (input, expectedLength) {
+        const result = await shuffle(toAsync(input as unknown[]));
+        expect(result).toHaveLength(expectedLength);
+
+        // Check all elements are present
+        if (Array.isArray(input)) {
+          expect(result.sort()).toEqual(input.sort());
+        } else {
+          expect(result.sort()).toEqual([...input].sort());
+        }
+      },
+    );
+
+    it("should handle empty async array", async function () {
+      const result = await shuffle(toAsync([]));
+      expect(result).toEqual([]);
+    });
+
+    it("should be able to be used as a curried function in the pipeline", async function () {
+      const res = await pipe([1, 2, 3, 4, 5, 6], toAsync, shuffle, toArray);
+      expect(res).toHaveLength(6);
+      expect(res.sort()).toEqual([1, 2, 3, 4, 5, 6]);
+    });
+
+    it("should produce different results on multiple calls (statistical test)", async function () {
+      const input = [1, 2, 3, 4, 5, 6, 7, 8];
+      const results = [];
+
+      // Generate multiple shuffles
+      for (let i = 0; i < 20; i++) {
+        results.push(await shuffle(toAsync(input)));
+      }
+
+      // Check that not all results are identical (very unlikely with proper shuffling)
+      const firstResult = JSON.stringify(results[0]);
+      const allIdentical = results.every(
+        (result) => JSON.stringify(result) === firstResult,
+      );
+      expect(allIdentical).toBe(false);
+    });
+  });
+});
diff --git a/type-check/Util/shuffle.test.ts b/type-check/Util/shuffle.test.ts
new file mode 100644
index 00000000..e66878ca
--- /dev/null
+++ b/type-check/Util/shuffle.test.ts
@@ -0,0 +1,21 @@
+import { pipe, shuffle, toAsync } from "../../src";
+import * as Test from "../../src/types/Test";
+
+const { checks, check } = Test;
+
+const res1 = shuffle([1, 2, 3, 4, 5]);
+const res2 = shuffle("hello");
+const res3 = shuffle(toAsync([1, 2, 3, 4, 5]));
+const res4 = shuffle(toAsync("hello"));
+
+checks([
+  check<typeof res1, number[], Test.Pass>(),
+  check<typeof res2, string[], Test.Pass>(),
+  check<typeof res3, Promise<number[]>, Test.Pass>(),
+  check<typeof res4, Promise<string[]>, Test.Pass>(),
+]);
+
+// Test in pipe
+const res8 = pipe([1, 2, 3, 4, 5], shuffle);
+
+checks([check<typeof res8, number[], Test.Pass>()]);