Skip to content

Commit 6688b50

Browse files
committed
Add find, findIndex utils
1 parent d250a02 commit 6688b50

File tree

7 files changed

+450
-0
lines changed

7 files changed

+450
-0
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ A collection of array-related async utilities.
2121
- [`asyncEveryStrict()`](#asyncEveryStrict)
2222
- [`asyncFilter()`](#asyncFilter)
2323
- [`asyncFilterStrict()`](#asyncFilterStrict)
24+
- [`asyncFind()`](#asyncFind)
25+
- [`asyncFindIndex()`](#asyncFindIndex)
2426
- [`asyncForEach()`](#asyncForEach)
2527
- [`asyncForEachStrict()`](#asyncForEachStrict)
2628
- [`asyncMap()`](#asyncMap)
@@ -91,6 +93,30 @@ const asyncFilteredArr = await asyncFilterStrict([1, 2, 3], async (el, index) =>
9193
console.log(indexes); // [0, 1, 2]
9294
```
9395

96+
### `asyncFind()`
97+
98+
Returns the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, `undefined` is returned.
99+
100+
#### Sample usage
101+
102+
```js
103+
import { asyncFind } from '@wojtekmaj/async-array-utils';
104+
105+
const asyncFoundEl = await asyncFind([1, 2, 3], async (el) => el > 1); // 2
106+
```
107+
108+
### `asyncFindIndex()`
109+
110+
Returns the index of the first element in an array that satisfies the provided testing function. If no elements satisfy the testing function, `-1` is returned.
111+
112+
#### Sample usage
113+
114+
```js
115+
import { asyncFindIndex } from '@wojtekmaj/async-array-utils';
116+
117+
const asyncFoundIndex = await asyncFindIndex([1, 2, 3], async (el) => el > 1); // 1
118+
```
119+
94120
### `asyncForEach()`
95121

96122
Executes a provided asynchronous function once for each array element.

src/find.spec.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import asyncFind from './find';
3+
4+
import {
5+
getTimer,
6+
inputArr,
7+
largerThanOneHundred,
8+
largerThanOneHundredInRandomTime,
9+
largerThanTwo,
10+
largerThanTwoInRandomTime,
11+
makeDelayed,
12+
throws,
13+
} from '../test-utils';
14+
15+
const firstElementLargerThanTwo = inputArr.findIndex(largerThanTwo);
16+
17+
describe('asyncFind()', () => {
18+
it('example from README works as described', async () => {
19+
const asyncFoundEl = await asyncFind([1, 2, 3], async (el) => el > 1);
20+
21+
expect(asyncFoundEl).toEqual(2);
22+
});
23+
24+
it('takes exactly the time necessary to execute', async () => {
25+
const delay = 100;
26+
27+
const timer = getTimer();
28+
29+
timer.start();
30+
31+
await asyncFind(
32+
[1, 2, 3],
33+
makeDelayed((el) => el > 1, delay),
34+
);
35+
36+
const timeElapsed = timer.stop();
37+
38+
expect(timeElapsed).toBeGreaterThanOrEqual(delay * 2);
39+
expect(timeElapsed).toBeLessThan(delay * 1.25 * 2);
40+
});
41+
42+
it('assertions below are valid for synchronous .find()', () => {
43+
const filter = vi.fn().mockImplementation(largerThanTwo);
44+
45+
inputArr.find(filter);
46+
47+
expect.assertions(1 + firstElementLargerThanTwo);
48+
49+
expect(filter).toHaveBeenCalledTimes(firstElementLargerThanTwo + 1);
50+
inputArr.slice(0, firstElementLargerThanTwo).forEach((el, idx) => {
51+
expect(filter).toHaveBeenCalledWith(el, idx, inputArr);
52+
});
53+
});
54+
55+
it('iterates over values properly', async () => {
56+
const filter = vi.fn().mockImplementation(largerThanTwoInRandomTime);
57+
58+
await asyncFind(inputArr, filter);
59+
60+
expect.assertions(1 + firstElementLargerThanTwo);
61+
62+
expect(filter).toHaveBeenCalledTimes(firstElementLargerThanTwo + 1);
63+
inputArr.slice(0, firstElementLargerThanTwo).forEach((el, idx) => {
64+
expect(filter).toHaveBeenCalledWith(el, idx, inputArr);
65+
});
66+
});
67+
68+
it.skip('assertions below are valid for synchronous .find()', () => {
69+
const filter = vi.fn().mockImplementation(largerThanTwo);
70+
71+
inputArr.find(filter);
72+
73+
expect.assertions(1 + firstElementLargerThanTwo);
74+
75+
expect(filter).toHaveBeenCalledTimes(firstElementLargerThanTwo + 1);
76+
inputArr.slice(0, firstElementLargerThanTwo).forEach((el, idx) => {
77+
expect(filter).toHaveBeenCalledWith(el, idx, inputArr);
78+
});
79+
});
80+
81+
it('iterates over values properly', async () => {
82+
const filter = vi.fn().mockImplementation(largerThanTwoInRandomTime);
83+
84+
await asyncFind(inputArr, filter);
85+
86+
expect.assertions(1 + firstElementLargerThanTwo);
87+
88+
expect(filter).toHaveBeenCalledTimes(firstElementLargerThanTwo + 1);
89+
inputArr.slice(0, firstElementLargerThanTwo).forEach((el, idx) => {
90+
expect(filter).toHaveBeenCalledWith(el, idx, inputArr);
91+
});
92+
});
93+
94+
it.skip('assertions below are valid for synchronous .find()', () => {
95+
const filter = vi.fn().mockImplementation(largerThanTwo);
96+
97+
const result = inputArr.find(filter);
98+
99+
expect(result).toEqual(3);
100+
});
101+
102+
it('returns truthy result properly', async () => {
103+
const filter = vi.fn().mockImplementation(largerThanTwoInRandomTime);
104+
105+
const result = await asyncFind(inputArr, filter);
106+
107+
expect(result).toEqual(3);
108+
});
109+
110+
it.skip('assertions below are valid for synchronous .find()', () => {
111+
const filter = vi.fn().mockImplementation(largerThanOneHundred);
112+
113+
const result = inputArr.find(filter);
114+
115+
expect(result).toEqual(undefined);
116+
});
117+
118+
it('returns undefined result properly', async () => {
119+
const filter = vi.fn().mockImplementation(largerThanOneHundredInRandomTime);
120+
121+
const result = await asyncFind(inputArr, filter);
122+
123+
expect(result).toEqual(undefined);
124+
});
125+
126+
it.skip('assertions below are valid for synchronous .find()', () => {
127+
const filter = vi.fn().mockImplementation(throws);
128+
129+
expect(() => inputArr.find(filter)).toThrow();
130+
});
131+
132+
it('throws if function passed throws', async () => {
133+
const filter = vi.fn().mockImplementation(throws);
134+
135+
await expect(asyncFind(inputArr, filter)).rejects.toThrow('Some error');
136+
});
137+
138+
it('returns type T | undefined given function that returns type Promise<boolean>', async () => {
139+
// @ts-expect-no-error
140+
const result: number | undefined = await asyncFind(inputArr, largerThanTwoInRandomTime);
141+
142+
expect(typeof result).toEqual('number');
143+
});
144+
145+
it('returns type undefined given function that returns type Promise<false>', async () => {
146+
async function falseInRandomTime() {
147+
return new Promise<false>((resolve) =>
148+
setTimeout(() => {
149+
resolve(false);
150+
}, Math.random() * 100),
151+
);
152+
}
153+
154+
// @ts-expect-no-error
155+
const result: undefined = await asyncFind(inputArr, falseInRandomTime);
156+
157+
expect(result).toEqual(undefined);
158+
});
159+
});

src/find.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import asyncForEachStrict from './forEach_strict';
2+
3+
function asyncFind<T>(
4+
arr: T[],
5+
fn: (cur: T, idx: number, arr: T[]) => Promise<false>,
6+
): Promise<undefined>;
7+
function asyncFind<T>(
8+
arr: T[],
9+
fn: (cur: T, idx: number, arr: T[]) => Promise<boolean>,
10+
): Promise<T | undefined>;
11+
function asyncFind<T>(
12+
arr: T[],
13+
fn: (cur: T, idx: number, arr: T[]) => Promise<boolean>,
14+
): Promise<T | undefined> {
15+
let resolved: boolean;
16+
return new Promise<T | undefined>((resolve, reject) => {
17+
asyncForEachStrict(
18+
arr,
19+
(cur, idx, arr2) =>
20+
new Promise<void>((resolve2, reject2) => {
21+
if (resolved) {
22+
return;
23+
}
24+
fn(cur, idx, arr2)
25+
.then((result) => {
26+
if (result) {
27+
resolve(cur);
28+
resolved = true;
29+
}
30+
resolve2();
31+
})
32+
.catch(reject2);
33+
}),
34+
)
35+
.then(() => {
36+
resolve(undefined);
37+
resolved = true;
38+
})
39+
.catch((error) => {
40+
reject(error);
41+
resolved = true;
42+
});
43+
});
44+
}
45+
46+
export default asyncFind;

0 commit comments

Comments
 (0)