├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── change.md │ └── feature.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .releaserc ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SUMMARY.md ├── commitlint.config.js ├── docs ├── api │ ├── combinatorics.md │ ├── instance.md │ ├── random.md │ ├── simplex.md │ └── static.md ├── faq.md └── installation.md ├── jest.config.js ├── package.json ├── src ├── Seq.ts ├── __tests__ │ ├── MAX_YIELDS.spec.ts │ ├── average.spec.ts │ ├── averageBy.spec.ts │ ├── chain.spec.ts │ ├── combinatorics.spec.ts │ ├── concat.spec.ts │ ├── distinct.spec.ts │ ├── every.spec.ts │ ├── filter.spec.ts │ ├── find.spec.ts │ ├── first.spec.ts │ ├── flat.spec.ts │ ├── flatMap.spec.ts │ ├── forEach.spec.ts │ ├── frequencies.spec.ts │ ├── groupBy.spec.ts │ ├── includes.spec.ts │ ├── index.spec.ts │ ├── interleave.spec.ts │ ├── interpose.spec.ts │ ├── isEmpty.spec.ts │ ├── map.spec.ts │ ├── nth.spec.ts │ ├── pairwise.spec.ts │ ├── partition.spec.ts │ ├── random.spec.ts │ ├── simplex.spec.ts │ ├── skipWhile.spec.ts │ ├── some.spec.ts │ ├── static.spec.ts │ ├── sum.spec.ts │ ├── sumBy.spec.ts │ ├── takeWhile.spec.ts │ ├── window.spec.ts │ ├── zip.spec.ts │ ├── zip2With.spec.ts │ └── zipWith.spec.ts ├── benchmarks │ ├── benchmark.ts │ └── lazy-bench.js ├── combinatorics.ts ├── index.ts ├── random.ts ├── simplex.ts └── static.ts ├── tsconfig.json ├── typedoc.json └── yarn.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.158.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 14, 12, 10 4 | ARG VARIANT="14-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN su node -c "npm install -g " 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.158.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 10, 12, 14 8 | "args": { 9 | "VARIANT": "14" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": { 15 | "terminal.integrated.shell.linux": "/bin/bash" 16 | }, 17 | 18 | // Add the IDs of extensions you want installed when the container is created. 19 | "extensions": ["dbaeumer.vscode-eslint"], 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | // "postCreateCommand": "yarn install", 26 | 27 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 28 | "remoteUser": "node" 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | "plugin:prettier/recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | ], 11 | parser: "@typescript-eslint/parser", 12 | parserOptions: { 13 | project: "tsconfig.json", 14 | sourceType: "module", 15 | }, 16 | plugins: ["@typescript-eslint"], 17 | rules: { 18 | "@typescript-eslint/explicit-function-return-type": 0, 19 | "@typescript-eslint/require-await": 0, 20 | semi: ["error", "never"], 21 | "@typescript-eslint/member-delimiter-style": 0, 22 | "@typescript-eslint/explicit-module-boundary-types": 0, 23 | "@typescript-eslint/no-unsafe-assignment": 0, 24 | "@typescript-eslint/no-unsafe-return": 0, 25 | "@typescript-eslint/no-unsafe-call": 0, 26 | "@typescript-eslint/no-unsafe-member-access": 0, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: tdreyno 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Found a bug? Let us know! 🐛 4 | --- 5 | 6 | # File / process 7 | 8 | # Observed behavior 9 | 10 | # Expected behavior 11 | 12 | # Environment (Browser, device, runtime, OS, etc.) 13 | 14 | # Steps to reproduce 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/change.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Change 3 | about: Update functionality that already exists. 4 | --- 5 | 6 | # Location 7 | 8 | # Motivation 9 | 10 | # Requirements 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: Add new functionality to the project. 4 | --- 5 | 6 | # Location 7 | 8 | # Requirements 9 | 10 | # Design comps 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | - package-ecosystem: npm 8 | directory: "/" 9 | schedule: 10 | interval: monthly 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | # Closes issue(s) 4 | 5 | # How to test / repro 6 | 7 | # Changes include 8 | 9 | - [ ] Bugfix (non-breaking change that solves an issue) 10 | - [ ] New feature (non-breaking change that adds functionality) 11 | - [ ] Breaking change (change that is not backwards-compatible and/or changes current functionality) 12 | 13 | # Checklist 14 | 15 | - [ ] I have tested this code 16 | 17 | # Other comments 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '18 22 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node: ["16", "14", "12"] 11 | name: Node ${{ matrix.node }} 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Setup node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: ${{ matrix.node }} 18 | - run: yarn install --ignore-engines 19 | - run: yarn test 20 | - run: yarn build 21 | - name: Code Climate Coverage Action 22 | uses: paambaati/codeclimate-action@v3.2.0 23 | env: 24 | CC_TEST_REPORTER_ID: 438d4172a92ae1de30d102341e7faac70f2c12dfb318e96cdbbaf27f8ba7ad52 25 | with: 26 | coverageCommand: yarn coverage 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 12 17 | - name: Install dependencies 18 | run: yarn install 19 | - name: Build 20 | run: yarn build 21 | - name: Release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | run: yarn semantic-release 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pkg 3 | .DS_Store 4 | coverage 5 | report.* 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | yarn.lock 3 | build 4 | pkg 5 | coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "arrowParens": "avoid", 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | ["@semantic-release/npm", { 6 | "pkgRoot": "pkg" 7 | }], 8 | ["@semantic-release/git", { 9 | "assets": ["package.json"], 10 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 11 | }], 12 | ["@semantic-release/github", { 13 | "assets": [ 14 | {"path": "pkg/dist-web/index.min.js", "label": "Minified Web Distribution"} 15 | ] 16 | }] 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [See Github releases](https://github.com/tdreyno/leisure/releases). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | - Trolling, insulting or derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | - Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Thomas Reynolds 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | - No Harm: The software may not be used by anyone for systems or activities that actively and knowingly endanger, harm, or otherwise threaten the physical, mental, economic, or general well-being of other individuals or groups, in violation of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/). 8 | 9 | - Services: If the Software is used to provide a service to others, the licensee shall, as a condition of use, require those others not to use the service in any way that violates the No Harm clause above. 10 | 11 | - Enforceability. If any portion or provision of this License shall to any extent be declared illegal or unenforceable by a court of competent jurisdiction, then the remainder of this License, or the application of such portion or provision in circumstances other than those as to which it is so declared illegal or unenforceable, shall not be affected thereby, and each portion and provision of this Agreement shall be valid and enforceable to the fullest extent permitted by law. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | 15 | This Hippocratic License is an Ethical Source license (https://ethicalsource.dev) derived from the MIT License, amended to limit the impact of the unethical use of open source software. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leisure 2 | 3 | [![Test Coverage](https://api.codeclimate.com/v1/badges/bade509a61c126d7f488/test_coverage)](https://codeclimate.com/github/tdreyno/leisure/test_coverage) 4 | [![npm latest version](https://img.shields.io/npm/v/@tdreyno/leisure/latest.svg)](https://www.npmjs.com/package/@tdreyno/leisure) 5 | 6 | `leisure` is a TypeScript library for creating and manipulating lazy sequences of data. The API surface resembles the ES6 `map`/`filter`/`reduce` API, but also adds many useful helpers often seen in [Lodash](https://lodash.com/) or [Ramda](https://ramdajs.com/). 7 | 8 | The difference between `leisure` and those non-lazy libraries is the guarantee that no computations will happen unless the result is actually used. Here's an example: 9 | 10 | {% code %} 11 | 12 | ```typescript 13 | type User = { name: string; address: string }; 14 | type Address = { city: string; streetNumber: number; owner: User }; 15 | 16 | const parseAddress = (address: string): Address => { /* Does parsing */ } 17 | 18 | // Assume we have a list of 100 users from the database. 19 | const users: User[] = [ user1, user2, user3, ..., user99, user100 ]; 20 | 21 | // Parse the data and find the users who live in my hometown. 22 | const neighbor = users 23 | .map(user => parseAddress(user.address)) 24 | .filter(address => address.city === "My Hometown") 25 | .map(address => address.owner)[0]; 26 | ``` 27 | 28 | {% endcode %} 29 | 30 | The above example will run `map` across the entire list of users. The code will parse 100 addresses. In this trivial example, that computation is likely very fast, but imagine if it were more intensive \(say, using computer vision in the browser to detect a landmark\). The `filter` will then also run 100 times to find the subset of addresses we are looking for. Then the second `map` will run 100 more times \(admittedly convoluted\). The total time of the computation is `O(3N)`. 31 | 32 | At the end of the computation we discover that we only need the first matching result. If we were to operate on this data in a lazy fashion, the best case scenario is that the user we are looking for is first in this list. Utilizing lazy execution the first `map` would one once on the first user, that resulting address would run once through the `filter` and pass, then that address would run once more through the second `map`. The total computation would be `O(3)`. The worst case computation, where there is only 1 match and it is at the very end would be as slow as the original, but every single other permutation would be faster using the lazy approach. 33 | 34 | Here is that same example using `leisure`: 35 | 36 | ```typescript 37 | import { fromArray } from "@tdreyno/leisure" 38 | 39 | // Assume the types and data are the same. 40 | const neighbor = fromArray(users) 41 | .map(user => parseAddress(user.address)) 42 | .filter(address => address.city === "My Hometown") 43 | .map(address => address.owner) 44 | .first() 45 | ``` 46 | 47 | As you can see, the API is nearly identical, with the exception of the `first` helper method. 48 | 49 | ## Infinite sequences 50 | 51 | One of the very interesting side-effects of being lazy is the ability to interact with infinite sequences and perform the smallest number of possible computations to search over all the possibilities. For a very contrived example, find me the first number in the Fibonacci sequence that contains my birthday \(let's say I was born at the beginning of the millennium: `2000_01_01`\). 52 | 53 | ```typescript 54 | import Seq from "@tdreyno/leisure" 55 | 56 | const myBirthDay: number = 2000_01_01 57 | 58 | const myBirthDayFib: number = Seq 59 | // Generate an infinite sequence that adds one Fib value each step. 60 | .iterate(([a, b]) => [b, a + b], [0, 1]) 61 | 62 | // Find the one which has my birthday as a substring. 63 | .find(([fib]) => fib.toString().includes(myBirthDay.toString())) 64 | 65 | // Get just the number, not the pair. 66 | .map(([fib]) => fib) 67 | ``` 68 | 69 | Maybe it will run forever? Maybe it will find one very fast :) 70 | 71 | ## Realization 72 | 73 | Most methods in the library ask for one value from the previous computation at a time. However, some require the entire sequence of values. The process of converting a lazy sequence into a non-lazy version is called "realization." The `.first` and `.find` methods we've been using are examples. They take a Sequence and return a normal JavaScript value. 74 | 75 | Here's an example which discards the first 10 numbers of an infinite sequence, then grabs the next five and uses them. 76 | 77 | ```typescript 78 | import { infinite } from "@tdreyno/leisure" 79 | 80 | const tenThroughFourteen: number[] = infinite() 81 | // Ignore 0-9 82 | .skip(10) 83 | // Grab 10-14 84 | .take(5) 85 | // Realize the sequence into a real array 86 | .toArray() 87 | ``` 88 | 89 | There are a handful of methods which require the entire sequence, which means they will run infinitely if given an infinite sequence. They are: `toArray`, `forEach`, `sum`, `sumBy`, `average`, `averageBy`, `frequencies` and `groupBy`. 90 | 91 | If logging and debugging in a chain of lazy processing is not running when you expect it to, remember to realize the sequence with a `.toArray()` to flush the results. 92 | 93 | To avoid infinite loops, `leisure` caps the maximum number of infinite values to `1_000_000`. This number is configurable by setting `Seq.MAX_YIELDS` to whatever you want your max loop size to be. 94 | 95 | ## Integration with non-lazy JavaScript 96 | 97 | `leisure` implements the Iterator protocol which means you can use it to lazily pull values using a normal `for` loop. 98 | 99 | ```typescript 100 | import { infinite } from "@tdreyno/leisure" 101 | 102 | for (let i of infinite()) { 103 | if (i > 10) { 104 | console.log("counted to ten") 105 | break 106 | } 107 | } 108 | ``` 109 | 110 | ## Common usage 111 | 112 | `leisure` can be used as a drop-in replacement for the ES6 `map`/`filter`/`reduce` API. If that's all you need, you can still benefit from the reduction in operations in the non-worst-case scenarios. 113 | 114 | However, if you dig into `leisure`'s helper methods, you'll be able to write more expressive data computations. With the ES6 API, developers often drop into `reduce` for more complex data transformations. These reducer bodies can be difficult to read and understand how they are actually transforming the data. `leisure` provides methods which do these common transformations and names them so your code is more readable. Dropping all the way down into a reducer is quite rare in `leisure` code. 115 | 116 | [Take a look at the full `leisure` API.](docs/api/instance.md) 117 | 118 | ## Benchmarks 119 | 120 | Lazy data structures should be used knowing what type of data you have and how to make the most of laziness. In general, you want to be searching large sets and either expect to find your result in the process. 121 | 122 | **Please take these benchmarks with a grain of salt. Nobody is actually searching massive spaces on the web. Even the worst performance is still far faster than you'd need or notice unless you're doing graphics or crypto.** 123 | 124 | These benchmarks search from a random array of 10,000 items. 125 | 126 | Run `yarn perf` from the repository to reproduce these numbers. 127 | 128 | ### Best case (first item is all we need). 129 | 130 | | approach | ops per second | 131 | | -------- | ----------------- | 132 | | leisure | 6,574,041 ops/sec | 133 | | native | 18,671 ops/sec | 134 | | lodash | 11,602 ops/sec | 135 | 136 | When we find what we are looking for early, our performance is outstanding. 137 | 138 | ### middle case (we need to search half the space). 139 | 140 | | approach | ops per second | 141 | | -------- | -------------- | 142 | | leisure | 7,011 ops/sec | 143 | | native | 18,897 ops/sec | 144 | | lodash | 9,850 ops/sec | 145 | 146 | When the result is in the middle, we are comperable in speed to lodash. 147 | 148 | ### Worst Case (we search the entire space) 149 | 150 | | approach | ops per second | 151 | | -------- | -------------- | 152 | | leisure | 3,304 ops/sec | 153 | | native | 20,487 ops/sec | 154 | | lodash | 10,357 ops/sec | 155 | 156 | In the absolute worst case, we are 3x as slow as lodash. 157 | 158 | ## Prior Art 159 | 160 | This library takes inspiration from Lazy Sequences in [Clojure](https://clojure.org/reference/sequences), [Kotlin](https://kotlinlang.org/docs/reference/sequences.html) and [F\#](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/sequences). 161 | 162 | [Lazy.js](https://github.com/dtao/lazy.js) is a popular lazy sequence library for JavaScript, but it was not written in TypeScript and I prefer to use libraries that think about types and transformations from the beginning. 163 | 164 | There is some overlap between Lazy Sequences and the Functional Reactive Programming libraries. In my opinion, FRP is more concerned about event systems and Lazy Sequences are more useful for data processing. 165 | 166 | ## License 167 | 168 | Leisure is licensed under the the [Hippocratic License](https://firstdonoharm.dev). It is an [Ethical Source license](https://ethicalsource.dev) derived from the MIT License, amended to limit the impact of the unethical use of open source software. 169 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | - [Introduction](README.md) 4 | - [Installation](docs/installation.md) 5 | - [Changelog](https://github.com/tdreyno/leisure/releases) 6 | - [F.A.Q.](docs/faq.md) 7 | 8 | ## API 9 | 10 | - [Static Methods](docs/api/static.md) 11 | - [Instance Methods](docs/api/instance.md) 12 | - [Simplex Methods](docs/api/simplex.md) 13 | - [Combinatorial Methods](docs/api/combinatorics.md) 14 | - [Random Number Generating Methods](docs/api/random.md) 15 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] } 2 | -------------------------------------------------------------------------------- /docs/api/combinatorics.md: -------------------------------------------------------------------------------- 1 | # Combinatorial Methods 2 | 3 | These methods create sequences of the very large combinations of values. 4 | 5 | ## cartesianProduct 6 | 7 | Generates sequence of the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of an arbitrary number of columns of data. 8 | 9 | {% tabs %} 10 | {% tab title="Usage" %} 11 | 12 | ```typescript 13 | const sequence: Seq<[string, number]> = cartesianProduct( 14 | ["a", "b", "c"], 15 | [1, 2, 3], 16 | ) 17 | ``` 18 | 19 | {% endtab %} 20 | 21 | {% tab title="Type Definition" %} 22 | 23 | ```typescript 24 | type cartesianProduct = (...inputs: T[][]) => Seq 25 | ``` 26 | 27 | {% endtab %} 28 | {% endtabs %} 29 | 30 | ## powerSet 31 | 32 | Generates sequence of Sets of a [Power set](https://en.wikipedia.org/wiki/Power_set). 33 | 34 | {% tabs %} 35 | {% tab title="Usage" %} 36 | 37 | ```typescript 38 | const sequence: Seq> = powerSet(["a", "b", "c"]) 39 | ``` 40 | 41 | {% endtab %} 42 | 43 | {% tab title="Type Definition" %} 44 | 45 | ```typescript 46 | type powerSet = (...items: T[]) => Seq> 47 | ``` 48 | 49 | {% endtab %} 50 | {% endtabs %} 51 | 52 | ## combination 53 | 54 | Generates sequence of Sets of a [Combination](https://en.wikipedia.org/wiki/Combination) of a given length. 55 | 56 | {% tabs %} 57 | {% tab title="Usage" %} 58 | 59 | ```typescript 60 | const sequence: Seq> = combination(["a", "b", "c"], 2) 61 | ``` 62 | 63 | {% endtab %} 64 | 65 | {% tab title="Type Definition" %} 66 | 67 | ```typescript 68 | type combination = (items: T[], size: number = items.length) => Seq> 69 | ``` 70 | 71 | {% endtab %} 72 | {% endtabs %} 73 | -------------------------------------------------------------------------------- /docs/api/instance.md: -------------------------------------------------------------------------------- 1 | # Instance Methods 2 | 3 | ## map 4 | 5 | It's `map`, the most important method in programming! Exactly the same as ES6 `map`, but lazy. 6 | 7 | {% tabs %} 8 | {% tab title="Usage" %} 9 | 10 | ```typescript 11 | const sequence: Seq = Seq.infinite().map(num => num.toString()) 12 | ``` 13 | 14 | {% endtab %} 15 | 16 | {% tab title="Type Definition" %} 17 | 18 | ```typescript 19 | type map = (fn: (value: T, index: number) => U) => Seq 20 | ``` 21 | 22 | {% endtab %} 23 | {% endtabs %} 24 | 25 | ## window 26 | 27 | `window` takes a sequence and groups it into "windows" of a certain length. This works well with infinite sequences where you want to process some number of values at a time. 28 | 29 | {% tabs %} 30 | {% tab title="Usage" %} 31 | 32 | ```typescript 33 | // Grab numbers in groups of 10. 34 | const sequence: Seq = Seq.infinite().window(10) 35 | ``` 36 | 37 | By default, only triggers chained responses when the window fills, guaranteeing the window is the exact size expect. Set `allowPartialWindow` to `false` to allow the trailing edge of a sequence to not be divisible by the window size. 38 | 39 | ```typescript 40 | // Gives: [0, 1, 2] -> [3, 4, 5] -> [6, 7, 8] -> [9, 10] 41 | const sequence: Seq = Seq.range(0, 10).window(3) 42 | ``` 43 | 44 | {% endtab %} 45 | 46 | {% tab title="Type Definition" %} 47 | 48 | ```typescript 49 | type window = (size: number, allowPartialWindow = true) => Seq 50 | ``` 51 | 52 | {% endtab %} 53 | {% endtabs %} 54 | 55 | ## pairwise 56 | 57 | Works like `window`, makes the window size 2. Groups a sequence as alternating pairs. Useful for processing data which alternates Map keys and values. 58 | 59 | {% tabs %} 60 | {% tab title="Usage" %} 61 | 62 | ```typescript 63 | const sequence: Seq<[string, number]> = Seq.fromArray(["a", 1, "b", 2]) 64 | ``` 65 | 66 | {% endtab %} 67 | 68 | {% tab title="Type Definition" %} 69 | 70 | ```typescript 71 | type pairwise = () => Seq<[T, T]> 72 | ``` 73 | 74 | {% endtab %} 75 | {% endtabs %} 76 | 77 | ## isEmpty 78 | 79 | Ask whether a sequence is empty. 80 | 81 | {% tabs %} 82 | {% tab title="Usage" %} 83 | 84 | ```typescript 85 | const anythingInThere: boolean = Seq.empty().isEmpty() 86 | ``` 87 | 88 | {% endtab %} 89 | 90 | {% tab title="Type Definition" %} 91 | 92 | ```typescript 93 | type isEmpty = () => boolean 94 | ``` 95 | 96 | {% endtab %} 97 | {% endtabs %} 98 | 99 | ## tap 100 | 101 | `tap` lets you run side-effect generating functions on a sequence. Allows you to "tap in to" a data flow. Very useful for logging and debugging what values are flowing through the chain at a given location. 102 | 103 | {% tabs %} 104 | {% tab title="Usage" %} 105 | 106 | ```typescript 107 | const sequence: Seq = Seq.infinite().tap(num => console.log(num)) 108 | ``` 109 | 110 | {% endtab %} 111 | 112 | {% tab title="Type Definition" %} 113 | 114 | ```typescript 115 | type tap = (fn: (value: T, index: number) => void) => Seq 116 | ``` 117 | 118 | {% endtab %} 119 | {% endtabs %} 120 | 121 | ## log 122 | 123 | `log` provides the most common use-case for `tap. Add this to a sequence chain to log each value that passes through it. 124 | 125 | {% tabs %} 126 | {% tab title="Usage" %} 127 | 128 | ```typescript 129 | const sequence: Seq = Seq.infinite().log() 130 | ``` 131 | 132 | {% endtab %} 133 | 134 | {% tab title="Type Definition" %} 135 | 136 | ```typescript 137 | type log = () => Seq 138 | ``` 139 | 140 | {% endtab %} 141 | {% endtabs %} 142 | 143 | ## flat 144 | 145 | Given a sequence where each item in an array of items, flatten all those arrays into a single flat sequence of values. 146 | 147 | Works just like `Array.prototype.flat`. [See more here.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat) 148 | 149 | {% tabs %} 150 | {% tab title="Usage" %} 151 | 152 | ```typescript 153 | type Person = { name: string; friends: Person[] } 154 | 155 | const sequence: Seq = Seq.fromArray([person1, person2]) 156 | .map(person => person.friends) 157 | .flat() 158 | ``` 159 | 160 | {% endtab %} 161 | 162 | {% tab title="Type Definition" %} 163 | 164 | ```typescript 165 | type flat = (this: Seq) => Seq 166 | ``` 167 | 168 | {% endtab %} 169 | {% endtabs %} 170 | 171 | ## flatMap 172 | 173 | `flatMap` is used when mapping a list to each items related items. For example, if you wanted to map from a list of people to each persons list of friends. Despite each mapping function returning an array, the final output is a flatten array of all the results concattenated. 174 | 175 | Works just like `Array.prototype.flatMap`. [See more here.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) 176 | 177 | Similar to `[].map().flat()`, but in `leisure` the item mappings won't execute until enough of the resulting values have been realized to trigger each map. 178 | 179 | {% tabs %} 180 | {% tab title="Usage" %} 181 | 182 | ```typescript 183 | type Person = { name: string; friends: Person[] } 184 | 185 | const sequence: Seq = Seq.fromArray([person1, person2]).flatMap( 186 | person => person.friends, 187 | ) 188 | ``` 189 | 190 | {% endtab %} 191 | 192 | {% tab title="Type Definition" %} 193 | 194 | ```typescript 195 | type flatMap = (fn: (value: T, index: number) => U[]) => Seq 196 | ``` 197 | 198 | {% endtab %} 199 | {% endtabs %} 200 | 201 | ## filter 202 | 203 | Runs a predicate function on each item in a sequence to produce a new sequence where only the values which responded with `true` remain. 204 | 205 | Exactly the same as `Array.prototype.filter`, but lazy. [See more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). 206 | 207 | {% tabs %} 208 | {% tab title="Usage" %} 209 | 210 | ```typescript 211 | // Create a sequence of only even numbers. 212 | const sequence: Seq = Seq.infinite().filter(num => num % 2 === 0) 213 | ``` 214 | 215 | {% endtab %} 216 | 217 | {% tab title="Type Definition" %} 218 | 219 | ```typescript 220 | type filter = (fn: (value: T, index: number) => unknown) => Seq 221 | ``` 222 | 223 | {% endtab %} 224 | {% endtabs %} 225 | 226 | ## concat 227 | 228 | Combines the current sequence with 1 or more additional sequences. 229 | 230 | Exactly the same as `Array.prototype.concat`, but lazy. [See more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat). 231 | 232 | {% tabs %} 233 | {% tab title="Usage" %} 234 | 235 | ```typescript 236 | const sequence: Seq = Seq.fromArray([0, 1]).concat( 237 | Seq.fromArray([2, 3]), 238 | Seq.fromArray([4, 5]), 239 | ) 240 | ``` 241 | 242 | {% endtab %} 243 | 244 | {% tab title="Type Definition" %} 245 | 246 | ```typescript 247 | type concat = (...tail: Array>) => Seq 248 | ``` 249 | 250 | {% endtab %} 251 | {% endtabs %} 252 | 253 | ## interleave 254 | 255 | Takes 1 or more sequences and creates a new sequence built by pulling the next value from each of the sequences in order. 256 | 257 | {% tabs %} 258 | {% tab title="Usage" %} 259 | 260 | ```typescript 261 | // Builds: a -> 1 -> b -> 2 -> c -> 3 262 | const sequence: Seq = Seq.fromArray([ 263 | "a", 264 | "b", 265 | "c", 266 | ]).interleave(Seq.range(1, 3)) 267 | ``` 268 | 269 | {% endtab %} 270 | 271 | {% tab title="Type Definition" %} 272 | 273 | ```typescript 274 | type interleave = (...tail: Array>) => Seq 275 | ``` 276 | 277 | {% endtab %} 278 | {% endtabs %} 279 | 280 | ## interpose 281 | 282 | Given a sequence, place a value between each value of the original sequence. Useful for adding punctuation between strings. 283 | 284 | {% tabs %} 285 | {% tab title="Usage" %} 286 | 287 | ```typescript 288 | // Builds: Apples -> , -> Oranges -> , -> Bananas 289 | const sequence: Seq = Seq.fromArray([ 290 | "Apples", 291 | "Oranges", 292 | "Bananas", 293 | ]).interpose(", ") 294 | 295 | console.log(sequence.toArray().join("")) 296 | ``` 297 | 298 | {% endtab %} 299 | 300 | {% tab title="Type Definition" %} 301 | 302 | ```typescript 303 | type interpose = (separator: T) => Seq 304 | ``` 305 | 306 | {% endtab %} 307 | {% endtabs %} 308 | 309 | ## distinct 310 | 311 | Given a sequence, only forwards the values which have no already been seen. Very similar to lodash's `uniq` method. 312 | 313 | {% tabs %} 314 | {% tab title="Usage" %} 315 | 316 | ```typescript 317 | // Builds: 1 -> 2 -> 3 -> 4 318 | const sequence: Seq = Seq.fromArray([1, 2, 3, 2, 1, 4]).distinct() 319 | ``` 320 | 321 | {% endtab %} 322 | 323 | {% tab title="Type Definition" %} 324 | 325 | ```typescript 326 | type distinct = () => Seq 327 | ``` 328 | 329 | {% endtab %} 330 | {% endtabs %} 331 | 332 | ## distinctBy 333 | 334 | Same as `distinct`, but allows a function to describe on what value the sequence should be unique. 335 | 336 | {% tabs %} 337 | {% tab title="Usage" %} 338 | 339 | ```typescript 340 | // Builds: { firstName: "A", lastName: "Z" } -> 341 | // { firstName: "B", lastName: "Y" } -> 342 | // { firstName: "C", lastName: "W" } 343 | type Person = { firstName: string; lastName: string } 344 | const sequence: Seq = Seq.fromArray([ 345 | { firstName: "A", lastName: "Z" }, 346 | { firstName: "B", lastName: "Y" }, 347 | { firstName: "A", lastName: "X" }, 348 | { firstName: "C", lastName: "W" }, 349 | ]).distinctBy(person => person.firstName) 350 | ``` 351 | 352 | {% endtab %} 353 | 354 | {% tab title="Type Definition" %} 355 | 356 | ```typescript 357 | type distinctBy = (fn: (value: T) => U) => Seq 358 | ``` 359 | 360 | {% endtab %} 361 | {% endtabs %} 362 | 363 | ## partitionBy 364 | 365 | Given a sequence, splits the values into two separate sequences. One represents the values where the partition function is `true` and the other for `false`. 366 | 367 | {% tabs %} 368 | {% tab title="Usage" %} 369 | 370 | ```typescript 371 | const [isEven, isOdd] = Seq.infinite().partitionBy(num => num % 2 === 0) 372 | ``` 373 | 374 | {% endtab %} 375 | 376 | {% tab title="Type Definition" %} 377 | 378 | ```typescript 379 | type partition = (fn: (value: T, index: number) => unknown) => [Seq, Seq] 380 | ``` 381 | 382 | {% endtab %} 383 | {% endtabs %} 384 | 385 | ## includes 386 | 387 | Lazily checks if the sequence includes a value. 388 | 389 | Exactly the same as `Array.prototype.includes`, but lazy. [See more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes). 390 | 391 | {% tabs %} 392 | {% tab title="Usage" %} 393 | 394 | ```typescript 395 | const doesItInclude = Seq.infinite().includes(10) 396 | ``` 397 | 398 | {% endtab %} 399 | 400 | {% tab title="Type Definition" %} 401 | 402 | ```typescript 403 | type includes = (value: T) => boolean 404 | ``` 405 | 406 | {% endtab %} 407 | {% endtabs %} 408 | 409 | ## find 410 | 411 | Lazily searches for a value that matches the predicate. 412 | 413 | Exactly the same as `Array.prototype.find`, but lazy. [See more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find). 414 | 415 | {% tabs %} 416 | {% tab title="Usage" %} 417 | 418 | ```typescript 419 | // Returns 11 420 | const gtTen = Seq.infinite().find(num => num > 10) 421 | ``` 422 | 423 | {% endtab %} 424 | 425 | {% tab title="Type Definition" %} 426 | 427 | ```typescript 428 | type find = (fn: (value: T, index: number) => unknown) => T | undefined 429 | ``` 430 | 431 | {% endtab %} 432 | {% endtabs %} 433 | 434 | ## reduce 435 | 436 | Exactly the same as `Array.prototype.reduce`. [See more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce). This causes a full realization of the data. Not lazy. 437 | 438 | {% tabs %} 439 | {% tab title="Usage" %} 440 | 441 | ```typescript 442 | // Returns 0 + 1 + 2 + 3 + 4 = 10 443 | const sum = Seq.infinite() 444 | .take(5) 445 | .reduce((sum, num) => sum + num) 446 | ``` 447 | 448 | {% endtab %} 449 | 450 | {% tab title="Type Definition" %} 451 | 452 | ```typescript 453 | type reduce = (fn: (sum: A, value: T, index: number) => A, initial: A) => A 454 | ``` 455 | 456 | {% endtab %} 457 | {% endtabs %} 458 | 459 | ## chain 460 | 461 | This method is helpful for chaining. Shocking, I know. Let's you "map" the entire sequence in a chain, rather than per-each-item. Allows adding arbitrary sequence helpers and methods to chain, even if they are written in user-land and not on the `Seq` prototype. 462 | 463 | {% tabs %} 464 | {% tab title="Usage" %} 465 | 466 | ```typescript 467 | // Same as `Seq.interpose(Seq.infinite(), Seq.infinite())` 468 | const sequence = Seq.infinite().chain(seq => seq.interpose(Seq.infinite())) 469 | ``` 470 | 471 | {% endtab %} 472 | 473 | {% tab title="Type Definition" %} 474 | 475 | ```typescript 476 | type chain = (fn: (value: Seq) => U) => U 477 | ``` 478 | 479 | {% endtab %} 480 | {% endtabs %} 481 | 482 | ## some 483 | 484 | Exactly the same as `Array.prototype.some`, but lazy. [See more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some). 485 | 486 | {% tabs %} 487 | {% tab title="Usage" %} 488 | 489 | ```typescript 490 | // Find the first even random number. 491 | const areAnyEven = Seq.random() 492 | .map(num => Math.round(num * 1000)) 493 | .some(num => num % 2 === 0) 494 | ``` 495 | 496 | {% endtab %} 497 | 498 | {% tab title="Type Definition" %} 499 | 500 | ```typescript 501 | type some = (fn: (value: T, index: number) => unknown) => boolean 502 | ``` 503 | 504 | {% endtab %} 505 | {% endtabs %} 506 | 507 | ## every 508 | 509 | Exactly the same as `Array.prototype.every`, but lazy. [See more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every). 510 | 511 | {% tabs %} 512 | {% tab title="Usage" %} 513 | 514 | ```typescript 515 | // Fails fast if there are negative numbers 516 | const areAllPositive = Seq.random() 517 | .map(num => Math.round(num * 1000) - 500) 518 | .every(num => num > 0) 519 | ``` 520 | 521 | {% endtab %} 522 | 523 | {% tab title="Type Definition" %} 524 | 525 | ```typescript 526 | type every = (fn: (value: T, index: number) => unknown) => boolean 527 | ``` 528 | 529 | {% endtab %} 530 | {% endtabs %} 531 | 532 | ## take 533 | 534 | Given a sequence of unknown length, create a sub sequence of just the first X number of items. 535 | 536 | {% tabs %} 537 | {% tab title="Usage" %} 538 | 539 | ```typescript 540 | // Grabs 0 -> 1 -> 2 -> 3 -> 4 541 | const firstFive = Seq.infinite().take(5) 542 | ``` 543 | 544 | {% endtab %} 545 | 546 | {% tab title="Type Definition" %} 547 | 548 | ```typescript 549 | type take = (num: number) => Seq 550 | ``` 551 | 552 | {% endtab %} 553 | {% endtabs %} 554 | 555 | ## takeWhile 556 | 557 | Given a sequence of unknown length, create a sub sequence of as many items in a row that satisfy the predicate. 558 | 559 | {% tabs %} 560 | {% tab title="Usage" %} 561 | 562 | ```typescript 563 | // Gives 0 -> 1 -> 2 -> 3 -> 4 564 | const lessThanFive = Seq.infinite().takeWhile(num => num < 5) 565 | ``` 566 | 567 | {% endtab %} 568 | 569 | {% tab title="Type Definition" %} 570 | 571 | ```typescript 572 | type takeWhile = (fn: (value: T, index: number) => unknown) => Seq 573 | ``` 574 | 575 | {% endtab %} 576 | {% endtabs %} 577 | 578 | ## skip 579 | 580 | Given a sequence of unknown length, skips the first X number of items. 581 | 582 | {% tabs %} 583 | {% tab title="Usage" %} 584 | 585 | ```typescript 586 | // Gives 5 -> 6 -> 7 -> 8 -> 9 587 | const secondFive = Seq.infinite().skip(5).take(5) 588 | ``` 589 | 590 | {% endtab %} 591 | 592 | {% tab title="Type Definition" %} 593 | 594 | ```typescript 595 | type skip = (num: number) => Seq 596 | ``` 597 | 598 | {% endtab %} 599 | {% endtabs %} 600 | 601 | ## skipWhile 602 | 603 | Given a sequence of unknown length, skip as many items in a row that satisfy the predicate. 604 | 605 | {% tabs %} 606 | {% tab title="Usage" %} 607 | 608 | ```typescript 609 | // Gives 5 -> 6 -> 7 -> 8 -> 9 610 | const greaterThanFive = Seq.infinite() 611 | .skipWhile(num => num < 5) 612 | .take(5) 613 | ``` 614 | 615 | {% endtab %} 616 | 617 | {% tab title="Type Definition" %} 618 | 619 | ```typescript 620 | type skipWhile = (fn: (value: T, index: number) => unknown) => Seq 621 | ``` 622 | 623 | {% endtab %} 624 | {% endtabs %} 625 | 626 | ## nth 627 | 628 | Returns the `nth` item. Items are 1-indexed. 629 | 630 | {% tabs %} 631 | {% tab title="Usage" %} 632 | 633 | ```typescript 634 | const thirdItem = Seq.infinite().nth(3) 635 | ``` 636 | 637 | {% endtab %} 638 | 639 | {% tab title="Type Definition" %} 640 | 641 | ```typescript 642 | type nth = (i: number) => T | undefined 643 | ``` 644 | 645 | {% endtab %} 646 | {% endtabs %} 647 | 648 | ## index 649 | 650 | Returns the `index` item. Items are 0-indexed. 651 | 652 | {% tabs %} 653 | {% tab title="Usage" %} 654 | 655 | ```typescript 656 | const fourthItem = Seq.infinite().index(3) 657 | ``` 658 | 659 | {% endtab %} 660 | 661 | {% tab title="Type Definition" %} 662 | 663 | ```typescript 664 | type nth = (i: number) => T | undefined 665 | ``` 666 | 667 | {% endtab %} 668 | {% endtabs %} 669 | 670 | ## first 671 | 672 | Gets the first value in the sequence. 673 | 674 | {% tabs %} 675 | {% tab title="Usage" %} 676 | 677 | ```typescript 678 | const fifth = Seq.infinite().skip(4).first() 679 | ``` 680 | 681 | {% endtab %} 682 | 683 | {% tab title="Type Definition" %} 684 | 685 | ```typescript 686 | type first = () => T | undefined 687 | ``` 688 | 689 | {% endtab %} 690 | {% endtabs %} 691 | 692 | ## zip 693 | 694 | Lazily combines a second sequence with this current one to produce a tuple with the current step in each of the two positions. Useful for zipping a sequence of keys with a sequence of values, before converting to a Map of key to value. 695 | 696 | {% tabs %} 697 | {% tab title="Usage" %} 698 | 699 | ```typescript 700 | const seq2 = Seq.range(0, 3) 701 | 702 | // Gives: ["zero", 0] -> ["one", 1] -> ["two", 2] -> ["three", 3] 703 | const sequence: Seq<[string, number]> = Seq.fromArray([ 704 | "zero", 705 | "one", 706 | "two", 707 | "three", 708 | ]).zip(seq2) 709 | ``` 710 | 711 | {% endtab %} 712 | 713 | {% tab title="Type Definition" %} 714 | 715 | ```typescript 716 | type zip = (seq2: Seq) => Seq<[T | undefined, T2 | undefined]> 717 | ``` 718 | 719 | {% endtab %} 720 | {% endtabs %} 721 | 722 | ## zipWith 723 | 724 | Takes a second sequence and lazily combines it to produce an arbitrary value by mapping the current value of the two positions through a user-supplied function. Useful for table \(row/col\) math. 725 | 726 | {% tabs %} 727 | {% tab title="Usage" %} 728 | 729 | ```typescript 730 | const seq2 = Seq.repeat(2) 731 | 732 | // Gives: 0 -> 2 -> 4 -> 6 733 | const sequence: Seq = Seq.range(0, 3).zipWith( 734 | ([num, multiplier]) => num * multiplier, 735 | seq2, 736 | ) 737 | ``` 738 | 739 | {% endtab %} 740 | 741 | {% tab title="Type Definition" %} 742 | 743 | ```typescript 744 | type zip2With = ( 745 | fn: ( 746 | [result1, result2, result3]: 747 | | [T, T2, T3] 748 | | [T, undefined, undefined] 749 | | [T, T2, undefined] 750 | | [T, undefined, T3] 751 | | [undefined, T2, undefined] 752 | | [undefined, T2, T3] 753 | | [undefined, undefined, T3], 754 | index: number, 755 | ) => T4, 756 | seq2: Seq, 757 | seq3: Seq, 758 | ) => Seq 759 | ``` 760 | 761 | {% endtab %} 762 | {% endtabs %} 763 | 764 | ## zip2 765 | 766 | Takes two sequences and lazily combines them with this one to produce a 3-tuple with the current step in each of the three positions. 767 | 768 | {% tabs %} 769 | {% tab title="Usage" %} 770 | 771 | ```typescript 772 | const seq2 = Seq.range(0, 3) 773 | const seq3 = Seq.range(3, 0) 774 | 775 | // Gives: ["zero", 0, 3] -> ["one", 1, 2] -> ["two", 2, 1] -> ["three", 3, 0] 776 | const sequence: Seq<[string, number]> = Seq.fromArray([ 777 | "zero", 778 | "one", 779 | "two", 780 | "three", 781 | ]).zip2(seq2, seq3) 782 | ``` 783 | 784 | {% endtab %} 785 | 786 | {% tab title="Type Definition" %} 787 | 788 | ```typescript 789 | type zip2 = ( 790 | seq2: Seq, 791 | seq3: Seq, 792 | ) => Seq<[T | undefined, T2 | undefined, T3 | undefined]> 793 | ``` 794 | 795 | {% endtab %} 796 | {% endtabs %} 797 | 798 | ## zip2With 799 | 800 | Takes two sequences and lazily combine them with this sequence to produce an arbitrary value by mapping the current value of the three positions through a user-supplied function. 801 | 802 | {% tabs %} 803 | {% tab title="Usage" %} 804 | 805 | ```typescript 806 | const seq2 = Seq.repeat(2) 807 | const seq3 = Seq.repeat(1) 808 | 809 | // Gives: 0 -> 2 -> 4 -> 6 810 | const sequence: Seq = Seq.range(0, 3).zip2With( 811 | ([num, multiplier, divisor]) => (num * multiplier) / divisor, 812 | seq2, 813 | seq3, 814 | ) 815 | ``` 816 | 817 | {% endtab %} 818 | 819 | {% tab title="Type Definition" %} 820 | 821 | ```typescript 822 | type zip2With = ( 823 | fn: ( 824 | [result1, result2, result3]: 825 | | [T, T2, T3] 826 | | [T, undefined, undefined] 827 | | [T, T2, undefined] 828 | | [T, undefined, T3] 829 | | [undefined, T2, undefined] 830 | | [undefined, T2, T3] 831 | | [undefined, undefined, T3], 832 | index: number, 833 | ) => T4, 834 | seq2: Seq, 835 | seq3: Seq, 836 | ) => Seq 837 | ``` 838 | 839 | {% endtab %} 840 | {% endtabs %} 841 | 842 | ## toArray 843 | 844 | Converts the sequence to a real JavaScript array. Realizes the entire sequence. 845 | 846 | {% tabs %} 847 | {% tab title="Usage" %} 848 | 849 | ```typescript 850 | const lessThanTen = Seq.infinite().take(10).toArray() 851 | ``` 852 | 853 | {% endtab %} 854 | 855 | {% tab title="Type Definition" %} 856 | 857 | ```typescript 858 | type toArray = () => T[] 859 | ``` 860 | 861 | {% endtab %} 862 | {% endtabs %} 863 | 864 | ## forEach 865 | 866 | Works just like `Array.prototype.forEach`. [See more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach). Realizes the full sequence. 867 | 868 | {% tabs %} 869 | {% tab title="Usage" %} 870 | 871 | ```typescript 872 | 873 | ``` 874 | 875 | {% endtab %} 876 | 877 | {% tab title="Type Definition" %} 878 | 879 | ```typescript 880 | type forEach = (fn: (value: T, index: number) => void) => void 881 | ``` 882 | 883 | {% endtab %} 884 | {% endtabs %} 885 | 886 | ## sum 887 | 888 | Given a sequence of numbers, adds them all together. This realizes the entire sequence. 889 | 890 | {% tabs %} 891 | {% tab title="Usage" %} 892 | 893 | ```typescript 894 | // Returns 0 + 1 + 2 + 3 + 4 = 10 895 | const sum = Seq.infinite().take(5).sum() 896 | ``` 897 | 898 | {% endtab %} 899 | 900 | {% tab title="Type Definition" %} 901 | 902 | ```typescript 903 | type sum = (this: Seq) => number 904 | ``` 905 | 906 | {% endtab %} 907 | {% endtabs %} 908 | 909 | ## sumBy 910 | 911 | Given a sequence of arbitrary data, adds together the result of the mapping function. This realizes the entire sequence. 912 | 913 | {% tabs %} 914 | {% tab title="Usage" %} 915 | 916 | ```typescript 917 | // Returns 0 + 1 + 2 + 3 + 4 = 10 918 | const sum = Seq.fromArray([ 919 | { balance: 0 }, 920 | { balance: 1 }, 921 | { balance: 2 }, 922 | { balance: 3 }, 923 | { balance: 4 }, 924 | ]).sumBy(user => user.balance) 925 | ``` 926 | 927 | ```` 928 | 929 | {% endtab %} 930 | 931 | {% tab title="Type Definition" %} 932 | 933 | ```typescript 934 | type sumBy = (fn: (value: T) => number) => number; 935 | ```` 936 | 937 | {% endtab %} 938 | {% endtabs %} 939 | 940 | ## average 941 | 942 | Given a sequence of numbers, averages them all together. Tise realizes the entire sequence. 943 | 944 | {% tabs %} 945 | {% tab title="Usage" %} 946 | 947 | ```typescript 948 | // Returns (0 + 1 + 2 + 3 + 4) / 5 = 2 949 | const sum = Seq.infinite().take(5).average() 950 | ``` 951 | 952 | {% endtab %} 953 | 954 | {% tab title="Type Definition" %} 955 | 956 | ```typescript 957 | type average = (this: Seq) => number 958 | ``` 959 | 960 | {% endtab %} 961 | {% endtabs %} 962 | 963 | ## averageBy 964 | 965 | Given a sequence of arbitrary data, averages together the result of the mapping function. This realizes the entire sequence. 966 | 967 | {% tabs %} 968 | {% tab title="Usage" %} 969 | 970 | ```typescript 971 | // Returns (0 + 1 + 2 + 3 + 4) / 5 = 2 972 | const sum = Seq.fromArray([ 973 | { balance: 0 }, 974 | { balance: 1 }, 975 | { balance: 2 }, 976 | { balance: 3 }, 977 | { balance: 4 }, 978 | ]).averageBy(user => user.balance) 979 | ``` 980 | 981 | {% endtab %} 982 | 983 | {% tab title="Type Definition" %} 984 | 985 | ```typescript 986 | type averageBy = (fn: (value: T) => number) => number 987 | ``` 988 | 989 | {% endtab %} 990 | {% endtabs %} 991 | 992 | ## frequencies 993 | 994 | Given a non-infinite sequence, return a `Map` which counts the occurances of each unique value. This realizes the entire sequence. 995 | 996 | {% tabs %} 997 | {% tab title="Usage" %} 998 | 999 | ```typescript 1000 | // Returns a Map of numbers from 0->100 and how many times they randomly occured in this set of 500. 1001 | const freq = Seq.random() 1002 | .map(num => Math.round(num * 100)) 1003 | .take(500) 1004 | .frequencies() 1005 | ``` 1006 | 1007 | {% endtab %} 1008 | 1009 | {% tab title="Type Definition" %} 1010 | 1011 | ```typescript 1012 | type frequencies = () => Map 1013 | ``` 1014 | 1015 | {% endtab %} 1016 | {% endtabs %} 1017 | 1018 | ## groupBy 1019 | 1020 | Group a sequence by the return of a mapping function. This realizes the entire sequence. 1021 | 1022 | {% tabs %} 1023 | {% tab title="Usage" %} 1024 | 1025 | ```typescript 1026 | // Random generates 1000 years between 0-2000 and 1027 | // groups them by decade. 1028 | const groupedByDecade = Seq.random() 1029 | .map(num => Math.round(num * 2000)) 1030 | .take(100) 1031 | .groupBy(year => Math.round(year / 10)) 1032 | ``` 1033 | 1034 | {% endtab %} 1035 | 1036 | {% tab title="Type Definition" %} 1037 | 1038 | ```typescript 1039 | type groupBy = (fn: (item: T) => U) => Map 1040 | ``` 1041 | 1042 | {% endtab %} 1043 | {% endtabs %} 1044 | -------------------------------------------------------------------------------- /docs/api/random.md: -------------------------------------------------------------------------------- 1 | # Random Number Generating Methods 2 | 3 | These methods create reproducible sequences of random numbers given an initial seed value. 4 | 5 | This methods rely on the [pure-rand](https://github.com/dubzzz/pure-rand) project. 6 | 7 | ## random 8 | 9 | Generates random numbers using the Mersenne Twister generator whose values are within the range 0 to 0xffffffff. 10 | 11 | {% tabs %} 12 | {% tab title="Usage" %} 13 | 14 | ```typescript 15 | const SEED = 5 16 | const sequence: Seq = random(SEED) 17 | ``` 18 | 19 | {% endtab %} 20 | 21 | {% tab title="Type Definition" %} 22 | 23 | ```typescript 24 | type random = (seed = DEFAULT_SEED) => Seq 25 | ``` 26 | 27 | {% endtab %} 28 | {% endtabs %} 29 | 30 | ## mersenne 31 | 32 | Generates random numbers using the Mersenne Twister generator whose values are within the range 0 to 0xffffffff. 33 | 34 | {% tabs %} 35 | {% tab title="Usage" %} 36 | 37 | ```typescript 38 | const SEED = 5 39 | const sequence: Seq = mersenne(SEED) 40 | ``` 41 | 42 | {% endtab %} 43 | 44 | {% tab title="Type Definition" %} 45 | 46 | ```typescript 47 | type mersenne = (seed = DEFAULT_SEED) => Seq 48 | ``` 49 | 50 | {% endtab %} 51 | {% endtabs %} 52 | 53 | ## xorshift128plus 54 | 55 | Generates random numbers using the xorshift128+ generator whose values are within the range -0x80000000 to 0x7fffffff. 56 | 57 | {% tabs %} 58 | {% tab title="Usage" %} 59 | 60 | ```typescript 61 | const SEED = 5 62 | const sequence: Seq = xorshift128plus(SEED) 63 | ``` 64 | 65 | {% endtab %} 66 | 67 | {% tab title="Type Definition" %} 68 | 69 | ```typescript 70 | type xorshift128plus = (seed = DEFAULT_SEED) => Seq 71 | ``` 72 | 73 | {% endtab %} 74 | {% endtabs %} 75 | 76 | ## xoroshiro128plus 77 | 78 | Generates random numbers using the xoroshiro128+ generator whose values are within the range -0x80000000 to 0x7fffffff. 79 | 80 | {% tabs %} 81 | {% tab title="Usage" %} 82 | 83 | ```typescript 84 | const SEED = 5 85 | const sequence: Seq = xoroshiro128plus(SEED) 86 | ``` 87 | 88 | {% endtab %} 89 | 90 | {% tab title="Type Definition" %} 91 | 92 | ```typescript 93 | type xoroshiro128plus = (seed = DEFAULT_SEED) => Seq 94 | ``` 95 | 96 | {% endtab %} 97 | {% endtabs %} 98 | 99 | ## congruential 100 | 101 | Generates random numbers using a Linear Congruential generator whose values are within the range 0 to 0x7fff. 102 | 103 | {% tabs %} 104 | {% tab title="Usage" %} 105 | 106 | ```typescript 107 | const SEED = 5 108 | const sequence: Seq = congruential(SEED) 109 | ``` 110 | 111 | {% endtab %} 112 | 113 | {% tab title="Type Definition" %} 114 | 115 | ```typescript 116 | type congruential = (seed = DEFAULT_SEED) => Seq 117 | ``` 118 | 119 | {% endtab %} 120 | {% endtabs %} 121 | 122 | ## congruential32 123 | 124 | Generates random numbers using a Linear Congruential generator whose values are within the range 0 to 0xffffffff. 125 | 126 | {% tabs %} 127 | {% tab title="Usage" %} 128 | 129 | ```typescript 130 | const SEED = 5 131 | const sequence: Seq = congruential32(SEED) 132 | ``` 133 | 134 | {% endtab %} 135 | 136 | {% tab title="Type Definition" %} 137 | 138 | ```typescript 139 | type congruential32 = (seed = DEFAULT_SEED) => Seq 140 | ``` 141 | 142 | {% endtab %} 143 | {% endtabs %} 144 | -------------------------------------------------------------------------------- /docs/api/simplex.md: -------------------------------------------------------------------------------- 1 | # Simplex Methods 2 | 3 | These methods create sequences of simplex noise. 4 | 5 | ## simplex2D 6 | 7 | Generates a 2d simplex noise. 8 | 9 | {% tabs %} 10 | {% tab title="Usage" %} 11 | 12 | ```typescript 13 | const SEED = 5 14 | const sequence: Seq = simplex2D((x, y) => x + y, SEED) 15 | ``` 16 | 17 | {% endtab %} 18 | 19 | {% tab title="Type Definition" %} 20 | 21 | ```typescript 22 | type simplex2D = ( 23 | fn: () => [number, number], 24 | seed: number = Date.now(), 25 | ) => Seq 26 | ``` 27 | 28 | {% endtab %} 29 | {% endtabs %} 30 | 31 | ## simplex3D 32 | 33 | Generates a 3d simplex noise. 34 | 35 | {% tabs %} 36 | {% tab title="Usage" %} 37 | 38 | ```typescript 39 | const SEED = 5 40 | const sequence: Seq = simplex3D((x, y, z) => x + y + z, SEED) 41 | ``` 42 | 43 | {% endtab %} 44 | 45 | {% tab title="Type Definition" %} 46 | 47 | ```typescript 48 | type simplex3D = ( 49 | fn: () => [number, number, number], 50 | seed: number = Date.now(), 51 | ) => Seq 52 | ``` 53 | 54 | {% endtab %} 55 | {% endtabs %} 56 | 57 | ## simplex4D 58 | 59 | Generates a 4d simplex noise. 60 | 61 | {% tabs %} 62 | {% tab title="Usage" %} 63 | 64 | ```typescript 65 | const SEED = 5 66 | const sequence: Seq = simplex4D((x, y, z, w) => x + y + z + w, SEED) 67 | ``` 68 | 69 | {% endtab %} 70 | 71 | {% tab title="Type Definition" %} 72 | 73 | ```typescript 74 | type simplex4D = ( 75 | fn: () => [number, number, number, number], 76 | seed: number = Date.now(), 77 | ) => Seq 78 | ``` 79 | 80 | {% endtab %} 81 | {% endtabs %} 82 | -------------------------------------------------------------------------------- /docs/api/static.md: -------------------------------------------------------------------------------- 1 | # Static Methods 2 | 3 | ## fromArray 4 | 5 | Takes a normal JavaScript array and turns it into a Sequence of the same type. 6 | 7 | {% tabs %} 8 | {% tab title="Usage" %} 9 | 10 | ```typescript 11 | const sequence: Seq = fromArray([1, 2, 3]) 12 | ``` 13 | 14 | {% endtab %} 15 | 16 | {% tab title="Type Definition" %} 17 | 18 | ```typescript 19 | type fromArray = (data: T[]) => Seq 20 | ``` 21 | 22 | {% endtab %} 23 | {% endtabs %} 24 | 25 | ## iterate 26 | 27 | The `iterate` method simplifies the construction of infinite sequences which are built using their previous value. 28 | 29 | {% tabs %} 30 | {% tab title="Usage" %} 31 | 32 | ```typescript 33 | // Builds an infinite sequence that counts up from 0. 34 | const sequence: Seq = iterate(a => a + 1, 0) 35 | ``` 36 | 37 | {% endtab %} 38 | 39 | {% tab title="Type Definition" %} 40 | 41 | ```typescript 42 | type iterate = (fn: (current: T) => T, start: T): Seq 43 | ``` 44 | 45 | {% endtab %} 46 | {% endtabs %} 47 | 48 | ## fib 49 | 50 | The `fib` method generates a sequence of fibonacci numbers. 51 | 52 | {% tabs %} 53 | {% tab title="Usage" %} 54 | 55 | ```typescript 56 | const sequence: Seq = fib() 57 | ``` 58 | 59 | {% endtab %} 60 | 61 | {% tab title="Type Definition" %} 62 | 63 | ```typescript 64 | type fib = () => Seq 65 | ``` 66 | 67 | {% endtab %} 68 | {% endtabs %} 69 | 70 | ## random 71 | 72 | Generates a random sequence using `Math.random`. 73 | 74 | {% tabs %} 75 | {% tab title="Usage" %} 76 | 77 | ```typescript 78 | // Builds sequence of random numbers between 0 and 1. 79 | const sequence: Seq = random() 80 | ``` 81 | 82 | {% endtab %} 83 | 84 | {% tab title="Type Definition" %} 85 | 86 | ```typescript 87 | type random = () => Seq 88 | ``` 89 | 90 | {% endtab %} 91 | {% endtabs %} 92 | 93 | ## range 94 | 95 | Creates a sequence that counts between a start and end value. Takes an optional step parameter. 96 | 97 | {% tabs %} 98 | {% tab title="Usage" %} 99 | 100 | ```typescript 101 | // Step by 1 from 0 through 10. 102 | const sequence: Seq = range(0, 10) 103 | ``` 104 | 105 | ```typescript 106 | // Step by 1 from 0 through Infinity. 107 | const sequence: Seq = range(0, Infinity) 108 | ``` 109 | 110 | ```typescript 111 | // Step by 1 from -10 through +10. 112 | const sequence: Seq = range(-10, 10) 113 | ``` 114 | 115 | ```typescript 116 | // Step down from +10 through -10. 117 | const sequence: Seq = range(10, -10) 118 | ``` 119 | 120 | ```typescript 121 | // Step by 2 from 0 through 10. 122 | const sequence: Seq = range(0, 10, 2) 123 | ``` 124 | 125 | {% endtab %} 126 | 127 | {% tab title="Type Definition" %} 128 | 129 | ```typescript 130 | type range( 131 | start: number, 132 | end: number, 133 | step = 1 134 | ) => Seq; 135 | ``` 136 | 137 | {% endtab %} 138 | {% endtabs %} 139 | 140 | ## infinite 141 | 142 | A sequence that counts from `0` to `Infinity`. 143 | 144 | {% tabs %} 145 | {% tab title="Usage" %} 146 | 147 | ```typescript 148 | const sequence: Seq = infinite() 149 | ``` 150 | 151 | {% endtab %} 152 | 153 | {% tab title="Type Definition" %} 154 | 155 | ```typescript 156 | type infinite = () => Seq 157 | ``` 158 | 159 | {% endtab %} 160 | {% endtabs %} 161 | 162 | ## of 163 | 164 | A sequence with only a single value inside. Also known as "singleton." 165 | 166 | [Similar to Array.of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/of). 167 | 168 | {% tabs %} 169 | {% tab title="Usage" %} 170 | 171 | ```typescript 172 | const sequence: Seq = of(2) 173 | ``` 174 | 175 | {% endtab %} 176 | 177 | {% tab title="Type Definition" %} 178 | 179 | ```typescript 180 | type of = (...values: T[]): Seq; 181 | ``` 182 | 183 | {% endtab %} 184 | {% endtabs %} 185 | 186 | ## cycle 187 | 188 | Create a sequence that is the infinite repetition of a series of values. 189 | 190 | {% tabs %} 191 | {% tab title="Usage" %} 192 | 193 | ```typescript 194 | const sequence: Seq = cycle(["a", "b", "c"]) 195 | ``` 196 | 197 | {% endtab %} 198 | 199 | {% tab title="Type Definition" %} 200 | 201 | ```typescript 202 | type cycle = (items: T[]) => Seq 203 | ``` 204 | 205 | {% endtab %} 206 | {% endtabs %} 207 | 208 | ## repeat 209 | 210 | Create a sequence which repeats the same value X number of times. The length of the sequence defaults to Infinity. 211 | 212 | {% tabs %} 213 | {% tab title="Usage" %} 214 | 215 | ```typescript 216 | // Creates a sequence of 5 true values. 217 | const sequence: Seq = repeat(true, 5) 218 | ``` 219 | 220 | ```typescript 221 | // Creates a sequence of 0 which repeats infinitely. 222 | const sequence: Seq = repeat(0) 223 | ``` 224 | 225 | {% endtab %} 226 | 227 | {% tab title="Type Definition" %} 228 | 229 | ```typescript 230 | type repeat = (value: T, times = Infinity) => Seq 231 | ``` 232 | 233 | {% endtab %} 234 | {% endtabs %} 235 | 236 | ## repeatedly 237 | 238 | Creates a sequence which pulls from an impure callback to generate the sequence. The second parameter can cap the length of the sequence. By default, it will call the callback infinitely to generate values. Useful for asking about current time or cache values. 239 | 240 | {% tabs %} 241 | {% tab title="Usage" %} 242 | 243 | ```typescript 244 | const sequence: Seq = repeatedly(() => new Date()) 245 | ``` 246 | 247 | {% endtab %} 248 | 249 | {% tab title="Type Definition" %} 250 | 251 | ```typescript 252 | type repeatedly = (value: () => T, times = Infinity) => Seq 253 | ``` 254 | 255 | {% endtab %} 256 | {% endtabs %} 257 | 258 | ## empty 259 | 260 | A sequence with nothing in it. Useful as a "no op" for certain code-paths when joining sequences together. 261 | 262 | {% tabs %} 263 | {% tab title="Usage" %} 264 | 265 | ```typescript 266 | const sequence: Seq = empty() 267 | ``` 268 | 269 | {% endtab %} 270 | 271 | {% tab title="Type Definition" %} 272 | 273 | ```typescript 274 | type empty = () => Seq 275 | ``` 276 | 277 | {% endtab %} 278 | {% endtabs %} 279 | 280 | ## zip 281 | 282 | Takes two sequences and lazily combines them to produce a tuple with the current step in each of the two positions. Useful for zipping a sequence of keys with a sequence of values, before converting to a Map of key to value. 283 | 284 | {% tabs %} 285 | {% tab title="Usage" %} 286 | 287 | ```typescript 288 | const seq1 = fromArray(["zero", "one", "two", "three"]) 289 | const seq2 = range(0, 3) 290 | 291 | // Gives: ["zero", 0] -> ["one", 1] -> ["two", 2] -> ["three", 3] 292 | const sequence: Seq<[string, number]> = zip(seq1, seq2) 293 | ``` 294 | 295 | {% endtab %} 296 | 297 | {% tab title="Type Definition" %} 298 | 299 | ```typescript 300 | type zip = ( 301 | seq1: Seq, 302 | seq2: Seq, 303 | ) => Seq<[T1 | undefined, T2 | undefined]> 304 | ``` 305 | 306 | {% endtab %} 307 | {% endtabs %} 308 | 309 | ## zipWith 310 | 311 | Takes two sequences and lazily combines them to produce an arbitrary value by mapping the current value of the two positions through a user-supplied function. Useful for table \(row/col\) math. 312 | 313 | {% tabs %} 314 | {% tab title="Usage" %} 315 | 316 | ```typescript 317 | const seq1 = range(0, 3) 318 | const seq2 = repeat(2) 319 | 320 | // Gives: 0 -> 2 -> 4 -> 6 321 | const sequence: Seq = zipWith( 322 | ([num, multiplier]) => num * multiplier, 323 | seq1, 324 | seq2, 325 | ) 326 | ``` 327 | 328 | {% endtab %} 329 | 330 | {% tab title="Type Definition" %} 331 | 332 | ```typescript 333 | type zipWith = ( 334 | fn: ( 335 | [result1, result2]: [T1, T2] | [T1, undefined] | [undefined, T2], 336 | index: number, 337 | ) => T3, 338 | seq1: Seq, 339 | seq2: Seq, 340 | ) => Seq 341 | ``` 342 | 343 | {% endtab %} 344 | {% endtabs %} 345 | 346 | ## zip3 347 | 348 | Takes three sequences and lazily combines them to produce a 3-tuple with the current step in each of the three positions. 349 | 350 | {% tabs %} 351 | {% tab title="Usage" %} 352 | 353 | ```typescript 354 | const seq1 = fromArray(["zero", "one", "two", "three"]) 355 | const seq2 = range(0, 3) 356 | const seq3 = range(3, 0) 357 | 358 | // Gives: ["zero", 0, 3] -> ["one", 1, 2] -> ["two", 2, 1] -> ["three", 3, 0] 359 | const sequence: Seq<[string, number]> = zip3(seq1, seq2, seq3) 360 | ``` 361 | 362 | {% endtab %} 363 | 364 | {% tab title="Type Definition" %} 365 | 366 | ```typescript 367 | type zip3 = ( 368 | seq1: Seq, 369 | seq2: Seq, 370 | seq3: Seq, 371 | ) => Seq<[T1 | undefined, T2 | undefined, T3 | undefined]> 372 | ``` 373 | 374 | {% endtab %} 375 | {% endtabs %} 376 | 377 | ## zip3With 378 | 379 | Takes three sequences and lazily combines them to produce an arbitrary value by mapping the current value of the three positions through a user-supplied function. 380 | 381 | {% tabs %} 382 | {% tab title="Usage" %} 383 | 384 | ```typescript 385 | const seq1 = range(0, 3) 386 | const seq2 = repeat(2) 387 | const seq3 = repeat(1) 388 | 389 | // Gives: 0 -> 2 -> 4 -> 6 390 | const sequence: Seq = zip3With( 391 | ([num, multiplier, divisor]) => (num * multiplier) / divisor, 392 | seq1, 393 | seq2, 394 | seq3, 395 | ) 396 | ``` 397 | 398 | {% endtab %} 399 | 400 | {% tab title="Type Definition" %} 401 | 402 | ```typescript 403 | type zip3With = ( 404 | fn: ( 405 | [result1, result2, resul3]: 406 | | [T1, T2, T3] 407 | | [T1, undefined, undefined] 408 | | [T1, T2, undefined] 409 | | [T1, undefined, T3] 410 | | [undefined, T2, undefined] 411 | | [undefined, T2, T3] 412 | | [undefined, undefined, T3], 413 | index: number, 414 | ) => T4, 415 | seq1: Seq, 416 | seq2: Seq, 417 | seq3: Seq, 418 | ) => Seq 419 | ``` 420 | 421 | {% endtab %} 422 | {% endtabs %} 423 | 424 | ## concat 425 | 426 | Combines 2 or more sequences into a single sequence. 427 | 428 | {% tabs %} 429 | {% tab title="Usage" %} 430 | 431 | ```typescript 432 | const sequence: Seq = concat( 433 | fromArray([0, 1]), 434 | fromArray([2, 3]), 435 | fromArray([4, 5]), 436 | ) 437 | ``` 438 | 439 | {% endtab %} 440 | 441 | {% tab title="Type Definition" %} 442 | 443 | ```typescript 444 | type concat = (...items: Array>) => Seq 445 | ``` 446 | 447 | {% endtab %} 448 | {% endtabs %} 449 | 450 | ## interleave 451 | 452 | Takes 2 or more sequences and creates a new sequence built by pulling the next value from each of the sequences in order. 453 | 454 | {% tabs %} 455 | {% tab title="Usage" %} 456 | 457 | ```typescript 458 | // Builds: a -> 1 -> b -> 2 -> c -> 3 459 | const sequence: Seq = interleave( 460 | fromArray(["a", "b", "c"]), 461 | range(1, 3), 462 | ) 463 | ``` 464 | 465 | {% endtab %} 466 | 467 | {% tab title="Type Definition" %} 468 | 469 | ```typescript 470 | type interleave = (...items: Array>) => Seq 471 | ``` 472 | 473 | {% endtab %} 474 | {% endtabs %} 475 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # F.A.Q. 2 | 3 | ## Why is this page so empty? 4 | 5 | This is a brand new project, we don't know what questions are being frequently asked. 6 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | `leisure` can be installed using Yarn (or npm if you really need to). 4 | 5 | ```bash 6 | $ yarn add @tdreyno/leisure 7 | ``` 8 | 9 | ## Importing 10 | 11 | Usually, you will be importing a Sequence creating method. [See the full list here](api/static.md). 12 | 13 | ```typescript 14 | import { fromArray } from "@tdreyno/leisure" 15 | ``` 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/vc/whbnb2sn65g8snn5h5skvxxw0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | transformIgnorePatterns: ["node_modules/(?!@druyan/druyan)"], 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: null, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: null, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: null, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: null, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | globals: { 64 | "ts-jest": { 65 | diagnostics: false, 66 | }, 67 | }, 68 | 69 | // An array of directory names to be searched recursively up from the requiring module's location 70 | // moduleDirectories: [ 71 | // "node_modules" 72 | // ], 73 | 74 | // An array of file extensions your modules use 75 | // moduleFileExtensions: [ 76 | // "js", 77 | // "json", 78 | // "jsx", 79 | // "ts", 80 | // "tsx", 81 | // "node" 82 | // ], 83 | 84 | // A map from regular expressions to module names that allow to stub out resources with a single module 85 | // moduleNameMapper: {}, 86 | 87 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 88 | // modulePathIgnorePatterns: [], 89 | 90 | // Activates notifications for test results 91 | // notify: false, 92 | 93 | // An enum that specifies notification mode. Requires { notify: true } 94 | // notifyMode: "failure-change", 95 | 96 | // A preset that is used as a base for Jest's configuration 97 | preset: "ts-jest", 98 | 99 | // Run tests from one or more projects 100 | // projects: null, 101 | 102 | // Use this configuration option to add custom reporters to Jest 103 | // reporters: undefined, 104 | 105 | // Automatically reset mock state between every test 106 | // resetMocks: false, 107 | 108 | // Reset the module registry before running each individual test 109 | // resetModules: false, 110 | 111 | // A path to a custom resolver 112 | // resolver: null, 113 | 114 | // Automatically restore mock state between every test 115 | // restoreMocks: false, 116 | 117 | // The root directory that Jest should scan for tests and modules within 118 | rootDir: "src", 119 | 120 | // A list of paths to directories that Jest should use to search for files in 121 | // roots: [ 122 | // "" 123 | // ], 124 | 125 | // Allows you to use a custom runner instead of Jest's default test runner 126 | // runner: "jest-runner", 127 | 128 | // The paths to modules that run some code to configure or set up the testing environment before each test 129 | // setupFiles: [], 130 | 131 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 132 | // setupFilesAfterEnv: ["@wordpress/jest-console"], 133 | 134 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 135 | // snapshotSerializers: [], 136 | 137 | // The test environment that will be used for testing 138 | testEnvironment: "node", 139 | 140 | // Options that will be passed to the testEnvironment 141 | // testEnvironmentOptions: {}, 142 | 143 | // Adds a location field to test results 144 | // testLocationInResults: false, 145 | 146 | // The glob patterns Jest uses to detect test files 147 | testMatch: ["**/__tests__/**/?(*.)+(spec|test|steps).ts?(x)"], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | testPathIgnorePatterns: ["/node_modules/", "/benchmarks/"], 151 | 152 | // The regexp pattern or array of patterns that Jest uses to detect test files 153 | // testRegex: [], 154 | 155 | // This option allows the use of a custom results processor 156 | // testResultsProcessor: null, 157 | 158 | // This option allows use of a custom test runner 159 | // testRunner: "jasmine2", 160 | 161 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 162 | // testURL: "http://localhost", 163 | 164 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 165 | timers: "fake", 166 | 167 | // A map from regular expressions to paths to transformers 168 | // transform: null, 169 | 170 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 171 | // transformIgnorePatterns: [ 172 | // "/node_modules/" 173 | // ], 174 | 175 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 176 | // unmockedModulePathPatterns: undefined, 177 | 178 | // Indicates whether each individual test should be reported during the run 179 | // verbose: null, 180 | 181 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 182 | // watchPathIgnorePatterns: [], 183 | 184 | // Whether to use watchman for file crawling 185 | // watchman: true, 186 | } 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tdreyno/leisure", 3 | "version": "2.1.0", 4 | "main": "build/index.js", 5 | "typings": "build/index.d.ts", 6 | "repository": "https://github.com/tdreyno/leisure.git", 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "scripts": { 11 | "lint": "eslint -c .eslintrc.js --ext .ts src", 12 | "build": "pika build && yarn compress", 13 | "compress": "terser pkg/dist-web/index.js -o pkg/dist-web/index.min.js --source-map --mangle --compress passes=2 --toplevel --mangle-props regex=/_$/ && gzip-size pkg/dist-web/index.min.js", 14 | "test": "jest --runInBand", 15 | "version": "yarn run build", 16 | "coverage": "jest --runInBand --coverage --coverageDirectory '../coverage'", 17 | "perf": "ts-node -O '{ \"module\": \"commonjs\" }' ./benchmarks/benchmark.ts", 18 | "commit": "git-cz", 19 | "semantic-release": "semantic-release" 20 | }, 21 | "@pika/pack": { 22 | "pipeline": [ 23 | [ 24 | "@pika/plugin-ts-standard-pkg", 25 | { 26 | "exclude": [ 27 | "__tests__/**/*" 28 | ] 29 | } 30 | ], 31 | [ 32 | "@pika/plugin-build-node" 33 | ], 34 | [ 35 | "@pika/plugin-build-web" 36 | ] 37 | ] 38 | }, 39 | "lint-staged": { 40 | "*.{ts,tsx}": [ 41 | "eslint -c .eslintrc.js --ext .ts src --fix", 42 | "prettier --write" 43 | ], 44 | "*.{css,md}": [ 45 | "prettier --write" 46 | ] 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 51 | "pre-commit": "lint-staged" 52 | } 53 | }, 54 | "volta": { 55 | "node": "14.11.0", 56 | "yarn": "1.22.4" 57 | }, 58 | "devDependencies": { 59 | "@commitlint/cli": "^17.4.4", 60 | "@commitlint/config-conventional": "^17.4.4", 61 | "@commitlint/prompt": "^17.5.0", 62 | "@pika/pack": "^0.5.0", 63 | "@pika/plugin-build-node": "^0.9.2", 64 | "@pika/plugin-build-web": "^0.9.2", 65 | "@pika/plugin-ts-standard-pkg": "^0.9.2", 66 | "@semantic-release/changelog": "^6.0.2", 67 | "@semantic-release/git": "^10.0.1", 68 | "@semantic-release/github": "^8.0.7", 69 | "@types/benchmark": "^2.1.2", 70 | "@types/jest": "^27.4.1", 71 | "@types/lodash": "^4.14.192", 72 | "@typescript-eslint/eslint-plugin": "^4.33.0", 73 | "@typescript-eslint/parser": "^4.33.0", 74 | "beautify-benchmark": "^0.2.4", 75 | "benchmark": "^2.1.4", 76 | "commitizen": "^4.2.5", 77 | "cz-conventional-changelog": "3.3.0", 78 | "eslint": "^7.32.0", 79 | "eslint-config-prettier": "^8.8.0", 80 | "eslint-plugin-prettier": "^4.2.1", 81 | "gzip-size-cli": "^5.1.0", 82 | "husky": "^8.0.3", 83 | "jest": "^27.5.1", 84 | "lint-staged": "^13.1.2", 85 | "lodash": "^4.17.21", 86 | "microtime": "^3.1.1", 87 | "prettier": "^2.8.1", 88 | "semantic-release": "^20.1.1", 89 | "terser": "^5.16.1", 90 | "ts-jest": "^27.1.4", 91 | "ts-node": "^10.9.1", 92 | "ts-toolbelt": "^9.6.0", 93 | "typescript": "^4.9.5" 94 | }, 95 | "dependencies": { 96 | "@tdreyno/figment": "^1.10.0", 97 | "open-simplex-noise": "^2.5.0", 98 | "pure-rand": "^5.0.3" 99 | }, 100 | "config": { 101 | "commitizen": { 102 | "path": "./node_modules/cz-conventional-changelog" 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Seq.ts: -------------------------------------------------------------------------------- 1 | import { identity } from "@tdreyno/figment" 2 | 3 | export const DONE = Symbol() 4 | 5 | type Tramp = () => typeof DONE | T 6 | 7 | export class Seq { 8 | public static MAX_YIELDS = 1_000_000 9 | 10 | private yields_ = 0 11 | 12 | constructor(private source_: () => Tramp) {} 13 | 14 | public map(fn: (value: T, index: number) => U): Seq { 15 | return new Seq(() => { 16 | const parentNext = this.createTrampoline_() 17 | let counter = 0 18 | 19 | return (): typeof DONE | U => { 20 | const result = parentNext() 21 | 22 | if (result === DONE) { 23 | return DONE 24 | } 25 | 26 | return fn(result, counter++) 27 | } 28 | }) 29 | } 30 | 31 | public window(size: number, allowPartialWindow = true): Seq { 32 | // eslint-disable-next-line @typescript-eslint/no-this-alias 33 | const self: Seq = this 34 | 35 | return new Seq(() => { 36 | let head: Seq = self 37 | 38 | return (): typeof DONE | T[] => { 39 | const items = head.take(size).toArray() 40 | 41 | if (!allowPartialWindow && items.length < size) { 42 | return DONE 43 | } 44 | 45 | head = head.skip(size) 46 | 47 | return items 48 | } 49 | }) 50 | } 51 | 52 | public pairwise(): Seq<[T, T]> { 53 | return this.window(2, false) as unknown as Seq<[T, T]> 54 | } 55 | 56 | public isEmpty(): boolean { 57 | const next = this.createTrampoline_() 58 | return next() === DONE 59 | } 60 | 61 | public tap(fn: (value: T, index: number) => void): Seq { 62 | return this.map((v, k) => { 63 | fn(v, k) 64 | 65 | return v 66 | }) 67 | } 68 | 69 | public log(): Seq { 70 | return this.tap((v, k) => console.log([k, v])) 71 | } 72 | 73 | public flatMap(fn: (value: T, index: number) => U[]): Seq { 74 | return this.map(fn).flat() 75 | } 76 | 77 | public flat(this: Seq): Seq { 78 | return new Seq(() => { 79 | const next = this.createTrampoline_() 80 | 81 | let items = next() 82 | let counter = 0 83 | 84 | return (): typeof DONE | U => { 85 | if (items === DONE) { 86 | return DONE 87 | } 88 | 89 | if (counter >= items.length) { 90 | items = next() 91 | counter = 0 92 | } 93 | 94 | if (items === DONE) { 95 | return DONE 96 | } 97 | 98 | return items[counter++] 99 | } 100 | }) 101 | } 102 | 103 | public filter(fn: (value: T, index: number) => unknown): Seq { 104 | return new Seq(() => { 105 | const next = this.createTrampoline_() 106 | 107 | let counter = 0 108 | 109 | return (): typeof DONE | T => { 110 | while (true) { 111 | const item = next() 112 | 113 | if (item === DONE) { 114 | return DONE 115 | } 116 | 117 | if (fn(item, counter++)) { 118 | return item 119 | } 120 | } 121 | } 122 | }) 123 | } 124 | 125 | public compact(): Seq { 126 | return this.filter(identity) as unknown as Seq 127 | } 128 | 129 | public concat(...tail: Array>): Seq { 130 | return new Seq(() => { 131 | const nexts = [ 132 | this.createTrampoline_(), 133 | ...tail.map(s => s.createTrampoline_()), 134 | ] 135 | 136 | return (): typeof DONE | T => { 137 | while (true) { 138 | if (nexts.length === 0) { 139 | return DONE 140 | } 141 | 142 | const currentSeq = nexts[0] 143 | const item = currentSeq() 144 | 145 | if (item === DONE) { 146 | nexts.shift() 147 | continue 148 | } 149 | 150 | return item 151 | } 152 | } 153 | }) 154 | } 155 | 156 | public interleave(...tail: Array>): Seq { 157 | return new Seq(() => { 158 | const nexts = [this.source_(), ...tail.map(s => s.source_())] 159 | 160 | let index = 0 161 | 162 | return (): typeof DONE | T => { 163 | while (true) { 164 | if (nexts.length === 0) { 165 | return DONE 166 | } 167 | 168 | const boundedIndex = index++ % nexts.length 169 | const next = nexts[boundedIndex] 170 | const item = next() 171 | 172 | if (item === DONE) { 173 | nexts.splice(boundedIndex, 1) 174 | continue 175 | } 176 | 177 | return item 178 | } 179 | } 180 | }) 181 | } 182 | 183 | public interpose(separator: T): Seq { 184 | return new Seq(() => { 185 | const next = this.createTrampoline_() 186 | 187 | let backPressure: T | undefined 188 | let lastWasSep = true 189 | 190 | return (): typeof DONE | T => { 191 | if (backPressure) { 192 | const previousItem = backPressure 193 | backPressure = undefined 194 | lastWasSep = false 195 | return previousItem 196 | } 197 | 198 | const item = next() 199 | 200 | if (item === DONE) { 201 | return DONE 202 | } 203 | 204 | if (!lastWasSep) { 205 | lastWasSep = true 206 | backPressure = item 207 | return separator 208 | } 209 | 210 | lastWasSep = false 211 | return item 212 | } 213 | }) 214 | } 215 | 216 | public distinctBy(fn: (value: T) => U): Seq { 217 | const seen = new Set() 218 | 219 | return this.filter(value => { 220 | const compareBy = fn(value) 221 | 222 | if (seen.has(compareBy)) { 223 | return false 224 | } 225 | 226 | seen.add(compareBy) 227 | 228 | return true 229 | }) 230 | } 231 | 232 | public distinct(): Seq { 233 | return this.distinctBy(identity) 234 | } 235 | 236 | public partitionBy( 237 | fn: (value: T, index: number) => unknown, 238 | ): [Seq, Seq] { 239 | // eslint-disable-next-line @typescript-eslint/no-this-alias 240 | const self = this 241 | 242 | const trueBackpressure: T[] = [] 243 | const falseBackpressure: T[] = [] 244 | 245 | let previousSource: ReturnType | undefined 246 | 247 | const singletonTrampoline = (): Tramp => { 248 | if (!previousSource) { 249 | previousSource = self.createTrampoline_() 250 | } 251 | 252 | return previousSource 253 | } 254 | 255 | return [ 256 | new Seq(() => { 257 | const next = singletonTrampoline() 258 | 259 | let counter = 0 260 | 261 | return (): typeof DONE | T => { 262 | while (true) { 263 | if (trueBackpressure.length > 0) { 264 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 265 | return trueBackpressure.shift()! 266 | } 267 | 268 | const item = next() 269 | 270 | if (item === DONE) { 271 | return DONE 272 | } 273 | 274 | if (fn(item, counter++)) { 275 | return item 276 | } else { 277 | falseBackpressure.push(item) 278 | } 279 | } 280 | } 281 | }), 282 | 283 | new Seq(() => { 284 | const next = singletonTrampoline() 285 | 286 | let counter = 0 287 | 288 | return (): typeof DONE | T => { 289 | while (true) { 290 | if (falseBackpressure.length > 0) { 291 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 292 | return falseBackpressure.shift()! 293 | } 294 | 295 | const item = next() 296 | 297 | if (item === DONE) { 298 | return DONE 299 | } 300 | 301 | if (!fn(item, counter++)) { 302 | return item 303 | } else { 304 | trueBackpressure.push(item) 305 | } 306 | } 307 | } 308 | }), 309 | ] 310 | } 311 | 312 | public includes(value: T): boolean { 313 | return !!this.find(a => a === value) 314 | } 315 | 316 | public find(fn: (value: T, index: number) => unknown): T | undefined { 317 | return this.filter(fn).first() 318 | } 319 | 320 | public reduce(fn: (sum: A, value: T, index: number) => A, initial: A): A { 321 | const parentNext = this.createTrampoline_() 322 | let counter = 0 323 | let current: A = initial 324 | 325 | while (true) { 326 | const result = parentNext() 327 | 328 | if (result === DONE) { 329 | return current 330 | } 331 | 332 | current = fn(current, result, counter++) 333 | } 334 | } 335 | 336 | public join(this: Seq, separator = ","): string { 337 | return this.reduce((sum, str) => sum + str + separator, "").slice(0, -1) 338 | } 339 | 340 | public pipe(fn: (value: Seq) => U): U { 341 | return fn(this) 342 | } 343 | 344 | public some(fn: (value: T, index: number) => unknown): boolean { 345 | const next = this.createTrampoline_() 346 | 347 | let counter = 0 348 | 349 | while (true) { 350 | const item = next() 351 | 352 | if (item === DONE) { 353 | return false 354 | } 355 | 356 | if (fn(item, counter++)) { 357 | return true 358 | } 359 | } 360 | } 361 | 362 | public every(fn: (value: T, index: number) => unknown): boolean { 363 | const next = this.createTrampoline_() 364 | 365 | let counter = 0 366 | 367 | while (true) { 368 | const item = next() 369 | 370 | if (item === DONE) { 371 | return true 372 | } 373 | 374 | if (!fn(item, counter++)) { 375 | return false 376 | } 377 | } 378 | } 379 | 380 | public takeWhile(fn: (value: T, index: number) => unknown): Seq { 381 | return new Seq(() => { 382 | const next = this.createTrampoline_() 383 | 384 | let counter = 0 385 | 386 | return (): typeof DONE | T => { 387 | const item = next() 388 | 389 | if (item === DONE) { 390 | return DONE 391 | } 392 | 393 | if (!fn(item, counter++)) { 394 | return DONE 395 | } 396 | 397 | return item 398 | } 399 | }) 400 | } 401 | 402 | public take(num: number): Seq { 403 | return new Seq(() => { 404 | const next = this.createTrampoline_() 405 | 406 | let i = 0 407 | 408 | return (): typeof DONE | T => { 409 | if (i++ >= num) { 410 | return DONE 411 | } 412 | 413 | const item = next() 414 | 415 | if (item === DONE) { 416 | return DONE 417 | } 418 | 419 | return item 420 | } 421 | }) 422 | } 423 | 424 | public skipWhile(fn: (value: T, index: number) => unknown): Seq { 425 | return new Seq(() => { 426 | const next = this.createTrampoline_() 427 | 428 | let counter = 0 429 | 430 | return (): typeof DONE | T => { 431 | while (true) { 432 | const item = next() 433 | 434 | if (item === DONE) { 435 | return DONE 436 | } 437 | 438 | if (fn(item, counter++)) { 439 | continue 440 | } 441 | 442 | return item 443 | } 444 | } 445 | }) 446 | } 447 | 448 | public skip(num: number): Seq { 449 | return new Seq(() => { 450 | const next = this.createTrampoline_() 451 | 452 | let doneSkipping = false 453 | 454 | return (): typeof DONE | T => { 455 | if (!doneSkipping) { 456 | for (let i = 0; i < num; i++) { 457 | const skippedItem = next() 458 | 459 | if (skippedItem === DONE) { 460 | return DONE 461 | } 462 | } 463 | 464 | doneSkipping = true 465 | } 466 | 467 | const item = next() 468 | 469 | if (item === DONE) { 470 | return DONE 471 | } 472 | 473 | return item 474 | } 475 | }) 476 | } 477 | 478 | public nth(i: number): T | undefined { 479 | return this.skip(i - 1).first() 480 | } 481 | 482 | public index(i: number): T | undefined { 483 | return this.skip(i).first() 484 | } 485 | 486 | public first(): T | undefined { 487 | const next = this.createTrampoline_() 488 | 489 | const item = next() 490 | 491 | if (item === DONE) { 492 | return undefined 493 | } 494 | 495 | return item 496 | } 497 | 498 | public zipWith( 499 | fn: ( 500 | [result1, result2]: [T, T2] | [T, undefined] | [undefined, T2], 501 | index: number, 502 | ) => T3, 503 | seq2: Seq, 504 | ): Seq { 505 | return new Seq(() => { 506 | const next1 = this.createTrampoline_() 507 | const next2 = seq2.createTrampoline_() 508 | 509 | let counter = 0 510 | 511 | return (): typeof DONE | T3 => { 512 | const result1 = next1() 513 | const result2 = next2() 514 | 515 | if (result1 === DONE && result2 === DONE) { 516 | return DONE 517 | } 518 | 519 | if (result1 === DONE && result2 !== DONE) { 520 | return fn([undefined, result2], counter++) 521 | } 522 | 523 | if (result1 !== DONE && result2 === DONE) { 524 | return fn([result1, undefined], counter++) 525 | } 526 | 527 | return fn([result1 as T, result2 as T2], counter++) 528 | } 529 | }) 530 | } 531 | 532 | public zip(seq2: Seq): Seq<[T | undefined, T2 | undefined]> { 533 | return this.zipWith(identity, seq2) 534 | } 535 | 536 | public zip2With( 537 | fn: ( 538 | [result1, result2, result3]: 539 | | [T, T2, T3] 540 | | [T, undefined, undefined] 541 | | [T, T2, undefined] 542 | | [T, undefined, T3] 543 | | [undefined, T2, undefined] 544 | | [undefined, T2, T3] 545 | | [undefined, undefined, T3], 546 | index: number, 547 | ) => T4, 548 | seq2: Seq, 549 | seq3: Seq, 550 | ): Seq { 551 | return new Seq(() => { 552 | const next1 = this.createTrampoline_() 553 | const next2 = seq2.createTrampoline_() 554 | const next3 = seq3.createTrampoline_() 555 | 556 | let counter = 0 557 | 558 | return (): typeof DONE | T4 => { 559 | const result1 = next1() 560 | const result2 = next2() 561 | const result3 = next3() 562 | 563 | if (result1 === DONE && result2 === DONE && result3 === DONE) { 564 | return DONE 565 | } 566 | 567 | if (result1 !== DONE && result2 === DONE && result3 === DONE) { 568 | return fn([result1, undefined, undefined], counter++) 569 | } 570 | 571 | if (result1 !== DONE && result2 !== DONE && result3 === DONE) { 572 | return fn([result1, result2, undefined], counter++) 573 | } 574 | 575 | if (result1 !== DONE && result2 === DONE && result3 !== DONE) { 576 | return fn([result1, undefined, result3], counter++) 577 | } 578 | 579 | if (result1 === DONE && result2 !== DONE && result3 === DONE) { 580 | return fn([undefined, result2, undefined], counter++) 581 | } 582 | 583 | if (result1 === DONE && result2 !== DONE && result3 !== DONE) { 584 | return fn([undefined, result2, result3], counter++) 585 | } 586 | 587 | if (result1 === DONE && result2 === DONE && result3 !== DONE) { 588 | return fn([undefined, undefined, result3], counter++) 589 | } 590 | 591 | return fn([result1 as T, result2 as T2, result3 as T3], counter++) 592 | } 593 | }) 594 | } 595 | 596 | public zip2( 597 | seq2: Seq, 598 | seq3: Seq, 599 | ): Seq<[T | undefined, T2 | undefined, T3 | undefined]> { 600 | return this.zip2With(identity, seq2, seq3) 601 | } 602 | 603 | public *[Symbol.iterator]() { 604 | const next = this.createTrampoline_() 605 | 606 | while (true) { 607 | const item = next() 608 | 609 | if (item === DONE) { 610 | return 611 | } 612 | 613 | yield item 614 | } 615 | } 616 | 617 | public toArray(): T[] { 618 | const next = this.createTrampoline_() 619 | 620 | const result: T[] = [] 621 | 622 | while (true) { 623 | const item = next() 624 | 625 | if (item === DONE) { 626 | return result 627 | } 628 | 629 | result.push(item) 630 | } 631 | } 632 | 633 | public forEach(fn: (value: T, index: number) => void): void { 634 | const next = this.createTrampoline_() 635 | 636 | let counter = 0 637 | 638 | while (true) { 639 | const item = next() 640 | 641 | if (item === DONE) { 642 | return 643 | } 644 | 645 | fn(item, counter++) 646 | } 647 | } 648 | 649 | public sumBy(fn: (value: T) => number): number { 650 | return this.map(fn).reduce((sum, num) => sum + num, 0) 651 | } 652 | 653 | public sum(this: Seq): number { 654 | return this.sumBy(identity) 655 | } 656 | 657 | public averageBy(fn: (value: T) => number): number { 658 | return this.map(fn).reduce((sum, num, i) => sum + (num - sum) / (i + 1), 0) 659 | } 660 | 661 | public average(this: Seq): number { 662 | return this.averageBy(identity) 663 | } 664 | 665 | public frequencies(): Map { 666 | return this.reduce((sum, value) => { 667 | if (sum.has(value)) { 668 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 669 | return sum.set(value, sum.get(value)! + 1) 670 | } 671 | 672 | return sum.set(value, 0 + 1) 673 | }, new Map()) 674 | } 675 | 676 | public groupBy(fn: (item: T) => U): Map { 677 | return this.reduce((sum, item) => { 678 | const group = fn(item) 679 | 680 | if (sum.has(group)) { 681 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 682 | const currentArray = sum.get(group)! 683 | currentArray.push(item) 684 | return sum.set(group, currentArray) 685 | } 686 | 687 | return sum.set(group, [item]) 688 | }, new Map()) 689 | } 690 | 691 | // Barely public. Do not use. 692 | public createTrampoline_(): () => typeof DONE | T { 693 | const nextCallback = this.source_() 694 | 695 | return (): typeof DONE | T => { 696 | const result = nextCallback() 697 | 698 | if (++this.yields_ > Seq.MAX_YIELDS) { 699 | throw new Error( 700 | `Seq has yielded ${this.yields_} times. If this is okay, set Seq.MAX_YIELDS to a higher number (currently ${Seq.MAX_YIELDS}).`, 701 | ) 702 | } 703 | 704 | return result 705 | } 706 | } 707 | } 708 | -------------------------------------------------------------------------------- /src/__tests__/MAX_YIELDS.spec.ts: -------------------------------------------------------------------------------- 1 | import { Seq } from "../Seq" 2 | import { infinite } from "../static" 3 | 4 | describe("MAX_YIELDS", () => { 5 | beforeEach(() => (Seq.MAX_YIELDS = 5)) 6 | afterEach(() => (Seq.MAX_YIELDS = 1_000_000)) 7 | 8 | test("should throw when running infinitely", () => { 9 | const cb = jest.fn() 10 | 11 | expect(() => infinite().tap(cb).toArray()).toThrow() 12 | 13 | expect(cb).toHaveBeenCalledTimes(Seq.MAX_YIELDS) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/__tests__/average.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("average", () => { 4 | test("should be able to average a sequence of numbers", () => { 5 | const result = infinite().take(4).average() 6 | 7 | expect(result).toEqual(1.5) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/__tests__/averageBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("averageBy", () => { 4 | test("should be able to average a sequence of anything", () => { 5 | const result = fromArray([ 6 | { data: 0 }, 7 | { data: 1 }, 8 | { data: 2 }, 9 | { data: 3 }, 10 | ]) 11 | .take(4) 12 | .averageBy(obj => obj.data) 13 | 14 | expect(result).toEqual(1.5) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/__tests__/chain.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("chain", () => { 4 | test("should map the current seq", () => { 5 | const result = fromArray([1, 2, 3, 4]).pipe(seq => seq.toArray()) 6 | 7 | expect(result).toEqual([1, 2, 3, 4]) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/__tests__/combinatorics.spec.ts: -------------------------------------------------------------------------------- 1 | import { cartesianProduct, combination, powerSet } from "../combinatorics" 2 | 3 | describe("combinatorics", () => { 4 | describe("cartesianProduct", () => { 5 | test("should generate 0d product of same type", () => { 6 | const result = cartesianProduct().toArray() 7 | 8 | expect(result).toEqual([]) 9 | }) 10 | 11 | test("should generate 1d product of same type", () => { 12 | const result = cartesianProduct(["a", "b"]).toArray() 13 | 14 | expect(result).toEqual([["a", "b"]]) 15 | }) 16 | 17 | test("should generate 2d product of same type", () => { 18 | const result = cartesianProduct(["a", "b"], ["!", "?"]).toArray() 19 | 20 | expect(result).toEqual([ 21 | ["a", "!"], 22 | ["a", "?"], 23 | ["b", "!"], 24 | ["b", "?"], 25 | ]) 26 | }) 27 | 28 | test("should generate 3d product of differnt types", () => { 29 | const result = cartesianProduct( 30 | ["a", "b"], 31 | [1, 2], 32 | [true, false], 33 | ).toArray() 34 | 35 | expect(result).toEqual([ 36 | ["a", 1, true], 37 | ["a", 1, false], 38 | ["a", 2, true], 39 | ["a", 2, false], 40 | ["b", 1, true], 41 | ["b", 1, false], 42 | ["b", 2, true], 43 | ["b", 2, false], 44 | ]) 45 | }) 46 | }) 47 | 48 | describe("powerSet", () => { 49 | test("should generate power set", () => { 50 | const result = powerSet(["a", "b", "c"]).toArray() 51 | 52 | expect(result).toEqual([ 53 | new Set([]), 54 | new Set(["a"]), 55 | new Set(["b"]), 56 | new Set(["a", "b"]), 57 | new Set(["c"]), 58 | new Set(["a", "c"]), 59 | new Set(["b", "c"]), 60 | new Set(["a", "b", "c"]), 61 | ]) 62 | }) 63 | }) 64 | 65 | describe("combination", () => { 66 | test("should possible combinations of 2 items", () => { 67 | const result = combination(["a", "b", "c", "d"], 2).toArray() 68 | 69 | expect(result).toEqual([ 70 | ["a", "b"], 71 | ["a", "c"], 72 | ["a", "d"], 73 | ["b", "c"], 74 | ["b", "d"], 75 | ["c", "d"], 76 | ]) 77 | }) 78 | 79 | test("should possible combinations of all items", () => { 80 | const result = combination(["a", "b", "c"], 3).toArray() 81 | 82 | expect(result).toEqual([["a", "b", "c"]]) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /src/__tests__/concat.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray, infinite } from "../static" 2 | 3 | describe("concat", () => { 4 | test("should concat multiple sequences on to the first", () => { 5 | const result = fromArray([1, 2, 3]) 6 | .concat(infinite().skip(4).take(2), fromArray([6, 7, 8])) 7 | .take(8) 8 | .toArray() 9 | 10 | expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8]) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/__tests__/distinct.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("distinct", () => { 4 | test("should only return unique items in a sequence", () => { 5 | const result = fromArray([1, 2, 1, 3, 2, 4, 4, 5]) 6 | .distinct() 7 | .take(4) 8 | .toArray() 9 | 10 | expect(result).toEqual([1, 2, 3, 4]) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/__tests__/every.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("every", () => { 4 | test("should work like normal every on success", () => { 5 | const result = fromArray([1, 3, 5]).every(val => val % 2 !== 0) 6 | 7 | expect(result).toEqual(true) 8 | }) 9 | 10 | test("should work like normal every on failure", () => { 11 | const cb = jest.fn() 12 | const result = fromArray([1, 3, 5]) 13 | .tap(cb) 14 | .every(val => val === 1) 15 | 16 | expect(cb).toBeCalledTimes(2) 17 | expect(result).toEqual(false) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/__tests__/filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("filter", () => { 4 | test("should request values until the predicate is false, but only keep the odd ones", () => { 5 | const cb = jest.fn() 6 | const cb2 = jest.fn() 7 | 8 | const result = infinite() 9 | .tap(cb) 10 | .filter(val => val % 2 !== 0) 11 | .tap(cb2) 12 | .take(5) 13 | .toArray() 14 | 15 | expect(result).toEqual([1, 3, 5, 7, 9]) 16 | expect(cb).toHaveBeenCalledTimes(10) // Searches through both even AND odd 17 | expect(cb2).toHaveBeenCalledTimes(5) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/__tests__/find.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("find", () => { 4 | test("should find the first matching item", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite() 8 | .tap(cb) 9 | .find(val => val === 3) 10 | 11 | expect(result).toEqual(3) 12 | expect(cb).toHaveBeenCalledTimes(4) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/__tests__/first.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("first", () => { 4 | test("should get the first value", () => { 5 | const result = infinite().first() 6 | 7 | expect(result).toEqual(0) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/__tests__/flat.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("flat", () => { 4 | test("should work like normal flat", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite() 8 | .tap(cb) 9 | .map((v, i) => [v * 4 - i, -1000]) 10 | .flat() 11 | .take(6) 12 | .toArray() 13 | 14 | expect(result).toEqual([0, -1000, 3, -1000, 6, -1000]) 15 | expect(cb).toHaveBeenCalledTimes(3) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/__tests__/flatMap.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("flatMap", () => { 4 | test("should work like normal flatMap", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite() 8 | .tap(cb) 9 | .flatMap((v, i) => [v * 4 - i, -1000]) 10 | .take(6) 11 | .toArray() 12 | 13 | expect(result).toEqual([0, -1000, 3, -1000, 6, -1000]) 14 | expect(cb).toHaveBeenCalledTimes(3) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/__tests__/forEach.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("forEach", () => { 4 | test("should flush the sequence and call a callback", () => { 5 | const cb = jest.fn() 6 | 7 | fromArray([1, 2, 3, 4]).forEach(cb) 8 | 9 | expect(cb).toBeCalledTimes(4) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/__tests__/frequencies.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("frequencies", () => { 4 | test("should count the occurances of a value", () => { 5 | const result = fromArray([1, 2, 3, 1, 2, 1]).frequencies() 6 | 7 | expect(result).toEqual( 8 | new Map([ 9 | [1, 3], // 3 occurances of value 1 10 | [2, 2], // 2 occurances of value 2 11 | [3, 1], /// 1 occurance of value 3 12 | ]), 13 | ) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/__tests__/groupBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("groupBy", () => { 4 | test("should be group arbitrarily", () => { 5 | const result = infinite() 6 | .take(8) 7 | .groupBy(item => (item % 2 === 0 ? "even" : "odd")) 8 | 9 | expect(result).toEqual( 10 | new Map([ 11 | ["even", [0, 2, 4, 6]], 12 | ["odd", [1, 3, 5, 7]], 13 | ]), 14 | ) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/__tests__/includes.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("includes", () => { 4 | test("should detect if a sequence includes a value", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite().tap(cb).includes(3) 8 | 9 | expect(result).toEqual(true) 10 | expect(cb).toHaveBeenCalledTimes(4) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("index", () => { 4 | test("should get the index value", () => { 5 | const result = infinite().index(0) 6 | 7 | expect(result).toEqual(0) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/__tests__/interleave.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray, infinite, range } from "../static" 2 | 3 | describe("interleave", () => { 4 | test("should alternate between sequences", () => { 5 | const result = range(100, 97) 6 | .interleave(infinite(), fromArray([-1000, -2000, -3000])) 7 | .take(12) 8 | .toArray() 9 | 10 | expect(result).toEqual([ 11 | 100, 0, -1000, 99, 1, -2000, 98, 2, -3000, 97, 3, 4, 12 | ]) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/__tests__/interpose.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("interpose", () => { 4 | test("should place the separator between items", () => { 5 | const result = fromArray(["one", "two", "three"]) 6 | .interpose(", ") 7 | .toArray() 8 | .join("") 9 | 10 | expect(result).toEqual("one, two, three") 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/__tests__/isEmpty.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray, infinite } from "../static" 2 | 3 | describe("isEmpty", () => { 4 | test("should know if the sequence is empty", () => { 5 | expect(fromArray([]).isEmpty()).toEqual(true) 6 | 7 | expect(infinite().isEmpty()).toEqual(false) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/__tests__/map.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite, range } from "../static" 2 | 3 | describe("map", () => { 4 | test("should work like normal map", () => { 5 | const cb = jest.fn() 6 | 7 | const result = range(0, 2) 8 | .tap(cb) 9 | .map((v, i) => v * 4 - i) 10 | .toArray() 11 | 12 | expect(result).toEqual([0, 3, 6]) 13 | expect(cb).toHaveBeenCalledTimes(3) 14 | }) 15 | 16 | test("should only map once if only 1 result is asked for", () => { 17 | const cb = jest.fn() 18 | 19 | const result = infinite() 20 | .tap(cb) 21 | .map((v, i) => v * 4 - i) 22 | .first() 23 | 24 | expect(result).toEqual(0) 25 | expect(cb).toHaveBeenCalledTimes(1) 26 | }) 27 | 28 | test("should only map for each item taken", () => { 29 | const cb = jest.fn() 30 | 31 | const result = infinite() 32 | .tap(cb) 33 | .map((v, i) => v * 4 - i) 34 | .take(2) 35 | .toArray() 36 | 37 | expect(result).toEqual([0, 3]) 38 | expect(cb).toHaveBeenCalledTimes(2) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/__tests__/nth.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("nth", () => { 4 | test("should get the nth value", () => { 5 | const result = infinite().nth(1) 6 | 7 | expect(result).toEqual(0) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/__tests__/pairwise.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("pairwise", () => { 4 | test("should group sequence into groups of 2", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite().tap(cb).pairwise().take(3).toArray() 8 | 9 | expect(result).toEqual([ 10 | [0, 1], 11 | [2, 3], 12 | [4, 5], 13 | ]) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/__tests__/partition.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("partition", () => { 4 | test("should create two lazy sequences that filter a parent sequence based on a predicate", () => { 5 | const cb = jest.fn() 6 | 7 | const [even, odd] = infinite() 8 | .tap(cb) 9 | .partitionBy(val => val % 2 === 0) 10 | 11 | const evenResult = even.take(4).toArray() 12 | expect(evenResult).toEqual([0, 2, 4, 6]) 13 | 14 | const oddResult = odd.take(4).toArray() 15 | expect(oddResult).toEqual([1, 3, 5, 7]) 16 | 17 | expect(cb).toHaveBeenCalledTimes(8) 18 | }) 19 | 20 | test("should handle false backpressure", () => { 21 | const cb = jest.fn() 22 | 23 | const [greater, lessOrEqual] = infinite() 24 | .tap(cb) 25 | .partitionBy(val => val > 5) 26 | 27 | const greaterResult = greater.take(2).toArray() 28 | expect(greaterResult).toEqual([6, 7]) 29 | 30 | const lessOrEqualResult = lessOrEqual.take(4).toArray() 31 | expect(lessOrEqualResult).toEqual([0, 1, 2, 3]) 32 | 33 | expect(cb).toHaveBeenCalledTimes(8) 34 | }) 35 | 36 | test("should handle true backpressure", () => { 37 | const cb = jest.fn() 38 | 39 | const [lessOrEqual, greater] = infinite() 40 | .tap(cb) 41 | .partitionBy(val => val <= 5) 42 | 43 | const lessOrEqualResult = lessOrEqual.take(2) 44 | expect(lessOrEqualResult.toArray()).toEqual([0, 1]) 45 | 46 | const greaterResult = greater.take(4).toArray() 47 | expect(greaterResult).toEqual([6, 7, 8, 9]) 48 | 49 | expect(cb).toHaveBeenCalledTimes(10) 50 | 51 | const lessOrEqualResult2 = lessOrEqualResult.take(2).toArray() 52 | expect(lessOrEqualResult2).toEqual([2, 3]) 53 | 54 | expect(cb).toHaveBeenCalledTimes(10) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/__tests__/random.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | random, 3 | xorshift128plus, 4 | mersenne, 5 | xoroshiro128plus, 6 | congruential, 7 | congruential32, 8 | } from "../random" 9 | 10 | describe("random", () => { 11 | describe("random", () => { 12 | test("should lazily produce seeded random numbers", () => { 13 | const result = random().take(4).toArray() 14 | 15 | expect(result).toEqual([953453411, 236996814, 3739766767, 3570525885]) 16 | }) 17 | }) 18 | 19 | describe("mersenne", () => { 20 | test("should lazily produce seeded mersenne numbers", () => { 21 | const result = mersenne().take(4).toArray() 22 | 23 | expect(result).toEqual([953453411, 236996814, 3739766767, 3570525885]) 24 | }) 25 | }) 26 | 27 | describe("xorshift128plus", () => { 28 | test("should lazily produce seeded xorshift128plus numbers", () => { 29 | const result = xorshift128plus().take(4).toArray() 30 | 31 | expect(result).toEqual([-6, 721420101, 782843908, -673277793]) 32 | }) 33 | }) 34 | 35 | describe("xoroshiro128plus", () => { 36 | test("should lazily produce seeded xoroshiro128plus numbers", () => { 37 | const result = xoroshiro128plus().take(4).toArray() 38 | 39 | expect(result).toEqual([-6, -84279452, 4201029, -1465784295]) 40 | }) 41 | }) 42 | 43 | describe("congruential", () => { 44 | test("should lazily produce seeded congruential numbers", () => { 45 | const result = congruential().take(4).toArray() 46 | 47 | expect(result).toEqual([54, 28693, 12255, 24449]) 48 | }) 49 | }) 50 | 51 | describe("congruential32", () => { 52 | test("should lazily produce seeded congruential32 numbers", () => { 53 | const result = congruential32().take(4).toArray() 54 | 55 | expect(result).toEqual([3087708127, 1980136134, 3799575376, 167603873]) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/__tests__/simplex.spec.ts: -------------------------------------------------------------------------------- 1 | import { simplex2D, simplex3D, simplex4D } from "../simplex" 2 | 3 | describe("simplex", () => { 4 | describe("simplex2D", () => { 5 | test("should generate 2d simplex noise", () => { 6 | const result = simplex2D(() => [1, 2], 1).first() 7 | 8 | expect(result).toEqual(0.5387352272965704) 9 | }) 10 | }) 11 | 12 | describe("simplex3D", () => { 13 | test("should generate 3d simplex noise", () => { 14 | const result = simplex3D(() => [1, 2, 3], 1).first() 15 | 16 | expect(result).toEqual(-1.4535286549334576e-65) 17 | }) 18 | }) 19 | 20 | describe("simplex4D", () => { 21 | test("should generate 4d simplex noise", () => { 22 | const result = simplex4D(() => [1, 2, 3, 4], 1).first() 23 | 24 | expect(result).toEqual(0.041806993617899316) 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/__tests__/skipWhile.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("skipWhile", () => { 4 | test("should skip while predicate is true", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite() 8 | .tap(cb) 9 | .skipWhile(val => val < 4) 10 | .take(4) 11 | .toArray() 12 | 13 | expect(result).toEqual([4, 5, 6, 7]) 14 | expect(cb).toHaveBeenCalledTimes(8) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/__tests__/some.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray, infinite } from "../static" 2 | 3 | describe("some", () => { 4 | test("should some as soon as we find some", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite() 8 | .tap(cb) 9 | .some(val => val === 3) 10 | 11 | expect(result).toEqual(true) 12 | expect(cb).toHaveBeenCalledTimes(4) 13 | }) 14 | 15 | test("should fail if we never find some", () => { 16 | const cb = jest.fn() 17 | 18 | const result = fromArray([1, 2, 3, 4]) 19 | .tap(cb) 20 | .some(val => val === 5) 21 | 22 | expect(result).toEqual(false) 23 | expect(cb).toHaveBeenCalledTimes(4) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/__tests__/static.spec.ts: -------------------------------------------------------------------------------- 1 | import { cycle, iterate, of, range, repeat, repeatedly, fib } from "../static" 2 | 3 | describe("static", () => { 4 | describe("range", () => { 5 | test("should lazily pull from the range that increases", () => { 6 | const cb = jest.fn() 7 | 8 | const result = range(-2, 2).tap(cb).take(4).toArray() 9 | 10 | expect(result).toEqual([-2, -1, 0, 1]) 11 | expect(cb).toHaveBeenCalledTimes(4) 12 | }) 13 | 14 | test("should lazily pull from the range that increases by step", () => { 15 | const cb = jest.fn() 16 | 17 | const result = range(-2, 2, 2).tap(cb).take(3).toArray() 18 | 19 | expect(result).toEqual([-2, 0, 2]) 20 | expect(cb).toHaveBeenCalledTimes(3) 21 | }) 22 | 23 | test("should lazily pull from the range that decreases", () => { 24 | const cb = jest.fn() 25 | 26 | const result = range(2, -2).tap(cb).take(4).toArray() 27 | 28 | expect(result).toEqual([2, 1, 0, -1]) 29 | expect(cb).toHaveBeenCalledTimes(4) 30 | }) 31 | 32 | test("should lazily pull from the range that decreases by step", () => { 33 | const cb = jest.fn() 34 | 35 | const result = range(2, -2, 2).tap(cb).take(3).toArray() 36 | 37 | expect(result).toEqual([2, 0, -2]) 38 | expect(cb).toHaveBeenCalledTimes(3) 39 | }) 40 | }) 41 | 42 | describe("of", () => { 43 | test("should use singleton sequence", () => { 44 | const result = of(5).first() 45 | 46 | expect(result).toEqual(5) 47 | }) 48 | 49 | test("should use multiple items", () => { 50 | const result = of(5, 6).toArray() 51 | 52 | expect(result).toEqual([5, 6]) 53 | }) 54 | }) 55 | 56 | describe("iterate", () => { 57 | test("should lazily pull from an iterator", () => { 58 | const result = iterate(a => a + 1, 1) 59 | .take(4) 60 | .toArray() 61 | 62 | expect(result).toEqual([1, 2, 3, 4]) 63 | }) 64 | }) 65 | 66 | describe("fib", () => { 67 | test("should lazily generate fibonacci numbers", () => { 68 | const result = fib().take(6).toArray() 69 | 70 | expect(result).toEqual([0, 1, 1, 2, 3, 5]) 71 | }) 72 | }) 73 | 74 | describe("cycle", () => { 75 | test("should infinitely repeat an array of values", () => { 76 | const result = cycle([1, 2, 3]).take(7).toArray() 77 | 78 | expect(result).toEqual([1, 2, 3, 1, 2, 3, 1]) 79 | }) 80 | }) 81 | 82 | describe("repeat", () => { 83 | test("should repeat a value X times", () => { 84 | const result = repeat(1, 5).toArray() 85 | expect(result).toEqual([1, 1, 1, 1, 1]) 86 | }) 87 | }) 88 | 89 | describe("repeatedly", () => { 90 | test("should repeatedly call a side-effect function", () => { 91 | const cb = jest.fn() 92 | 93 | const result = repeatedly(() => { 94 | cb() 95 | return Date.now() 96 | }, 5).toArray() 97 | 98 | expect(cb).toBeCalledTimes(5) 99 | expect(result).toHaveLength(5) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/__tests__/sum.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("sum", () => { 4 | test("should be able to sum a sequence of numbers", () => { 5 | const result = infinite().take(4).sum() 6 | 7 | expect(result).toEqual(6) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/__tests__/sumBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("sumBy", () => { 4 | test("should be able to sum a sequence of anything", () => { 5 | const result = fromArray([ 6 | { data: 0 }, 7 | { data: 1 }, 8 | { data: 2 }, 9 | { data: 3 }, 10 | ]) 11 | .take(4) 12 | .sumBy(obj => obj.data) 13 | 14 | expect(result).toEqual(6) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/__tests__/takeWhile.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("takeWhile", () => { 4 | test("should request values until the predicate is false", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite() 8 | .tap(cb) 9 | .takeWhile(val => val < 4) 10 | .toArray() 11 | 12 | expect(result).toEqual([0, 1, 2, 3]) 13 | expect(cb).toHaveBeenCalledTimes(5) // Take-while always calls +1 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/__tests__/window.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("window", () => { 4 | test("should group sequence into groups of N", () => { 5 | const cb = jest.fn() 6 | 7 | const result = infinite().tap(cb).window(4).take(2).toArray() 8 | 9 | expect(result).toEqual([ 10 | [0, 1, 2, 3], 11 | [4, 5, 6, 7], 12 | ]) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/__tests__/zip.spec.ts: -------------------------------------------------------------------------------- 1 | import { infinite } from "../static" 2 | 3 | describe("zip", () => { 4 | test("should combine two sequences", () => { 5 | const result = infinite().zip(infinite()).take(4).toArray() 6 | 7 | expect(result).toEqual([ 8 | [0, 0], 9 | [1, 1], 10 | [2, 2], 11 | [3, 3], 12 | ]) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/__tests__/zip2With.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("zip2With", () => { 4 | test("should combine three sequences with a combinator function (longer first seq)", () => { 5 | const result = fromArray([1, 2, 3]) 6 | .zip2With( 7 | ([result1, result2, result3]) => 8 | result1 && result2 && result3 ? result1 + result2 + result3 : -1000, 9 | fromArray([10, 20]), 10 | fromArray([5, 10]), 11 | ) 12 | .take(4) 13 | .toArray() 14 | 15 | expect(result).toEqual([16, 32, -1000]) 16 | }) 17 | 18 | test("should combine three sequences with a combinator function (longer second seq)", () => { 19 | const result = fromArray([1, 2]) 20 | .zip2With( 21 | ([result1, result2, result3]) => 22 | result1 && result2 && result3 ? result1 + result2 + result3 : -1000, 23 | fromArray([10, 20, 30]), 24 | fromArray([5, 10]), 25 | ) 26 | .take(4) 27 | .toArray() 28 | 29 | expect(result).toEqual([16, 32, -1000]) 30 | }) 31 | 32 | test("should combine three sequences with a combinator function (longer last seq)", () => { 33 | const result = fromArray([1, 2]) 34 | .zip2With( 35 | ([result1, result2, result3]) => 36 | result1 && result2 && result3 ? result1 + result2 + result3 : -1000, 37 | fromArray([10, 20]), 38 | fromArray([5, 10, 15]), 39 | ) 40 | .take(4) 41 | .toArray() 42 | 43 | expect(result).toEqual([16, 32, -1000]) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/__tests__/zipWith.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromArray } from "../static" 2 | 3 | describe("zipWith", () => { 4 | test("should combine two sequences with a combinator function (longer first seq)", () => { 5 | const result = fromArray([1, 2, 3]) 6 | .zipWith( 7 | ([result1, result2]) => 8 | result1 && result2 ? result1 + result2 : -1000, 9 | fromArray([10, 20]), 10 | ) 11 | .take(4) 12 | .toArray() 13 | 14 | expect(result).toEqual([11, 22, -1000]) 15 | }) 16 | 17 | test("should combine two sequences with a combinator function (longer last seq)", () => { 18 | const result = fromArray([1, 2]) 19 | .zipWith( 20 | ([result1, result2]) => 21 | result1 && result2 ? result1 + result2 : -1000, 22 | fromArray([10, 20, 30]), 23 | ) 24 | .take(4) 25 | .toArray() 26 | 27 | expect(result).toEqual([11, 22, -1000]) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/benchmarks/benchmark.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 3 | import Benchmark from "benchmark" 4 | // eslint-disable-next-line @typescript-eslint/no-var-requires 5 | const benchmarks = require("beautify-benchmark") 6 | import _ from "lodash" 7 | import { fromArray, infinite, iterate, range } from "../index" 8 | 9 | // const inc = (x: number) => x + 1; 10 | // const dec = (x: number) => x - 1; 11 | const square = (x: number) => x * x 12 | const isEven = (x: number) => x % 2 === 0 13 | // const identity = (x: T) => x; 14 | const arr = (from: number, to: number) => range(from, to).toArray() 15 | const rand = (min: number, max: number) => 16 | Math.floor(Math.random() * (max - min) + min) 17 | const dupes = (min: number, max: number, count: number) => 18 | iterate(() => rand(min, max), rand(min, max)) 19 | .take(count) 20 | .toArray() 21 | 22 | const jaggedArray = [ 23 | [1, 2, 3], 24 | [4, 5, 6, 7, 8, 9], 25 | [23, 24, 25], 26 | [26], 27 | [27], 28 | [28], 29 | [29, 30], 30 | [31, 32, 33], 31 | [34, 35], 32 | ] 33 | 34 | const lotsOfNumbers = infinite().take(10000).toArray() 35 | 36 | const bench = ( 37 | name: string, 38 | leisure?: () => any, 39 | native?: () => any, 40 | lodash?: () => any, 41 | ) => { 42 | const suite = new Benchmark.Suite(name) 43 | 44 | if (leisure) { 45 | suite.add(name + ":leisure", leisure) 46 | } 47 | 48 | if (native) { 49 | suite.add(name + ":native", native) 50 | } 51 | 52 | if (lodash) { 53 | suite.add(name + ":lodash", lodash) 54 | } 55 | 56 | return suite 57 | .on("cycle", (event: Event) => benchmarks.add(event.target)) 58 | .on("complete", () => benchmarks.log()) 59 | .run({ async: false }) 60 | } 61 | 62 | bench( 63 | "map (best case)", 64 | () => fromArray(lotsOfNumbers).map(square).first(), 65 | () => lotsOfNumbers.map(square), 66 | () => _.map(lotsOfNumbers, square), 67 | ) 68 | 69 | bench( 70 | "map (middle case)", 71 | () => fromArray(lotsOfNumbers).map(square).nth(5000), 72 | () => lotsOfNumbers.map(square), 73 | () => _.map(lotsOfNumbers, square), 74 | ) 75 | 76 | bench( 77 | "map (worth case)", 78 | () => fromArray(lotsOfNumbers).map(square).nth(10000), 79 | () => lotsOfNumbers.map(square), 80 | () => _.map(lotsOfNumbers, square), 81 | ) 82 | 83 | bench( 84 | "filter", 85 | () => fromArray(lotsOfNumbers).filter(isEven).first(), 86 | () => lotsOfNumbers.filter(isEven), 87 | () => _.filter(lotsOfNumbers, isEven), 88 | ) 89 | 90 | bench( 91 | "flat", 92 | () => fromArray(jaggedArray).flat().first(), 93 | () => (jaggedArray as any).flat(), 94 | () => _.flatten(jaggedArray), 95 | ) 96 | 97 | const halfDupes = dupes(0, 50, 100) 98 | bench( 99 | "distinct", 100 | () => fromArray(halfDupes).distinct().first(), 101 | undefined, 102 | () => _.uniq(halfDupes), 103 | ) 104 | 105 | const firstConcatArray = arr(0, 100) 106 | const secondConcatArray = arr(50, 150) 107 | bench( 108 | "concat", 109 | () => 110 | fromArray(firstConcatArray).concat(fromArray(secondConcatArray)).first(), 111 | () => firstConcatArray.concat(secondConcatArray), 112 | () => _.concat(firstConcatArray, secondConcatArray), 113 | ) 114 | -------------------------------------------------------------------------------- /src/benchmarks/lazy-bench.js: -------------------------------------------------------------------------------- 1 | compareAlternatives("shuffle", { 2 | lazy: function (arr) { 3 | return Lazy(arr).shuffle() 4 | }, 5 | underscore: function (arr) { 6 | return _(arr).shuffle() 7 | }, 8 | shouldMatch: false, 9 | }) 10 | 11 | compareAlternatives("zip", { 12 | lazy: function (arr, other) { 13 | return Lazy(arr).zip(other) 14 | }, 15 | underscore: function (arr, other) { 16 | return _(arr).zip(other) 17 | }, 18 | inputs: [ 19 | [arr(0, 10), arr(5, 15)], 20 | [arr(0, 100), arr(50, 150)], 21 | ], 22 | }) 23 | 24 | // ---------- Chained operations ----------// 25 | 26 | compareAlternatives("map -> filter", { 27 | lazy: function (arr) { 28 | return Lazy(arr).map(inc).filter(isEven) 29 | }, 30 | underscore: function (arr) { 31 | return _.chain(arr).map(inc).filter(isEven) 32 | }, 33 | }) 34 | 35 | compareAlternatives("flatten -> map", { 36 | lazy: function (arr) { 37 | return Lazy(arr).flatten().map(inc) 38 | }, 39 | underscore: function (arr) { 40 | return _.chain(arr).flatten().map(inc) 41 | }, 42 | 43 | inputs: [[jaggedArray]], 44 | }) 45 | 46 | compareAlternatives("map -> uniq", { 47 | lazy: function (arr) { 48 | return Lazy(arr).map(inc).uniq() 49 | }, 50 | underscore: function (arr) { 51 | return _.chain(arr).map(inc).uniq() 52 | }, 53 | 54 | inputs: [[dupes(0, 5, 10)], [dupes(0, 50, 100)]], 55 | }) 56 | 57 | compareAlternatives("map -> union", { 58 | lazy: function (arr, other) { 59 | return Lazy(arr).map(inc).union(other) 60 | }, 61 | underscore: function (arr, other) { 62 | return _.chain(arr).map(inc).union(other) 63 | }, 64 | 65 | inputs: [ 66 | [arr(0, 10), arr(5, 15)], 67 | [arr(0, 100), arr(50, 150)], 68 | ], 69 | }) 70 | 71 | compareAlternatives("map -> intersection", { 72 | lazy: function (arr, other) { 73 | return Lazy(arr).map(inc).intersection(other) 74 | }, 75 | underscore: function (arr, other) { 76 | return _.chain(arr).map(inc).intersection(other) 77 | }, 78 | 79 | inputs: [ 80 | [arr(0, 10), arr(5, 15)], 81 | [arr(0, 100), arr(50, 150)], 82 | ], 83 | }) 84 | 85 | compareAlternatives("map -> shuffle", { 86 | lazy: function (arr) { 87 | return Lazy(arr).map(inc).shuffle() 88 | }, 89 | underscore: function (arr) { 90 | return _.chain(arr).map(inc).shuffle() 91 | }, 92 | 93 | shouldMatch: false, 94 | }) 95 | 96 | compareAlternatives("map -> zip", { 97 | lazy: function (arr, other) { 98 | return Lazy(arr).map(inc).zip(other) 99 | }, 100 | underscore: function (arr, other) { 101 | return _.chain(arr).map(inc).zip(other) 102 | }, 103 | 104 | inputs: [ 105 | [arr(0, 10), arr(5, 15)], 106 | [arr(0, 100), arr(50, 150)], 107 | ], 108 | }) 109 | 110 | // ---------- Short-circuited operations ---------- // 111 | 112 | compareAlternatives("map -> indexOf", { 113 | lazy: function (arr, value) { 114 | return Lazy(arr).map(inc).indexOf(value) 115 | }, 116 | underscore: function (arr, value) { 117 | return _.chain(arr).map(inc).indexOf(value) 118 | }, 119 | 120 | inputs: [ 121 | [arr(0, 10), 4], 122 | [arr(0, 100), 35], 123 | ], 124 | }) 125 | 126 | compareAlternatives("map -> sortedIndex", { 127 | lazy: function (arr) { 128 | return Lazy(arr) 129 | .map(inc) 130 | .sortedIndex(arr[arr.length / 2]) 131 | }, 132 | underscore: function (arr) { 133 | return _.chain(arr) 134 | .map(inc) 135 | .sortedIndex(arr[arr.length / 2]) 136 | }, 137 | 138 | inputs: [ 139 | [arr(0, 10), 4], 140 | [arr(0, 100), 35], 141 | ], 142 | }) 143 | 144 | compareAlternatives("map -> take", { 145 | lazy: function (arr) { 146 | return Lazy(arr).map(inc).take(5) 147 | }, 148 | underscore: function (arr) { 149 | return _.chain(arr).map(inc).take(5) 150 | }, 151 | }) 152 | 153 | compareAlternatives("filter -> take", { 154 | lazy: function (arr) { 155 | return Lazy(arr).filter(isEven).take(5) 156 | }, 157 | underscore: function (arr) { 158 | return _.chain(arr).filter(isEven).first(5) 159 | }, 160 | }) 161 | 162 | compareAlternatives("map -> filter -> take", { 163 | lazy: function (arr) { 164 | return Lazy(arr).map(inc).filter(isEven).take(5) 165 | }, 166 | underscore: function (arr) { 167 | return _.chain(arr).map(inc).filter(isEven).take(5) 168 | }, 169 | }) 170 | 171 | compareAlternatives("map -> drop -> take", { 172 | lazy: function (arr) { 173 | return Lazy(arr).map(inc).drop(5).take(5) 174 | }, 175 | underscore: function (arr) { 176 | return _.chain(arr).map(inc).rest(5).take(5) 177 | }, 178 | }) 179 | 180 | compareAlternatives("filter -> drop -> take", { 181 | lazy: function (arr) { 182 | return Lazy(arr).filter(isEven).drop(5).take(5) 183 | }, 184 | underscore: function (arr) { 185 | return _.chain(arr).filter(isEven).rest(5).first(5) 186 | }, 187 | }) 188 | 189 | compareAlternatives("flatten -> take", { 190 | lazy: function (arr) { 191 | return Lazy(arr).flatten().take(5) 192 | }, 193 | underscore: function (arr) { 194 | return _.chain(arr).flatten().first(5) 195 | }, 196 | 197 | inputs: [[jaggedArray]], 198 | }) 199 | 200 | compareAlternatives("uniq -> take", { 201 | lazy: function (arr) { 202 | return Lazy(arr).uniq().take(5) 203 | }, 204 | underscore: function (arr) { 205 | return _.chain(arr).uniq().first(5) 206 | }, 207 | 208 | inputs: [[dupes(0, 5, 10)], [dupes(0, 10, 100)]], 209 | }) 210 | 211 | compareAlternatives("union -> take", { 212 | lazy: function (arr, other) { 213 | return Lazy(arr).union(other).take(5) 214 | }, 215 | underscore: function (arr, other) { 216 | return _.chain(arr).union(other).first(5) 217 | }, 218 | 219 | inputs: [ 220 | [arr(0, 10), arr(5, 15)], 221 | [arr(0, 100), arr(50, 150)], 222 | ], 223 | }) 224 | 225 | compareAlternatives("intersection -> take", { 226 | lazy: function (arr, other) { 227 | return Lazy(arr).intersection(other).take(5) 228 | }, 229 | underscore: function (arr, other) { 230 | return _.chain(arr).intersection(other).first(5) 231 | }, 232 | 233 | inputs: [ 234 | [arr(0, 10), arr(5, 15)], 235 | [arr(0, 100), arr(50, 150)], 236 | ], 237 | }) 238 | 239 | compareAlternatives("without -> take", { 240 | lazy: function (arr, other) { 241 | return Lazy(arr).without(other).take(5) 242 | }, 243 | underscore: function (arr, other) { 244 | return _.chain(arr).difference(other).first(5) 245 | }, 246 | 247 | inputs: [ 248 | [arr(0, 10), arr(3, 7)], 249 | [arr(0, 100), arr(25, 75)], 250 | ], 251 | }) 252 | 253 | compareAlternatives("shuffle -> take", { 254 | lazy: function (arr) { 255 | return Lazy(arr).shuffle().take(5) 256 | }, 257 | underscore: function (arr) { 258 | return _.chain(arr).shuffle().first(5) 259 | }, 260 | 261 | shouldMatch: false, 262 | }) 263 | 264 | compareAlternatives("zip -> take", { 265 | lazy: function (arr, other) { 266 | return Lazy(arr).zip(other).take(5) 267 | }, 268 | underscore: function (arr, other) { 269 | return _.chain(arr).zip(other).first(5) 270 | }, 271 | 272 | inputs: [ 273 | [arr(0, 10), arr(5, 15)], 274 | [arr(0, 100), arr(50, 150)], 275 | ], 276 | }) 277 | 278 | compareAlternatives("map -> any", { 279 | lazy: function (arr) { 280 | return Lazy(arr).map(inc).any(isEven) 281 | }, 282 | underscore: function (arr) { 283 | return _.chain(arr).map(inc).any(isEven) 284 | }, 285 | }) 286 | 287 | compareAlternatives("map -> all", { 288 | lazy: function (arr) { 289 | return Lazy(arr).map(inc).all(isEven) 290 | }, 291 | underscore: function (arr) { 292 | return _.chain(arr).map(inc).every(isEven) 293 | }, 294 | }) 295 | -------------------------------------------------------------------------------- /src/combinatorics.ts: -------------------------------------------------------------------------------- 1 | import { DONE, Seq } from "./Seq" 2 | import { empty, of } from "./static" 3 | 4 | export function cartesianProduct(): Seq 5 | export function cartesianProduct(a: T[]): Seq 6 | export function cartesianProduct(a: T[], b: U[]): Seq<[T, U]> 7 | export function cartesianProduct( 8 | a: T[], 9 | b: U[], 10 | c: V[], 11 | ): Seq<[T, U, V]> 12 | export function cartesianProduct( 13 | a: T[], 14 | b: U[], 15 | c: V[], 16 | d: W[], 17 | ): Seq<[T, U, V, W]> 18 | export function cartesianProduct( 19 | a: T[], 20 | b: U[], 21 | c: V[], 22 | d: W[], 23 | e: X[], 24 | ): Seq<[T, U, V, W, X]> 25 | export function cartesianProduct( 26 | a: T[], 27 | b: U[], 28 | c: V[], 29 | d: W[], 30 | e: X[], 31 | f: Y[], 32 | ): Seq<[T, U, V, W, X, Y]> 33 | export function cartesianProduct( 34 | a: T[], 35 | b: U[], 36 | c: V[], 37 | d: W[], 38 | e: X[], 39 | f: Y[], 40 | g: Z[], 41 | ): Seq<[T, U, V, W, X, Y, Z]> 42 | export function cartesianProduct(...inputs: T[][]): Seq { 43 | if (inputs.length === 0) { 44 | return empty() 45 | } else if (inputs.length === 1) { 46 | return of(inputs[0]) 47 | } 48 | 49 | return new Seq(() => { 50 | const dm: Array<[number, number]> = [] 51 | let length: number 52 | 53 | for (let f = 1, l, i = inputs.length; i--; f *= l) { 54 | dm[i] = [f, (l = inputs[i].length)] 55 | length = f * l 56 | } 57 | 58 | let n = 0 59 | 60 | return (): typeof DONE | T[] => { 61 | if (n >= length) { 62 | return DONE 63 | } 64 | 65 | const c: T[] = [] 66 | 67 | for (let i = inputs.length; i--; ) { 68 | c[i] = inputs[i][((n / dm[i][0]) << 0) % dm[i][1]] 69 | } 70 | 71 | n++ 72 | 73 | return c 74 | } 75 | }) 76 | } 77 | 78 | export const powerSet = (items: T[]): Seq> => 79 | new Seq(() => { 80 | const numberOfCombinations = 2 ** items.length 81 | let counter = 0 82 | 83 | return (): typeof DONE | Set => { 84 | if (counter >= numberOfCombinations) { 85 | return DONE 86 | } 87 | 88 | const set: Set = new Set() 89 | 90 | for ( 91 | let setElementIndex = 0; 92 | setElementIndex < items.length; 93 | setElementIndex += 1 94 | ) { 95 | if (counter & (1 << setElementIndex)) { 96 | set.add(items[setElementIndex]) 97 | } 98 | } 99 | 100 | counter++ 101 | 102 | return set 103 | } 104 | }) 105 | 106 | export const combination = ( 107 | items: T[], 108 | size: number = items.length, 109 | ): Seq => 110 | new Seq(() => { 111 | const indexes: number[] = [] 112 | 113 | for (let j = 0; j < size; j++) { 114 | indexes[j] = j 115 | } 116 | 117 | const n = items.length 118 | let i = size - 1 // Index to keep track of maximum unsaturated element in array 119 | 120 | return (): typeof DONE | T[] => { 121 | // indexes[0] can only be n-size+1 exactly once - our termination condition! 122 | if (indexes[0] >= n - size + 1) { 123 | return DONE 124 | } 125 | 126 | // If outer elements are saturated, keep decrementing i till you find unsaturated element 127 | while (i > 0 && indexes[i] === n - size + i) { 128 | i-- 129 | } 130 | 131 | const result = indexes.map(j => items[j]) 132 | 133 | indexes[i]++ 134 | 135 | // Reset each outer element to prev element + 1 136 | while (i < size - 1) { 137 | indexes[i + 1] = indexes[i] + 1 138 | i++ 139 | } 140 | 141 | return result 142 | } 143 | }) 144 | 145 | // permutation 146 | // permutationCombination 147 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Seq } from "./Seq" 2 | export * from "./static" 3 | export * from "./random" 4 | export * from "./combinatorics" 5 | export * from "./simplex" 6 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | import prand, { RandomGenerator } from "pure-rand" 2 | import { iterate } from "./static" 3 | import { first } from "@tdreyno/figment" 4 | 5 | const DEFAULT_SEED = 5 6 | 7 | const prandWrapper_ = ( 8 | generator: 9 | | typeof prand.xorshift128plus 10 | | typeof prand.xoroshiro128plus 11 | | typeof prand.mersenne 12 | | typeof prand.congruential 13 | | typeof prand.congruential32, 14 | seed: number, 15 | ) => 16 | iterate<[number, RandomGenerator]>( 17 | ([, gen]) => gen.next(), 18 | generator(seed).next(), 19 | ).map(first) 20 | 21 | export const random = (seed = DEFAULT_SEED) => 22 | prandWrapper_(prand.mersenne, seed) 23 | 24 | export const xorshift128plus = (seed = DEFAULT_SEED) => 25 | prandWrapper_(prand.xorshift128plus, seed) 26 | 27 | export const xoroshiro128plus = (seed = DEFAULT_SEED) => 28 | prandWrapper_(prand.xoroshiro128plus, seed) 29 | 30 | export const mersenne = (seed = DEFAULT_SEED) => 31 | prandWrapper_(prand.mersenne, seed) 32 | 33 | export const congruential = (seed = DEFAULT_SEED) => 34 | prandWrapper_(prand.congruential, seed) 35 | 36 | export const congruential32 = (seed = DEFAULT_SEED) => 37 | prandWrapper_(prand.congruential32, seed) 38 | -------------------------------------------------------------------------------- /src/simplex.ts: -------------------------------------------------------------------------------- 1 | import { makeNoise2D, makeNoise3D, makeNoise4D } from "open-simplex-noise" 2 | import { Seq } from "./Seq" 3 | import { iterate } from "./static" 4 | 5 | export const simplex2D = ( 6 | fn: () => [number, number], 7 | seed: number = Date.now(), 8 | ) => { 9 | const noise2D = makeNoise2D(seed) 10 | const step = (): number => noise2D(...fn()) 11 | 12 | return iterate(step, step()) 13 | } 14 | 15 | export const simplex3D = ( 16 | fn: () => [number, number, number], 17 | seed: number = Date.now(), 18 | ) => { 19 | const noise3D = makeNoise3D(seed) 20 | const step = (): number => noise3D(...fn()) 21 | 22 | return iterate(step, step()) 23 | } 24 | 25 | export const simplex4D = ( 26 | fn: () => [number, number, number, number], 27 | seed: number = Date.now(), 28 | ): Seq => { 29 | const noise4D = makeNoise4D(seed) 30 | const step = (): number => noise4D(...fn()) 31 | 32 | return iterate(step, step()) 33 | } 34 | -------------------------------------------------------------------------------- /src/static.ts: -------------------------------------------------------------------------------- 1 | import { DONE, Seq } from "./Seq" 2 | import { constant, identity, first } from "@tdreyno/figment" 3 | 4 | export const fromArray = (data: T[]) => 5 | new Seq(() => { 6 | const len = data.length 7 | let i = 0 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 10 | return (): typeof DONE | T => (i >= len ? DONE : data[i++]!) 11 | }) 12 | 13 | export const iterate = (fn: (current: T) => T, start: T) => 14 | new Seq(() => { 15 | let counter = 0 16 | let previous: T = start 17 | 18 | return (): T => { 19 | if (counter++ === 0) { 20 | return start 21 | } 22 | 23 | previous = fn(previous) 24 | counter++ 25 | 26 | return previous 27 | } 28 | }) 29 | 30 | export const fib = () => iterate(([a, b]) => [b, a + b], [0, 1]).map(first) 31 | 32 | export const of = (head: T, ...values: T[]) => fromArray([head, ...values]) 33 | 34 | export const range = (start: number, end: number, step = 1) => 35 | new Seq(() => { 36 | const isForwards = start < end 37 | let i = 0 38 | 39 | return isForwards 40 | ? (): typeof DONE | number => { 41 | const num = start + i 42 | 43 | if (num > end) { 44 | return DONE 45 | } 46 | 47 | i += step 48 | 49 | return num 50 | } 51 | : (): typeof DONE | number => { 52 | const num = start - i 53 | 54 | if (num < end) { 55 | return DONE 56 | } 57 | 58 | i += step 59 | 60 | return num 61 | } 62 | }) 63 | 64 | export const cycle = (items: T[]) => 65 | new Seq(() => { 66 | const len = items.length 67 | let i = 0 68 | 69 | return (): T => items[i++ % len] 70 | }) 71 | 72 | export const repeatedly = (value: () => T, times = Infinity) => 73 | new Seq(() => { 74 | let index = 0 75 | return (): typeof DONE | T => (index++ + 1 > times ? DONE : value()) 76 | }) 77 | 78 | export const repeat = (value: T, times = Infinity) => 79 | repeatedly(constant(value), times) 80 | 81 | export const empty = () => fromArray([]) 82 | 83 | export const infinite = () => range(0, Infinity) 84 | 85 | export const zipWith = ( 86 | fn: ( 87 | [result1, result2]: [T1, T2] | [T1, undefined] | [undefined, T2], 88 | index: number, 89 | ) => T3, 90 | seq1: Seq, 91 | seq2: Seq, 92 | ) => seq1.zipWith(fn, seq2) 93 | 94 | export const zip = (seq1: Seq, seq2: Seq) => 95 | zipWith(identity, seq1, seq2) 96 | 97 | export const zip3With = ( 98 | fn: ( 99 | [result1, result2, resul3]: 100 | | [T1, T2, T3] 101 | | [T1, undefined, undefined] 102 | | [T1, T2, undefined] 103 | | [T1, undefined, T3] 104 | | [undefined, T2, undefined] 105 | | [undefined, T2, T3] 106 | | [undefined, undefined, T3], 107 | index: number, 108 | ) => T4, 109 | seq1: Seq, 110 | seq2: Seq, 111 | seq3: Seq, 112 | ) => seq1.zip2With(fn, seq2, seq3) 113 | 114 | export const zip3 = (seq1: Seq, seq2: Seq, seq3: Seq) => 115 | zip3With(identity, seq1, seq2, seq3) 116 | 117 | export const concat = (head: Seq, ...remaining: Array>) => 118 | head.concat(...remaining) 119 | 120 | export const interleave = (head: Seq, ...remaining: Array>) => 121 | head.interleave(...remaining) 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "skipLibCheck": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true, 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "isolatedModules": false, 14 | "allowJs": false, 15 | "noEmit": false, 16 | "sourceMap": true, 17 | "declaration": true, 18 | "declarationMap": true, 19 | "baseUrl": "./packages", 20 | "rootDir": "./src", 21 | "outDir": "./pkg/dist-types" 22 | }, 23 | "include": ["./src"], 24 | "exclude": ["build", "pkg", "dist", "node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "file", 3 | "out": "docs", 4 | "exclude": ["**/__tests__/*.ts", "**/__mocks/*.ts"], 5 | "excludeExternals": true, 6 | "excludeNotExported": true, 7 | "excludePrivate": true, 8 | "excludeProtected": true 9 | } 10 | --------------------------------------------------------------------------------