Repository: marpple/FxTS (ts)

Issue:
Add `findIndex` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

Returns the index of the first element of Iterable/AsyncIterable which matches `f`, or `-1` if no element matches.


## 💻 Use Cases

<!-- What do you want to use this for? -->

```ts
const arr = [{a:1}, {a:2}, {a:3}]
findIndex((obj) =>  obj.a === 1, arr); // 0 
findIndex((obj) =>  obj.a === 2, arr); // 1 
findIndex((obj) =>  obj.a === 4, arr); // -1
```


Pull request:
feat: add findIndex function
Fixes #41 


Patch:
diff --git a/src/find.ts b/src/find.ts
index c59ca452..40ee96f3 100644
--- a/src/find.ts
+++ b/src/find.ts
@@ -1,15 +1,9 @@
 import filter from "./Lazy/filter";
 import head from "./head";
 import { isAsyncIterable, isIterable } from "./_internal/utils";
+import ReturnValueType from "./types/ReturnValueType";
 import IterableInfer from "./types/IterableInfer";
 
-type FindReturnType<T extends Iterable<unknown> | AsyncIterable<unknown>> =
-  T extends AsyncIterable<infer A>
-    ? Promise<A | undefined>
-    : T extends Iterable<infer A>
-    ? A | undefined
-    : never;
-
 /**
  * Looks through each value in Iterable/AsyncIterable, returning the first one that passes a truth test `f`,
  * or `undefined` if no value passes the test.
@@ -33,7 +27,7 @@ function find<T>(
 function find<T extends Iterable<unknown> | AsyncIterable<unknown>>(
   f: (a: IterableInfer<T>) => unknown,
   iterable?: T,
-): (iterable: T) => FindReturnType<T>;
+): (iterable: T) => ReturnValueType<T, IterableInfer<T> | undefined>;
 
 function find<T extends Iterable<unknown> | AsyncIterable<unknown>>(
   f: (a: IterableInfer<T>) => unknown,
@@ -41,9 +35,10 @@ function find<T extends Iterable<unknown> | AsyncIterable<unknown>>(
 ):
   | (IterableInfer<T> | undefined)
   | Promise<IterableInfer<T> | undefined>
-  | ((iterable: T) => FindReturnType<T>) {
+  | ((iterable: T) => ReturnValueType<T, IterableInfer<T> | undefined>) {
   if (iterable === undefined) {
-    return (iterable: T) => find(f, iterable) as any;
+    return (iterable: T) =>
+      find(f, iterable) as ReturnValueType<T, IterableInfer<T> | undefined>;
   }
   if (isIterable(iterable)) {
     return head(filter(f, iterable as Iterable<IterableInfer<T>>));
diff --git a/src/findIndex.ts b/src/findIndex.ts
new file mode 100644
index 00000000..4fd6706a
--- /dev/null
+++ b/src/findIndex.ts
@@ -0,0 +1,63 @@
+import find from "./find";
+import zipWithIndex from "./Lazy/zipWithIndex";
+import pipe from "./pipe";
+import pipe1 from "./pipe1";
+import Arrow from "./types/Arrow";
+import IterableInfer from "./types/IterableInfer";
+import ReturnValueType from "./types/ReturnValueType";
+import { isAsyncIterable, isIterable } from "./_internal/utils";
+
+/**
+ * Returns the index of the first element of Iterable/AsyncIterable which matches f, or -1 if no element matches.
+ *
+ * @example
+ * ```ts
+ * const arr = [{a:1}, {a:2}, {a:3}]
+ * findIndex((obj) =>  obj.a === 1, arr); // 0
+ * findIndex((obj) =>  obj.a === 2, arr); // 1
+ * findIndex((obj) =>  obj.a === 4, arr); // -1
+ * ```
+ */
+function findIndex<T extends readonly []>(f: Arrow, iterable: T): -1;
+
+function findIndex<T>(f: (a: T) => unknown, iterable: Iterable<T>): number;
+
+function findIndex<T>(
+  f: (a: T) => unknown,
+  iterable: AsyncIterable<T>,
+): Promise<number>;
+
+function findIndex<T extends Iterable<unknown> | AsyncIterable<unknown>>(
+  f: (a: IterableInfer<T>) => unknown,
+  iterable?: T,
+): (iterable: T) => ReturnValueType<T, number>;
+
+function findIndex<T extends Iterable<unknown> | AsyncIterable<unknown>>(
+  f: (a: IterableInfer<T>) => unknown,
+  iterable?: T,
+): number | Promise<number> | ((iterable: T) => ReturnValueType<T, number>) {
+  if (iterable === undefined) {
+    return (iterable: T) =>
+      findIndex(f, iterable as any) as ReturnValueType<T, number>;
+  }
+
+  if (isIterable(iterable)) {
+    return pipe(
+      zipWithIndex(iterable) as Iterable<[number, IterableInfer<T>]>,
+      find(([, a]) => f(a)),
+      (res) => (res ? res[0] : -1),
+    );
+  }
+
+  if (isAsyncIterable(iterable)) {
+    return pipe(
+      zipWithIndex(iterable) as AsyncIterable<[number, IterableInfer<T>]>,
+      find(([, a]) => pipe1(a, f)),
+      (res) => (res ? res[0] : -1),
+    );
+  }
+
+  throw new TypeError("'iterable' must be type of Iterable or AsyncIterable");
+}
+
+export default findIndex;
diff --git a/src/index.ts b/src/index.ts
index 720e140a..fd3cf699 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,6 +4,7 @@ import delay from "./delay";
 import each from "./each";
 import every from "./every";
 import find from "./find";
+import findIndex from "./findIndex";
 import groupBy from "./groupBy";
 import head from "./head";
 import identity from "./identity";
@@ -31,6 +32,7 @@ export {
   each,
   every,
   find,
+  findIndex,
   groupBy,
   head,
   head as first,


Test patch:
diff --git a/test/findIndex.spec.ts b/test/findIndex.spec.ts
new file mode 100644
index 00000000..2927c3a4
--- /dev/null
+++ b/test/findIndex.spec.ts
@@ -0,0 +1,59 @@
+import Arrow from "../src/types/Arrow";
+import { filter, findIndex, map, pipe, toAsync } from "../src/index";
+import { AsyncFunctionException } from "../src/_internal/error";
+
+describe("findIndex", function () {
+  describe("sync", function () {
+    it.each([
+      [(a: string) => a === "r", "marpple", 2],
+      [(a: number) => a === 2, [1, 2, 3, 4], 1],
+      [(a: number) => a === 5, [1, 2, 3, 4], -1],
+    ])(
+      `should return result when passed arguments are synchronous function and 'Iterable'`,
+      (f, iterable, result) => {
+        expect(findIndex(f as Arrow, iterable)).toEqual(result);
+      },
+    );
+
+    it("should be able to be used as a curried function in the pipeline", function () {
+      const res1 = pipe(
+        [1, 2, 3, 4],
+        map((a) => a + 10),
+        filter((a) => a % 2 === 0),
+        findIndex((a) => a === 14),
+      );
+      expect(res1).toEqual(1);
+    });
+
+    it("should throw an error when the callback is asynchronous", function () {
+      const res = () =>
+        pipe(
+          [1, 2, 3, 4],
+          map((a) => a + 10),
+          filter((a) => a % 2 === 0),
+          findIndex((a) => Promise.resolve(a === 14)),
+        );
+      expect(res).toThrowError(new AsyncFunctionException());
+    });
+  });
+
+  describe("async", function () {
+    it("should findIndex out the result by the callback to given 'AsyncIterable'", async function () {
+      const res1 = await findIndex((a) => a === 5, toAsync([1, 2, 3, 4, 5, 6]));
+      expect(res1).toEqual(4);
+
+      const res2 = await findIndex((a) => a === 7, toAsync([1, 2, 3, 4, 5, 6]));
+      expect(res2).toEqual(-1);
+    });
+  });
+
+  it("should be able to be used as a curried function in the pipeline", async function () {
+    const res1 = await pipe(
+      toAsync([1, 2, 3, 4]),
+      map((a) => a + 10),
+      filter((a) => a % 2 === 0),
+      findIndex((a) => a === 14),
+    );
+    expect(res1).toEqual(1);
+  });
+});
diff --git a/type-check/findIndex.test.ts b/type-check/findIndex.test.ts
new file mode 100644
index 00000000..87b6e25e
--- /dev/null
+++ b/type-check/findIndex.test.ts
@@ -0,0 +1,28 @@
+import * as Test from "../src/types/Test";
+import { findIndex, pipe, toAsync } from "../src";
+
+const { checks, check } = Test;
+
+const res1 = findIndex((a) => a === 3, []);
+const res2 = findIndex((a) => a === 3, [1, 2, 3, 4, 5]);
+const res3 = findIndex((a) => a === 7, [1, 2, 3, 4, 5]);
+const res4 = findIndex((a) => a === "e", ["a", "b", "c", "d"]);
+
+const res5 = pipe(
+  [1, 2, 3, 4, 5],
+  findIndex((a) => a === 3),
+);
+const res6 = pipe(
+  ["a", "b", "c", "d"],
+  toAsync,
+  findIndex((a) => Promise.resolve(a === "e")),
+);
+
+checks([
+  check<typeof res1, -1, Test.Pass>(),
+  check<typeof res2, number, Test.Pass>(),
+  check<typeof res3, number, Test.Pass>(),
+  check<typeof res4, number, Test.Pass>(),
+  check<typeof res5, number, Test.Pass>(),
+  check<typeof res6, Promise<number>, Test.Pass>(),
+]);