├── .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 | License 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 | --------------------------------------------------------------------------------