├── .github
└── workflows
│ ├── ci.yml
│ └── insiders.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jest.setup.js
├── package.json
├── pnpm-lock.yaml
├── src
├── __snapshots__
│ └── index.test.ts.snap
├── at.test.ts
├── at.ts
├── average.test.ts
├── average.ts
├── batch.test.ts
├── batch.ts
├── chunk.test.ts
├── chunk.ts
├── compact.test.ts
├── compact.ts
├── compose.test.ts
├── compose.ts
├── concat.test.ts
├── concat.ts
├── delay.test.ts
├── delay.ts
├── every.test.ts
├── every.ts
├── filter.test.ts
├── filter.ts
├── find.test.ts
├── find.ts
├── findIndex.test.ts
├── findIndex.ts
├── flatMap.test.ts
├── flatMap.ts
├── flatten.test.ts
├── flatten.ts
├── generate.test.ts
├── generate.ts
├── groupBy.test.ts
├── groupBy.ts
├── head.test.ts
├── head.ts
├── includes.test.ts
├── includes.ts
├── index.test.ts
├── index.ts
├── join.test.ts
├── join.ts
├── map.test.ts
├── map.ts
├── max.test.ts
├── max.ts
├── min.test.ts
├── min.ts
├── partition.test.ts
├── partition.ts
├── pipe.test.ts
├── pipe.ts
├── product.test.ts
├── product.ts
├── range.test.ts
├── range.ts
├── reduce.test.ts
├── reduce.ts
├── replace.test.ts
├── replace.ts
├── reverse.test.ts
├── reverse.ts
├── shared-types.ts
├── skip.test.ts
├── skip.ts
├── slice.test.ts
├── slice.ts
├── some.test.ts
├── some.ts
├── sort.test.ts
├── sort.ts
├── sum.test.ts
├── sum.ts
├── take.test.ts
├── take.ts
├── takeWhile.test.ts
├── takeWhile.ts
├── tap.test.ts
├── tap.ts
├── toArray.test.ts
├── toArray.ts
├── toLength.test.ts
├── toLength.ts
├── toSet.test.ts
├── toSet.ts
├── unique.test.ts
├── unique.ts
├── utils
│ ├── clamp.ts
│ ├── ensureFunction.ts
│ └── iterator.ts
├── where.test.ts
├── where.ts
├── windows.test.ts
├── windows.ts
├── zip.test.ts
└── zip.ts
└── tsconfig.json
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | pull_request:
8 | branches: ['**']
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | matrix:
16 | node-version: [16.x, 18.x, 19.x, 20.x]
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: pnpm/action-setup@v2
21 | with:
22 | version: 8
23 |
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v3
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'pnpm'
29 |
30 | - name: Install dependencies
31 | run: pnpm install
32 |
33 | - name: Build
34 | run: pnpm build
35 |
36 | - name: Test
37 | run: |
38 | pnpm test || \
39 | pnpm test || \
40 | pnpm test || exit 1
41 | env:
42 | FAST_CHECK_RUNS: 1000
43 |
--------------------------------------------------------------------------------
/.github/workflows/insiders.yml:
--------------------------------------------------------------------------------
1 | name: Release Insiders
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | env:
8 | NODE_VERSION: 20
9 | RELEASE_CHANNEL: insiders
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: pnpm/action-setup@v2
18 | with:
19 | version: 8
20 |
21 | - name: Use Node.js ${{ env.NODE_VERSION }}
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: ${{ env.NODE_VERSION }}
25 | registry-url: 'https://registry.npmjs.org'
26 | cache: 'pnpm'
27 |
28 | - name: Install dependencies
29 | run: pnpm install
30 |
31 | - name: Build
32 | run: pnpm build
33 |
34 | - name: Test
35 | run: |
36 | pnpm test || \
37 | pnpm test || \
38 | pnpm test || exit 1
39 | env:
40 | FAST_CHECK_RUNS: 1000
41 |
42 | - name: Resolve environment variables
43 | run: |
44 | echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
45 |
46 | - name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
47 | run: pnpm version 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }} --force --no-git-tag-version
48 |
49 | - name: Publish
50 | run: pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
51 | env:
52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .*
2 | !.gitignore
3 | !.nvmrc
4 | !.github
5 | !.babelrc.js
6 | !.prettierignore
7 |
8 | node_modules/
9 | coverage/
10 | dist/
11 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | pnpm-lock.yaml
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/0.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | - Nothing yet!
11 |
12 | ## [0.11.2] - 2023-09-09
13 |
14 | ### Fixed
15 |
16 | - Fix order of arguments in `toLength` ([a49c24b](https://github.com/RobinMalfait/lazy-collections/commit/a49c24b7758d85042554bca66c1346c4a808eda8))
17 |
18 | ## [0.11.1] - 2023-09-09
19 |
20 | ### Fixed
21 |
22 | - Expose `toLength` ([81863b4](https://github.com/RobinMalfait/lazy-collections/commit/81863b4be918d757392319d990bbb2643b5bfa7d))
23 |
24 | ## [0.11.0] - 2023-09-09
25 |
26 | ### Added
27 |
28 | - Add `includes` feature ([#24](https://github.com/RobinMalfait/lazy-collections/pull/24), [#29](https://github.com/RobinMalfait/lazy-collections/pull/29))
29 | - Add `at` feature ([#25](https://github.com/RobinMalfait/lazy-collections/pull/25), [#30](https://github.com/RobinMalfait/lazy-collections/pull/30))
30 | - Add `product` feature ([#33](https://github.com/RobinMalfait/lazy-collections/pull/33))
31 | - Add `toSet` feature ([#34](https://github.com/RobinMalfait/lazy-collections/pull/34))
32 |
33 | ### Fixed
34 |
35 | - Ensure that the `types` are generated properly ([#32](https://github.com/RobinMalfait/lazy-collections/pull/32))
36 | - Ensure all (new) functions are exported ([#31](https://github.com/RobinMalfait/lazy-collections/pull/31))
37 |
38 | ## [0.10.0] - 2022-08-27
39 |
40 | ### Added
41 |
42 | - Everything!
43 |
44 | [unreleased]: https://github.com/RobinMalfait/lazy-collections/compare/v0.11.2...HEAD
45 | [0.11.2]: https://github.com/RobinMalfait/lazy-collections/compare/v0.11.1...v0.11.2
46 | [0.11.1]: https://github.com/RobinMalfait/lazy-collections/compare/v0.11.0...v0.11.1
47 | [0.11.0]: https://github.com/RobinMalfait/lazy-collections/compare/v0.10.0...v0.11.0
48 | [0.10.0]: https://github.com/RobinMalfait/lazy-collections/releases/tag/v0.10.0
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Robin Malfait
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Lazy Collections
3 |
4 |
5 |
6 | Fast and lazy collection operations.
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ---
18 |
19 | Working with methods like `.map()`, `.filter()` and `.reduce()` is nice,
20 | however they create new arrays and everything is eagerly done before going to
21 | the next step.
22 |
23 | This is where lazy collections come in, under the hood we use [iterators][1] and
24 | async iterators so that your data flows like a stream to have the optimal speed.
25 |
26 | All functions should work with both `iterator` and `asyncIterator`, if one of
27 | the functions uses an `asyncIterator` (for example when you introduce
28 | `delay(100)`), don't forget to `await` the result!
29 |
30 | [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol
31 |
32 | ```js
33 | let program = pipe(
34 | map((x) => x * 2),
35 | filter((x) => x % 4 === 0),
36 | filter((x) => x % 100 === 0),
37 | filter((x) => x % 400 === 0),
38 | toArray()
39 | )
40 |
41 | program(range(0, 1000000))
42 | ```
43 |
44 | ### Table of Contents
45 |
46 | - [Benchmark](#benchmark)
47 | - [API](#api)
48 | - [Composing functions](#composing-functions)
49 | - [`compose`](#compose)
50 | - [`pipe`](#pipe)
51 | - [Known array functions](#known-array-functions)
52 | - [`at`](#at)
53 | - [`concat`](#concat)
54 | - [`every`](#every)
55 | - [`filter`](#filter)
56 | - [`find`](#find)
57 | - [`findIndex`](#findindex)
58 | - [`flatMap`](#flatMap)
59 | - [`includes`](#includes)
60 | - [`join`](#join)
61 | - [`map`](#map)
62 | - [`reduce`](#reduce)
63 | - [`replace`](#replace)
64 | - [`reverse`](#reverse)
65 | - [`some`](#some)
66 | - [`sort`](#sort)
67 | - [Math / Statistics](#math--statistics)
68 | - [`average`](#average)
69 | - [`max`](#max)
70 | - [`min`](#min)
71 | - [`sum`](#sum)
72 | - [`product`](#product)
73 | - [Utilities](#utilities)
74 | - [`batch`](#batch)
75 | - [`chunk`](#chunk)
76 | - [`compact`](#compact)
77 | - [`delay`](#delay)
78 | - [`flatten`](#flatten)
79 | - [`generate`](#generate)
80 | - [`groupBy`](#groupby)
81 | - [`head`](#head)
82 | - [`partition`](#partition)
83 | - [`range`](#range)
84 | - [`skip`](#skip)
85 | - [`slice`](#slice)
86 | - [`take`](#take)
87 | - [`takeWhile`](#takewhile)
88 | - [`tap`](#tap)
89 | - [`toArray`](#toarray)
90 | - [`toLength`](#tolength)
91 | - [`toSet`](#toset)
92 | - [`unique`](#unique)
93 | - [`wait`](#wait)
94 | - [`where`](#where)
95 | - [`windows`](#windows)
96 | - [`zip`](#zip)
97 |
98 | ## Benchmark
99 |
100 | > :warning: This is not a scientific benchmark, there are flaws with this. This
101 | > is just meant to showcase the power of lazy-collections.
102 |
103 | | | Lazy | Eager | |
104 | | ---------------: | :-------: | :---------: | ----------------- |
105 | | Duration | `2.19ms` | `1.29s` | `589x` faster |
106 | | Memory heapTotal | `9.48 MB` | `297.96 MB` | `31x` less memory |
107 | | Memory heapUsed | `5.89 MB` | `265.46 MB` | `45x` less memory |
108 |
109 | Memory data collected using: http://nodejs.org/api/process.html#process_process_memoryusage
110 |
111 | ```js
112 | import { pipe, range, filter, takeWhile, slice, toArray } from 'lazy-collections'
113 |
114 | // Lazy example
115 | let program = pipe(
116 | range(0, 10_000_000),
117 | filter((x) => x % 100 === 0),
118 | filter((x) => x % 4 === 0),
119 | filter((x) => x % 400 === 0),
120 | takeWhile((x) => x < 1_000),
121 | slice(0, 1_000),
122 | toArray()
123 | )
124 |
125 | program() // [ 0, 400, 800 ]
126 | ```
127 |
128 | ```js
129 | // Eager example
130 | function program() {
131 | return (
132 | // Equivalent of the range()
133 | [...new Array(10_000_000).keys()]
134 | .filter((x) => x % 100 === 0)
135 | .filter((x) => x % 4 === 0)
136 | .filter((x) => x % 400 === 0)
137 |
138 | // Equivalent of the takeWhile
139 | .reduce((acc, current) => {
140 | return current < 1_000 ? (acc.push(current), acc) : acc
141 | }, [])
142 | .slice(0, 1_000)
143 | )
144 | }
145 |
146 | program() // [ 0, 400, 800 ]
147 | ```
148 |
149 | ---
150 |
151 | This is actually a stupid non-real-world example. However, it is way more
152 | efficient at doing things. That said, _yes_ you can optimize the eager example
153 | way more if you want to. You can combine the `filter` / `reduce` / `...`. However,
154 | what I want to achieve is that we can have separated logic in different `filter`
155 | or `map` steps _without_ thinking about performance bottlenecks.
156 |
157 | ## API
158 |
159 | ### Composing functions
160 |
161 | #### `compose`
162 |
163 | [Table of contents](#table-of-contents)
164 |
165 | We can use compose to compose functions together and return a new function which
166 | combines all other functions.
167 |
168 | ```js
169 | import { compose } from 'lazy-collections'
170 |
171 | // Create a program (or a combination of functions)
172 | let program = compose(fn1, fn2, fn3)
173 |
174 | program()
175 | // fn1(fn2(fn3()))
176 | ```
177 |
178 | #### `pipe`
179 |
180 | [Table of contents](#table-of-contents)
181 |
182 | We can use pipe to compose functions together and return a new function which
183 | combines all other functions.
184 |
185 | The difference between `pipe` and `compose` is the order of execution of the
186 | functions.
187 |
188 | ```js
189 | import { pipe } from 'lazy-collections'
190 |
191 | // Create a program (or a combination of functions)
192 | let program = pipe(fn1, fn2, fn3)
193 |
194 | program()
195 | // fn3(fn2(fn1()))
196 | ```
197 |
198 | ### Known array functions
199 |
200 | #### `at`
201 |
202 | [Table of contents](#table-of-contents)
203 |
204 | Returns the value at the given index.
205 |
206 | ```js
207 | import { pipe, at } from 'lazy-collections'
208 |
209 | let program = pipe(at(2))
210 |
211 | program([1, 2, 3, 4])
212 |
213 | // 3
214 | ```
215 |
216 | You can also pass a negative index to `at` to count back from the end of the array or iterator.
217 |
218 | > **Warning**: Performance may be degraded because it has to exhaust the full iterator before it can count backwards!
219 |
220 | ```js
221 | import { pipe, at } from 'lazy-collections'
222 |
223 | let program = pipe(at(-2))
224 |
225 | program([1, 2, 3, 4])
226 |
227 | // 3
228 | ```
229 |
230 | If a value can not be found at the given index, then `undefined` will be returned.
231 |
232 | ```js
233 | import { pipe, at } from 'lazy-collections'
234 |
235 | let program = pipe(at(12))
236 |
237 | program([1, 2, 3, 4])
238 |
239 | // undefined
240 | ```
241 |
242 | #### `concat`
243 |
244 | [Table of contents](#table-of-contents)
245 |
246 | Concat multiple iterators or arrays into a single iterator.
247 |
248 | ```js
249 | import { pipe, concat, toArray } from 'lazy-collections'
250 |
251 | let program = pipe(concat([0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]), toArray())
252 |
253 | program()
254 | // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
255 | ```
256 |
257 | #### `every`
258 |
259 | [Table of contents](#table-of-contents)
260 |
261 | Should return true if all values match the predicate.
262 |
263 | ```js
264 | import { pipe, every } from 'lazy-collections'
265 |
266 | let program = pipe(every((x) => x === 2))
267 |
268 | program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
269 | // false
270 | ```
271 |
272 | #### `filter`
273 |
274 | [Table of contents](#table-of-contents)
275 |
276 | Filter out values that do not meet the condition.
277 |
278 | ```js
279 | import { pipe, filter, toArray } from 'lazy-collections'
280 |
281 | let program = pipe(
282 | filter((x) => x % 2 === 0),
283 | toArray()
284 | )
285 |
286 | program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
287 | // [ 2, 4, 6, 8, 10 ]
288 | ```
289 |
290 | #### `find`
291 |
292 | [Table of contents](#table-of-contents)
293 |
294 | Find a value based on the given predicate.
295 |
296 | ```js
297 | import { pipe, find } from 'lazy-collections'
298 |
299 | let program = pipe(find((x) => x === 2))
300 |
301 | program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
302 | // 2
303 | ```
304 |
305 | #### `findIndex`
306 |
307 | [Table of contents](#table-of-contents)
308 |
309 | Find an index based on the given predicate.
310 |
311 | ```js
312 | import { pipe, findIndex } from 'lazy-collections'
313 |
314 | let program = pipe(findIndex((x) => x === 2))
315 |
316 | program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
317 | // 1
318 | ```
319 |
320 | #### `flatMap`
321 |
322 | [Table of contents](#table-of-contents)
323 |
324 | Map a value from A to B and flattens it afterwards.
325 |
326 | ```js
327 | import { pipe, flatMap, toArray } from 'lazy-collections'
328 |
329 | let program = pipe(
330 | flatMap((x) => [x * 2, x * 4]),
331 | toArray()
332 | )
333 |
334 | program([1, 2, 3])
335 | // [ 2, 4, 4, 8, 6, 12 ]
336 | ```
337 |
338 | #### `includes`
339 |
340 | [Table of contents](#table-of-contents)
341 |
342 | Check if a value is included in an array or iterator.
343 |
344 | ```js
345 | import { pipe, includes } from 'lazy-collections'
346 |
347 | let program = pipe(includes(1))
348 |
349 | program([1, 2, 3, 4])
350 |
351 | // true
352 | ```
353 |
354 | Each value is compared using `Object.is`. This will guarantee that edge cases with `NaN` also work the same as `Array.prototype.includes`.
355 |
356 | Optionally, you can start searching from a positive index:
357 |
358 | ```js
359 | import { pipe, includes } from 'lazy-collections'
360 |
361 | let program = pipe(includes(1, 1))
362 |
363 | program([1, 2, 3, 4])
364 |
365 | // false
366 | ```
367 |
368 | #### `join`
369 |
370 | [Table of contents](#table-of-contents)
371 |
372 | Join an array or iterator of strings.
373 |
374 | ```js
375 | import { pipe, join } from 'lazy-collections'
376 |
377 | let program = pipe(join())
378 |
379 | program(['foo', 'bar', 'baz'])
380 | // 'foo,bar,baz'
381 | ```
382 |
383 | Optionally, you can join with a separator string:
384 |
385 | ```js
386 | import { pipe, join } from 'lazy-collections'
387 |
388 | let program = pipe(join(' '))
389 |
390 | program(['foo', 'bar', 'baz'])
391 | // 'foo bar baz'
392 | ```
393 |
394 | #### `toLength`
395 |
396 | [Table of contents](#table-of-contents)
397 |
398 | > **Warning**: Performance warning, it has to exhaust the full iterator before it can calculate length!
399 |
400 | Get the length of an array or iterator.
401 |
402 | ```js
403 | import { pipe, toLength, filter } from 'lazy-collections'
404 |
405 | let program = pipe(
406 | filter((x) => x % 2 === 0),
407 | toLength()
408 | )
409 |
410 | program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
411 | // 5
412 | ```
413 |
414 | #### `map`
415 |
416 | [Table of contents](#table-of-contents)
417 |
418 | Map a value from A to B.
419 |
420 | ```js
421 | import { pipe, map, toArray } from 'lazy-collections'
422 |
423 | let program = pipe(
424 | map((x) => x * 2),
425 | toArray()
426 | )
427 |
428 | program([1, 2, 3])
429 | // [ 2, 4, 6 ]
430 | ```
431 |
432 | #### `reduce`
433 |
434 | [Table of contents](#table-of-contents)
435 |
436 | Reduce the data to a single value.
437 |
438 | ```js
439 | import { pipe, reduce } from 'lazy-collections'
440 |
441 | let program = pipe(reduce((total, current) => total + current, 0))
442 |
443 | program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
444 | // 55
445 | ```
446 |
447 | #### `replace`
448 |
449 | [Table of contents](#table-of-contents)
450 |
451 | Replace an item at a given index with a new value.
452 |
453 | ```js
454 | import { pipe, replace } from 'lazy-collections'
455 |
456 | let program = pipe(replace(2, 42))
457 |
458 | program([1, 2, 3, 4])
459 | // [ 1, 2, 42, 4 ]
460 | ```
461 |
462 | #### `reverse`
463 |
464 | > **Warning**: Performance may be degraded because it has to exhaust the full iterator before it can reverse it!
465 |
466 | [Table of contents](#table-of-contents)
467 |
468 | Reverses the iterator.
469 |
470 | ```js
471 | import { pipe, reverse, toArray } from 'lazy-collections'
472 |
473 | let program = pipe(range(0, 5), reverse(), toArray())
474 |
475 | program()
476 | // [ 5, 4, 3, 2, 1, 0 ]
477 | ```
478 |
479 | #### `some`
480 |
481 | [Table of contents](#table-of-contents)
482 |
483 | Should return true if some of the values match the predicate.
484 |
485 | ```js
486 | import { pipe, some } from 'lazy-collections'
487 |
488 | let program = pipe(some((x) => x === 2))
489 |
490 | program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
491 | // true
492 | ```
493 |
494 | #### `sort`
495 |
496 | > **Warning**: Performance may be degraded because it has to exhaust the full iterator before it can sort it!
497 |
498 | [Table of contents](#table-of-contents)
499 |
500 | Should sort the data. You can also provide a comparator function to the `sort` function.
501 |
502 | ```js
503 | import { pipe, generate, take, sort, toArray } from 'lazy-collections'
504 |
505 | let program = pipe(
506 | generate(() => (Math.random() * 100) | 0),
507 | take(5),
508 | sort(),
509 | toArray()
510 | )
511 |
512 | program()
513 | // [ 11, 18, 24, 27, 83 ]
514 | ```
515 |
516 | ### Math / Statistics
517 |
518 | #### `average`
519 |
520 | [Table of contents](#table-of-contents)
521 |
522 | > Alias: `mean`
523 |
524 | Gets the average of number of values.
525 |
526 | ```js
527 | import { pipe, average, toArray } from 'lazy-collections'
528 |
529 | let program = pipe(average())
530 |
531 | program([6, 7, 8, 9, 10])
532 | // 8
533 | ```
534 |
535 | #### `max`
536 |
537 | [Table of contents](#table-of-contents)
538 |
539 | Find the maximum value of the given list
540 |
541 | ```js
542 | import { pipe, range, max } from 'lazy-collections'
543 |
544 | let program = pipe(range(0, 5), max())
545 |
546 | program()
547 | // 5
548 | ```
549 |
550 | #### `min`
551 |
552 | [Table of contents](#table-of-contents)
553 |
554 | Find the minimum value of the given list
555 |
556 | ```js
557 | import { pipe, range, min } from 'lazy-collections'
558 |
559 | let program = pipe(range(5, 10), min())
560 |
561 | program()
562 | // 5
563 | ```
564 |
565 | #### `sum`
566 |
567 | [Table of contents](#table-of-contents)
568 |
569 | Should sum an array or iterator.
570 |
571 | ```js
572 | import { pipe, sum } from 'lazy-collections'
573 |
574 | let program = pipe(sum())
575 |
576 | program([1, 1, 2, 3, 2, 4, 5])
577 | // 18
578 | ```
579 |
580 | #### `product`
581 |
582 | [Table of contents](#table-of-contents)
583 |
584 | Should multiply an array or iterator.
585 |
586 | ```js
587 | import { pipe, product } from 'lazy-collections'
588 |
589 | let program = pipe(product())
590 |
591 | program([1, 1, 2, 3, 2, 4, 5])
592 | // 240
593 | ```
594 |
595 | ### Utilities
596 |
597 | #### `batch`
598 |
599 | [Table of contents](#table-of-contents)
600 |
601 | This will call up to `N` amount of items in the stream immediately and wait for them in the correct
602 | order. If you have a list of API calls, then you can use this method to start calling the API in
603 | batches of `N` instead of waiting for each API call sequentially.
604 |
605 | ```js
606 | import { pipe, range, map, batch, toArray } from 'lazy-collections'
607 |
608 | let program = pipe(
609 | range(0, 9),
610 | map(() => fetch(`/users/${id}`)),
611 | batch(5), // Will create 2 "batches" of 5 API calls
612 | toArray()
613 | )
614 |
615 | await program()
616 | // [ User1, User2, User3, User4, User5, User6, User7, User8, User9, User10 ];
617 | ```
618 |
619 | #### `chunk`
620 |
621 | [Table of contents](#table-of-contents)
622 |
623 | Chunk the data into pieces of a certain size.
624 |
625 | ```js
626 | import { pipe, chunk, toArray } from 'lazy-collections'
627 |
628 | let program = pipe(chunk(3), toArray())
629 |
630 | program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
631 | // [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ], [ 10 ] ];
632 | ```
633 |
634 | #### `compact`
635 |
636 | [Table of contents](#table-of-contents)
637 |
638 | Filters out all falsey values.
639 |
640 | ```js
641 | import { pipe, compact, toArray } from 'lazy-collections'
642 |
643 | let program = pipe(compact(), toArray())
644 |
645 | program([0, 1, true, false, null, undefined, '', 'test', NaN])
646 | // [ 1, true, 'test' ];
647 | ```
648 |
649 | #### `delay`
650 |
651 | [Table of contents](#table-of-contents)
652 |
653 | Will make he whole program async. It will add a delay of x milliseconds when an
654 | item goes through the stream.
655 |
656 | ```js
657 | import { pipe, range, delay, map, toArray } from 'lazy-collections'
658 |
659 | let program = pipe(
660 | range(0, 4),
661 | delay(5000), // 5 seconds
662 | map(() => new Date().toLocaleTimeString()),
663 | toArray()
664 | )
665 |
666 | await program()
667 | // [ '10:00:00', '10:00:05', '10:00:10', '10:00:15', '10:00:20' ];
668 | ```
669 |
670 | #### `flatten`
671 |
672 | [Table of contents](#table-of-contents)
673 |
674 | By default we will flatten recursively deep.
675 |
676 | ```js
677 | import { pipe, flatten, toArray } from 'lazy-collections'
678 |
679 | let program = pipe(flatten(), toArray())
680 |
681 | program([1, 2, 3, [4, 5, 6, [7, 8], 9, 10]])
682 | // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
683 | ```
684 |
685 | But you can also just flatten shallowly
686 |
687 | ```js
688 | import { pipe, flatten, toArray } from 'lazy-collections'
689 |
690 | let program = pipe(flatten({ shallow: true }), toArray())
691 |
692 | program([1, 2, 3, [4, 5, 6, [7, 8], 9, 10]])
693 | // [ 1, 2, 3, 4, 5, 6, [ 7, 8 ], 9, 10 ]
694 | ```
695 |
696 | #### `generate`
697 |
698 | [Table of contents](#table-of-contents)
699 |
700 | Generate accepts a function that function will be called over and over again.
701 | Don't forget to combine this with a function that ensures that the data stream
702 | will end. For example, you can use `take`, `takeWhile` or `slice`.
703 |
704 | ```js
705 | import { pipe, generate, take, toArray } from 'lazy-collections'
706 |
707 | let program = pipe(generate(Math.random), take(3), toArray())
708 |
709 | program()
710 | // [ 0.7495421596380878, 0.09819118640607383, 0.2453718461872143 ]
711 | ```
712 |
713 | #### `groupBy`
714 |
715 | [Table of contents](#table-of-contents)
716 |
717 | Groups the iterator to an object, using the keySelector function.
718 |
719 | ```js
720 | import { pipe, groupBy, range } from 'lazy-collections'
721 |
722 | // A function that will map the value to the nearest multitude. In this example
723 | // we will map values to the nearest multitude of 5. So that we can group by
724 | // this value.
725 | function snap(multitude: number, value: number) {
726 | return Math.ceil(value / multitude) * multitude
727 | }
728 |
729 | let program = pipe(
730 | range(0, 10),
731 | groupBy((x: number) => snap(5, x))
732 | )
733 |
734 | program()
735 | // {
736 | // 0: [0],
737 | // 5: [1, 2, 3, 4, 5],
738 | // 10: [6, 7, 8, 9, 10],
739 | // }
740 | ```
741 |
742 | #### `head`
743 |
744 | [Table of contents](#table-of-contents)
745 |
746 | > Alias: `first`
747 |
748 | Gets the first value of the array / iterator. Returns `undefined` if there is no
749 | value.
750 |
751 | ```js
752 | import { pipe, chunk, toArray } from 'lazy-collections'
753 |
754 | let program = pipe(head())
755 |
756 | program([6, 7, 8, 9, 10])
757 | // 6
758 | ```
759 |
760 | #### `partition`
761 |
762 | [Table of contents](#table-of-contents)
763 |
764 | Partition data into 2 groups based on the predicate.
765 |
766 | ```js
767 | import { pipe, partition, range, toArray } from 'lazy-collections'
768 |
769 | let program = pipe(
770 | range(1, 4),
771 | partition((x) => x % 2 !== 0),
772 | toArray()
773 | )
774 |
775 | program()
776 | // [ [ 1, 3 ], [ 2, 4 ] ]
777 | ```
778 |
779 | #### `range`
780 |
781 | [Table of contents](#table-of-contents)
782 |
783 | Create a range of data using a lowerbound, upperbound and step. The step is
784 | optional and defaults to `1`.
785 |
786 | ```js
787 | import { pipe, range, toArray } from 'lazy-collections'
788 |
789 | let program = pipe(range(5, 20, 5), toArray())
790 |
791 | program()
792 | // [ 5, 10, 15, 20 ]
793 | ```
794 |
795 | #### `skip`
796 |
797 | [Table of contents](#table-of-contents)
798 |
799 | Allows you to skip X values of the input.
800 |
801 | ```js
802 | import { pipe, range, skip, toArray } from 'lazy-collections'
803 |
804 | let program = pipe(range(0, 10), skip(3), toArray())
805 |
806 | program()
807 | // [ 3, 4, 5, 6, 7, 8, 9, 10 ]
808 | ```
809 |
810 | #### `slice`
811 |
812 | [Table of contents](#table-of-contents)
813 |
814 | Slice a certain portion from your data set. It accepts a start index and an end
815 | index.
816 |
817 | ```js
818 | import { pipe, range, slice, toArray } from 'lazy-collections'
819 |
820 | let program = pipe(range(0, 10), slice(3, 5), toArray())
821 |
822 | program()
823 | // [ 3, 4, 5 ]
824 |
825 | // Without the slice this would have generated
826 | // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
827 | ```
828 |
829 | #### `take`
830 |
831 | [Table of contents](#table-of-contents)
832 |
833 | Allows you to take X values of the input.
834 |
835 | ```js
836 | import { pipe, range, take, toArray } from 'lazy-collections'
837 |
838 | let program = pipe(range(0, 10), take(3), toArray())
839 |
840 | program()
841 | // [ 0, 1, 2 ]
842 | ```
843 |
844 | #### `takeWhile`
845 |
846 | [Table of contents](#table-of-contents)
847 |
848 | This is similar to `take`, but instead of a number as a value it takes a
849 | function as a condition.
850 |
851 | ```js
852 | import { pipe, range, takeWhile, toArray } from 'lazy-collections'
853 |
854 | let program = pipe(
855 | range(0, 10),
856 | takeWhile((x) => x < 5),
857 | toArray()
858 | )
859 |
860 | program()
861 | // [ 0, 1, 2, 3, 4 ]
862 | ```
863 |
864 | #### `tap`
865 |
866 | [Table of contents](#table-of-contents)
867 |
868 | Allows you to tap into the stream, this way you can intercept each value.
869 |
870 | ```js
871 | import { pipe, range, tap, toArray } from 'lazy-collections'
872 |
873 | let program = pipe(
874 | range(0, 5),
875 | tap((x) => {
876 | console.log('x:', x)
877 | }),
878 | toArray()
879 | )
880 |
881 | program()
882 | // x: 0
883 | // x: 1
884 | // x: 2
885 | // x: 3
886 | // x: 4
887 | // x: 5
888 | // [ 0, 1, 2, 3, 4, 5 ]
889 | ```
890 |
891 | #### `toArray`
892 |
893 | [Table of contents](#table-of-contents)
894 |
895 | Converts an array or an iterator to an actual array.
896 |
897 | ```js
898 | import { pipe, range, toArray } from 'lazy-collections'
899 |
900 | let program = pipe(range(0, 10), toArray())
901 |
902 | program()
903 | // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
904 | ```
905 |
906 | #### `toSet`
907 |
908 | [Table of contents](#table-of-contents)
909 |
910 | Converts an array or an iterator to Set.
911 |
912 | ```js
913 | import { pipe, range, toSet } from 'lazy-collections'
914 |
915 | let program = pipe(range(0, 10), toSet())
916 |
917 | program()
918 | // Set (11) { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
919 | ```
920 |
921 | #### `unique`
922 |
923 | [Table of contents](#table-of-contents)
924 |
925 | Make your data unique.
926 |
927 | ```js
928 | import { pipe, unique, toArray } from 'lazy-collections'
929 |
930 | let program = pipe(unique(), toArray())
931 |
932 | program([1, 1, 2, 3, 2, 4, 5])
933 | // [ 1, 2, 3, 4, 5 ]
934 | ```
935 |
936 | #### `wait`
937 |
938 | [Table of contents](#table-of-contents)
939 |
940 | Will make he whole program async. It is similar to delay, but there is no actual delay involved. If
941 | your stream contains promises it will resolve those promises instead of possibly resolving to an
942 | array of pending promises.
943 |
944 | > **Note**: This will execute the fetch calls sequentially, it will go to the next call once the
945 | > first call is done. To prevent this you can use the [`batch`](#batch) function to help with this.
946 |
947 | ```js
948 | import { pipe, range, map, wait, toArray } from 'lazy-collections'
949 |
950 | let program = pipe(
951 | range(0, 4),
952 | map((id) => fetch(`/my-api/users/${id}`)),
953 | wait(),
954 | toArray()
955 | )
956 |
957 | await program()
958 | // [ User1, User2, User3, User4, User5 ];
959 | ```
960 |
961 | #### `where`
962 |
963 | [Table of contents](#table-of-contents)
964 |
965 | Filter out values based on the given properties.
966 |
967 | ```js
968 | import { pipe, where, range, map, where, toArray } from 'lazy-collections'
969 |
970 | let program = pipe(
971 | range(15, 20),
972 | map((age) => ({ age })),
973 | where({ age: 18 }),
974 | toArray()
975 | )
976 |
977 | program()
978 | // [ { age: 18 } ]
979 | ```
980 |
981 | #### `windows`
982 |
983 | [Table of contents](#table-of-contents)
984 |
985 | Get a sliding window of a certain size, for the given input.
986 |
987 | ```js
988 | import { pipe, windows, toArray } from 'lazy-collections'
989 |
990 | let program = pipe(windows(2), toArray())
991 |
992 | program(['l', 'a', 'z', 'y'])
993 | // [ [ 'l', 'a' ], [ 'a', 'z' ], [ 'z', 'y' ] ]
994 | ```
995 |
996 | #### `zip`
997 |
998 | [Table of contents](#table-of-contents)
999 |
1000 | Zips multiple arrays / iterators together.
1001 |
1002 | ```js
1003 | import { pipe, zip, toArray } from 'lazy-collections'
1004 |
1005 | let program = pipe(zip(), toArray())
1006 |
1007 | program([
1008 | [0, 1, 2],
1009 | ['A', 'B', 'C'],
1010 | ])
1011 | // [ [ 0, 'A' ], [ 1, 'B' ], [ 2, 'C' ] ]
1012 | ```
1013 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | let fc = require('fast-check')
2 |
3 | fc.configureGlobal({
4 | numRuns: Number(process.env.FAST_CHECK_RUNS),
5 | })
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lazy-collections",
3 | "version": "0.11.2",
4 | "description": "Collection of fast and lazy operations",
5 | "main": "./dist/index.js",
6 | "type": "module",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/RobinMalfait/lazy-collections.git"
10 | },
11 | "author": {
12 | "name": "Robin Malfait",
13 | "email": "malfait.robin@gmail.com"
14 | },
15 | "engines": {
16 | "node": ">=13"
17 | },
18 | "license": "MIT",
19 | "homepage": "https://github.com/RobinMalfait/lazy-collections",
20 | "files": [
21 | "dist"
22 | ],
23 | "prettier": {
24 | "printWidth": 100,
25 | "semi": false,
26 | "singleQuote": true,
27 | "trailingComma": "es5"
28 | },
29 | "jest": {
30 | "setupFiles": [
31 | "./jest.setup.js"
32 | ],
33 | "transform": {
34 | "\\.ts$": "@swc/jest"
35 | }
36 | },
37 | "scripts": {
38 | "format": "prettier . --write",
39 | "build": "tsup ./src/index.ts --format esm --clean --minify --dts",
40 | "test": "jest",
41 | "tdd": "jest --watchAll",
42 | "prepack": "npm run build"
43 | },
44 | "devDependencies": {
45 | "@swc/core": "^1.3.83",
46 | "@swc/jest": "^0.2.29",
47 | "@types/jest": "^29.5.4",
48 | "fast-check": "^3.13.0",
49 | "jest": "^29.6.4",
50 | "prettier": "^3.0.3",
51 | "tslib": "^2.6.2",
52 | "tsup": "^7.2.0",
53 | "typescript": "^5.2.2"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/__snapshots__/index.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should export all the things 1`] = `
4 | {
5 | "at": [Function],
6 | "average": [Function],
7 | "batch": [Function],
8 | "chunk": [Function],
9 | "compact": [Function],
10 | "compose": [Function],
11 | "concat": [Function],
12 | "delay": [Function],
13 | "every": [Function],
14 | "filter": [Function],
15 | "find": [Function],
16 | "findIndex": [Function],
17 | "first": [Function],
18 | "flatMap": [Function],
19 | "flatten": [Function],
20 | "generate": [Function],
21 | "groupBy": [Function],
22 | "head": [Function],
23 | "includes": [Function],
24 | "join": [Function],
25 | "map": [Function],
26 | "max": [Function],
27 | "mean": [Function],
28 | "min": [Function],
29 | "partition": [Function],
30 | "pipe": [Function],
31 | "product": [Function],
32 | "range": [Function],
33 | "reduce": [Function],
34 | "reverse": [Function],
35 | "skip": [Function],
36 | "slice": [Function],
37 | "some": [Function],
38 | "sort": [Function],
39 | "sum": [Function],
40 | "take": [Function],
41 | "takeWhile": [Function],
42 | "tap": [Function],
43 | "toArray": [Function],
44 | "toLength": [Function],
45 | "toSet": [Function],
46 | "unique": [Function],
47 | "wait": [Function],
48 | "where": [Function],
49 | "windows": [Function],
50 | "zip": [Function],
51 | }
52 | `;
53 |
--------------------------------------------------------------------------------
/src/at.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, at, map, delay } from './'
2 |
3 | it('should return the element at the given index', () => {
4 | let program = pipe(
5 | map((x: number) => String.fromCharCode(x + 65)),
6 | at(25)
7 | )
8 |
9 | expect(program(range(0, 25))).toBe('Z')
10 | expect(program(range(0, 25))).toBe('Z')
11 | })
12 |
13 | it('should return undefined when the given index is out of bounds', () => {
14 | let program = pipe(
15 | map((x: number) => String.fromCharCode(x + 65)),
16 | at(25)
17 | )
18 |
19 | expect(program(range(0, 24))).toBeUndefined()
20 | expect(program(range(0, 24))).toBeUndefined()
21 | })
22 |
23 | it('should return the element at the given index (negative)', () => {
24 | let program = pipe(
25 | map((x: number) => String.fromCharCode(x + 65)),
26 | at(-1)
27 | )
28 |
29 | expect(program(range(0, 25))).toBe('Z')
30 | expect(program(range(0, 25))).toBe('Z')
31 | })
32 |
33 | it('should return the element at the given index (async)', async () => {
34 | let program = pipe(
35 | delay(0),
36 | map((x: number) => String.fromCharCode(x + 65)),
37 | at(25)
38 | )
39 |
40 | expect(await program(range(0, 25))).toBe('Z')
41 | expect(await program(range(0, 25))).toBe('Z')
42 | })
43 |
44 | it('should return undefined when the given index is out of bounds (async)', async () => {
45 | let program = pipe(
46 | delay(0),
47 | map((x: number) => String.fromCharCode(x + 65)),
48 | at(25)
49 | )
50 |
51 | expect(await program(range(0, 24))).toBeUndefined()
52 | expect(await program(range(0, 24))).toBeUndefined()
53 | })
54 |
55 | it('should return the element at the given index (negative) (async)', async () => {
56 | let program = pipe(
57 | delay(0),
58 | map((x: number) => String.fromCharCode(x + 65)),
59 | at(-1)
60 | )
61 |
62 | expect(await program(range(0, 25))).toBe('Z')
63 | expect(await program(range(0, 25))).toBe('Z')
64 | })
65 |
66 | it('should return the element at the given index (Promise async)', async () => {
67 | let program = pipe(
68 | map((x: number) => String.fromCharCode(x + 65)),
69 | at(25)
70 | )
71 |
72 | expect(await program(Promise.resolve(range(0, 25)))).toBe('Z')
73 | expect(await program(Promise.resolve(range(0, 25)))).toBe('Z')
74 | })
75 |
76 | it('should return undefined when the given index is out of bounds (Promise async)', async () => {
77 | let program = pipe(at(25))
78 |
79 | expect(await program(Promise.resolve(range(0, 24)))).toBeUndefined()
80 | expect(await program(Promise.resolve(range(0, 24)))).toBeUndefined()
81 | })
82 |
83 | it('should return the element at the given index (negative) (Promise async)', async () => {
84 | let program = pipe(
85 | map((x: number) => String.fromCharCode(x + 65)),
86 | at(-1)
87 | )
88 |
89 | expect(await program(Promise.resolve(range(0, 25)))).toBe('Z')
90 | expect(await program(Promise.resolve(range(0, 25)))).toBe('Z')
91 | })
92 |
--------------------------------------------------------------------------------
/src/at.ts:
--------------------------------------------------------------------------------
1 | import { find, pipe, toArray } from './'
2 |
3 | import { isAsyncIterable } from './utils/iterator'
4 | import { LazyIterable } from './shared-types'
5 |
6 | export function at(index: number) {
7 | if (index >= 0) {
8 | return function atFn(data: LazyIterable) {
9 | return find((_, i) => i === index)(data)
10 | }
11 | }
12 |
13 | /**
14 | * To support counting back with a negative index, the whole iteraror has to be
15 | * converted to an array. Then, `Array.prototype.at` can be used. This is slow,
16 | * but it helps deliver a consistent UX.
17 | */
18 | return function atFn(data: LazyIterable) {
19 | if (isAsyncIterable(data) || data instanceof Promise) {
20 | return (async () => {
21 | let stream = data instanceof Promise ? await data : data
22 |
23 | let program = pipe(toArray())
24 | let array = await program(stream)
25 |
26 | return array.at(index)
27 | })()
28 | }
29 |
30 | return Array.from(data).at(index)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/average.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, average, delay } from './'
2 |
3 | it('should be possible to get an average of all the values', () => {
4 | let program = pipe(average())
5 |
6 | expect(program([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(5)
7 | expect(program([10, 10, 10])).toEqual(10)
8 | })
9 |
10 | it('should be possible to get an average of all the values (async)', async () => {
11 | let program = pipe(delay(0), average())
12 |
13 | expect(await program([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(5)
14 | expect(await program([10, 10, 10])).toEqual(10)
15 | })
16 |
17 | it('should be possible to get an average of all the values (Promise async)', async () => {
18 | let program = pipe(average())
19 |
20 | expect(await program(Promise.resolve([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))).toEqual(5)
21 | expect(await program(Promise.resolve([10, 10, 10]))).toEqual(10)
22 | })
23 |
--------------------------------------------------------------------------------
/src/average.ts:
--------------------------------------------------------------------------------
1 | import { reduce, chunk, map, head, pipe } from './'
2 |
3 | import { LazyIterable } from './shared-types'
4 |
5 | export function average() {
6 | return function averageFn(data: LazyIterable) {
7 | let program = pipe(
8 | reduce<[number, number], number>(
9 | (acc, current) => {
10 | acc[0] += current
11 | acc[1] += 1
12 | return acc
13 | },
14 | [0, 0]
15 | ),
16 | chunk(2),
17 | map(([sum, count]: [number, number]) => sum / count),
18 | head()
19 | )
20 |
21 | return program(data)
22 | }
23 | }
24 |
25 | // Alias
26 | export let mean = average
27 |
--------------------------------------------------------------------------------
/src/batch.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, batch, map, wait, toArray, generate, take, chunk, average } from './'
2 |
3 | async function bench(cb: Function) {
4 | let start = process.hrtime.bigint()
5 | await cb()
6 | let end = process.hrtime.bigint()
7 | return Number((end - start) / BigInt(1e6)) // Nanoseconds to milliseconds
8 | }
9 |
10 | it('should batch async operations', async () => {
11 | let DELAY = 50
12 | function fetch(resultResolver: Function) {
13 | return new Promise((resolve) => setTimeout(() => resolve(resultResolver()), DELAY))
14 | }
15 |
16 | let start = Date.now()
17 | let program = pipe(
18 | generate(() => null), // Generate a forever stream
19 | take(10), // Only take 10 items
20 | map(() => fetch(() => Date.now())),
21 | batch(4), // batch in groups of 4
22 | wait(), // This is important, otherwise we will resolve to an array of promises
23 | map((value) => value - start), // Diff compared to when we started
24 | chunk(4), // Group each chunk of 4 items again
25 | toArray()
26 | )
27 |
28 | let avg = pipe(average())
29 |
30 | let [a, b, c] = await program()
31 |
32 | // Each item of each chunk should be really close together, whereas the next group should be
33 | // further apart from the previous one.
34 | expect(avg(a)).toBeGreaterThanOrEqual(DELAY * 1)
35 | expect(avg(a)).toBeLessThan(DELAY * 2)
36 |
37 | expect(avg(b)).toBeGreaterThanOrEqual(DELAY * 2)
38 | expect(avg(b)).toBeLessThan(DELAY * 3)
39 |
40 | expect(avg(c)).toBeGreaterThanOrEqual(DELAY * 3)
41 | expect(avg(c)).toBeLessThan(DELAY * 4)
42 | })
43 |
44 | it('should be possible to batch load an API call for example (from sync -> async)', async () => {
45 | function fetch(path: string) {
46 | return new Promise((resolve) => setTimeout(resolve, 50, path))
47 | }
48 |
49 | let program = pipe(
50 | map((id) => `/user/${id}`),
51 | map((path) => fetch(path)),
52 | batch(4),
53 | wait(), // This is important, otherwise we will resolve to an array of promises
54 | toArray()
55 | )
56 |
57 | let diff = await bench(() => {
58 | return expect(program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).resolves.toEqual([
59 | '/user/1',
60 | '/user/2',
61 | '/user/3',
62 | '/user/4',
63 | '/user/5',
64 | '/user/6',
65 | '/user/7',
66 | '/user/8',
67 | '/user/9',
68 | '/user/10',
69 | ])
70 | })
71 |
72 | // Because we should at least wait 150ms per call, because we create batches of 4. In this case
73 | // that means that we create this structure basically:
74 | //
75 | // [ [ 50ms, 50ms, 50ms, 50ms ], [ 50ms, 50ms, 50ms, 50ms ], [ 50ms ] ]
76 | //
77 | // Each group will at least take 50ms, and since we end up with 3 groups we should wait at least
78 | // 150ms
79 | expect(diff).toBeGreaterThanOrEqual(150)
80 |
81 | // If batch() was not there, then it would take ~500ms, so to verify that we did made some
82 | // "parallel" calls we can check if it is way smaller than that.
83 | expect(diff).toBeLessThanOrEqual(200)
84 |
85 | let diff2 = await bench(() => {
86 | return expect(program([1, 2, 3, 4, 5])).resolves.toEqual([
87 | '/user/1',
88 | '/user/2',
89 | '/user/3',
90 | '/user/4',
91 | '/user/5',
92 | ])
93 | })
94 |
95 | expect(diff2).toBeGreaterThanOrEqual(100)
96 | expect(diff2).toBeLessThanOrEqual(150)
97 | })
98 |
--------------------------------------------------------------------------------
/src/batch.ts:
--------------------------------------------------------------------------------
1 | import { getIterator, isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | export function batch(size: number) {
5 | return function batchFn(data: LazyIterable) {
6 | if (data == null) return
7 |
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return {
10 | async *[Symbol.asyncIterator]() {
11 | let stream = data instanceof Promise ? await data : data
12 | let iterator = getIterator(stream)
13 |
14 | let buffer = []
15 |
16 | loop: while (true) {
17 | for (let i = 0; i < size; i++) {
18 | buffer.push(iterator.next())
19 | }
20 |
21 | for (let { value, done } of await Promise.all(buffer.splice(0))) {
22 | if (done) break loop
23 | yield value
24 | }
25 | }
26 | },
27 | }
28 | }
29 |
30 | return {
31 | *[Symbol.iterator]() {
32 | let buffer = []
33 | let iterator = data[Symbol.iterator]()
34 |
35 | loop: while (true) {
36 | for (let i = 0; i < size; i++) {
37 | buffer.push(iterator.next())
38 | }
39 |
40 | for (let { value, done } of buffer.splice(0)) {
41 | if (done) break loop
42 | yield value
43 | }
44 | }
45 | },
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/chunk.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, chunk, toArray, delay } from './'
2 |
3 | it('should create chunked items', () => {
4 | let program = pipe(chunk(3), toArray())
5 |
6 | expect(program(range(0, 10))).toEqual([
7 | [0, 1, 2],
8 | [3, 4, 5],
9 | [6, 7, 8],
10 | [9, 10],
11 | ])
12 | expect(program(range(0, 10))).toEqual([
13 | [0, 1, 2],
14 | [3, 4, 5],
15 | [6, 7, 8],
16 | [9, 10],
17 | ])
18 | })
19 |
20 | it('should create chunked items (async)', async () => {
21 | let program = pipe(delay(0), chunk(3), toArray())
22 |
23 | expect(await program(range(0, 10))).toEqual([
24 | [0, 1, 2],
25 | [3, 4, 5],
26 | [6, 7, 8],
27 | [9, 10],
28 | ])
29 | expect(await program(range(0, 10))).toEqual([
30 | [0, 1, 2],
31 | [3, 4, 5],
32 | [6, 7, 8],
33 | [9, 10],
34 | ])
35 | })
36 |
37 | it('should create chunked items (Promise async)', async () => {
38 | let program = pipe(chunk(3), toArray())
39 |
40 | expect(await program(Promise.resolve(range(0, 10)))).toEqual([
41 | [0, 1, 2],
42 | [3, 4, 5],
43 | [6, 7, 8],
44 | [9, 10],
45 | ])
46 | expect(await program(Promise.resolve(range(0, 10)))).toEqual([
47 | [0, 1, 2],
48 | [3, 4, 5],
49 | [6, 7, 8],
50 | [9, 10],
51 | ])
52 | })
53 |
--------------------------------------------------------------------------------
/src/chunk.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | export function chunk(size: number) {
5 | return function chunkFn(data: LazyIterable) {
6 | if (isAsyncIterable(data) || data instanceof Promise) {
7 | return {
8 | async *[Symbol.asyncIterator]() {
9 | let stream = data instanceof Promise ? await data : data
10 |
11 | // Let's have a placeholder for our current chunk
12 | let chunk = []
13 |
14 | // Loop over our data
15 | for await (let datum of stream) {
16 | // Add item to our current chunk
17 | chunk.push(datum)
18 |
19 | if (chunk.length === size) {
20 | // Our current chunk is full, let's yield it
21 | yield chunk
22 |
23 | // Let's also clear our chunk for the next chunk
24 | chunk = []
25 | }
26 | }
27 |
28 | // When the chunk is not full yet, but when we are at the end of the data we
29 | // have to ensure that this one is also yielded
30 | if (chunk.length > 0) yield chunk
31 | },
32 | }
33 | }
34 |
35 | return {
36 | *[Symbol.iterator]() {
37 | // Let's have a placeholder for our current chunk
38 | let chunk = []
39 |
40 | // Loop over our data
41 | for (let datum of data) {
42 | // Add item to our current chunk
43 | chunk.push(datum)
44 |
45 | if (chunk.length === size) {
46 | // Our current chunk is full, let's yield it
47 | yield chunk
48 |
49 | // Let's also clear our chunk for the next chunk
50 | chunk = []
51 | }
52 | }
53 |
54 | // When the chunk is not full yet, but when we are at the end of the data we
55 | // have to ensure that this one is also yielded
56 | if (chunk.length > 0) yield chunk
57 | },
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/compact.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, compact, toArray, delay } from './'
2 |
3 | it('should remove all falsey values', () => {
4 | let program = pipe(compact(), toArray())
5 |
6 | expect(program([0, 1, true, false, null, undefined, '', 'test', NaN])).toEqual([1, true, 'test'])
7 | expect(program([0, 1, true, false, null, undefined, '', 'test', NaN])).toEqual([1, true, 'test'])
8 | })
9 |
10 | it('should remove all falsey values (async)', async () => {
11 | let program = pipe(delay(0), compact(), toArray())
12 |
13 | expect(await program([0, 1, true, false, null, undefined, '', 'test', NaN])).toEqual([
14 | 1,
15 | true,
16 | 'test',
17 | ])
18 | expect(await program([0, 1, true, false, null, undefined, '', 'test', NaN])).toEqual([
19 | 1,
20 | true,
21 | 'test',
22 | ])
23 | })
24 |
25 | it('should remove all falsey values (Promise async)', async () => {
26 | let program = pipe(compact(), toArray())
27 |
28 | expect(
29 | await program(Promise.resolve([0, 1, true, false, null, undefined, '', 'test', NaN]))
30 | ).toEqual([1, true, 'test'])
31 | expect(
32 | await program(Promise.resolve([0, 1, true, false, null, undefined, '', 'test', NaN]))
33 | ).toEqual([1, true, 'test'])
34 | })
35 |
--------------------------------------------------------------------------------
/src/compact.ts:
--------------------------------------------------------------------------------
1 | import { filter } from './'
2 |
3 | export function compact() {
4 | return filter(Boolean)
5 | }
6 |
--------------------------------------------------------------------------------
/src/compose.test.ts:
--------------------------------------------------------------------------------
1 | import { compose, generate, take, range, toArray } from './'
2 |
3 | it('should be possible to compose multiple functions', () => {
4 | let program = compose(
5 | (a: string) => `fn1(${a})`,
6 | (a: string) => `fn2(${a})`,
7 | (a: number, b: number) => `fn3(${a}, ${b})`
8 | )
9 |
10 | expect(program(2, 3)).toEqual('fn1(fn2(fn3(2, 3)))')
11 | })
12 |
13 | it('should be possible to pass a generator as first argument', () => {
14 | let program = compose(toArray(), take(10), generate(Math.random))
15 | let result = program()
16 | expect(result).toHaveLength(10)
17 | })
18 |
19 | it('should be possible to pass a generator as only argument', () => {
20 | let program = compose(range(0, 10))
21 | let result = program()
22 | expect(Array.from(result)).toHaveLength(11)
23 | })
24 |
--------------------------------------------------------------------------------
/src/compose.ts:
--------------------------------------------------------------------------------
1 | import { ensureFunction } from './utils/ensureFunction'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (...args: any) => any
5 |
6 | export function compose(fn: Fn | LazyIterable, ...fns: (Fn | LazyIterable)[]): Fn {
7 | return (fns as Fn[]).reduce((f, g) => {
8 | let g_ = ensureFunction(g)
9 | return (...args) => f(g_(...args))
10 | }, ensureFunction(fn))
11 | }
12 |
--------------------------------------------------------------------------------
/src/concat.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, concat, range, toArray, delay } from './'
2 |
3 | it('should concat arrays', () => {
4 | let program = pipe(concat([0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]), toArray())
5 |
6 | expect(program()).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
7 | })
8 |
9 | it('should concat iterators', () => {
10 | let program = pipe(concat(range(0, 3), range(4, 7), range(8, 10)), toArray())
11 |
12 | expect(program()).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
13 | })
14 |
15 | it('should concat iterators (async)', async () => {
16 | let program = pipe(concat(range(0, 3), delay(0)(range(4, 7)), range(8, 10)), toArray())
17 |
18 | expect(await program()).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
19 | })
20 |
21 | it('should concat iterators (Promise async)', async () => {
22 | let program = pipe(concat(range(0, 3), range(4, 7), Promise.resolve(range(8, 10))), toArray())
23 |
24 | expect(await program()).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
25 | })
26 |
--------------------------------------------------------------------------------
/src/concat.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | export function concat(...data: LazyIterable[]) {
5 | if (data.some(isAsyncIterable) || data.some((datum) => datum instanceof Promise)) {
6 | return {
7 | async *[Symbol.asyncIterator]() {
8 | for await (let datum of await Promise.all(data)) yield* datum
9 | },
10 | }
11 | }
12 |
13 | return {
14 | *[Symbol.iterator]() {
15 | for (let datum of data as Iterable[]) yield* datum
16 | },
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/delay.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, delay, map, every, tap } from './'
2 |
3 | it('should delay each value by 50ms', async () => {
4 | let DELAY = 50
5 | let counter = jest.fn()
6 |
7 | let first: number | null = null
8 |
9 | let program = pipe(
10 | // Create a range of 6 values.
11 | range(0, 5),
12 |
13 | // Delay each value with 100ms.
14 | delay(DELAY),
15 |
16 | // Map each value to the current date.
17 | map(() => {
18 | if (first === null) {
19 | first = Date.now()
20 | }
21 |
22 | return Date.now() - first
23 | }),
24 |
25 | // Call the counter, just to be sure that we actually saw 6 values.
26 | tap(() => counter()),
27 |
28 | // Ensure that each item took at least DELAY ms to be processed, and can't exceed DELAY * 2.
29 | // This also ensures that they are truly handled sequentially.
30 | every((current: number, i) => current >= DELAY * i && current <= DELAY * (i + 1))
31 | )
32 |
33 | let result = await program()
34 |
35 | expect(result).toBe(true)
36 | expect(counter).toHaveBeenCalledTimes(6)
37 | })
38 |
--------------------------------------------------------------------------------
/src/delay.ts:
--------------------------------------------------------------------------------
1 | import { LazyIterable } from './shared-types'
2 |
3 | function sleep(ms: number) {
4 | return new Promise((resolve) => setTimeout(resolve, ms))
5 | }
6 |
7 | export function delay(ms: number) {
8 | return async function* delayFn(data: LazyIterable) {
9 | if (data == null) return
10 |
11 | let stream = data instanceof Promise ? await data : data
12 |
13 | for await (let datum of stream) {
14 | if (ms !== 0) await sleep(ms)
15 | yield datum
16 | }
17 | }
18 | }
19 |
20 | // Looks like a no-op, but will resolve the promises. It's just a bit nicer to see `pipe(map(() =>
21 | // fetch()), wait(), toArray())` than `pipe(map(() => fetch()), delay(0), toArray())`. Because the
22 | // `toArray()` will _not_ wait/resolve the promises, otherwise you can't map to an array of promises
23 | // anymore.
24 | export function wait() {
25 | return delay(0)
26 | }
27 |
--------------------------------------------------------------------------------
/src/every.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, every, delay } from './'
2 |
3 | it('should return true when every value matches the predicate', () => {
4 | let program = pipe(
5 | range(0, 25),
6 | every((x) => typeof x === 'number')
7 | )
8 |
9 | expect(program()).toEqual(true)
10 | })
11 |
12 | it('should return false when no stream is passing through it', () => {
13 | let program = pipe(every((x) => typeof x === 'number'))
14 |
15 | expect(program()).toEqual(false)
16 | })
17 |
18 | it("should return false when one of the values doesn't meet the predicate", () => {
19 | let program = pipe(
20 | range(0, 100),
21 | every((x: number) => x < 100) // 100 is not less than 100
22 | )
23 |
24 | expect(program()).toEqual(false)
25 | })
26 |
27 | it('should return true when every value matches the predicate (async)', async () => {
28 | let program = pipe(
29 | delay(0),
30 | every((x) => typeof x === 'number')
31 | )
32 |
33 | expect(await program(range(0, 25))).toEqual(true)
34 | expect(await program(range(0, 25))).toEqual(true)
35 | })
36 |
37 | it("should return false when one of the values doesn't meet the predicate (async)", async () => {
38 | let program = pipe(
39 | delay(0),
40 | every((x: number) => x < 100) // 100 is not less than 100
41 | )
42 |
43 | expect(await program(range(0, 100))).toEqual(false)
44 | expect(await program(range(0, 100))).toEqual(false)
45 | })
46 |
47 | it('should return true when every value matches the predicate (Promise async)', async () => {
48 | let program = pipe(every((x) => typeof x === 'number'))
49 |
50 | expect(await program(Promise.resolve(range(0, 25)))).toEqual(true)
51 | expect(await program(Promise.resolve(range(0, 25)))).toEqual(true)
52 | })
53 |
54 | it("should return false when one of the values doesn't meet the predicate (Promise async)", async () => {
55 | let program = pipe(
56 | every((x: number) => x < 100) // 100 is not less than 100
57 | )
58 |
59 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(false)
60 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(false)
61 | })
62 |
63 | it('should take the index as second argument', async () => {
64 | let program = pipe(
65 | every((_x: number, i) => i < 100) // 100 is not less than 100
66 | )
67 |
68 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(false)
69 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(false)
70 | })
71 |
--------------------------------------------------------------------------------
/src/every.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (input: T, index: number) => boolean
5 |
6 | export function every(predicate: Fn) {
7 | return function everyFn(data: LazyIterable) {
8 | if (data == null) return false
9 |
10 | if (isAsyncIterable(data) || data instanceof Promise) {
11 | return (async () => {
12 | let stream = data instanceof Promise ? await data : data
13 |
14 | let i = 0
15 | for await (let datum of stream) {
16 | if (!predicate(datum, i++)) return false
17 | }
18 |
19 | return true
20 | })()
21 | }
22 |
23 | let i = 0
24 | for (let datum of data) {
25 | if (!predicate(datum, i++)) return false
26 | }
27 |
28 | return true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/filter.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, filter, toArray, delay } from './'
2 |
3 | it('should be possible to filter data', () => {
4 | let program = pipe(
5 | filter((x: number) => x % 2 === 0), // Is even
6 | toArray()
7 | )
8 |
9 | expect(program([1, 2, 3])).toEqual([2])
10 | expect(program([1, 2, 3])).toEqual([2])
11 | })
12 |
13 | it('should be possible to filter data (async)', async () => {
14 | let program = pipe(
15 | delay(0),
16 | filter((x: number) => x % 2 === 0), // Is even
17 | toArray()
18 | )
19 |
20 | expect(await program([1, 2, 3])).toEqual([2])
21 | expect(await program([1, 2, 3])).toEqual([2])
22 | })
23 |
24 | it('should be possible to filter data (Promise async)', async () => {
25 | let program = pipe(
26 | filter((x: number) => x % 2 === 0), // Is even
27 | toArray()
28 | )
29 |
30 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual([2])
31 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual([2])
32 | })
33 |
34 | it('should take the index as second argument', async () => {
35 | let program = pipe(
36 | filter((_x: number, i) => i % 2 === 0), // Is even
37 | toArray()
38 | )
39 |
40 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual([1, 3])
41 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual([1, 3])
42 | })
43 |
--------------------------------------------------------------------------------
/src/filter.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (datum: T, index: number) => boolean
5 |
6 | export function filter(fn: Fn) {
7 | return function filterFn(data: LazyIterable) {
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return {
10 | async *[Symbol.asyncIterator]() {
11 | let stream = data instanceof Promise ? await data : data
12 |
13 | let i = 0
14 | for await (let datum of stream) {
15 | // Ignore values that do not meet the criteria
16 | if (!fn(datum, i++)) continue
17 | yield datum
18 | }
19 | },
20 | }
21 | }
22 |
23 | return {
24 | *[Symbol.iterator]() {
25 | let i = 0
26 | for (let datum of data as Iterable) {
27 | // Ignore values that do not meet the criteria
28 | if (!fn(datum, i++)) continue
29 | yield datum
30 | }
31 | },
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/find.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, find, delay } from './'
2 |
3 | it('should find a value in the stream', () => {
4 | let program = pipe(find((x) => x === 2))
5 |
6 | expect(program(range(0, 100))).toEqual(2)
7 | expect(program(range(0, 100))).toEqual(2)
8 | })
9 |
10 | it('should return undefined when the value is not found', () => {
11 | let program = pipe(find((x) => x === 101))
12 |
13 | expect(program(range(0, 100))).toEqual(undefined)
14 | expect(program(range(0, 100))).toEqual(undefined)
15 | })
16 |
17 | it('should find a value in the stream (async)', async () => {
18 | let program = pipe(
19 | delay(0),
20 | find((x) => x === 2)
21 | )
22 |
23 | expect(await program(range(0, 100))).toEqual(2)
24 | expect(await program(range(0, 100))).toEqual(2)
25 | })
26 |
27 | it('should return undefined when the value is not found (async)', async () => {
28 | let program = pipe(
29 | delay(0),
30 | find((x) => x === 101)
31 | )
32 |
33 | expect(await program(range(0, 100))).toEqual(undefined)
34 | expect(await program(range(0, 100))).toEqual(undefined)
35 | })
36 |
37 | it('should find a value in the stream (Promise async)', async () => {
38 | let program = pipe(find((x) => x === 2))
39 |
40 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(2)
41 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(2)
42 | })
43 |
44 | it('should return undefined when the value is not found (Promise async)', async () => {
45 | let program = pipe(find((x) => x === 101))
46 |
47 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(undefined)
48 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(undefined)
49 | })
50 |
51 | it('should take the index as second argument', async () => {
52 | let program = pipe(find((_x: number, i) => i === 50))
53 |
54 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(50)
55 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(50)
56 | })
57 |
--------------------------------------------------------------------------------
/src/find.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (input: T, index: number) => boolean
5 |
6 | export function find(predicate: Fn) {
7 | return function findFn(data: LazyIterable): (T | undefined) | Promise {
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return (async () => {
10 | let stream = data instanceof Promise ? await data : data
11 |
12 | let i = 0
13 | for await (let datum of stream) {
14 | if (predicate(datum, i++)) return datum
15 | }
16 |
17 | return undefined
18 | })()
19 | }
20 |
21 | let i = 0
22 | for (let datum of data) {
23 | if (predicate(datum, i++)) return datum
24 | }
25 |
26 | return undefined
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/findIndex.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, findIndex, map, delay } from './'
2 |
3 | it('should find the index based on the predicate', () => {
4 | let program = pipe(
5 | map((x: number) => String.fromCharCode(x + 65)),
6 | findIndex((x) => x === 'T')
7 | )
8 |
9 | expect(program(range(0, 25))).toEqual('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf('T'))
10 | expect(program(range(0, 25))).toEqual('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf('T'))
11 | })
12 |
13 | it('should return -1 when the index is not found', () => {
14 | let program = pipe(findIndex((x) => x === 101))
15 |
16 | expect(program(range(0, 100))).toEqual(-1)
17 | expect(program(range(0, 100))).toEqual(-1)
18 | })
19 |
20 | it('should find the index based on the predicate (async)', async () => {
21 | let program = pipe(
22 | delay(0),
23 | map((x: number) => String.fromCharCode(x + 65)),
24 | findIndex((x) => x === 'T')
25 | )
26 |
27 | expect(await program(range(0, 25))).toEqual('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf('T'))
28 | expect(await program(range(0, 25))).toEqual('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf('T'))
29 | })
30 |
31 | it('should return -1 when the index is not found (async)', async () => {
32 | let program = pipe(
33 | delay(0),
34 | findIndex((x) => x === 101)
35 | )
36 |
37 | expect(await program(range(0, 100))).toEqual(-1)
38 | expect(await program(range(0, 100))).toEqual(-1)
39 | })
40 |
41 | it('should find the index based on the predicate (Promise async)', async () => {
42 | let program = pipe(
43 | map((x: number) => String.fromCharCode(x + 65)),
44 | findIndex((x) => x === 'T')
45 | )
46 |
47 | expect(await program(Promise.resolve(range(0, 25)))).toEqual(
48 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf('T')
49 | )
50 | expect(await program(Promise.resolve(range(0, 25)))).toEqual(
51 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf('T')
52 | )
53 | })
54 |
55 | it('should return -1 when the index is not found (Promise async)', async () => {
56 | let program = pipe(findIndex((x) => x === 101))
57 |
58 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(-1)
59 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(-1)
60 | })
61 |
62 | it('should take the index as second argument', async () => {
63 | let program = pipe(findIndex((_x: number, i) => i === 50))
64 |
65 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(50)
66 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(50)
67 | })
68 |
--------------------------------------------------------------------------------
/src/findIndex.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (input: T, index: number) => boolean
5 |
6 | export function findIndex(predicate: Fn) {
7 | return function findIndexFn(data: LazyIterable): number | Promise {
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return (async () => {
10 | let stream = data instanceof Promise ? await data : data
11 |
12 | let i = 0
13 | for await (let datum of stream) {
14 | if (predicate(datum, i)) return i
15 | i++
16 | }
17 |
18 | return -1
19 | })()
20 | }
21 |
22 | let i = 0
23 | for (let datum of data) {
24 | if (predicate(datum, i)) return i
25 | i++
26 | }
27 |
28 | return -1
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/flatMap.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, flatMap, toArray, delay } from './'
2 |
3 | it('should be possible to flatMap data from A to B', () => {
4 | let program = pipe(
5 | flatMap((x: number) => [x * 1, x * 2, x * 4]),
6 | toArray()
7 | )
8 |
9 | expect(program([1, 2, 3])).toEqual([1, 2, 4, 2, 4, 8, 3, 6, 12])
10 | expect(program([1, 2, 3])).toEqual([1, 2, 4, 2, 4, 8, 3, 6, 12])
11 | })
12 |
13 | it('should return undefined when no stream is passing through it', () => {
14 | let program = pipe(
15 | flatMap((x: number) => [x * 1, x * 2, x * 4]),
16 | toArray()
17 | )
18 |
19 | expect(program()).toEqual(undefined)
20 | expect(program()).toEqual(undefined)
21 | })
22 |
23 | it('should be possible to flatMap data from A to B (async)', async () => {
24 | let program = pipe(
25 | delay(0),
26 | flatMap((x: number) => [x * 1, x * 2, x * 4]),
27 | toArray()
28 | )
29 |
30 | expect(await program([1, 2, 3])).toEqual([1, 2, 4, 2, 4, 8, 3, 6, 12])
31 | expect(await program([1, 2, 3])).toEqual([1, 2, 4, 2, 4, 8, 3, 6, 12])
32 | })
33 |
34 | it('should be possible to flatMap data from A to B (Promise async)', async () => {
35 | let program = pipe(
36 | delay(0),
37 | flatMap((x: number) => [x * 1, x * 2, x * 4]),
38 | toArray()
39 | )
40 |
41 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual([1, 2, 4, 2, 4, 8, 3, 6, 12])
42 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual([1, 2, 4, 2, 4, 8, 3, 6, 12])
43 | })
44 |
45 | it('should take the index as second argument', () => {
46 | let program = pipe(
47 | flatMap((_x: number, i) => i),
48 | toArray()
49 | )
50 |
51 | expect(program([1, 2, 3])).toEqual([0, 1, 2])
52 | expect(program([1, 2, 3])).toEqual([0, 1, 2])
53 | })
54 |
--------------------------------------------------------------------------------
/src/flatMap.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable, isIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (datum: T, index: number) => R
5 |
6 | export function flatMap(fn: Fn) {
7 | return function flatMapFn(data: LazyIterable) {
8 | if (data == null) return
9 |
10 | // Handle the async version
11 | if (isAsyncIterable(data) || data instanceof Promise) {
12 | return {
13 | async *[Symbol.asyncIterator]() {
14 | let stream = data instanceof Promise ? await data : data
15 |
16 | let i = 0
17 | for await (let datum of stream) {
18 | let result = fn(datum, i++)
19 | if (isAsyncIterable(result) || result instanceof Promise) {
20 | let stream = (result instanceof Promise ? await result : result) as AsyncIterable
21 | yield* stream
22 | } else if (isIterable(result)) {
23 | yield* result
24 | } else {
25 | yield result
26 | }
27 | }
28 | },
29 | }
30 | }
31 |
32 | // Handle the sync version
33 | return {
34 | *[Symbol.iterator]() {
35 | let i = 0
36 | for (let datum of data) {
37 | let result = fn(datum, i++)
38 | if (isIterable(result)) {
39 | yield* result
40 | } else {
41 | yield result
42 | }
43 | }
44 | },
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/flatten.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, flatten, range, toArray, delay } from './'
2 |
3 | it('should be possible to flatten data (shallow)', () => {
4 | let program = pipe(flatten({ shallow: true }), toArray())
5 |
6 | expect(program([1, [2], range(3, 10)])).toEqual(Array.from(range(1, 10)))
7 | expect(program([1, [2], range(3, 10)])).toEqual(Array.from(range(1, 10)))
8 | })
9 |
10 | it('should be possible to deep flatten data', () => {
11 | let program = pipe(flatten(), toArray())
12 |
13 | expect(program([1, [2, [3, [[[4]], [5]]]]])).toEqual(Array.from(range(1, 5)))
14 | expect(program([1, [2, [3, [[[4]], [5]]]]])).toEqual(Array.from(range(1, 5)))
15 | })
16 |
17 | it('should be possible to deep flatten data (async)', async () => {
18 | let program = pipe(delay(0), flatten(), toArray())
19 |
20 | expect(await program([1, [2, [3, [[[4]], [5]]]]])).toEqual(Array.from(range(1, 5)))
21 | expect(await program([1, [2, [3, [[[4]], [5]]]]])).toEqual(Array.from(range(1, 5)))
22 | })
23 |
24 | it('should be possible to deep flatten data (Promise async)', async () => {
25 | let program = pipe(flatten(), toArray())
26 |
27 | expect(await program(Promise.resolve([1, [2, [3, [[[4]], [5]]]]]))).toEqual(
28 | Array.from(range(1, 5))
29 | )
30 | expect(await program(Promise.resolve([1, [2, [3, [[[4]], [5]]]]]))).toEqual(
31 | Array.from(range(1, 5))
32 | )
33 | })
34 |
--------------------------------------------------------------------------------
/src/flatten.ts:
--------------------------------------------------------------------------------
1 | import { LazyIterable } from './shared-types'
2 | import { isAsyncIterable, isIterable } from './utils/iterator'
3 |
4 | type Options = {
5 | shallow?: boolean
6 | }
7 |
8 | export function flatten(options: Options = {}) {
9 | let { shallow = false } = options
10 |
11 | return function flattenFn(data: LazyIterable): any {
12 | if (data == null) return
13 |
14 | if (isAsyncIterable(data) || data instanceof Promise) {
15 | return {
16 | async *[Symbol.asyncIterator]() {
17 | let stream = data instanceof Promise ? await data : data
18 |
19 | for await (let datum of stream) {
20 | if (shallow) {
21 | // If the value itself is an iterator, we have to flatten that as
22 | // well.
23 | if (isAsyncIterable(datum)) {
24 | yield* datum
25 | } else {
26 | yield datum
27 | }
28 | } else {
29 | // Let's go recursive
30 | yield* await flattenFn(datum as any)
31 | }
32 | }
33 | },
34 | }
35 | }
36 |
37 | return {
38 | *[Symbol.iterator]() {
39 | if (!Array.isArray(data)) {
40 | yield data
41 | } else {
42 | for (let datum of data) {
43 | if (shallow) {
44 | // If the value itself is an iterator, we have to flatten that as
45 | // well.
46 | if (isIterable(datum)) {
47 | yield* datum
48 | } else {
49 | yield datum
50 | }
51 | } else {
52 | // Let's go recursive
53 | yield* flattenFn(datum)
54 | }
55 | }
56 | }
57 | },
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/generate.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, generate, slice, range, take, toArray } from './'
2 |
3 | it('should be possible to create a stream using the generate function', () => {
4 | let program = pipe(slice(0, 10), toArray())
5 |
6 | let i = 0
7 | expect(program(generate(() => i++))).toEqual(Array.from(range(0, 10)))
8 | })
9 |
10 | it('should be possible to create a fibonacci iterator', () => {
11 | function createFibonacciGenerator() {
12 | let x = 1
13 | let y = 1
14 |
15 | return () => {
16 | let previous = x
17 | ;[x, y] = [y, x + y]
18 | return previous
19 | }
20 | }
21 |
22 | function fibonacci(x: number) {
23 | return pipe(generate(createFibonacciGenerator()), take(x), toArray())()
24 | }
25 |
26 | expect(fibonacci(10)).toEqual([1, 1, 2, 3, 5, 8, 13, 21, 34, 55])
27 | })
28 |
--------------------------------------------------------------------------------
/src/generate.ts:
--------------------------------------------------------------------------------
1 | export function* generate(generator: () => T) {
2 | while (true) yield generator()
3 | }
4 |
--------------------------------------------------------------------------------
/src/groupBy.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, groupBy, delay } from './'
2 |
3 | function snap(multitude: number, value: number) {
4 | return Math.ceil(value / multitude) * multitude
5 | }
6 |
7 | it('should be possible to group an iterator by something', () => {
8 | let program = pipe(
9 | range(0, 10),
10 | groupBy((x: number) => snap(5, x))
11 | )
12 |
13 | expect(program()).toEqual({
14 | 0: [0],
15 | 5: [1, 2, 3, 4, 5],
16 | 10: [6, 7, 8, 9, 10],
17 | })
18 | })
19 |
20 | it('should be possible to group an iterator by something (async)', async () => {
21 | let program = pipe(
22 | range(0, 10),
23 | delay(0),
24 | groupBy((x: number) => snap(5, x))
25 | )
26 |
27 | expect(await program()).toEqual({
28 | 0: [0],
29 | 5: [1, 2, 3, 4, 5],
30 | 10: [6, 7, 8, 9, 10],
31 | })
32 | })
33 |
34 | it('should be possible to group an iterator by something (Promise async)', async () => {
35 | let program = pipe(
36 | Promise.resolve(range(0, 10)),
37 | groupBy((x: number) => snap(5, x))
38 | )
39 |
40 | expect(await program()).toEqual({
41 | 0: [0],
42 | 5: [1, 2, 3, 4, 5],
43 | 10: [6, 7, 8, 9, 10],
44 | })
45 | })
46 |
47 | it('should take the index as second argument', async () => {
48 | let program = pipe(
49 | Promise.resolve(range(0, 10)),
50 | groupBy((_x: number, i) => snap(5, i))
51 | )
52 |
53 | expect(await program()).toEqual({
54 | 0: [0],
55 | 5: [1, 2, 3, 4, 5],
56 | 10: [6, 7, 8, 9, 10],
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/src/groupBy.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type KeyFn = (input: T, index: number) => string | number
5 |
6 | export function groupBy(keySelector: KeyFn) {
7 | return function groupByFn(data: LazyIterable) {
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return (async () => {
10 | let stream = data instanceof Promise ? await data : data
11 | let map: Record, T[]> = {}
12 | let i = 0
13 | for await (let datum of stream) {
14 | let key = keySelector(datum, i++)
15 | if (map[key] === undefined) map[key] = []
16 | map[key].push(datum)
17 | }
18 |
19 | return map
20 | })()
21 | }
22 |
23 | let map: Record, T[]> = {}
24 | let i = 0
25 | for (let datum of data) {
26 | let key = keySelector(datum, i++)
27 | if (map[key] === undefined) map[key] = []
28 | map[key].push(datum)
29 | }
30 |
31 | return map
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/head.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, head, first, delay } from './'
2 |
3 | it('should return the first element of the iterator', () => {
4 | let program = pipe(range(20, 25), head())
5 |
6 | expect(program()).toEqual(20)
7 | })
8 |
9 | it('should return the first element of the iterator (using an alias)', () => {
10 | let program = pipe(range(20, 25), first())
11 |
12 | expect(program()).toEqual(20)
13 | })
14 |
15 | it('should return undefined when there is no data', () => {
16 | let program = pipe(first())
17 |
18 | expect(program()).toEqual(undefined)
19 | })
20 |
21 | it('should return undefined when there is an empty array', () => {
22 | let program = pipe(first())
23 |
24 | expect(program([])).toEqual(undefined)
25 | })
26 |
27 | it('should return the first element of the iterator (async)', async () => {
28 | let program = pipe(range(20, 25), delay(0), head())
29 |
30 | expect(await program()).toEqual(20)
31 | })
32 |
33 | it('should return the first element of the iterator (using an alias) (async)', async () => {
34 | let program = pipe(range(20, 25), delay(0), first())
35 |
36 | expect(await program()).toEqual(20)
37 | })
38 |
39 | it('should return undefined when there is no data (async)', async () => {
40 | let program = pipe(delay(0), first())
41 |
42 | expect(await program()).toEqual(undefined)
43 | })
44 |
45 | it('should return undefined when there is an empty array (async)', async () => {
46 | let program = pipe(delay(0), first())
47 |
48 | expect(await program([])).toEqual(undefined)
49 | })
50 |
--------------------------------------------------------------------------------
/src/head.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | export function head() {
5 | return function headFn(data: LazyIterable): T | undefined | Promise {
6 | if (data == null) return
7 |
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return (async () => {
10 | let stream = data instanceof Promise ? await data : data
11 |
12 | for await (let datum of stream) return datum
13 | return undefined
14 | })()
15 | }
16 |
17 | for (let datum of data) return datum
18 | return undefined
19 | }
20 | }
21 |
22 | // Alias
23 | export let first = head
24 |
--------------------------------------------------------------------------------
/src/includes.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, includes, map, delay } from './'
2 |
3 | it('should return true when the search element is found', () => {
4 | let program = pipe(
5 | map((x: number) => String.fromCharCode(x + 65)),
6 | includes('Z')
7 | )
8 |
9 | expect(program(range(0, 25))).toBe(true)
10 | expect(program(range(0, 25))).toBe(true)
11 | })
12 |
13 | it('should return true when the search element is found and the search element is an edge case (NaN)', () => {
14 | let program = pipe(includes(NaN))
15 |
16 | expect(program([1, 2, 3, NaN])).toBe(true)
17 | expect(program([1, 2, 3, NaN])).toBe(true)
18 | })
19 |
20 | it('should return false when the search element is not found', () => {
21 | let program = pipe(
22 | map((x: number) => String.fromCharCode(x + 65)),
23 | includes('Z')
24 | )
25 |
26 | expect(program(range(0, 24))).toBe(false)
27 | expect(program(range(0, 24))).toBe(false)
28 | })
29 |
30 | it('should return true when the search element is found, starting at a given index', () => {
31 | let program = pipe(
32 | map((x: number) => String.fromCharCode(x + 65)),
33 | includes('Z', 1)
34 | )
35 |
36 | expect(program(range(0, 25))).toBe(true)
37 | expect(program(range(0, 25))).toBe(true)
38 | })
39 |
40 | it('should return false when the search element is not found, starting at a given index', () => {
41 | let program = pipe(
42 | map((x: number) => String.fromCharCode(x + 65)),
43 | includes('A', 1)
44 | )
45 |
46 | expect(program(range(0, 25))).toBe(false)
47 | expect(program(range(0, 25))).toBe(false)
48 | })
49 |
50 | it('should return true when the search element is found (async)', async () => {
51 | let program = pipe(
52 | delay(0),
53 | map((x: number) => String.fromCharCode(x + 65)),
54 | includes('Z')
55 | )
56 |
57 | expect(await program(range(0, 25))).toBe(true)
58 | expect(await program(range(0, 25))).toBe(true)
59 | })
60 |
61 | it('should return false when the search element is not found (async)', async () => {
62 | let program = pipe(
63 | delay(0),
64 | map((x: number) => String.fromCharCode(x + 65)),
65 | includes('Z')
66 | )
67 |
68 | expect(await program(range(0, 24))).toBe(false)
69 | expect(await program(range(0, 24))).toBe(false)
70 | })
71 |
72 | it('should return true when the search element is found, starting at a given index (async)', async () => {
73 | let program = pipe(
74 | delay(0),
75 | map((x: number) => String.fromCharCode(x + 65)),
76 | includes('Z', 1)
77 | )
78 |
79 | expect(await program(range(0, 25))).toBe(true)
80 | expect(await program(range(0, 25))).toBe(true)
81 | })
82 |
83 | it('should return false when the search element is not found, starting at a given index (async)', async () => {
84 | let program = pipe(
85 | delay(0),
86 | map((x: number) => String.fromCharCode(x + 65)),
87 | includes('A', 1)
88 | )
89 |
90 | expect(await program(range(0, 25))).toBe(false)
91 | expect(await program(range(0, 25))).toBe(false)
92 | })
93 |
94 | it('should return true when the search element is found (Promise async)', async () => {
95 | let program = pipe(
96 | map((x: number) => String.fromCharCode(x + 65)),
97 | includes('Z')
98 | )
99 |
100 | expect(await program(Promise.resolve(range(0, 25)))).toBe(true)
101 | expect(await program(Promise.resolve(range(0, 25)))).toBe(true)
102 | })
103 |
104 | it('should return false when the search element is not found (Promise async)', async () => {
105 | let program = pipe(includes('Z'))
106 |
107 | expect(await program(Promise.resolve(range(0, 24)))).toBe(false)
108 | expect(await program(Promise.resolve(range(0, 24)))).toBe(false)
109 | })
110 |
111 | it('should return true when the search element is found, starting at a given index (Promise async)', async () => {
112 | let program = pipe(
113 | map((x: number) => String.fromCharCode(x + 65)),
114 | includes('Z', 1)
115 | )
116 |
117 | expect(await program(Promise.resolve(range(0, 25)))).toBe(true)
118 | expect(await program(Promise.resolve(range(0, 25)))).toBe(true)
119 | })
120 |
121 | it('should return false when the search element is not found, starting at a given index (Promise async)', async () => {
122 | let program = pipe(
123 | map((x: number) => String.fromCharCode(x + 65)),
124 | includes('A', 1)
125 | )
126 |
127 | expect(await program(Promise.resolve(range(0, 25)))).toBe(false)
128 | expect(await program(Promise.resolve(range(0, 25)))).toBe(false)
129 | })
130 |
--------------------------------------------------------------------------------
/src/includes.ts:
--------------------------------------------------------------------------------
1 | import { findIndex } from './'
2 |
3 | import { isAsyncIterable } from './utils/iterator'
4 | import { LazyIterable } from './shared-types'
5 |
6 | export function includes(searchElement: T, fromIndex = 0) {
7 | function predicate(element: T, index: number) {
8 | if (index < fromIndex) return false
9 | return Object.is(element, searchElement)
10 | }
11 |
12 | return function includesFn(data: LazyIterable) {
13 | if (isAsyncIterable(data) || data instanceof Promise) {
14 | return (async () => {
15 | let index = await findIndex(predicate)(data)
16 | return index !== -1
17 | })()
18 | }
19 |
20 | let index = findIndex(predicate)(data)
21 | return index !== -1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import * as lazy from '.'
2 |
3 | it('should export all the things', () => {
4 | expect(lazy).toMatchSnapshot()
5 | })
6 |
7 | it('should be possible to create a chain of actions and combine them in a nice stream', () => {
8 | let program = lazy.pipe(
9 | // Triple
10 | lazy.map((x: number) => x * 3),
11 |
12 | // Even
13 | lazy.filter((x: number) => x % 2 === 0),
14 |
15 | // Take only the ones below 100 000
16 | lazy.takeWhile((x: number) => x < 100_000),
17 |
18 | // Take the first 10
19 | lazy.take(10),
20 |
21 | // To array
22 | lazy.toArray()
23 | )
24 |
25 | expect(program(lazy.range(1_000, 1_000_000))).toEqual([
26 | 3000, 3006, 3012, 3018, 3024, 3030, 3036, 3042, 3048, 3054,
27 | ])
28 | })
29 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Main program composers
2 | export * from './compose'
3 | export * from './pipe'
4 |
5 | // Known array methods
6 | export * from './at'
7 | export * from './concat'
8 | export * from './every'
9 | export * from './filter'
10 | export * from './find'
11 | export * from './findIndex'
12 | export * from './flatMap'
13 | export * from './join'
14 | export * from './includes'
15 | export * from './map'
16 | export * from './reduce'
17 | export * from './reverse'
18 | export * from './slice'
19 | export * from './some'
20 | export * from './sort'
21 |
22 | // Useful utilities
23 | export * from './average'
24 | export * from './batch'
25 | export * from './chunk'
26 | export * from './compact'
27 | export * from './delay'
28 | export * from './flatten'
29 | export * from './generate'
30 | export * from './groupBy'
31 | export * from './head'
32 | export * from './max'
33 | export * from './min'
34 | export * from './partition'
35 | export * from './product'
36 | export * from './range'
37 | export * from './skip'
38 | export * from './sum'
39 | export * from './take'
40 | export * from './takeWhile'
41 | export * from './tap'
42 | export * from './toArray'
43 | export * from './toLength'
44 | export * from './toSet'
45 | export * from './unique'
46 | export * from './where'
47 | export * from './windows'
48 | export * from './zip'
49 |
--------------------------------------------------------------------------------
/src/join.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, join, delay, concat } from './'
2 |
3 | it('should be possible to join an array', () => {
4 | let program = pipe(join())
5 |
6 | expect(program(['l', 'a', 'z', 'y'])).toEqual('l,a,z,y')
7 | expect(program(['l', 'a', 'z', 'y'])).toEqual('l,a,z,y')
8 | })
9 |
10 | it('should be possible to join an iterator', () => {
11 | let program = pipe(join())
12 |
13 | expect(program(concat(['l', 'a', 'z', 'y']))).toEqual('l,a,z,y')
14 | expect(program(concat(['l', 'a', 'z', 'y']))).toEqual('l,a,z,y')
15 | })
16 |
17 | it('should be possible to join an array (async)', async () => {
18 | let program = pipe(delay(0), join())
19 |
20 | expect(await program(['l', 'a', 'z', 'y'])).toEqual('l,a,z,y')
21 | expect(await program(['l', 'a', 'z', 'y'])).toEqual('l,a,z,y')
22 | })
23 |
24 | it('should be possible to join an iterator (async)', async () => {
25 | let program = pipe(delay(0), join())
26 |
27 | expect(await program(concat(['l', 'a', 'z', 'y']))).toEqual('l,a,z,y')
28 | expect(await program(concat(['l', 'a', 'z', 'y']))).toEqual('l,a,z,y')
29 | })
30 |
31 | it('should be possible to join an array (Promise async)', async () => {
32 | let program = pipe(join())
33 |
34 | expect(await program(Promise.resolve(['l', 'a', 'z', 'y']))).toEqual('l,a,z,y')
35 | expect(await program(Promise.resolve(['l', 'a', 'z', 'y']))).toEqual('l,a,z,y')
36 | })
37 |
38 | it('should be possible to join an iterator (Promise async)', async () => {
39 | let program = pipe(join())
40 |
41 | expect(await program(Promise.resolve(concat(['l', 'a', 'z', 'y'])))).toEqual('l,a,z,y')
42 | expect(await program(Promise.resolve(concat(['l', 'a', 'z', 'y'])))).toEqual('l,a,z,y')
43 | })
44 |
45 | it('should be possible to join an array with a separator', () => {
46 | let program = pipe(join(''))
47 |
48 | expect(program(['l', 'a', 'z', 'y'])).toEqual('lazy')
49 | expect(program(['l', 'a', 'z', 'y'])).toEqual('lazy')
50 | })
51 |
--------------------------------------------------------------------------------
/src/join.ts:
--------------------------------------------------------------------------------
1 | import { reduce } from './'
2 |
3 | export function join(separator: string = ',') {
4 | return reduce(
5 | (joined, current, index) => (index === 0 ? current : joined + separator + current),
6 | ''
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/src/map.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, map, toArray, delay } from './'
2 |
3 | it('should be possible to map data from A to B', () => {
4 | let program = pipe(
5 | map((x: number) => x * 2), // Double
6 | toArray()
7 | )
8 |
9 | expect(program([1, 2, 3])).toEqual([2, 4, 6])
10 | expect(program([1, 2, 3])).toEqual([2, 4, 6])
11 | })
12 |
13 | it('should return undefined when no stream is passing through it', () => {
14 | let program = pipe(
15 | map((x: number) => x * 2), // Double
16 | toArray()
17 | )
18 |
19 | expect(program()).toEqual(undefined)
20 | expect(program()).toEqual(undefined)
21 | })
22 |
23 | it('should be possible to map data from A to B (async)', async () => {
24 | let program = pipe(
25 | delay(0),
26 | map((x: number) => x * 2), // Double
27 | toArray()
28 | )
29 |
30 | expect(await program([1, 2, 3])).toEqual([2, 4, 6])
31 | expect(await program([1, 2, 3])).toEqual([2, 4, 6])
32 | })
33 |
34 | it('should be possible to map data from A to B (Promise async)', async () => {
35 | let program = pipe(
36 | delay(0),
37 | map((x: number) => x * 2), // Double
38 | toArray()
39 | )
40 |
41 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual([2, 4, 6])
42 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual([2, 4, 6])
43 | })
44 |
45 | it('should take the index as second argument', () => {
46 | let program = pipe(
47 | map((_x: number, i) => i),
48 | toArray()
49 | )
50 |
51 | expect(program([1, 2, 3])).toEqual([0, 1, 2])
52 | expect(program([1, 2, 3])).toEqual([0, 1, 2])
53 | })
54 |
--------------------------------------------------------------------------------
/src/map.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (datum: T, index: number) => R
5 |
6 | export function map(fn: Fn) {
7 | return function mapFn(data: LazyIterable) {
8 | if (data == null) return
9 |
10 | // Handle the async version
11 | if (isAsyncIterable(data) || data instanceof Promise) {
12 | return {
13 | async *[Symbol.asyncIterator]() {
14 | let stream = data instanceof Promise ? await data : data
15 |
16 | let i = 0
17 | for await (let datum of stream) yield fn(datum, i++)
18 | },
19 | }
20 | }
21 |
22 | // Handle the sync version
23 | return {
24 | *[Symbol.iterator]() {
25 | let i = 0
26 | for (let datum of data) yield fn(datum, i++)
27 | },
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/max.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, max, delay } from './'
2 |
3 | it('should find the max value of the iterator', () => {
4 | let program = pipe(range(5, 10), max())
5 |
6 | expect(program()).toEqual(10)
7 | })
8 |
9 | it('should find the max value of the iterator (async)', async () => {
10 | let program = pipe(range(5, 10), delay(0), max())
11 |
12 | expect(await program()).toEqual(10)
13 | })
14 |
--------------------------------------------------------------------------------
/src/max.ts:
--------------------------------------------------------------------------------
1 | import { reduce } from './'
2 |
3 | export function max() {
4 | return reduce((lhs, rhs) => Math.max(lhs, rhs), -Infinity)
5 | }
6 |
--------------------------------------------------------------------------------
/src/min.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, min, delay } from './'
2 |
3 | it('should find the min value of the iterator', () => {
4 | let program = pipe(range(5, 10), min())
5 |
6 | expect(program()).toEqual(5)
7 | })
8 |
9 | it('should find the min value of the iterator (async)', async () => {
10 | let program = pipe(range(5, 10), delay(0), min())
11 |
12 | expect(await program()).toEqual(5)
13 | })
14 |
--------------------------------------------------------------------------------
/src/min.ts:
--------------------------------------------------------------------------------
1 | import { reduce } from './'
2 |
3 | export function min() {
4 | return reduce((lhs, rhs) => Math.min(lhs, rhs), Infinity)
5 | }
6 |
--------------------------------------------------------------------------------
/src/partition.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, toArray, partition, delay } from './'
2 |
3 | it('should partition the data into 2 streams based on the predicate', () => {
4 | let program = pipe(
5 | range(1, 4),
6 | partition((x: number) => x % 2 !== 0),
7 | toArray()
8 | )
9 |
10 | expect(program()).toEqual([
11 | [1, 3],
12 | [2, 4],
13 | ])
14 | })
15 |
16 | it('should return undefined when no stream is passing through it', () => {
17 | let program = pipe(
18 | partition((x: number) => x % 2 !== 0),
19 | toArray()
20 | )
21 |
22 | expect(program()).toEqual(undefined)
23 | })
24 |
25 | it('should partition the data into 2 streams based on the predicate (async)', async () => {
26 | let program = pipe(
27 | range(1, 4),
28 | delay(0),
29 | partition((x: number) => x % 2 !== 0),
30 | toArray()
31 | )
32 |
33 | expect(await program()).toEqual([
34 | [1, 3],
35 | [2, 4],
36 | ])
37 | })
38 |
39 | it('should partition the data into 2 streams based on the predicate (Promise async)', async () => {
40 | let program = pipe(
41 | Promise.resolve(range(1, 4)),
42 | partition((x: number) => x % 2 !== 0),
43 | toArray()
44 | )
45 |
46 | expect(await program()).toEqual([
47 | [1, 3],
48 | [2, 4],
49 | ])
50 | })
51 |
52 | it('should take the index as second argument', async () => {
53 | let program = pipe(
54 | Promise.resolve(range(1, 4)),
55 | partition((_x: number, i) => i % 2 !== 0),
56 | toArray()
57 | )
58 |
59 | expect(await program()).toEqual([
60 | [2, 4],
61 | [1, 3],
62 | ])
63 | })
64 |
--------------------------------------------------------------------------------
/src/partition.ts:
--------------------------------------------------------------------------------
1 | import { MaybePromise, LazyIterable } from './shared-types'
2 | import { isAsyncIterable } from './utils/iterator'
3 |
4 | type Fn = (input: T, index: number) => boolean
5 |
6 | export function partition(predicate: Fn) {
7 | return function partitionFn(data: LazyIterable): MaybePromise<[T[], T[]]> | undefined {
8 | if (data == null) return
9 |
10 | if (isAsyncIterable(data) || data instanceof Promise) {
11 | return (async () => {
12 | let stream = data instanceof Promise ? await data : data
13 |
14 | let a: T[] = []
15 | let b: T[] = []
16 |
17 | let i = 0
18 | for await (let datum of stream) {
19 | if (predicate(datum, i++)) {
20 | a.push(datum)
21 | } else {
22 | b.push(datum)
23 | }
24 | }
25 |
26 | return [a, b] as [T[], T[]]
27 | })()
28 | }
29 |
30 | let a = []
31 | let b = []
32 |
33 | let i = 0
34 | for (let datum of data) {
35 | if (predicate(datum, i++)) {
36 | a.push(datum)
37 | } else {
38 | b.push(datum)
39 | }
40 | }
41 |
42 | return [a, b]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/pipe.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, generate, take, range, toArray } from './'
2 |
3 | it('should be possible to pipe multiple functions', () => {
4 | let program = pipe(
5 | (a: number, b: number) => `fn1(${a}, ${b})`,
6 | (a: string) => `fn2(${a})`,
7 | (a: string) => `fn3(${a})`
8 | )
9 |
10 | expect(program(2, 3)).toEqual('fn3(fn2(fn1(2, 3)))')
11 | })
12 |
13 | it('should be possible to pass a generator as first argument', () => {
14 | let program = pipe(generate(Math.random), take(10), toArray())
15 | let result = program()
16 | expect(result).toHaveLength(10)
17 | })
18 |
19 | it('should be possible to pass a generator as only argument', () => {
20 | let program = pipe(range(0, 10))
21 | let result = program()
22 | expect(Array.from(result)).toHaveLength(11)
23 | })
24 |
--------------------------------------------------------------------------------
/src/pipe.ts:
--------------------------------------------------------------------------------
1 | import { ensureFunction } from './utils/ensureFunction'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (...args: any) => any
5 |
6 | export function pipe(...fns: (Fn | LazyIterable)[]): Fn {
7 | return (fns as Fn[]).reduceRight((f, g) => {
8 | let g_ = ensureFunction(g)
9 | return (...args) => f(g_(...args))
10 | }, ensureFunction(fns.pop()))
11 | }
12 |
--------------------------------------------------------------------------------
/src/product.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, product, range, delay } from './'
2 |
3 | it('should be possible to multiply an array', () => {
4 | let program = pipe(product())
5 |
6 | expect(program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(3628800)
7 | expect(program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(3628800)
8 | })
9 |
10 | it('should be possible to multiply an iterator', () => {
11 | let program = pipe(product())
12 |
13 | expect(program(range(1, 10))).toEqual(3628800)
14 | expect(program(range(1, 10))).toEqual(3628800)
15 | })
16 |
17 | it('should be possible to multiply an array (async)', async () => {
18 | let program = pipe(delay(0), product())
19 |
20 | expect(await program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(3628800)
21 | expect(await program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(3628800)
22 | })
23 |
24 | it('should be possible to multiply an iterator (async)', async () => {
25 | let program = pipe(delay(0), product())
26 |
27 | expect(await program(range(1, 10))).toEqual(3628800)
28 | expect(await program(range(1, 10))).toEqual(3628800)
29 | })
30 |
31 | it('should be possible to multiply an array (Promise async)', async () => {
32 | let program = pipe(product())
33 |
34 | expect(await program(Promise.resolve([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))).toEqual(3628800)
35 | expect(await program(Promise.resolve([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))).toEqual(3628800)
36 | })
37 |
38 | it('should be possible to multiply an iterator (Promise async)', async () => {
39 | let program = pipe(product())
40 |
41 | expect(await program(Promise.resolve(range(1, 10)))).toEqual(3628800)
42 | expect(await program(Promise.resolve(range(1, 10)))).toEqual(3628800)
43 | })
44 |
--------------------------------------------------------------------------------
/src/product.ts:
--------------------------------------------------------------------------------
1 | import { reduce } from './'
2 |
3 | export function product() {
4 | return reduce((total, current) => total * current, 1)
5 | }
6 |
--------------------------------------------------------------------------------
/src/range.test.ts:
--------------------------------------------------------------------------------
1 | import * as fc from 'fast-check'
2 | import { range, take, pipe, chunk } from './'
3 |
4 | it('should create a range', () => {
5 | let program = pipe(take(10), chunk(2))
6 |
7 | fc.assert(
8 | fc.property(
9 | fc.integer(), // Lowerbound
10 | fc.integer(), // Upperbound
11 | fc.oneof(fc.integer(), fc.constant(undefined)), // Step
12 | (lowerbound, upperbound, step) => {
13 | let data = program(range(lowerbound, upperbound, step))
14 |
15 | // Verify that it generates an ordered list, ascending or descending
16 | for (let chunk of data) {
17 | if (chunk.length === 2) {
18 | let [a, b] = chunk
19 | if (step === undefined) {
20 | // We should default to a step of 1
21 | expect(Math.abs(a - b)).toBe(1)
22 | } else {
23 | // We should verify that 2 values next to eachother are a step away
24 | expect(Math.abs(a - b)).toBe(Math.abs(step))
25 | }
26 | }
27 | }
28 | }
29 | )
30 | )
31 | })
32 |
--------------------------------------------------------------------------------
/src/range.ts:
--------------------------------------------------------------------------------
1 | import { clamp } from './utils/clamp'
2 |
3 | export function* range(lowerbound: number, upperbound: number, step: number = 1) {
4 | let fixed_step = clamp(
5 | lowerbound < upperbound ? (step > 0 ? step : -step) : step > 0 ? -step : step
6 | )
7 |
8 | let lowerbound_ = clamp(lowerbound)
9 | let upperbound_ = clamp(upperbound)
10 |
11 | for (
12 | let i = lowerbound_;
13 | lowerbound_ < upperbound_ ? i <= upperbound_ : i >= upperbound_;
14 | i += fixed_step
15 | ) {
16 | yield i
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/reduce.test.ts:
--------------------------------------------------------------------------------
1 | import { reduce, pipe, delay } from './'
2 |
3 | it('should be possible to sum numbers (via reduce)', () => {
4 | let program = reduce((total, current) => total + current, 0)
5 | expect(program([1, 2, 3])).toEqual(6)
6 | expect(program([1, 2, 3])).toEqual(6)
7 | })
8 |
9 | it('should be possible to sum numbers (via reduce) (async)', async () => {
10 | let program = pipe(
11 | delay(0),
12 | reduce((total, current) => total + current, 0)
13 | )
14 | expect(await program([1, 2, 3])).toEqual(6)
15 | expect(await program([1, 2, 3])).toEqual(6)
16 | })
17 |
18 | it('should be possible to sum numbers (via reduce) (Promise async)', async () => {
19 | let program = pipe(reduce((total, current) => total + current, 0))
20 |
21 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual(6)
22 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual(6)
23 | })
24 |
25 | it('should take the index as second argument', async () => {
26 | let program = pipe(reduce((total, current, i) => total + current + i, 0))
27 |
28 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual(9)
29 | expect(await program(Promise.resolve([1, 2, 3]))).toEqual(9)
30 | })
31 |
--------------------------------------------------------------------------------
/src/reduce.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (acc: R, datum: T, index: number) => R
5 |
6 | export function reduce(fn: Fn, initial: R) {
7 | return function reduceFn(data: LazyIterable) {
8 | if (data == null) return
9 |
10 | let acc = initial
11 |
12 | if (isAsyncIterable(data) || data instanceof Promise) {
13 | return (async () => {
14 | let stream = data instanceof Promise ? await data : data
15 | let i = 0
16 | for await (let datum of stream) acc = fn(acc, datum, i++)
17 | return acc
18 | })()
19 | }
20 |
21 | let i = 0
22 | for (let datum of data) acc = fn(acc, datum, i++)
23 | return acc
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/replace.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe } from './pipe'
2 | import { replace } from './replace'
3 | import { range } from './range'
4 | import { toArray } from './toArray'
5 | import { delay } from './delay'
6 |
7 | it('should replace the item at the given index with the new value', () => {
8 | let program = pipe(replace(2, 42), toArray())
9 |
10 | expect(program(range(0, 2))).toEqual([0, 1, 42])
11 | expect(program(range(0, 2))).toEqual([0, 1, 42])
12 | })
13 |
14 | it('should replace the item at the given index with the new value (async)', async () => {
15 | let program = pipe(delay(0), replace(2, 42), toArray())
16 |
17 | expect(await program(range(0, 2))).toEqual([0, 1, 42])
18 | expect(await program(range(0, 2))).toEqual([0, 1, 42])
19 | })
20 |
21 | it('should replace the item at the given index with the new value (Promise async)', async () => {
22 | let program = pipe(replace(2, 42), toArray())
23 |
24 | expect(await program(Promise.resolve(range(0, 2)))).toEqual([0, 1, 42])
25 | expect(await program(Promise.resolve(range(0, 2)))).toEqual([0, 1, 42])
26 | })
27 |
--------------------------------------------------------------------------------
/src/replace.ts:
--------------------------------------------------------------------------------
1 | import { map } from './map'
2 |
3 | export function replace(index: number, value: T) {
4 | return map((item, i) => (i === index ? value : item))
5 | }
6 |
--------------------------------------------------------------------------------
/src/reverse.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, toArray, take, reverse, delay } from './'
2 |
3 | it('should be possible to reverse an iterator', () => {
4 | let program = pipe(range(0, 100), reverse(), take(5), toArray())
5 |
6 | expect(program()).toEqual([100, 100 - 1, 100 - 2, 100 - 3, 100 - 4])
7 | })
8 |
9 | it('should be possible to reverse an iterator (async)', async () => {
10 | let program = pipe(range(0, 100), delay(0), reverse(), take(5), toArray())
11 |
12 | expect(await program()).toEqual([100, 100 - 1, 100 - 2, 100 - 3, 100 - 4])
13 | })
14 |
15 | it('should be possible to reverse an iterator (Promise async)', async () => {
16 | let program = pipe(Promise.resolve(range(0, 100)), reverse(), take(5), toArray())
17 |
18 | expect(await program()).toEqual([100, 100 - 1, 100 - 2, 100 - 3, 100 - 4])
19 | })
20 |
--------------------------------------------------------------------------------
/src/reverse.ts:
--------------------------------------------------------------------------------
1 | import { pipe, toArray } from './'
2 |
3 | import { isAsyncIterable } from './utils/iterator'
4 | import { LazyIterable } from './shared-types'
5 |
6 | /**
7 | * This is pretty slow because it has to first go through the whole iterator
8 | * (to make it an array), then reverse the whole thing and then start
9 | * yielding again.
10 | */
11 | export function reverse() {
12 | return function reverseFn(data: LazyIterable) {
13 | if (isAsyncIterable(data) || data instanceof Promise) {
14 | return {
15 | async *[Symbol.asyncIterator]() {
16 | let stream = data instanceof Promise ? await data : data
17 |
18 | let program = pipe(toArray())
19 | let array = await program(stream)
20 |
21 | for await (let datum of array.reverse()) yield datum
22 | },
23 | }
24 | }
25 |
26 | return {
27 | *[Symbol.iterator]() {
28 | /**
29 | * This is pretty slow because it has to first go through the whole iterator
30 | * (to make it an array), then reverse the whole thing and then start
31 | * yielding again.
32 | */
33 | for (let datum of Array.from(data).reverse()) yield datum
34 | },
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/shared-types.ts:
--------------------------------------------------------------------------------
1 | export type MaybePromise = T | Promise
2 |
3 | export type LazyIterable = MaybePromise | AsyncIterable>
4 |
--------------------------------------------------------------------------------
/src/skip.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, toArray, skip, take, delay } from './'
2 |
3 | it('should skip x values', () => {
4 | let program = pipe(skip(5), toArray())
5 |
6 | expect(program(range(0, 10))).toEqual([5, 6, 7, 8, 9, 10])
7 | })
8 |
9 | it('should skip x values and take y values', () => {
10 | let program = pipe(skip(5), take(3), toArray())
11 |
12 | expect(program(range(0, 10))).toEqual([5, 6, 7])
13 | })
14 |
15 | it('should skip x values (async)', async () => {
16 | let program = pipe(delay(0), skip(5), toArray())
17 |
18 | expect(await program(range(0, 10))).toEqual([5, 6, 7, 8, 9, 10])
19 | })
20 |
21 | it('should skip x values and take y values (async)', async () => {
22 | let program = pipe(delay(0), skip(5), take(3), toArray())
23 |
24 | expect(await program(range(0, 10))).toEqual([5, 6, 7])
25 | })
26 |
27 | it('should skip x values and take y values (Promise async)', async () => {
28 | let program = pipe(skip(5), take(3), toArray())
29 |
30 | expect(await program(Promise.resolve(range(0, 10)))).toEqual([5, 6, 7])
31 | })
32 |
--------------------------------------------------------------------------------
/src/skip.ts:
--------------------------------------------------------------------------------
1 | import { slice } from './'
2 |
3 | export function skip(amount: number) {
4 | return slice(Math.max(0, amount))
5 | }
6 |
--------------------------------------------------------------------------------
/src/slice.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, slice, toArray, delay } from './'
2 |
3 | it.each([
4 | [0, 10, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
5 | [4, 6, [4, 5, 6]],
6 | ])('should be possible to slice from %p until %p', (start, end, expected) => {
7 | let program = pipe(slice(start, end), toArray())
8 |
9 | expect(program(range(0, 1_000_000_000))).toEqual(expected)
10 | expect(program(range(0, 1_000_000_000))).toEqual(expected)
11 | })
12 |
13 | it.each([
14 | [0, 10, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
15 | [4, 6, [4, 5, 6]],
16 | ])('should be possible to slice from %p until %p (async)', async (start, end, expected) => {
17 | let program = pipe(delay(0), slice(start, end), toArray())
18 |
19 | expect(await program(range(0, 1_000_000_000))).toEqual(expected)
20 | expect(await program(range(0, 1_000_000_000))).toEqual(expected)
21 | })
22 |
23 | it('should be possible to pass an array to slice', () => {
24 | let program = pipe(slice(0, 1), toArray())
25 | expect(program([1, 2, 3, 4])).toEqual([1, 2])
26 | })
27 |
28 | it('should be possible to pass an iterator to slice', () => {
29 | let program = pipe(slice(0, 1), toArray())
30 | expect(program(range(1, 4))).toEqual([1, 2])
31 | })
32 |
33 | it('should be possible to pass an array to slice (async)', async () => {
34 | let program = pipe(delay(0), slice(0, 1), toArray())
35 | expect(await program([1, 2, 3, 4])).toEqual([1, 2])
36 | })
37 |
38 | it('should be possible to pass an iterator to slice (async)', async () => {
39 | let program = pipe(delay(0), slice(0, 1), toArray())
40 | expect(await program(range(1, 4))).toEqual([1, 2])
41 | })
42 |
43 | it('should be possible to pass an array to slice (Promise async)', async () => {
44 | let program = pipe(slice(0, 1), toArray())
45 | expect(await program(Promise.resolve([1, 2, 3, 4]))).toEqual([1, 2])
46 | })
47 |
48 | it('should be possible to pass an iterator to slice (Promise async)', async () => {
49 | let program = pipe(slice(0, 1), toArray())
50 | expect(await program(Promise.resolve(range(1, 4)))).toEqual([1, 2])
51 | })
52 |
--------------------------------------------------------------------------------
/src/slice.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | export function slice(begin = 0, end = Infinity) {
5 | return function sliceFn(data: LazyIterable) {
6 | if (isAsyncIterable(data) || data instanceof Promise) {
7 | return {
8 | async *[Symbol.asyncIterator]() {
9 | let stream = data instanceof Promise ? await data : data
10 | let iterator = stream as AsyncIterableIterator
11 |
12 | let local_begin = begin
13 | let local_end = end - local_begin
14 |
15 | // Skip the first X values
16 | while (local_begin-- > 0) iterator.next()
17 |
18 | // Loop through the remaining items until the end is reached
19 | for await (let datum of iterator) {
20 | yield datum
21 | if (--local_end < 0) return
22 | }
23 | },
24 | }
25 | }
26 |
27 | return {
28 | *[Symbol.iterator]() {
29 | let iterator = Array.isArray(data)
30 | ? (data[Symbol.iterator]() as IterableIterator)
31 | : (data as IterableIterator)
32 |
33 | let local_begin = begin
34 | let local_end = end - local_begin
35 |
36 | // Skip the first X values
37 | while (local_begin-- > 0) iterator.next()
38 |
39 | // Loop through the remaining items until the end is reached
40 | for (let datum of iterator) {
41 | yield datum
42 | if (--local_end < 0) return
43 | }
44 | },
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/some.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, some, delay } from './'
2 |
3 | it('should return true when some value matches the predicate', () => {
4 | let program = pipe(
5 | range(0, 25),
6 | some((x) => x === 12)
7 | )
8 |
9 | expect(program()).toEqual(true)
10 | })
11 |
12 | it('should return false when non of the values match the predicate', () => {
13 | let program = pipe(
14 | range(0, 100),
15 | some((x: number) => x > 100)
16 | )
17 |
18 | expect(program()).toEqual(false)
19 | })
20 |
21 | it('should return true when some value matches the predicate (async)', async () => {
22 | let program = pipe(
23 | delay(0),
24 | some((x) => x === 12)
25 | )
26 |
27 | expect(await program(range(0, 25))).toEqual(true)
28 | expect(await program(range(0, 25))).toEqual(true)
29 | })
30 |
31 | it('should return false when non of the values match the predicate (async)', async () => {
32 | let program = pipe(
33 | delay(0),
34 | some((x: number) => x > 100)
35 | )
36 |
37 | expect(await program(range(0, 100))).toEqual(false)
38 | expect(await program(range(0, 100))).toEqual(false)
39 | })
40 |
41 | it('should return true when some value matches the predicate (Promise async)', async () => {
42 | let program = pipe(some((x) => x === 12))
43 |
44 | expect(await program(Promise.resolve(range(0, 25)))).toEqual(true)
45 | expect(await program(Promise.resolve(range(0, 25)))).toEqual(true)
46 | })
47 |
48 | it('should return false when non of the values match the predicate (Promise async)', async () => {
49 | let program = pipe(some((x: number) => x > 100))
50 |
51 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(false)
52 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(false)
53 | })
54 |
55 | it('should take the index as second argument', async () => {
56 | let program = pipe(some((_x: number, i) => i > 100))
57 |
58 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(false)
59 | expect(await program(Promise.resolve(range(0, 100)))).toEqual(false)
60 | })
61 |
--------------------------------------------------------------------------------
/src/some.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (input: T, index: number) => boolean
5 |
6 | export function some(predicate: Fn) {
7 | return function someFn(data: LazyIterable): boolean | Promise {
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return (async () => {
10 | let stream = data instanceof Promise ? await data : data
11 | let i = 0
12 |
13 | for await (let datum of stream) {
14 | if (predicate(datum, i++)) return true
15 | }
16 |
17 | return false
18 | })()
19 | }
20 |
21 | let i = 0
22 | for (let datum of data) {
23 | if (predicate(datum, i++)) return true
24 | }
25 |
26 | return false
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/sort.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, toArray, sort, delay } from './'
2 |
3 | it('should be possible to sort a stream of numbers', () => {
4 | let program = pipe(
5 | sort((a, z) => a - z),
6 | toArray()
7 | )
8 |
9 | expect(program([6, 1, 4, 2, 3, 5, 9, 7, 8])).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9])
10 | })
11 |
12 | it('should be possible to sort an async stream of numbers', async () => {
13 | let program = pipe(
14 | sort((a, z) => a - z),
15 | toArray()
16 | )
17 |
18 | expect(await program(Promise.resolve([6, 1, 4, 2, 3, 5, 9, 7, 8]))).toEqual([
19 | 1, 2, 3, 4, 5, 6, 7, 8, 9,
20 | ])
21 | })
22 |
23 | it('should be possible to sort an async stream of numbers (2)', async () => {
24 | let program = pipe(
25 | delay(10),
26 | sort((a, z) => a - z),
27 | toArray()
28 | )
29 |
30 | expect(await program([6, 1, 4, 2, 3, 5, 9, 7, 8])).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9])
31 | })
32 |
--------------------------------------------------------------------------------
/src/sort.ts:
--------------------------------------------------------------------------------
1 | import { LazyIterable } from './shared-types'
2 | import { getIterator, isAsyncIterable } from './utils/iterator'
3 |
4 | export function sort(comparator: (a: T, z: T) => number) {
5 | return function sortFn(data: LazyIterable) {
6 | if (isAsyncIterable(data) || data instanceof Promise) {
7 | return {
8 | async *[Symbol.asyncIterator]() {
9 | let stream = data instanceof Promise ? await data : data
10 |
11 | let result = []
12 | // @ts-expect-error I'm probably doing something stupid here...
13 | for await (let datum of getIterator(stream)) result.push(datum)
14 | yield* result.sort(comparator)
15 | },
16 | }
17 | }
18 |
19 | return {
20 | *[Symbol.iterator]() {
21 | for (let datum of Array.from(data).sort(comparator)) {
22 | yield datum
23 | }
24 | },
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, sum, range, delay } from './'
2 |
3 | it('should be possible to sum an array', () => {
4 | let program = pipe(sum())
5 |
6 | expect(program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(55)
7 | expect(program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(55)
8 | })
9 |
10 | it('should be possible to sum an iterator', () => {
11 | let program = pipe(sum())
12 |
13 | expect(program(range(0, 10))).toEqual(55)
14 | expect(program(range(0, 10))).toEqual(55)
15 | })
16 |
17 | it('should be possible to sum an array (async)', async () => {
18 | let program = pipe(delay(0), sum())
19 |
20 | expect(await program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(55)
21 | expect(await program([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toEqual(55)
22 | })
23 |
24 | it('should be possible to sum an iterator (async)', async () => {
25 | let program = pipe(delay(0), sum())
26 |
27 | expect(await program(range(0, 10))).toEqual(55)
28 | expect(await program(range(0, 10))).toEqual(55)
29 | })
30 |
31 | it('should be possible to sum an array (Promise async)', async () => {
32 | let program = pipe(sum())
33 |
34 | expect(await program(Promise.resolve([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))).toEqual(55)
35 | expect(await program(Promise.resolve([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))).toEqual(55)
36 | })
37 |
38 | it('should be possible to sum an iterator (Promise async)', async () => {
39 | let program = pipe(sum())
40 |
41 | expect(await program(Promise.resolve(range(0, 10)))).toEqual(55)
42 | expect(await program(Promise.resolve(range(0, 10)))).toEqual(55)
43 | })
44 |
--------------------------------------------------------------------------------
/src/sum.ts:
--------------------------------------------------------------------------------
1 | import { reduce } from './'
2 |
3 | export function sum() {
4 | return reduce((total, current) => total + current, 0)
5 | }
6 |
--------------------------------------------------------------------------------
/src/take.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, take, toArray, delay } from './'
2 |
3 | it('should take a only X values', () => {
4 | let program = pipe(take(5), toArray())
5 |
6 | expect(program(range(0, 1_000))).toEqual([0, 1, 2, 3, 4])
7 | })
8 |
9 | it('should take a only X values (async)', async () => {
10 | let program = pipe(delay(0), take(5), toArray())
11 |
12 | expect(await program(range(0, 1_000))).toEqual([0, 1, 2, 3, 4])
13 | })
14 |
15 | it('should take a only X values (Promise async)', async () => {
16 | let program = pipe(take(5), toArray())
17 |
18 | expect(await program(Promise.resolve(range(0, 1_000)))).toEqual([0, 1, 2, 3, 4])
19 | })
20 |
--------------------------------------------------------------------------------
/src/take.ts:
--------------------------------------------------------------------------------
1 | import { slice } from './'
2 |
3 | export function take(amount: number) {
4 | return slice(0, Math.max(0, amount - 1))
5 | }
6 |
--------------------------------------------------------------------------------
/src/takeWhile.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, takeWhile, range, toArray, delay } from './'
2 |
3 | it('should be possible to take values as long as they meet a certain condition', () => {
4 | let program = pipe(
5 | takeWhile((x: number) => x < 5),
6 | toArray()
7 | )
8 |
9 | expect(program(range(0, 1_000))).toEqual([0, 1, 2, 3, 4])
10 | })
11 |
12 | it('should be possible to take values as long as they meet a certain condition (async)', async () => {
13 | let program = pipe(
14 | delay(0),
15 | takeWhile((x: number) => x < 5),
16 | toArray()
17 | )
18 |
19 | expect(await program(range(0, 1_000))).toEqual([0, 1, 2, 3, 4])
20 | })
21 |
22 | it('should be possible to take values as long as they meet a certain condition (Promise async)', async () => {
23 | let program = pipe(
24 | takeWhile((x: number) => x < 5),
25 | toArray()
26 | )
27 |
28 | expect(await program(Promise.resolve(range(0, 1_000)))).toEqual([0, 1, 2, 3, 4])
29 | })
30 |
31 | it('should take the index as second argument', async () => {
32 | let program = pipe(
33 | takeWhile((_x: number, i) => i < 5),
34 | toArray()
35 | )
36 |
37 | expect(await program(Promise.resolve(range(0, 1_000)))).toEqual([0, 1, 2, 3, 4])
38 | })
39 |
--------------------------------------------------------------------------------
/src/takeWhile.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | type Fn = (datum: T, index: number) => boolean
5 |
6 | export function takeWhile(fn: Fn) {
7 | return function takeWhileFn(data: LazyIterable) {
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return {
10 | async *[Symbol.asyncIterator]() {
11 | let stream = data instanceof Promise ? await data : data
12 | let i = 0
13 |
14 | for await (let datum of stream) {
15 | if (!fn(datum, i++)) return
16 | yield datum
17 | }
18 | },
19 | }
20 | }
21 |
22 | return {
23 | *[Symbol.iterator]() {
24 | let i = 0
25 | for (let datum of data) {
26 | if (!fn(datum, i++)) return
27 | yield datum
28 | }
29 | },
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/tap.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, toArray, range, delay, tap } from './'
2 |
3 | it('should be possible to tap into the current sequence', () => {
4 | let fn = jest.fn()
5 | let program = pipe(
6 | range(0, 5),
7 | tap((value) => {
8 | fn(value)
9 | }),
10 | toArray()
11 | )
12 |
13 | expect(program()).toEqual([0, 1, 2, 3, 4, 5])
14 | expect(fn).toHaveBeenCalledTimes(6)
15 | })
16 |
17 | it('should be possible to tap into the current sequence (async)', async () => {
18 | let fn = jest.fn()
19 | let program = pipe(
20 | range(0, 5),
21 | delay(0),
22 | tap((value) => {
23 | fn(value)
24 | }),
25 | toArray()
26 | )
27 |
28 | expect(await program()).toEqual([0, 1, 2, 3, 4, 5])
29 | expect(fn).toHaveBeenCalledTimes(6)
30 | })
31 |
32 | it('should be possible to tap into the current sequence (Promise async)', async () => {
33 | let fn = jest.fn()
34 | let program = pipe(
35 | Promise.resolve(range(0, 5)),
36 | tap((value) => {
37 | fn(value)
38 | }),
39 | toArray()
40 | )
41 |
42 | expect(await program()).toEqual([0, 1, 2, 3, 4, 5])
43 | expect(fn).toHaveBeenCalledTimes(6)
44 | })
45 |
46 | it('should take the index as second argument', async () => {
47 | let fn = jest.fn()
48 | let program = pipe(
49 | Promise.resolve(range(0, 5)),
50 | tap((_value, index) => {
51 | fn(index)
52 | }),
53 | toArray()
54 | )
55 |
56 | expect(await program()).toEqual([0, 1, 2, 3, 4, 5])
57 | expect(fn).toHaveBeenCalledTimes(6)
58 | })
59 |
--------------------------------------------------------------------------------
/src/tap.ts:
--------------------------------------------------------------------------------
1 | import { map } from './'
2 |
3 | type Fn = (datum: T, index: number) => void
4 |
5 | export function tap(fn: Fn) {
6 | return map((datum: T, index) => {
7 | fn(datum, index)
8 | return datum
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/src/toArray.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, toArray, delay } from './'
2 |
3 | it('should convert an iterator to an array', () => {
4 | let program = pipe(range(0, 10), toArray())
5 |
6 | expect(program()).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
7 | })
8 |
9 | it('should convert an iterator to an array (async)', async () => {
10 | let program = pipe(range(0, 10), delay(0), toArray())
11 |
12 | expect(await program()).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
13 | })
14 |
15 | it('should convert an iterator to an array (Promise async)', async () => {
16 | let program = pipe(Promise.resolve(range(0, 10)), toArray())
17 |
18 | expect(await program()).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
19 | })
20 |
--------------------------------------------------------------------------------
/src/toArray.ts:
--------------------------------------------------------------------------------
1 | import { reduce } from './'
2 |
3 | import { LazyIterable } from './shared-types'
4 |
5 | export function toArray() {
6 | return (data: LazyIterable) => {
7 | return reduce((acc, current) => {
8 | acc.push(current)
9 | return acc
10 | }, [])(data)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/toLength.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe } from './pipe'
2 | import { range } from './range'
3 | import { toLength } from './toLength'
4 | import { delay } from './delay'
5 |
6 | it('should return the length of the iterable', () => {
7 | let program = pipe(toLength())
8 |
9 | expect(program(range(0, 25))).toBe(26)
10 | expect(program(range(0, 25))).toBe(26)
11 | })
12 |
13 | it('should return the length of the iterable (async)', async () => {
14 | let program = pipe(delay(0), toLength())
15 |
16 | expect(await program(range(0, 25))).toBe(26)
17 | expect(await program(range(0, 25))).toBe(26)
18 | })
19 |
20 | it('should return the length of the iterable (Promise async)', async () => {
21 | let program = pipe(toLength())
22 |
23 | expect(await program(Promise.resolve(range(0, 25)))).toBe(26)
24 | expect(await program(Promise.resolve(range(0, 25)))).toBe(26)
25 | })
26 |
--------------------------------------------------------------------------------
/src/toLength.ts:
--------------------------------------------------------------------------------
1 | import { reduce } from './reduce'
2 |
3 | export function toLength() {
4 | return reduce((acc) => acc + 1, 0)
5 | }
6 |
--------------------------------------------------------------------------------
/src/toSet.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, flatMap, toSet, delay } from './'
2 |
3 | it('should convert an iterator to an array', () => {
4 | let program = pipe(range(0, 10), toSet())
5 |
6 | expect(program()).toEqual(new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
7 | })
8 |
9 | it('should remove duplicate items (default Set behaviour)', () => {
10 | let program = pipe(
11 | range(0, 10),
12 | flatMap((value) => [value, value, value + 1]),
13 | toSet()
14 | )
15 |
16 | expect(program()).toEqual(new Set([0, 1, 10, 11, 2, 3, 4, 5, 6, 7, 8, 9]))
17 | })
18 |
19 | it('should convert an iterator to an array (async)', async () => {
20 | let program = pipe(range(0, 10), delay(0), toSet())
21 |
22 | expect(await program()).toEqual(new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
23 | })
24 |
25 | it('should convert an iterator to an array (Promise async)', async () => {
26 | let program = pipe(Promise.resolve(range(0, 10)), toSet())
27 |
28 | expect(await program()).toEqual(new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
29 | })
30 |
--------------------------------------------------------------------------------
/src/toSet.ts:
--------------------------------------------------------------------------------
1 | import { reduce } from './'
2 |
3 | import { LazyIterable } from './shared-types'
4 |
5 | export function toSet() {
6 | return (data: LazyIterable) => {
7 | return reduce, T>((acc, current) => {
8 | acc.add(current)
9 | return acc
10 | }, new Set())(data)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/unique.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, unique, map, toArray, delay } from './'
2 |
3 | function snap(multitude: number, value: number) {
4 | return Math.ceil(value / multitude) * multitude
5 | }
6 |
7 | it('should be possible to create a unique stream', () => {
8 | let program = pipe(
9 | map((x: number) => snap(5, x)),
10 | unique(),
11 | toArray()
12 | )
13 |
14 | expect(program(range(0, 10))).toEqual([0, 5, 10])
15 | })
16 |
17 | it('should be possible to create a unique stream (async)', async () => {
18 | let program = pipe(
19 | delay(0),
20 | map((x: number) => snap(5, x)),
21 | unique(),
22 | toArray()
23 | )
24 |
25 | expect(await program(range(0, 10))).toEqual([0, 5, 10])
26 | })
27 |
28 | it('should be possible to create a unique stream (Promise async)', async () => {
29 | let program = pipe(unique(), toArray())
30 |
31 | expect(await program(Promise.resolve([0, 0, 5, 5, 5, 10, 10]))).toEqual([0, 5, 10])
32 | })
33 |
--------------------------------------------------------------------------------
/src/unique.ts:
--------------------------------------------------------------------------------
1 | import { isAsyncIterable } from './utils/iterator'
2 | import { LazyIterable } from './shared-types'
3 |
4 | export function unique() {
5 | return function uniqueFn(data: LazyIterable) {
6 | let seen = new Set([])
7 |
8 | if (isAsyncIterable(data) || data instanceof Promise) {
9 | return {
10 | async *[Symbol.asyncIterator]() {
11 | let stream = data instanceof Promise ? await data : data
12 |
13 | for await (let datum of stream) {
14 | if (!seen.has(datum)) {
15 | seen.add(datum)
16 | yield datum
17 | }
18 | }
19 | },
20 | }
21 | }
22 |
23 | return {
24 | *[Symbol.iterator]() {
25 | for (let datum of data) {
26 | if (!seen.has(datum)) {
27 | seen.add(datum)
28 | yield datum
29 | }
30 | }
31 | },
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/clamp.ts:
--------------------------------------------------------------------------------
1 | export function clamp(input: number, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
2 | return Math.min(max, Math.max(min, input))
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/ensureFunction.ts:
--------------------------------------------------------------------------------
1 | type Fn = (...args: any) => any
2 |
3 | export function ensureFunction(input: any): Fn {
4 | return typeof input === 'function' ? input : () => input
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/iterator.ts:
--------------------------------------------------------------------------------
1 | export function isIterable(input: any): input is Iterable {
2 | if (typeof input !== 'object' || input === null) return false
3 | return input[Symbol.iterator] !== undefined
4 | }
5 |
6 | export function isAsyncIterable(input: any): input is AsyncIterable {
7 | if (typeof input !== 'object' || input === null) return false
8 | return input[Symbol.asyncIterator] !== undefined
9 | }
10 |
11 | export function getIterator(
12 | input: Iterable | AsyncIterable
13 | ): Iterator | AsyncIterator {
14 | if (isAsyncIterable(input)) {
15 | return input[Symbol.asyncIterator]()
16 | }
17 |
18 | if (isIterable(input)) {
19 | return input[Symbol.iterator]()
20 | }
21 |
22 | throw new Error('`input` is not an iterable')
23 | }
24 |
--------------------------------------------------------------------------------
/src/where.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, range, map, toArray, where, delay } from './'
2 |
3 | it('should be possible to get the items containing certain properties', () => {
4 | let program = pipe(
5 | range(0, 10),
6 | map((x: number) => ({ x, y: x + 1 })),
7 | where({ x: 3, y: 4 }),
8 | toArray()
9 | )
10 |
11 | expect(program()).toEqual([{ x: 3, y: 4 }])
12 | })
13 |
14 | it('should not crash on values that it does not understand', () => {
15 | let program = pipe(where({ include: true }), toArray())
16 |
17 | expect(
18 | program([
19 | 0,
20 | null,
21 | true,
22 | false,
23 | 'hello',
24 | Object.assign(function () {}, { include: false }),
25 | { include: true, name: 'winner' },
26 | ])
27 | ).toEqual([{ include: true, name: 'winner' }])
28 | })
29 |
30 | it('should be possible to get the items containing certain magic properties like array lengths', () => {
31 | let program = pipe(
32 | range(0, 3),
33 | map((x: number) => [x, x]),
34 | where({ length: 2 }),
35 | toArray()
36 | )
37 |
38 | expect(program()).toEqual([
39 | [0, 0],
40 | [1, 1],
41 | [2, 2],
42 | [3, 3],
43 | ])
44 | })
45 |
46 | it('should be possible to get the items containing certain properties (async)', async () => {
47 | let program = pipe(
48 | range(0, 10),
49 | delay(0),
50 | map((x: number) => ({ x, y: x + 1 })),
51 | where({ x: 3, y: 4 }),
52 | toArray()
53 | )
54 |
55 | expect(await program()).toEqual([{ x: 3, y: 4 }])
56 | })
57 |
58 | it('should be possible to get the items containing certain magic properties like array lengths (async)', async () => {
59 | let program = pipe(
60 | range(0, 3),
61 | delay(0),
62 | map((x: number) => [x, x]),
63 | where({ length: 2 }),
64 | toArray()
65 | )
66 |
67 | expect(await program()).toEqual([
68 | [0, 0],
69 | [1, 1],
70 | [2, 2],
71 | [3, 3],
72 | ])
73 | })
74 |
75 | it('should be possible to get the items containing certain properties (Promise async)', async () => {
76 | let program = pipe(
77 | Promise.resolve(range(0, 10)),
78 | map((x: number) => ({ x, y: x + 1 })),
79 | where({ x: 3, y: 4 }),
80 | toArray()
81 | )
82 |
83 | expect(await program()).toEqual([{ x: 3, y: 4 }])
84 | })
85 |
86 | it('should be possible to get the items containing certain magic properties like array lengths (Promise async)', async () => {
87 | let program = pipe(
88 | Promise.resolve(range(0, 3)),
89 | map((x: number) => [x, x]),
90 | where({ length: 2 }),
91 | toArray()
92 | )
93 |
94 | expect(await program()).toEqual([
95 | [0, 0],
96 | [1, 1],
97 | [2, 2],
98 | [3, 3],
99 | ])
100 | })
101 |
--------------------------------------------------------------------------------
/src/where.ts:
--------------------------------------------------------------------------------
1 | import { filter } from './'
2 |
3 | export function where(properties: Record) {
4 | let entries = Object.entries(properties)
5 |
6 | return filter((datum) => {
7 | if (datum == null) return false
8 | return entries.every(([key, value]) => (datum as any)[key] === value)
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/src/windows.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, toArray, windows } from './'
2 |
3 | it('should result in a sliding window', () => {
4 | let program = pipe(windows(2), toArray())
5 |
6 | expect(program(['w', 'i', 'n', 'd', 'o', 'w', 's'])).toEqual([
7 | ['w', 'i'],
8 | ['i', 'n'],
9 | ['n', 'd'],
10 | ['d', 'o'],
11 | ['o', 'w'],
12 | ['w', 's'],
13 | ])
14 | })
15 |
--------------------------------------------------------------------------------
/src/windows.ts:
--------------------------------------------------------------------------------
1 | export function windows(size: number) {
2 | return function* windowsFn(data: Iterable) {
3 | let result: T[] = []
4 |
5 | for (let item of data) {
6 | result.push(item)
7 |
8 | if (result.length === size) {
9 | yield result.slice(-size)
10 | result.shift()
11 | }
12 | }
13 |
14 | result.splice(0)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/zip.test.ts:
--------------------------------------------------------------------------------
1 | import { pipe, toArray, zip, take, range, chunk } from './'
2 |
3 | it('should be possible to zip data together', () => {
4 | let program = pipe(
5 | [
6 | [0, 1, 2, 3],
7 | ['A', 'B', 'C', 'D'],
8 | ],
9 | zip(),
10 | toArray()
11 | )
12 |
13 | expect(program()).toEqual([
14 | [0, 'A'],
15 | [1, 'B'],
16 | [2, 'C'],
17 | [3, 'D'],
18 | ])
19 | })
20 |
21 | it('should be possible to zip data together from a generator', () => {
22 | let program = pipe(range(0, 1_000), chunk(4), take(5), zip(), take(5), toArray())
23 |
24 | expect(program()).toEqual([
25 | [0, 4, 8, 12, 16],
26 | [1, 5, 9, 13, 17],
27 | [2, 6, 10, 14, 18],
28 | [3, 7, 11, 15, 19],
29 | ])
30 | })
31 |
32 | it('should drop non matchable values', () => {
33 | // If array A has 3 items and array B has 4 items, the last item of array B
34 | // will be dropped.
35 | let program = pipe(
36 | [
37 | [0, 1, 2, 3],
38 | ['A', 'B', 'C'],
39 | ],
40 | zip(),
41 | toArray()
42 | )
43 |
44 | expect(program()).toEqual([
45 | [0, 'A'],
46 | [1, 'B'],
47 | [2, 'C'],
48 | ])
49 | })
50 |
51 | it('should be chainable with a take so that only a few items are zipped', () => {
52 | let program = pipe(
53 | [
54 | [0, 1, 2, 3],
55 | ['A', 'B', 'C'],
56 | ],
57 | zip(),
58 | take(2),
59 | toArray()
60 | )
61 |
62 | expect(program()).toEqual([
63 | [0, 'A'],
64 | [1, 'B'],
65 | ])
66 | })
67 |
68 | it('should zip multiple iterators together', () => {
69 | let program = pipe([range(0, 999), range(999, 0)], zip(), take(5), toArray())
70 |
71 | expect(program()).toEqual([
72 | [0, 999],
73 | [1, 998],
74 | [2, 997],
75 | [3, 996],
76 | [4, 995],
77 | ])
78 | })
79 |
--------------------------------------------------------------------------------
/src/zip.ts:
--------------------------------------------------------------------------------
1 | export function zip() {
2 | return function* zipFn(data: Iterable>) {
3 | // Map each item of `data` to an iterator
4 | let iterators = Array.from(data).map((datum) => datum[Symbol.iterator]())
5 |
6 | while (true) {
7 | // Take the next value of each iterator
8 | let values = iterators.map((datum) => datum.next())
9 |
10 | // Stop once some values are done
11 | if (values.some((value) => value.done)) return
12 |
13 | // Yield the actual values zipped together
14 | yield values.map((value) => value.value)
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "rootDir": "./src",
5 | "strict": true,
6 | "noUnusedLocals": true,
7 | "noUnusedParameters": true,
8 | "noImplicitReturns": true,
9 | "noFallthroughCasesInSwitch": true,
10 | "downlevelIteration": true,
11 | "moduleResolution": "node",
12 | "baseUrl": "./",
13 | "esModuleInterop": true,
14 | "target": "ESNext",
15 | "allowJs": true,
16 | "skipLibCheck": true,
17 | "forceConsistentCasingInFileNames": true,
18 | "resolveJsonModule": true,
19 | "isolatedModules": true
20 | },
21 | "include": ["src/**/*"],
22 | "exclude": ["node_modules", "dist"]
23 | }
24 |
--------------------------------------------------------------------------------