├── .github ├── contributing.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── README.md │ ├── release-and-publish-on-merge.yml │ ├── semver-on-release-labeled-pr.yml │ ├── test-on-master.yml │ └── test-on-pr.yml ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── banner.png ├── cdn ├── radash.esm.js ├── radash.js └── radash.min.js ├── chiller.json ├── docs ├── array │ ├── alphabetical.mdx │ ├── boil.mdx │ ├── cluster.mdx │ ├── counting.mdx │ ├── diff.mdx │ ├── first.mdx │ ├── flat.mdx │ ├── fork.mdx │ ├── group.mdx │ ├── intersects.mdx │ ├── iterate.mdx │ ├── last.mdx │ ├── list.mdx │ ├── max.mdx │ ├── merge.mdx │ ├── min.mdx │ ├── objectify.mdx │ ├── range.mdx │ ├── replace-or-append.mdx │ ├── replace.mdx │ ├── select.mdx │ ├── shift.mdx │ ├── sift.mdx │ ├── sort.mdx │ ├── sum.mdx │ ├── toggle.mdx │ ├── unique.mdx │ ├── zip-to-object.mdx │ └── zip.mdx ├── async │ ├── all.mdx │ ├── defer.mdx │ ├── guard.mdx │ ├── map.mdx │ ├── parallel.mdx │ ├── reduce.mdx │ ├── retry.mdx │ ├── sleep.mdx │ └── tryit.mdx ├── core-concepts.mdx ├── curry │ ├── chain.mdx │ ├── compose.mdx │ ├── debounce.mdx │ ├── memo.mdx │ ├── partial.mdx │ ├── partob.mdx │ ├── proxied.mdx │ └── throttle.mdx ├── favicon.ico ├── getting-started.mdx ├── installation.mdx ├── logo-dark.png ├── logo-light.png ├── number │ ├── in-range.mdx │ ├── to-float.mdx │ └── to-int.mdx ├── object │ ├── assign.mdx │ ├── clone.mdx │ ├── construct.mdx │ ├── crush.mdx │ ├── get.mdx │ ├── invert.mdx │ ├── keys.mdx │ ├── listify.mdx │ ├── lowerize.mdx │ ├── map-entries.mdx │ ├── map-keys.mdx │ ├── map-values.mdx │ ├── omit.mdx │ ├── pick.mdx │ ├── set.mdx │ ├── shake.mdx │ └── upperize.mdx ├── random │ ├── draw.mdx │ ├── random.mdx │ ├── shuffle.mdx │ └── uid.mdx ├── series │ └── series.mdx ├── string │ ├── camel.mdx │ ├── capitalize.mdx │ ├── dash.mdx │ ├── pascal.mdx │ ├── snake.mdx │ ├── template.mdx │ ├── title.mdx │ └── trim.mdx └── typed │ ├── is-array.mdx │ ├── is-date.mdx │ ├── is-empty.mdx │ ├── is-equal.mdx │ ├── is-float.mdx │ ├── is-function.mdx │ ├── is-int.mdx │ ├── is-number.mdx │ ├── is-object.mdx │ ├── is-primitive.mdx │ ├── is-promise.mdx │ ├── is-string.mdx │ └── is-symbol.mdx ├── jest.config.js ├── package.json ├── rollup.config.mjs ├── src ├── array.ts ├── async.ts ├── curry.ts ├── index.ts ├── number.ts ├── object.ts ├── random.ts ├── series.ts ├── string.ts ├── tests │ ├── array.test.ts │ ├── async.test.ts │ ├── curry.test.ts │ ├── number.test.ts │ ├── object.test.ts │ ├── random.test.ts │ ├── series.test.ts │ ├── string.test.ts │ └── typed.test.ts └── typed.ts ├── tsconfig.json └── yarn.lock /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Radash 2 | 3 | Thank you for investing your time in contributing to radash! 4 | 5 | > The [documentation site](https://radash-docs.vercel.app) has a [separate project](https://github.com/rayepps/radash-docs). This repo and guide is specifically for the radash library source code. 6 | 7 | ## You have a question? 8 | 9 | If you have a general question about radash, how to use it, the roadmap, or an idea to chat about you can ask it on the [discussions](https://github.com/rayepps/radash/discussions) page. Before you do, search to see if it's been asked before. If a related topic doesn't exist, you can start a new one. 10 | 11 | ## You have a problem? 12 | 13 | If you have an issue with radash, you want to report a bug, or you need an improvement you can create an issue on the [issues](https://github.com/rayepps/radash/issues) page. Before you do, search to see if it's already been brought up. If a similar issue doesn't exist, you can create a new one. 14 | 15 | ## You want to contribute? 16 | 17 | Scan through the [existing issues](https://github.com/rayepps/radash/issues) to find one that interests you. As a general rule, I don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix. Feel free to ask questions about the implementation or design in a comment on the issue before diving in. 18 | 19 | ## You want to write code? 20 | 21 | - To get started, run `yarn` in the project's root directory to install the dependencies. 22 | - You can run the unit tests with `yarn test`. They require Node v16+. You can run `nvm use` in the root directory to change to the correct Node version. The tests should pass (duh) and maintain 100% code coverage. 23 | - To get familiar with the existing code I would recommend looking through the docs and the codebase in parallel. For each function in the docs, find the implementation in the source and skim over the code. 24 | - When coding, try not to create internal APIs (any function or module that is not exported to be used by the client). 25 | - Also, try not to use functions from other radash modules. This is a softer rule but we want to make it easy for readers to understand a function without having to navigate the codebase. As a utility library users should ideally be able to copy/paste a function from radash into their project. Most radash functions should be easy to write in isolation. 26 | - If an implementation needs more than ~20 lines of code it might be better suited for another package or library. This is another softer rule. 27 | 28 | ## You're ready to push a change? 29 | 30 | Once you've made your changes on a fork of the radash repo, create a pull request to the `master` branch of the [radash](https://github.com/rayepps/radash) repository. 31 | - Be sure to fill in a description that lets readers and reviewers understand both the implementation and intent of your changes. 32 | - Don't forget to [link the PR to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one. 33 | - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. 34 | 35 | Once you submit your PR, I (@rayepps) will review it. I might ask questions or request additional information. 36 | 37 | ## Your PR gets merged! 38 | 39 | Congratulations :tada::tada: Currently, the package publishing process is manual. Your PR will be updated with a comment when the next release is published. This should happen within 24-48 hours of your PR being merged. 40 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please provide a detailed description of the changes and the intent behind them :) 4 | 5 | ## Checklist 6 | 7 | - [ ] Changes are covered by tests if behavior has been changed or added 8 | - [ ] Tests have 100% coverage 9 | - [ ] If code changes were made, the version in `package.json` has been bumped (matching semver) 10 | - [ ] If code changes were made, the `yarn build` command has been run and to update the `cdn` directory 11 | - [ ] If code changes were made, the documentation (in the `/docs` directory) has been updated 12 | 13 | ## Resolves 14 | 15 | If the PR resolves an open issue tag it here. For example, `Resolves #34` 16 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # Workflows 2 | 3 | ## Test 4 | 1. When a PR is created or updated, run unit tests on the PR (**require success to merge**) 5 | 2. When a commit is pushed to master, run unit tests 6 | 7 | ## Semver 8 | 1. When a PR has the `release` label added, require semver bump (`semver-on-label:release.yml`) 9 | 2. When a PR has the `release` label removed, automatically pass the semver bump (`semver-on-label:release.yml`) 10 | 3. When a PR is created, automatically pass the semver bump (`semver-on-label:release.yml`) 11 | 12 | ## Release 13 | 1. When a new commit is pushed to `master` and the linked pull request has the `release` label create a new release for the version in the `package.json` 14 | 15 | ## Publish 16 | 1. When a new release is created, publish to npm 17 | -------------------------------------------------------------------------------- /.github/workflows/release-and-publish-on-merge.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | check: 7 | name: 'Check if release is needed' 8 | runs-on: 'ubuntu-latest' 9 | outputs: 10 | exists: ${{ steps.check-tag.outputs.exists }} 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Get Current Version 14 | id: get-version 15 | run: echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT 16 | - uses: mukunku/tag-exists-action@v1.1.0 17 | id: check-tag 18 | with: 19 | tag: v${{ steps.get-version.outputs.version }} 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | release: 23 | name: 'Release' 24 | needs: check 25 | if: needs.check.outputs.exists == 'false' 26 | runs-on: 'ubuntu-latest' 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Get Current Version 30 | id: get-version 31 | run: echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT 32 | - name: Create Release 33 | uses: actions/create-release@v1 34 | id: create-release 35 | with: 36 | draft: false 37 | prerelease: false 38 | release_name: v${{ steps.get-version.outputs.version}} 39 | tag_name: v${{ steps.get-version.outputs.version}} 40 | env: 41 | GITHUB_TOKEN: ${{ github.token }} 42 | publish: 43 | runs-on: ubuntu-latest 44 | needs: check 45 | if: needs.check.outputs.exists == 'false' 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: actions/setup-node@v3 49 | with: 50 | node-version: 18 51 | registry-url: 'https://registry.npmjs.org' 52 | - run: yarn 53 | - run: yarn build 54 | - run: npm publish 55 | env: 56 | NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}' -------------------------------------------------------------------------------- /.github/workflows/semver-on-release-labeled-pr.yml: -------------------------------------------------------------------------------- 1 | name: Check Semver for Pull Request 2 | on: 3 | pull_request: 4 | branches: [master] 5 | types: [labeled, unlabeled, opened, synchronize] 6 | jobs: 7 | verify-version: 8 | if: contains(github.event.pull_request.labels.*.name, 'release') 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Get Pull Request Version 13 | id: pr-version 14 | run: echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT 15 | - uses: actions/checkout@v3 16 | with: 17 | ref: master 18 | - name: Get Base Version 19 | id: base-version 20 | run: echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | - run: yarn add semver 25 | - uses: actions/github-script@v6 26 | env: 27 | PR_VERSION: ${{steps.pr-version.outputs.version}} 28 | BASE_VERSION: ${{steps.base-version.outputs.version}} 29 | with: 30 | script: | 31 | const semver = require("semver") 32 | const { PR_VERSION: pr, BASE_VERSION: base } = process.env 33 | const pr_is_greater = semver.gt(pr, base) 34 | if (pr_is_greater) { 35 | core.debug(`Success, the pr version (${pr}) is higher than the base version (${base}).`) 36 | } else { 37 | core.setFailed(`The pr version (${pr}) is not greater than the base version (${base}). A pull request labeled with 'release' must have a valid version bump.`) 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/test-on-master.yml: -------------------------------------------------------------------------------- 1 | name: Test Master Branch 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14.x, 16.x, 18.17.1] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: yarn 18 | - run: yarn test 19 | -------------------------------------------------------------------------------- /.github/workflows/test-on-pr.yml: -------------------------------------------------------------------------------- 1 | name: Check Pull Request 2 | on: 3 | pull_request: 4 | branches: [master] 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14.x, 16.x, 18.17.1] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: yarn 18 | - run: yarn test 19 | check-format: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: '20.x' 26 | - run: yarn 27 | - run: yarn format:check 28 | verify-cdn-build-matches-src: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: actions/setup-node@v3 33 | with: 34 | node-version: '20.x' 35 | - run: yarn 36 | - run: npm install -g checksum 37 | - name: Calc current cdn checksums 38 | id: current-checksums 39 | run: echo "checksums=$(checksum cdn/* | base64 | tr '\n' ' ')" >> $GITHUB_OUTPUT 40 | - run: yarn build 41 | - name: Calc built cdn checksums 42 | id: built-checksums 43 | run: echo "checksums=$(checksum cdn/* | base64 | tr '\n' ' ')" >> $GITHUB_OUTPUT 44 | - uses: actions/github-script@v6 45 | env: 46 | CURRENT_CHECKSUMS: ${{steps.current-checksums.outputs.checksums}} 47 | BUILT_CHECKSUMS: ${{steps.built-checksums.outputs.checksums}} 48 | with: 49 | script: | 50 | const { 51 | CURRENT_CHECKSUMS: current, 52 | BUILT_CHECKSUMS: built 53 | } = process.env 54 | if (current.trim() !== built.trim()) { 55 | core.setFailed(`The files in the cdn directory don't match the expected output given the source. Maybe you didn't run yarn build before pushing?`) 56 | } else { 57 | core.debug(`Success, the cdn build files reflect the current source code.`) 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .coverage/ 3 | dist 4 | coverage 5 | archive.zip 6 | archive.tar.gz 7 | .chiller/ 8 | .next/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "bracketSameLine": false, 9 | "arrowParens": "avoid", 10 | "singleAttributePerLine": true 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 radash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radash 2 | 3 | :loud_sound: `/raw-dash/` 4 | 5 |
6 |

7 | radash 8 |

9 |
10 | 11 |
12 |

13 | Functional utility library - modern, simple, typed, powerful 14 |

15 |
16 | 17 |

18 | 19 | bundle size 20 | 21 | 22 | npm downloads 23 | 24 | 25 | npm version 26 | 27 | 28 | MIT license 29 | 30 |

31 | 32 |
33 | 34 | Full Documentation 35 | 36 |
37 | 38 | ## Install 39 | 40 | ``` 41 | yarn add radash 42 | ``` 43 | 44 | ## Usage 45 | 46 | A very brief kitchen sink. See the full documentation [here](https://radash-docs.vercel.app). 47 | 48 | ```ts 49 | import * as _ from 'radash' 50 | 51 | const gods = [{ 52 | name: 'Ra', 53 | power: 'sun', 54 | rank: 100, 55 | culture: 'egypt' 56 | }, { 57 | name: 'Loki', 58 | power: 'tricks', 59 | rank: 72, 60 | culture: 'norse' 61 | }, { 62 | name: 'Zeus', 63 | power: 'lightning', 64 | rank: 96, 65 | culture: 'greek' 66 | }] 67 | 68 | _.max(gods, g => g.rank) // => ra 69 | _.sum(gods, g => g.rank) // => 268 70 | _.fork(gods, g => g.culture === 'norse') // => [[loki], [ra, zeus]] 71 | _.sort(gods, g => g.rank) // => [ra, zeus, loki] 72 | _.boil(gods, (a, b) => a.rank > b.rank ? a : b) // => ra 73 | 74 | _.objectify( 75 | gods, 76 | g => g.name.toLowerCase(), 77 | g => _.pick(g, ['power', 'rank', 'culture']) 78 | ) // => { ra, zeus, loki } 79 | 80 | const godName = _.get(gods, g => g[0].name) 81 | 82 | const [err, god] = await _.try(api.gods.findByName)(godName) 83 | 84 | const allGods = await _.map(gods, async ({ name }) => { 85 | return api.gods.findByName(name) 86 | }) 87 | ``` 88 | 89 | ## Contributing 90 | 91 | Contributions are welcome and appreciated! Check out the [contributing guide](./.github/contributing.md) before you dive in. 92 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sodiray/radash/069b26cdd7d62e6ac16a0ad3baa1c9abcca420bc/banner.png -------------------------------------------------------------------------------- /cdn/radash.min.js: -------------------------------------------------------------------------------- 1 | var radash=function(i){"use strict";const E=e=>!!e&&e.constructor===Symbol,w=Array.isArray,k=e=>!!e&&e.constructor===Object,N=e=>e==null||typeof e!="object"&&typeof e!="function",y=e=>!!(e&&e.constructor&&e.call&&e.apply),K=e=>typeof e=="string"||e instanceof String,W=e=>h(e)&&e%1===0,J=e=>h(e)&&e%1!==0,h=e=>{try{return Number(e)===e}catch{return!1}},T=e=>Object.prototype.toString.call(e)==="[object Date]",S=e=>!(!e||!e.then||!y(e.then)),X=e=>{if(e===!0||e===!1||e==null)return!0;if(h(e))return e===0;if(T(e))return isNaN(e.getTime());if(y(e)||E(e))return!1;const t=e.length;if(h(t))return t===0;const n=e.size;return h(n)?n===0:Object.keys(e).length===0},j=(e,t)=>{if(Object.is(e,t))return!0;if(e instanceof Date&&t instanceof Date)return e.getTime()===t.getTime();if(e instanceof RegExp&&t instanceof RegExp)return e.toString()===t.toString();if(typeof e!="object"||e===null||typeof t!="object"||t===null)return!1;const n=Reflect.ownKeys(e),r=Reflect.ownKeys(t);if(n.length!==r.length)return!1;for(let s=0;se.reduce((n,r)=>{const s=t(r);return n[s]||(n[s]=[]),n[s].push(r),n},{});function H(...e){return!e||!e.length?[]:new Array(Math.max(...e.map(({length:t})=>t))).fill([]).map((t,n)=>e.map(r=>r[n]))}function Q(e,t){if(!e||!e.length)return{};const n=y(t)?t:w(t)?(r,s)=>t[s]:(r,s)=>t;return e.reduce((r,s,u)=>(r[s]=n(s,u),r),{})}const O=(e,t)=>!e||(e.length??0)===0?null:e.reduce(t);function V(e,t){return(e||[]).reduce((n,r)=>n+(t?t(r):r),0)}const G=(e,t=void 0)=>e?.length>0?e[0]:t,x=(e,t=void 0)=>e?.length>0?e[e.length-1]:t,z=(e,t,n=!1)=>{if(!e)return[];const r=(u,c)=>t(u)-t(c),s=(u,c)=>t(c)-t(u);return e.slice().sort(n===!0?s:r)},ee=(e,t,n="asc")=>{if(!e)return[];const r=(u,c)=>`${t(u)}`.localeCompare(t(c)),s=(u,c)=>`${t(c)}`.localeCompare(t(u));return e.slice().sort(n==="desc"?s:r)},te=(e,t)=>e?e.reduce((n,r)=>{const s=t(r);return n[s]=(n[s]??0)+1,n},{}):{},ne=(e,t,n)=>{if(!e)return[];if(t===void 0)return[...e];for(let r=0;rr)=>e.reduce((r,s)=>(r[t(s)]=n(s),r),{}),re=(e,t,n)=>e?e.reduce((r,s,u)=>(n(s,u)&&r.push(t(s,u)),r),[]):[];function se(e,t){const n=t??(r=>r);return O(e,(r,s)=>n(r)>n(s)?r:s)}function ie(e,t){const n=t??(r=>r);return O(e,(r,s)=>n(r){const n=Math.ceil(e.length/t);return new Array(n).fill(null).map((r,s)=>e.slice(s*t,s*t+t))},ce=(e,t)=>{const n=e.reduce((r,s)=>{const u=t?t(s):s;return r[u]||(r[u]=s),r},{});return Object.values(n)};function*A(e,t,n=s=>s,r=1){const s=y(n)?n:()=>n,u=t?e:0,c=t??e;for(let o=u;o<=c&&(yield s(o),!(o+r>c));o+=r);}const C=(e,t,n,r)=>Array.from(A(e,t,n,r)),oe=e=>e.reduce((t,n)=>(t.push(...n),t),[]),fe=(e,t,n)=>{if(!e||!t)return!1;const r=n??(u=>u),s=t.reduce((u,c)=>(u[r(c)]=!0,u),{});return e.some(u=>s[r(u)])},R=(e,t)=>e?e.reduce((n,r)=>{const[s,u]=n;return t(r)?[[...s,r],u]:[s,[...u,r]]},[[],[]]):[[],[]],le=(e,t,n)=>!t&&!e?[]:t?e?n?e.reduce((r,s)=>{const u=t.find(c=>n(s)===n(c));return u?r.push(u):r.push(s),r},[]):e:[]:e,ae=(e,t,n)=>{if(!e&&!t)return[];if(!t)return[...e];if(!e)return[t];for(let r=0;r{if(!e&&!t)return[];if(!e)return[t];if(!t)return[...e];const s=n?(o,a)=>n(o,a)===n(t,a):o=>o===t;return e.find(s)?e.filter((o,a)=>!s(o,a)):(r?.strategy??"append")==="append"?[...e,t]:[t,...e]},ge=e=>e?.filter(t=>!!t)??[],B=(e,t,n)=>{let r=n;for(let s=1;s<=e;s++)r=t(r,s);return r},he=(e,t,n=r=>r)=>{if(!e?.length&&!t?.length)return[];if(e?.length===void 0)return[...t];if(!t?.length)return[...e];const r=t.reduce((s,u)=>(s[n(u)]=!0,s),{});return e.filter(s=>!r[n(s)])};function me(e,t){if(e.length===0)return e;const n=t%e.length;return n===0?e:[...e.slice(-n,e.length),...e.slice(0,-n)]}const we=async(e,t,n)=>{const r=n!==void 0;if(!r&&e?.length<1)throw new Error("Cannot reduce empty array with no init value");const s=r?e:e.slice(1);let u=r?n:e[0];for(const[c,o]of s.entries())u=await t(u,o,c);return u},ye=async(e,t)=>{if(!e)return[];let n=[],r=0;for(const s of e){const u=await t(s,r++);n.push(u)}return n},pe=async e=>{const t=[],n=(u,c)=>t.push({fn:u,rethrow:c?.rethrow??!1}),[r,s]=await m(e)(n);for(const{fn:u,rethrow:c}of t){const[o]=await m(u)(r);if(o&&c)throw o}if(r)throw r;return s};class L extends Error{constructor(t=[]){super();const n=t.find(r=>r.name)?.name??"";this.name=`AggregateError(${n}...)`,this.message=`AggregateError with ${t.length} errors`,this.stack=t.find(r=>r.stack)?.stack??this.stack,this.errors=t}}const be=async(e,t,n)=>{const r=t.map((d,b)=>({index:b,item:d})),s=async d=>{const b=[];for(;;){const f=r.pop();if(!f)return d(b);const[l,g]=await m(n)(f.item);b.push({error:l,result:g,index:f.index})}},u=C(1,e).map(()=>new Promise(s)),c=await Promise.all(u),[o,a]=R(z(c.flat(),d=>d.index),d=>!!d.error);if(o.length>0)throw new L(o.map(d=>d.error));return a.map(d=>d.result)};async function ke(e){const t=w(e)?e.map(s=>[null,s]):Object.entries(e),n=await Promise.all(t.map(([s,u])=>u.then(c=>({result:c,exc:null,key:s})).catch(c=>({result:null,exc:c,key:s})))),r=n.filter(s=>s.exc);if(r.length>0)throw new L(r.map(s=>s.exc));return w(e)?n.map(s=>s.result):n.reduce((s,u)=>({...s,[u.key]:u.result}),{})}const Oe=async(e,t)=>{const n=e?.times??3,r=e?.delay,s=e?.backoff??null;for(const u of A(1,n)){const[c,o]=await m(t)(a=>{throw{_exited:a}});if(!c)return o;if(c._exited)throw c._exited;if(u===n)throw c;r&&await $(r),s&&await $(s(u))}},$=e=>new Promise(t=>setTimeout(t,e)),m=e=>(...t)=>{try{const n=e(...t);return S(n)?n.then(r=>[void 0,r]).catch(r=>[r,void 0]):[void 0,n]}catch(n){return[n,void 0]}},Ae=(e,t)=>{const n=s=>{if(t&&!t(s))throw s},r=s=>s instanceof Promise;try{const s=e();return r(s)?s.catch(n):s}catch(s){return n(s)}};function Ce(...e){return(...t)=>e.slice(1).reduce((n,r)=>r(n),e[0](...t))}function $e(...e){return e.reverse().reduce((t,n)=>n(t))}const Pe=(e,...t)=>(...n)=>e(...t,...n),_e=(e,t)=>n=>e({...t,...n}),Ee=e=>new Proxy({},{get:(t,n)=>e(n)}),Ne=(e,t,n,r)=>function(...u){const c=n?n(...u):JSON.stringify({args:u}),o=e[c];if(o!==void 0&&(!o.exp||o.exp>new Date().getTime()))return o.value;const a=t(...u);return e[c]={exp:r?new Date().getTime()+r:null,value:a},a},Te=(e,t={})=>Ne({},e,t.key??null,t.ttl??null),Se=({delay:e},t)=>{let n,r=!0;const s=(...u)=>{r?(clearTimeout(n),n=setTimeout(()=>{r&&t(...u),n=void 0},e)):t(...u)};return s.isPending=()=>n!==void 0,s.cancel=()=>{r=!1},s.flush=(...u)=>t(...u),s},je=({interval:e},t)=>{let n=!0,r;const s=(...u)=>{n&&(t(...u),n=!1,r=setTimeout(()=>{n=!0,r=void 0},e))};return s.isThrottled=()=>r!==void 0,s},ze=(e,t)=>{const n=()=>{};return new Proxy(Object.assign(n,e),{get:(r,s)=>r[s],set:(r,s,u)=>(r[s]=u,!0),apply:(r,s,u)=>t(Object.assign({},r))(...u)})};function Me(e,t,n){return typeof e=="number"&&typeof t=="number"&&(typeof n>"u"||typeof n=="number")?(typeof n>"u"&&(n=t,t=0),e>=Math.min(t,n)&&e{const n=t===void 0?0:t;if(e==null)return n;const r=parseFloat(e);return isNaN(r)?n:r},Z=(e,t)=>{const n=t===void 0?0:t;if(e==null)return n;const r=parseInt(e);return isNaN(r)?n:r},Be=(e,t=n=>n===void 0)=>e?Object.keys(e).reduce((r,s)=>(t(e[s])||(r[s]=e[s]),r),{}):{},P=(e,t)=>Object.keys(e).reduce((r,s)=>(r[t(s,e[s])]=e[s],r),{}),Le=(e,t)=>Object.keys(e).reduce((r,s)=>(r[s]=t(e[s],s),r),{}),Ze=(e,t)=>e?Object.entries(e).reduce((n,[r,s])=>{const[u,c]=t(r,s);return n[u]=c,n},{}):{},De=e=>e?Object.keys(e).reduce((n,r)=>(n[e[r]]=r,n),{}):{},Fe=e=>P(e,t=>t.toLowerCase()),qe=e=>P(e,t=>t.toUpperCase()),D=e=>{if(N(e))return e;if(typeof e=="function")return e.bind({});const t=new e.constructor;return Object.getOwnPropertyNames(e).forEach(n=>{t[n]=e[n]}),t},Ie=(e,t)=>{if(!e)return[];const n=Object.entries(e);return n.length===0?[]:n.reduce((r,s)=>(r.push(t(s[0],s[1])),r),[])},ve=(e,t)=>e?t.reduce((n,r)=>(Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]),n),{}):{},Ue=(e,t)=>e?!t||t.length===0?e:t.reduce((n,r)=>(delete n[r],n),{...e}):{},F=(e,t,n)=>{const r=t.split(/[\.\[\]]/g);let s=e;for(const u of r){if(s===null||s===void 0)return n;const c=u.replace(/['"]/g,"");c.trim()!==""&&(s=s[c])}return s===void 0?n:s},q=(e,t,n)=>{if(!e)return{};if(!t||n===void 0)return e;const r=t.split(/[\.\[\]]/g).filter(c=>!!c.trim()),s=c=>{if(r.length>1){const o=r.shift(),a=Z(r[0],null)!==null;c[o]=c[o]===void 0?a?[]:{}:c[o],s(c[o])}else c[r[0]]=n},u=D(e);return s(u),u},I=(e,t)=>!e||!t?e??t??{}:Object.entries({...e,...t}).reduce((n,[r,s])=>({...n,[r]:(()=>k(e[r])?I(e[r],s):s)()}),{}),v=e=>{if(!e)return[];const t=(n,r)=>k(n)?Object.entries(n).flatMap(([s,u])=>t(u,[...r,s])):w(n)?n.flatMap((s,u)=>t(s,[...r,`${u}`])):[r.join(".")];return t(e,[])},Ke=e=>e?M(v(e),t=>t,t=>F(e,t)):{},We=e=>e?Object.keys(e).reduce((t,n)=>q(t,n,e[n]),{}):{},_=(e,t)=>Math.floor(Math.random()*(t-e+1)+e),Je=e=>{const t=e.length;if(t===0)return null;const n=_(0,t-1);return e[n]},Xe=e=>e.map(t=>({rand:Math.random(),value:t})).sort((t,n)=>t.rand-n.rand).map(t=>t.value),Ye=(e,t="")=>{const n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"+t;return B(e,r=>r+n.charAt(_(0,n.length-1)),"")},He=(e,t=n=>`${n}`)=>{const{indexesByKey:n,itemsByIndex:r}=e.reduce((f,l,g)=>({indexesByKey:{...f.indexesByKey,[t(l)]:g},itemsByIndex:{...f.itemsByIndex,[g]:l}}),{indexesByKey:{},itemsByIndex:{}}),s=(f,l)=>n[t(f)]n[t(f)]>n[t(l)]?f:l,c=()=>r[0],o=()=>r[e.length-1],a=(f,l)=>r[n[t(f)]+1]??l??c(),d=(f,l)=>r[n[t(f)]-1]??l??o();return{min:s,max:u,first:c,last:o,next:a,previous:d,spin:(f,l)=>{if(l===0)return f;const g=Math.abs(l),rt=g>e.length?g%e.length:g;return C(0,rt-1).reduce(U=>l>0?a(U):d(U),f)}}},p=e=>{if(!e||e.length===0)return"";const t=e.toLowerCase();return t.substring(0,1).toUpperCase()+t.substring(1,t.length)},Qe=e=>{const t=e?.replace(/([A-Z])+/g,p)?.split(/(?=[A-Z])|[\.\-\s_]/).map(n=>n.toLowerCase())??[];return t.length===0?"":t.length===1?t[0]:t.reduce((n,r)=>`${n}${r.charAt(0).toUpperCase()}${r.slice(1)}`)},Ve=(e,t)=>{const n=e?.replace(/([A-Z])+/g,p).split(/(?=[A-Z])|[\.\-\s_]/).map(s=>s.toLowerCase())??[];if(n.length===0)return"";if(n.length===1)return n[0];const r=n.reduce((s,u)=>`${s}_${u.toLowerCase()}`);return t?.splitOnNumber===!1?r:r.replace(/([A-Za-z]{1}[0-9]{1})/,s=>`${s[0]}_${s[1]}`)},Ge=e=>{const t=e?.replace(/([A-Z])+/g,p)?.split(/(?=[A-Z])|[\.\-\s_]/).map(n=>n.toLowerCase())??[];return t.length===0?"":t.length===1?t[0]:t.reduce((n,r)=>`${n}-${r.toLowerCase()}`)},xe=e=>{const t=e?.split(/[\.\-\s_]/).map(n=>n.toLowerCase())??[];return t.length===0?"":t.map(n=>n.charAt(0).toUpperCase()+n.slice(1)).join("")},et=e=>e?e.split(/(?=[A-Z])|[\.\-\s_]/).map(t=>t.trim()).filter(t=>!!t).map(t=>p(t.toLowerCase())).join(" "):"",tt=(e,t,n=/\{\{(.+?)\}\}/g)=>Array.from(e.matchAll(n)).reduce((r,s)=>r.replace(s[0],t[s[1]]),e),nt=(e,t=" ")=>{if(!e)return"";const n=t.replace(/[\W]{1}/g,"\\$&"),r=new RegExp(`^[${n}]+|[${n}]+$`,"g");return e.replace(r,"")};return i.all=ke,i.alphabetical=ee,i.assign=I,i.boil=O,i.callable=ze,i.camel=Qe,i.capitalize=p,i.chain=Ce,i.clone=D,i.cluster=ue,i.compose=$e,i.construct=We,i.counting=te,i.crush=Ke,i.dash=Ge,i.debounce=Se,i.defer=pe,i.diff=he,i.draw=Je,i.first=G,i.flat=oe,i.fork=R,i.get=F,i.group=Y,i.guard=Ae,i.inRange=Me,i.intersects=fe,i.invert=De,i.isArray=w,i.isDate=T,i.isEmpty=X,i.isEqual=j,i.isFloat=J,i.isFunction=y,i.isInt=W,i.isNumber=h,i.isObject=k,i.isPrimitive=N,i.isPromise=S,i.isString=K,i.isSymbol=E,i.iterate=B,i.keys=v,i.last=x,i.list=C,i.listify=Ie,i.lowerize=Fe,i.map=ye,i.mapEntries=Ze,i.mapKeys=P,i.mapValues=Le,i.max=se,i.memo=Te,i.merge=le,i.min=ie,i.objectify=M,i.omit=Ue,i.parallel=be,i.partial=Pe,i.partob=_e,i.pascal=xe,i.pick=ve,i.proxied=Ee,i.random=_,i.range=A,i.reduce=we,i.replace=ne,i.replaceOrAppend=ae,i.retry=Oe,i.select=re,i.series=He,i.set=q,i.shake=Be,i.shift=me,i.shuffle=Xe,i.sift=ge,i.sleep=$,i.snake=Ve,i.sort=z,i.sum=V,i.template=tt,i.throttle=je,i.title=et,i.toFloat=Re,i.toInt=Z,i.toggle=de,i.trim=nt,i.try=m,i.tryit=m,i.uid=Ye,i.unique=ce,i.upperize=qe,i.zip=H,i.zipToObject=Q,i}({}); 2 | -------------------------------------------------------------------------------- /chiller.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Radash", 3 | "favicon": "docs/favicon.ico", 4 | "domain": "https://radash-docs.vercel.app", 5 | "description": "A functional utility library - modern, simple, typed, powerful", 6 | "logo": { 7 | "light": "docs/logo-light.png", 8 | "dark": "docs/logo-dark.png" 9 | }, 10 | "version": "v10.x.x", 11 | "index": "docs/getting-started", 12 | "pages": "./docs/**/*.mdx", 13 | "theme": "red-400", 14 | "repo": "https://github.com/rayepps/radash", 15 | "branch": "master", 16 | "sidebar": { 17 | "links": [ 18 | { 19 | "url": "/docs/getting-started", 20 | "icon": "book", 21 | "label": "Documentation" 22 | }, 23 | { 24 | "url": "https://npm.runkit.com/radash", 25 | "icon": "code", 26 | "label": "Playground" 27 | }, 28 | { 29 | "url": "https://github.com/rayepps/radash/discussions", 30 | "icon": "chat", 31 | "label": "Community" 32 | } 33 | ], 34 | "order": [ 35 | "Getting Started", 36 | "Array", 37 | "Async", 38 | "Curry", 39 | "Number", 40 | "Object", 41 | "Random", 42 | "Series", 43 | "String", 44 | "Typed" 45 | ] 46 | } 47 | } -------------------------------------------------------------------------------- /docs/array/alphabetical.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: alphabetical 3 | group: 'Array' 4 | description: Sorts an array of objects alphabetically by a property 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of objects and a callback function used to determine 10 | the property to use for sorting, return a new array with the objects 11 | sorted alphabetically. A third, and optional, argument allows you to 12 | sort in descending order instead of the default ascending order. 13 | 14 | For numerical sorting, see the [sort](./sort) function. 15 | 16 | ```ts 17 | import { alphabetical } from 'radash' 18 | 19 | const gods = [ 20 | { 21 | name: 'Ra', 22 | power: 100 23 | }, 24 | { 25 | name: 'Zeus', 26 | power: 98 27 | }, 28 | { 29 | name: 'Loki', 30 | power: 72 31 | }, 32 | { 33 | name: 'Vishnu', 34 | power: 100 35 | } 36 | ] 37 | 38 | alphabetical(gods, g => g.name) // => [Loki, Ra, Vishnu, Zeus] 39 | alphabetical(gods, g => g.name, 'desc') // => [Zeus, Vishnu, Ra, Loki] 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/array/boil.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: boil 3 | group: 'Array' 4 | description: 'Reduce a list of items down to one item' 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items return the final item that wins the comparison condition. Useful for more complicated min/max. 10 | 11 | ```ts 12 | import { boil } from 'radash' 13 | 14 | const gods = [ 15 | { 16 | name: 'Ra', 17 | power: 100 18 | }, 19 | { 20 | name: 'Zeus', 21 | power: 98 22 | }, 23 | { 24 | name: 'Loki', 25 | power: 72 26 | } 27 | ] 28 | 29 | boil(gods, (a, b) => (a.power > b.power ? a : b)) 30 | // => { name: 'Ra', power: 100 } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/array/cluster.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: cluster 3 | group: 'Array' 4 | description: Split a list into many lists of the given size 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items and a desired cluster size (`n`), returns an array 10 | of arrays. Each child array containing `n` (cluster size) items 11 | split as evenly as possible. 12 | 13 | ```ts 14 | import { cluster } from 'radash' 15 | 16 | const gods = ['Ra', 'Zeus', 'Loki', 'Vishnu', 'Icarus', 'Osiris', 'Thor', 'Apollo', 'Artemis', 'Athena'] 17 | 18 | cluster(gods, 3) 19 | // => [ 20 | // [ 'Ra', 'Zeus', 'Loki' ], 21 | // [ 'Vishnu', 'Icarus', 'Osiris' ], 22 | // ['Thor', 'Apollo', 'Artemis'], 23 | // ['Athena'] 24 | // ] 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/array/counting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: counting 3 | group: 'Array' 4 | description: Creates an object with counts of occurrences of items 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of objects and an identity callback function to determine 10 | how each object should be identified. Returns an object where the keys 11 | are the id values the callback returned and each value is an integer 12 | telling how many times that id occurred. 13 | 14 | ```ts 15 | import { counting } from 'radash' 16 | 17 | const gods = [ 18 | { 19 | name: 'Ra', 20 | culture: 'egypt' 21 | }, 22 | { 23 | name: 'Zeus', 24 | culture: 'greek' 25 | }, 26 | { 27 | name: 'Loki', 28 | culture: 'greek' 29 | } 30 | ] 31 | 32 | counting(gods, g => g.culture) // => { egypt: 1, greek: 2 } 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/array/diff.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: diff 3 | group: 'Array' 4 | description: Create an array of differences between two arrays 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given two arrays, returns an array of all items that exist in the first array 10 | but do not exist in the second array. 11 | 12 | ```ts 13 | import { diff } from 'radash' 14 | 15 | const oldWorldGods = ['ra', 'zeus'] 16 | const newWorldGods = ['vishnu', 'zeus'] 17 | 18 | diff(oldWorldGods, newWorldGods) // => ['ra'] 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/array/first.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: first 3 | group: 'Array' 4 | description: Get the first item from a list 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items return the first item or a default value if no items exists. 10 | 11 | ```ts 12 | import { first } from 'radash' 13 | 14 | const gods = ['ra', 'loki', 'zeus'] 15 | 16 | first(gods) // => 'ra' 17 | first([], 'vishnu') // => 'vishnu' 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/array/flat.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: flat 3 | group: 'Array' 4 | description: Flatten an array of arrays into a single dimension 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array that contains many arrays, return a new array where all items from the children are present at the top level. 10 | 11 | ```ts 12 | import { flat } from 'radash' 13 | 14 | const gods = [['ra', 'loki'], ['zeus']] 15 | 16 | flat(gods) // => [ra, loki, zeus] 17 | ``` 18 | 19 | Note, `_.flat` is not recursive and will not flatten children of children of children ... of children. It will only flatten `T[][]` an array of arrays. 20 | -------------------------------------------------------------------------------- /docs/array/fork.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: fork 3 | group: 'Array' 4 | description: Split an array into two arrays by a condition 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items and a condition, returns two arrays where the first contains all items that passed the condition and the second contains all items that failed the condition. 10 | 11 | ```ts 12 | import { fork } from 'radash' 13 | 14 | const gods = [ 15 | { 16 | name: 'Ra', 17 | power: 100 18 | }, 19 | { 20 | name: 'Zeus', 21 | power: 98 22 | }, 23 | { 24 | name: 'Loki', 25 | power: 72 26 | }, 27 | { 28 | name: 'Vishnu', 29 | power: 100 30 | } 31 | ] 32 | 33 | const [finalGods, lesserGods] = fork(gods, f => f.power > 90) // [[ra, vishnu, zues], [loki]] 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/array/group.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: group 3 | group: 'Array' 4 | description: 'Sort an array of items into groups' 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items, `group` will build up an object where each key is an array of the items that belong in that group. Generally, this can be useful to categorize an array. 10 | 11 | ```ts 12 | import { group } from 'radash' 13 | 14 | const fish = [ 15 | { 16 | name: 'Marlin', 17 | source: 'ocean' 18 | }, 19 | { 20 | name: 'Bass', 21 | source: 'lake' 22 | }, 23 | { 24 | name: 'Trout', 25 | source: 'lake' 26 | } 27 | ] 28 | 29 | const fishBySource = group(fish, f => f.source) // => { ocean: [marlin], lake: [bass, trout] } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/array/intersects.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: intersects 3 | group: 'Array' 4 | description: Determine if two arrays have a common item 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given two arrays of items, returns true if any item exists in both arrays. 10 | 11 | ```ts 12 | import { intersects } from 'radash' 13 | 14 | const oceanFish = ['tuna', 'tarpon'] 15 | const lakeFish = ['bass', 'trout'] 16 | 17 | intersects(oceanFish, lakeFish) // => false 18 | 19 | const brackishFish = ['tarpon', 'snook'] 20 | 21 | intersects(oceanFish, brackishFish) // => true 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/array/iterate.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: iterate 3 | group: 'Array' 4 | description: Iterate over a callback n times 5 | --- 6 | 7 | ## Basic usage 8 | 9 | A bit like `forEach` meets `reduce`. Useful for running a function `n` number of times to generate a value. The `_.iterate` function takes a count (the number of times to run the callback), a callback function, and an initial value. The callback is run _count_ many times as a reducer and the accumulated value is then returned. 10 | 11 | ```ts 12 | import { iterate } from 'radash' 13 | 14 | const value = iterate( 15 | 4, 16 | (acc, idx) => { 17 | return acc + idx 18 | }, 19 | 0 20 | ) // => 10 21 | ``` 22 | 23 | Note, this is **NOT** zero indexed. If you pass a `count` of 5 24 | you will get an index of 1, 2, 3, 4, 5 in the callback 25 | function. 26 | -------------------------------------------------------------------------------- /docs/array/last.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: last 3 | group: 'Array' 4 | description: Get the last item from a list 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items return the last item or a default value if no items exists. 10 | 11 | ```ts 12 | import { last } from 'radash' 13 | 14 | const fish = ['marlin', 'bass', 'trout'] 15 | 16 | const lastFish = last(fish) // => 'trout' 17 | const lastItem = last([], 'bass') // => 'bass' 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/array/list.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: list 3 | group: 'Array' 4 | description: Create a list with specific items 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a start, end, value, and step size returns a list with values from start to end by step size. 10 | 11 | The interface is identical to `range`. 12 | 13 | _A hat tip to Python's `range` functionality_ 14 | 15 | ```ts 16 | import { list } from 'radash' 17 | 18 | list(3) // [0, 1, 2, 3] 19 | list(0, 3) // [0, 1, 2, 3] 20 | list(0, 3, 'y') // [y, y, y, y] 21 | list(0, 3, () => 'y') // [y, y, y, y] 22 | list(0, 3, i => i) // [0, 1, 2, 3] 23 | list(0, 3, i => `y${i}`) // [y0, y1, y2, y3] 24 | list(0, 3, obj) // [obj, obj, obj, obj] 25 | list(0, 6, i => i, 2) // [0, 2, 4, 6] 26 | ``` 27 | 28 | ## Signatures 29 | 30 | The list function can do a lot with different arguments. 31 | 32 | ### list(size) 33 | 34 | When givin a single argument, it's treated as the `size`. Returns a list with values from 0 to `size`. 35 | 36 | ```ts 37 | list(3) // [0, 1, 2, 3] 38 | ``` 39 | 40 | ### list(start, end) 41 | 42 | When given two arguments, they're treated as the `start` and `end`. Returns a list with values from `start` to `end` 43 | 44 | ```ts 45 | list(2, 6) // [2, 3, 4, 5, 6] 46 | ``` 47 | 48 | ### list(start, end, value) 49 | 50 | When given a third argument it's treated as the `value` to be used in the list. If the `value` is a function it will be called, with an index argument, to create every value. 51 | 52 | ```ts 53 | list(2, 4, {}) // [{}, {}, {}] 54 | list(2, 4, null) // [null, null, null] 55 | list(2, 4, (i) => i) // [2, 3, 4] 56 | ``` 57 | 58 | ### list(start, end, value, step) 59 | 60 | When given a fourth argument it's treated as the `step` size to skip when generating values from `start` to `end`. 61 | 62 | ```ts 63 | list(2, 4, i => i, 2) // [2, 4] 64 | list(25, 100, i => i, 25) // [25, 50, 75, 100] 65 | ``` -------------------------------------------------------------------------------- /docs/array/max.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: max 3 | group: 'Array' 4 | description: Get the largest item from an array 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items and a function to get the value of each item, returns the item with the largest value. Uses `_.boil` under the hood. 10 | 11 | ```ts 12 | import { max } from 'radash' 13 | 14 | const fish = [ 15 | { 16 | name: 'Marlin', 17 | weight: 105, 18 | source: 'ocean' 19 | }, 20 | { 21 | name: 'Bass', 22 | weight: 8, 23 | source: 'lake' 24 | }, 25 | { 26 | name: 'Trout', 27 | weight: 13, 28 | source: 'lake' 29 | } 30 | ] 31 | 32 | max(fish, f => f.weight) // => {name: "Marlin", weight: 105, source: "ocean"} 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/array/merge.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: merge 3 | group: 'Array' 4 | description: Combine two lists overriding items in the first 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given two arrays of items and an identity function, returns the first 10 | list with all items from the second list where there was a match. 11 | 12 | ```ts 13 | import { merge } from 'radash' 14 | 15 | const gods = [ 16 | { 17 | name: 'Zeus', 18 | power: 92 19 | }, 20 | { 21 | name: 'Ra', 22 | power: 97 23 | } 24 | ] 25 | 26 | const newGods = [ 27 | { 28 | name: 'Zeus', 29 | power: 100 30 | } 31 | ] 32 | 33 | merge(gods, newGods, f => f.name) // => [{name: "Zeus" power: 100}, {name: "Ra", power: 97}] 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/array/min.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: min 3 | group: 'Array' 4 | description: Get the smallest item from an array 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items and a function to get the value of each item, returns the item with the smallest value. Uses `_.boil` under the hood. 10 | 11 | ```ts 12 | import { min } from 'radash' 13 | 14 | const fish = [ 15 | { 16 | name: 'Marlin', 17 | weight: 105, 18 | source: 'ocean' 19 | }, 20 | { 21 | name: 'Bass', 22 | weight: 8, 23 | source: 'lake' 24 | }, 25 | { 26 | name: 'Trout', 27 | weight: 13, 28 | source: 'lake' 29 | } 30 | ] 31 | 32 | min(fish, f => f.weight) // => {name: "Bass", weight: 8, source: "lake"} 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/array/objectify.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: objectify 3 | group: 'Array' 4 | description: Convert a list to a dictionary object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items, create a dictionary with keys and values mapped by given functions. 10 | First argument is the array to map. The second argument is the function to determine the key 11 | for each item. The third argument is optional and determines the value for each item. 12 | 13 | ```ts 14 | import { objectify } from 'radash' 15 | 16 | const fish = [ 17 | { 18 | name: 'Marlin', 19 | weight: 105 20 | }, 21 | { 22 | name: 'Bass', 23 | weight: 8 24 | }, 25 | { 26 | name: 'Trout', 27 | weight: 13 28 | } 29 | ] 30 | 31 | objectify(fish, f => f.name) // => { Marlin: [marlin object], Bass: [bass object], ... } 32 | objectify( 33 | fish, 34 | f => f.name, 35 | f => f.weight 36 | ) // => { Marlin: 105, Bass: 8, Trout: 13 } 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/array/range.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: range 3 | group: 'Array' 4 | description: Create a range used for iterating 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a start, end, value, and step size returns a generator that will yield values from start to end by step size. Useful for replacing `for (let i = 0)` with `for of`. Range will return a generator that `for of` will call one at a time, so it's safe to create large ranges. 10 | 11 | The interface is identical to `list`. 12 | 13 | _A hat tip to Python's `range` functionality_ 14 | 15 | ```ts 16 | import { range } from 'radash' 17 | 18 | range(3) // yields 0, 1, 2, 3 19 | range(0, 3) // yields 0, 1, 2, 3 20 | range(0, 3, 'y') // yields y, y, y, y 21 | range(0, 3, () => 'y') // yields y, y, y, y 22 | range(0, 3, i => i) // yields 0, 1, 2, 3 23 | range(0, 3, i => `y${i}`) // yields y0, y1, y2, y3 24 | range(0, 3, obj) // yields obj, obj, obj, obj 25 | range(0, 6, i => i, 2) // yields 0, 2, 4, 6 26 | 27 | for (const i of range(0, 200, 10)) { 28 | console.log(i) // => 0, 10, 20, 30 ... 190, 200 29 | } 30 | 31 | for (const i of range(0, 5)) { 32 | console.log(i) // => 0, 1, 2, 3, 4, 5 33 | } 34 | ``` 35 | 36 | ## Signatures 37 | 38 | The range function can do a lot with different arguments. 39 | 40 | ### range(size) 41 | 42 | When givin a single argument, it's treated as the `size`. Returns a generator that yields values from 0 to `size`. 43 | 44 | ```ts 45 | range(3) // yields 0, 1, 2, 3 46 | ``` 47 | 48 | ### range(start, end) 49 | 50 | When given two arguments, they're treated as the `start` and `end`. Returns a generator that yields values from `start` to `end` 51 | 52 | ```ts 53 | range(2, 6) // yields 2, 3, 4, 5, 6 54 | ``` 55 | 56 | ### range(start, end, value) 57 | 58 | When given a third argument it's treated as the `value` to be yielded in the generator. If the `value` is a function it will be called, with an index argument, to create every value. 59 | 60 | ```ts 61 | range(2, 4, {}) // yields {}, {}, {} 62 | range(2, 4, null) // yields null, null, null 63 | range(2, 4, (i) => i) // yields 2, 3, 4 64 | ``` 65 | 66 | ### range(start, end, value, step) 67 | 68 | When given a fourth argument it's treated as the `step` size to skip when yielding values from `start` to `end`. 69 | 70 | ```ts 71 | range(2, 4, i => i, 2) // yields 2, 4 72 | range(25, 100, i => i, 25) // yields 25, 50, 75, 100 73 | ``` -------------------------------------------------------------------------------- /docs/array/replace-or-append.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: replaceOrAppend 3 | group: 'Array' 4 | description: Replace item in array or append if no match 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items, an item and an identity function, returns a new array with the item either replaced at the index of the existing item -- if it exists, else it is appended at the end. 10 | 11 | ```ts 12 | import { replaceOrAppend } from 'radash' 13 | 14 | const fish = [ 15 | { 16 | name: 'Marlin', 17 | weight: 105 18 | }, 19 | { 20 | name: 'Salmon', 21 | weight: 19 22 | }, 23 | { 24 | name: 'Trout', 25 | weight: 13 26 | } 27 | ] 28 | 29 | const salmon = { 30 | name: 'Salmon', 31 | weight: 22 32 | } 33 | 34 | const sockeye = { 35 | name: 'Sockeye', 36 | weight: 8 37 | } 38 | 39 | replaceOrAppend(fish, salmon, f => f.name === 'Salmon') // => [marlin, salmon (weight:22), trout] 40 | replaceOrAppend(fish, sockeye, f => f.name === 'Sockeye') // => [marlin, salmon, trout, sockeye] 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/array/replace.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: replace 3 | group: 'Array' 4 | description: Replace an item in an array 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items, replace the one that matches the given condition function. Only replaces the first match. Always returns a copy of the original array. 10 | 11 | ```ts 12 | import { replace } from 'radash' 13 | 14 | const fish = [ 15 | { 16 | name: 'Marlin', 17 | weight: 105 18 | }, 19 | { 20 | name: 'Bass', 21 | weight: 8 22 | }, 23 | { 24 | name: 'Trout', 25 | weight: 13 26 | } 27 | ] 28 | 29 | const salmon = { 30 | name: 'Salmon', 31 | weight: 22 32 | } 33 | 34 | // read: replace fish with salmon where the name is Bass 35 | replace(fish, salmon, f => f.name === 'Bass') // => [marlin, salmon, trout] 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/array/select.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: select 3 | group: 'Array' 4 | description: Filter and map an array 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Applies a filter and a map operation at once and in one pass. 10 | 11 | ```ts 12 | import { select } from 'radash' 13 | 14 | const fish = [ 15 | { 16 | name: 'Marlin', 17 | weight: 105, 18 | source: 'ocean' 19 | }, 20 | { 21 | name: 'Bass', 22 | weight: 8, 23 | source: 'lake' 24 | }, 25 | { 26 | name: 'Trout', 27 | weight: 13, 28 | source: 'lake' 29 | } 30 | ] 31 | 32 | select( 33 | fish, 34 | f => f.weight, 35 | f => f.source === 'lake' 36 | ) // => [8, 13] 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/array/shift.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: shift 3 | description: Shift array items by n steps 4 | group: 'Array' 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a list of items, return an array that shift right n positions. 10 | 11 | ```ts 12 | import { shift } from 'radash' 13 | const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] 14 | shift(arr, 3) // => [7, 8, 9, 1, 2, 3, 4, 5, 6] 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/array/sift.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: sift 3 | group: 'Array' 4 | description: Remove all falsy items from list 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a list of items, return a new list with all items that are not falsy. 10 | 11 | ```ts 12 | import { sift } from 'radash' 13 | 14 | const fish = ['salmon', null, false, NaN, 'sockeye', 'bass'] 15 | 16 | sift(fish) // => ['salmon', 'sockeye', 'bass'] 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/array/sort.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: sort 3 | group: 'Array' 4 | description: Sort a list of objects by a numerical property 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of objects, return a new array sorted by the numerical property specified in the get function. A third, and optional, argument allows you to sort in descending order instead of the default ascending order. 10 | 11 | This function only supports numerical sorting. For alphabetic sorting, see the [alphabetical](./alphabetical) function. 12 | 13 | ```ts 14 | import { sort } from 'radash' 15 | 16 | const fish = [ 17 | { 18 | name: 'Marlin', 19 | weight: 105 20 | }, 21 | { 22 | name: 'Bass', 23 | weight: 8 24 | }, 25 | { 26 | name: 'Trout', 27 | weight: 13 28 | } 29 | ] 30 | 31 | sort(fish, f => f.weight) // => [bass, trout, marlin] 32 | sort(fish, f => f.weight, true) // => [marlin, trout, bass] 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/array/sum.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: sum 3 | group: 'Array' 4 | description: Add up all items of an array 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items, and an optional function to map each item to a number, add up all the items. 10 | 11 | ```ts 12 | import { sum } from 'radash' 13 | 14 | const fish = [ 15 | { 16 | name: 'Marlin', 17 | weight: 100 18 | }, 19 | { 20 | name: 'Bass', 21 | weight: 10 22 | }, 23 | { 24 | name: 'Trout', 25 | weight: 15 26 | } 27 | ] 28 | 29 | sum(fish, f => f.weight) // => 125 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/array/toggle.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: toggle 3 | description: Toggles an items existance in an array 4 | group: 'Array' 5 | --- 6 | 7 | ## Basic usage 8 | 9 | If the item matching the condition already exists in the list it will be removed. If it does not it will be added. 10 | 11 | ```ts 12 | import { toggle } from 'radash' 13 | 14 | const gods = ['ra', 'zeus', 'loki'] 15 | 16 | toggle(gods, 'ra') // => [zeus, loki] 17 | toggle(gods, 'vishnu') // => [ra, zeus, loki, vishnu] 18 | ``` 19 | 20 | ### toggle(list, item, identity) 21 | 22 | You can pass an optional `toKey` function to determine the identity of non-primitive values. Helpful when working with more complex data types. 23 | 24 | ```ts 25 | import { toggle } from 'radash' 26 | 27 | const ra = { name: 'Ra' } 28 | const zeus = { name: 'Zeus' } 29 | const loki = { name: 'Loki' } 30 | const vishnu = { name: 'Vishnu' } 31 | 32 | const gods = [ra, zeus, loki] 33 | 34 | toggle(gods, ra, g => g.name) // => [zeus, loki] 35 | toggle(gods, vishnu, g => g.name) // => [ra, zeus, loki, vishnu] 36 | ``` 37 | 38 | ### toggle(list, item, identity, options) 39 | 40 | By default, toggle will append the item if it does not exist. If you need to prepend the item instead you can override the `strategy` in the options argument. 41 | 42 | ```ts 43 | import { toggle } from 'radash' 44 | 45 | const gods = ['ra', 'zeus', 'loki'] 46 | 47 | toggle(gods, 'vishnu', g => g, { strategy: 'prepend' }) // => [vishnu, ra, zeus, loki] 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/array/unique.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: unique 3 | group: 'Array' 4 | description: Remove duplicates from an array 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items -- and optionally, a function to determine their identity -- return a new array without any duplicates. 10 | 11 | The function does not preserve the original order of items. 12 | 13 | ```ts 14 | import { unique } from 'radash' 15 | 16 | const fish = [ 17 | { 18 | name: 'Marlin', 19 | weight: 105, 20 | source: 'ocean' 21 | }, 22 | { 23 | name: 'Salmon', 24 | weight: 22, 25 | source: 'river' 26 | }, 27 | { 28 | name: 'Salmon', 29 | weight: 22, 30 | source: 'river' 31 | } 32 | ] 33 | 34 | unique( fish, f => f.name ) 35 | // [ 36 | // { name: 'Marlin', weight: 105, source: 'ocean' }, 37 | // { name: 'Salmon', weight: 22, source: 'river' } 38 | // ] 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/array/zip-to-object.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: zipToObject 3 | group: 'Array' 4 | description: Combine multiple arrays in sets 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Creates an object mapping the keys in the first array to their corresponding values in the second array. 10 | 11 | ```ts 12 | import { zipToObject } from 'radash' 13 | 14 | const names = ['ra', 'zeus', 'loki'] 15 | const cultures = ['egypt', 'greek', 'norse'] 16 | 17 | zipToObject(names, cultures) 18 | // => { ra: egypt, zeus: greek, loki: norse } 19 | 20 | zipToObject(names, (k, i) => k + i) 21 | // => { ra: ra0, zeus: zeus1, loki: loki2 } 22 | 23 | zipToObject(names, null) 24 | // => { ra: null, zeus: null, loki: null } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/array/zip.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: zip 3 | group: 'Array' 4 | description: Combine multiple arrays in sets 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Creates an array of grouped elements, the first of which contains the first elements of the given arrays, the second of which contains the second elements of the given arrays, and so on. 10 | 11 | ```ts 12 | import { zip } from 'radash' 13 | 14 | const names = ['ra', 'zeus', 'loki'] 15 | const cultures = ['egypt', 'greek', 'norse'] 16 | 17 | zip(names, cultures) 18 | // => [ 19 | // [ra, egypt] 20 | // [zeus, greek] 21 | // [loki, norse] 22 | // ] 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/async/all.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: all 3 | group: 'Async' 4 | description: Await many promises 5 | --- 6 | 7 | The `all` function is similar to the builtin Promise.all or Promise.allSettled 8 | functions. Given a list (or object) of promises, if any errors are thrown, all 9 | errors are gathered and thrown in an AggregateError. 10 | 11 | ## Using an Array 12 | 13 | Passing an array as an argument will return the resolved promise values as an array in the same order. 14 | 15 | ```ts 16 | import { all } from 'radash' 17 | 18 | const [user] = await all([ 19 | api.users.create(...), 20 | s3.buckets.create(...), 21 | slack.customerSuccessChannel.sendMessage(...) 22 | ]) 23 | ``` 24 | 25 | ## Using an Object 26 | 27 | Passing an object as an argument will return an object with the same keys and the values as the resolved promise values. 28 | 29 | ```ts 30 | import { all } from 'radash' 31 | 32 | const { user } = await all({ 33 | user: api.users.create(...), 34 | bucket: s3.buckets.create(...), 35 | message: slack.customerSuccessChannel.sendMessage(...) 36 | }) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/async/defer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: defer 3 | group: 'Async' 4 | description: Run an async function with deferred functions 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | The `_.defer` functions lets you run an async function, registering functions as you go 13 | that should be deferred until the async function completes, and then executed. This is 14 | really useful in scripts where failure up to or after a specific point will require some 15 | cleanup. It's a bit like a `finally` block. 16 | 17 | A hat tip to Swift's `defer` for the inspiration. 18 | 19 | The function passed to `_.defer` is called with a single `register` function argument that 20 | can be used to register the work you want to be called when the function completes. If your function throws an error and then a registered cleanup function throws 21 | and error it is ignored by default. The register 22 | function supports an optional second `options` argument that lets you configure a rethrow 23 | strategy so that error in the cleanup function is rethrown. 24 | 25 | ```ts 26 | import { defer } from 'radash' 27 | 28 | await defer(async (cleanup) => { 29 | const buildDir = await createBuildDir() 30 | 31 | cleanup(() => fs.unlink(buildDir)) 32 | 33 | await build() 34 | }) 35 | 36 | await defer(async (register) => { 37 | const org = await api.org.create() 38 | register(async () => api.org.delete(org.id), { rethrow: true }) 39 | 40 | const user = await api.user.create() 41 | register(async () => api.users.delete(user.id), { rethrow: true }) 42 | 43 | await executeTest(org, user) 44 | }) 45 | ``` 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/async/guard.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: guard 3 | group: 'Async' 4 | description: Have a function return undefined if it errors out 5 | --- 6 | 7 | ## Basic usage 8 | 9 | This lets you set a default value if an async function errors out. 10 | 11 | ```ts 12 | const users = (await guard(fetchUsers)) ?? [] 13 | ``` 14 | 15 | You can choose to guard only specific errors too 16 | 17 | ```ts 18 | const isInvalidUserError = (err: any) => err.code === 'INVALID_ID' 19 | const user = (await guard(fetchUser, isInvalidUserError)) ?? DEFAULT_USER 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/async/map.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: map 3 | group: 'Async' 4 | description: Map an array with an async function 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | A map that handles callback functions that return a promise. 13 | 14 | ```ts 15 | import { map } from 'radash' 16 | 17 | const userIds = [1, 2, 3, 4] 18 | 19 | const users = await map(userIds, async (userId) => { 20 | return await api.users.find(userId) 21 | }) 22 | ``` 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/async/parallel.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: parallel 3 | group: 'Async' 4 | description: Run many async function in parallel 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | Like `_.map` but built specifically to run the async callback functions 13 | in parallel. The first argument is a limit of how many functions should 14 | be allowed to run at once. Returns an array of results. 15 | 16 | ```ts 17 | import { parallel } from 'radash' 18 | 19 | const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9] 20 | 21 | // Will run the find user async function 3 at a time 22 | // starting another request when one of the 3 is freed 23 | const users = await parallel(3, userIds, async (userId) => { 24 | return await api.users.find(userId) 25 | }) 26 | ``` 27 | 28 | ## Errors 29 | 30 | When all work is complete parallel will check for errors. If any 31 | occurred they will all be thrown in a single `AggregateError` that 32 | has an `errors` property that is all the errors that were thrown. 33 | 34 | ```ts 35 | import { parallel, try as tryit } from 'radash' 36 | 37 | const userIds = [1, 2, 3] 38 | 39 | const [err, users] = await tryit(parallel)(3, userIds, async (userId) => { 40 | throw new Error(`No, I don\'t want to find user ${userId}`) 41 | }) 42 | 43 | console.log(err) // => AggregateError 44 | console.log(err.errors) // => [Error, Error, Error] 45 | console.log(err.errors[1].message) // => No, I don't want to find user 2 46 | ``` 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/async/reduce.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: reduce 3 | group: 'Async' 4 | description: Reduce an array with an async function 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | A reduce that handles callback functions that return a promise. 13 | 14 | ```ts 15 | import { reduce } from 'radash' 16 | 17 | const userIds = [1, 2, 3, 4] 18 | 19 | const users = await reduce(userIds, async (acc, userId) => { 20 | const user = await api.users.find(userId) 21 | return { 22 | ...acc, 23 | [userId]: user 24 | } 25 | }, {}) 26 | ``` 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/async/retry.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: retry 3 | group: 'Async' 4 | description: Run an async function retrying if it fails 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | The `_.retry` function allows you to run an async function and automagically retry it if it fails. Given the async func to run, an optional max number of retries (`r`), and an optional milliseconds to delay between retries (`d`), the given async function will be called, retrying `r` many times, and waiting `d` milliseconds between retries. 13 | 14 | The `times` option defaults to `3`. The `delay` option (defaults to null) can specify milliseconds to sleep between attempts. 15 | 16 | The `backoff` option is like delay but uses a function to sleep -- makes for easy exponential backoff. 17 | 18 | ```ts 19 | import { retry } from 'radash' 20 | 21 | await retry({}, api.users.list) 22 | await retry({ times: 10 }, api.users.list) 23 | await retry({ times: 2, delay: 1000 }, api.users.list) 24 | 25 | // exponential backoff 26 | await retry({ backoff: i => 10**i }, api.users.list) 27 | ``` 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/async/sleep.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: sleep 3 | group: 'Async' 4 | description: Asynchronously wait for time to pass 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | The `_.sleep` function allows you to delay in milliseconds. 13 | 14 | ```ts 15 | import { sleep } from 'radash' 16 | 17 | await sleep(2000) // => waits 2 seconds 18 | ``` 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/async/tryit.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: tryit 3 | group: 'Async' 4 | description: Convert a function to an error-first function 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Error-first callbacks were cool. Using mutable variables to hoist state when doing try/catch was not cool. 10 | 11 | The `tryit` function let's you wrap a function to convert it to an error-first function. **Works for both async and sync functions.** 12 | 13 | ```ts 14 | import { tryit } from 'radash' 15 | 16 | const [err, user] = await tryit(api.users.find)(userId) 17 | ``` 18 | 19 | ### Currying 20 | 21 | You can curry `tryit` if you like. 22 | 23 | ```ts 24 | import { tryit } from 'radash' 25 | 26 | const findUser = tryit(api.users.find) 27 | 28 | const [err, user] = await findUser(userId) 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/core-concepts.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Core Concepts" 3 | group: "Getting Started" 4 | description: "Why is Radash so dashing?" 5 | --- 6 | 7 | ## Keep it Simple 8 | 9 | ### Readable 10 | 11 | The Radash source is easy to read and understand. We don't make you navigate through internal library modules and classes, reading a hundred lines of code, to understand what a function does or how it works. 12 | 13 | As an example, here's a look at [the source](https://github.com/rayepps/radash/blob/master/src/curry.ts#L11-L13) for the `_.compose` function. 14 | 15 | ```ts 16 | export const compose = (...funcs: Func[]) => { 17 | return funcs.reverse().reduce((acc, fn) => fn(acc)) 18 | } 19 | ``` 20 | 21 | Thats it... thats the function. 22 | 23 | ### Semi-Functional 24 | 25 | Functional programming has incredible design patterns that we often pull from. However, we're not dire hard functional engineers. You don't have to understand monads to use Radash. Most Radash functions are deterministic and/or pure. 26 | 27 | ## Safe 28 | 29 | ### Types 30 | 31 | Radash is written in TypeScript and provides full typing out of the box. 32 | 33 | -------------------------------------------------------------------------------- /docs/curry/chain.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: chain 3 | group: 'Curry' 4 | description: Create a chain of function to run in order 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Chaining functions will cause them to execute one after another, passing the output from each function as the input to the next, returning the final output at the end of the chain. 10 | ```ts 11 | import { chain } from 'radash' 12 | 13 | const add = (y: number) => (x: number) => x + y 14 | const mult = (y: number) => (x: number) => x * y 15 | const addFive = add(5) 16 | const double = mult(2) 17 | 18 | const chained = chain(addFive, double) 19 | 20 | chained(0) // => 10 21 | chained(7) // => 24 22 | ``` 23 | 24 | ### Example 25 | ```ts 26 | import { chain } from 'radash' 27 | 28 | type Deity = { 29 | name: string 30 | rank: number 31 | } 32 | 33 | const gods: Deity[] = [ 34 | { rank: 8, name: 'Ra' }, 35 | { rank: 7, name: 'Zeus' }, 36 | { rank: 9, name: 'Loki' } 37 | ] 38 | 39 | const getName = (god: Deity) => item.name 40 | const upperCase = (text: string) => text.toUpperCase() as Uppercase 41 | 42 | const getUpperName = chain( 43 | getName, 44 | upperCase 45 | ) 46 | 47 | getUpperName(gods[0]) // => 'RA' 48 | gods.map(getUpperName) // => ['RA', 'ZEUS', 'LOKI'] 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/curry/compose.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: compose 3 | group: 'Curry' 4 | description: Create a composition of functions 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | In a composition of functions, each function is given the next function as an argument and must call it to continue executing. 13 | 14 | ```ts 15 | import { compose } from 'radash' 16 | 17 | const useZero = (fn: any) => () => fn(0) 18 | const objectize = (fn: any) => (num: any) => fn({ num }) 19 | const increment = (fn: any) => ({ num }: any) => fn({ num: num + 1 }) 20 | const returnArg = (arg: any) => (args: any) => args[arg] 21 | 22 | const composed = compose( 23 | useZero, 24 | objectize, 25 | increment, 26 | increment, 27 | returnArg('num') 28 | ) 29 | 30 | composed() // => 2 31 | ``` 32 | 33 | This can be a little jarring if you haven't seen it before. Here's a broken down composition. It's equivelent to the code above. 34 | 35 | ```ts 36 | const decomposed = ( 37 | useZero( 38 | objectize( 39 | increment( 40 | increment( 41 | returnArg('num'))))) 42 | ) 43 | 44 | decomposed() // => 2 45 | ``` 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/curry/debounce.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: debounce 3 | group: 'Curry' 4 | description: Create a debounced callback function 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Debounce accepts an options object with a `delay` and a source function to call 10 | when invoked. When the returned function is invoked it will only call the source 11 | function after the `delay` milliseconds of time has passed. Calls that don't result 12 | in invoking the source reset the delay, pushing off the next invocation. 13 | 14 | ```ts 15 | import { debounce } from 'radash' 16 | 17 | const makeSearchRequest = (event) => { 18 | api.movies.search(event.target.value) 19 | } 20 | 21 | input.addEventListener('change', debounce({ delay: 100 }, makeSearchRequest)) 22 | ``` 23 | 24 | ## Timing 25 | 26 | A visual of the debounce behavior when `delay` is `100`. The debounce function 27 | returned by `debounce` can be called every millisecond but it will only call 28 | the given callback after `delay` milliseconds have passed. 29 | 30 | ```sh 31 | Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - - 32 | debounce Invocations: x x x x - - - - - - - - x x x x x x x x x x - - - - - - - - - - - - 33 | Source Invocations: - - - - - - - - - - x - - - - - - - - - - - - - - - - - x - - - - - 34 | ``` 35 | 36 | ### Cancel 37 | 38 | The function returned by `debounce` has a `cancel` method that when called will permanently stop the source function from being debounced. 39 | 40 | ```ts 41 | const debounced = debounce({ delay: 100 }, api.feed.refresh) 42 | 43 | // ... sometime later 44 | 45 | debounced.cancel() 46 | ``` 47 | 48 | ### Flush 49 | 50 | The function returned by `debounce` has a `flush` method that when called will directly invoke the source function. 51 | 52 | ```ts 53 | const debounced = debounce({ delay: 100 }, api.feed.refresh) 54 | 55 | // ... sometime later 56 | 57 | debounced.flush(event) 58 | ``` 59 | 60 | ### isPending 61 | 62 | The function returned by `debounce` has a `isPending` method that when called will return if there is any pending invocation the source function. 63 | 64 | ```ts 65 | const debounced = debounce({ delay: 100 }, api.feed.refresh) 66 | 67 | // ... sometime later 68 | 69 | debounced.isPending() 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /docs/curry/memo.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: memo 3 | group: 'Curry' 4 | description: Memoize a function 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | Wrap a function with memo to get a function back that automagically returns values that have already been calculated. 13 | 14 | ```ts 15 | import { memo } from 'radash' 16 | 17 | const timestamp = memo(() => Date.now()) 18 | 19 | const now = timestamp() 20 | const later = timestamp() 21 | 22 | now === later // => true 23 | ``` 24 | 25 | ## Expiration 26 | 27 | You can optionally pass a `ttl` (time to live) that will expire memoized results. In versions prior to version 10, `ttl` had a value of 300 milliseconds if not specified. 28 | 29 | ```ts 30 | import { memo, sleep } from 'radash' 31 | 32 | const timestamp = memo(() => Date.now(), { 33 | ttl: 1000 // milliseconds 34 | }) 35 | 36 | const now = timestamp() 37 | const later = timestamp() 38 | 39 | await sleep(2000) 40 | 41 | const muchLater = timestamp() 42 | 43 | now === later // => true 44 | now === muchLater // => false 45 | ``` 46 | 47 | ## Key Function 48 | 49 | You can optionally customize how values are stored when memoized. 50 | 51 | ```ts 52 | const timestamp = memo(({ group }: { group: string }) => { 53 | const ts = Date.now() 54 | return `${ts}::${group}` 55 | }, { 56 | key: ({ group }: { group: string }) => group 57 | }) 58 | 59 | const now = timestamp({ group: 'alpha' }) 60 | const later = timestamp({ group: 'alpha' }) 61 | const beta = timestamp({ group: 'beta' }) 62 | 63 | now === later // => true 64 | beta === now // => false 65 | ``` 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/curry/partial.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: partial 3 | group: 'Curry' 4 | description: Create a partial a function 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | Create a partial function by providing some -- or all -- of the arguments the given function needs. 13 | 14 | ```ts 15 | import { partial } from 'radash' 16 | 17 | const add = (a: number, b: number) => a + b 18 | 19 | const addFive = partial(add, 5) 20 | 21 | addFive(2) // => 7 22 | ``` 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/curry/partob.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: partob 3 | group: 'Curry' 4 | description: Create a partob a function 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | Modern javascript destructuring means a lot of developers, libraries, and frameworks are all opting for unary functions that take a single object that contains the arguments. The `_.partob` function let's you partob these unary functions. 13 | 14 | ```ts 15 | import { partob } from 'radash' 16 | 17 | const add = (props: { a: number; b: number }) => props.a + props.b 18 | 19 | const addFive = partob(add, { a: 5 }) 20 | 21 | addFive({ b: 2 }) // => 7 22 | ``` 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/curry/proxied.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: proxied 3 | group: 'Curry' 4 | description: Create a dynamic proxied a object 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | Javascript's `Proxy` object is powerful but a bit awkward to use. The `_.proxied` function creates the `Proxy` for you and handles calling back to your handler when functions on the `Proxy` are called or properties are accessed. 13 | 14 | ```ts 15 | import { proxied } from 'radash' 16 | 17 | type Property = 'name' | 'size' | 'getLocation' 18 | 19 | const person = proxied((prop: Property) => { 20 | switch (prop) { 21 | case 'name': 22 | return 'Joe' 23 | case 'size': 24 | return 20 25 | case 'getLocation' 26 | return () => 'here' 27 | } 28 | }) 29 | 30 | person.name // => Joe 31 | person.size // => 20 32 | person.getLocation() // => here 33 | ``` 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/curry/throttle.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: throttle 3 | group: 'Curry' 4 | description: Create a throttled callback function 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | Throttle accepts an options object with an `interval` and a source function to call 13 | when invoked. When the returned function is invoked it will only call the source 14 | function if the `interval` milliseconds of time has passed. Otherwise, it will ignore 15 | the invocation. 16 | 17 | 18 | ```ts 19 | import { throttle } from 'radash' 20 | 21 | const onMouseMove = () => { 22 | rerender() 23 | } 24 | 25 | addEventListener('mousemove', throttle({ interval: 200 }, onMouseMove)) 26 | ``` 27 | 28 | ## Timing 29 | 30 | A visual of the throttle behavior when `interval` is `200`. The throttle function 31 | returned by `throttle` can be called every millisecond but it will only call 32 | the given callback after `interval` milliseconds have passed. 33 | 34 | ```sh 35 | Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - - 36 | Throttle Invocations: x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x - - - 37 | Source Invocations: x - - - - - - - - - - - - x - - - - - - - - - - - - - x - - - - - - 38 | ``` 39 | 40 | ### isThrottled 41 | 42 | The function returned by `throttle` has a `isThrottled` method that when called will return if there is any active throttle. 43 | 44 | ```ts 45 | const debounced = throttle({ interval: 200 }, onMouseMove) 46 | 47 | // ... sometime later 48 | 49 | debounced.isThrottled() 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sodiray/radash/069b26cdd7d62e6ac16a0ad3baa1c9abcca420bc/docs/favicon.ico -------------------------------------------------------------------------------- /docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | group: "Getting Started" 4 | description: "Welcome to Radash" 5 | --- 6 | 7 | ## Getting Started 8 | 9 | ### Welcome to 2020s 10 | 11 | Radash is the next library you can't live without. First and foremost, it's powerful. With those 12 | powerful functions, you get strong types and zero dependencies. If you can step out in a bit of 13 | faith and try the functions out, I have no doubt you'll find yourself falling in love. 14 | 15 | ### Featured Functions 16 | 17 | Come, dip your toe in the water. Here are a few functions we can't live without anymore. 18 | Hopefully, you'll find them useful as well. 19 | 20 | #### try 21 | The `_.try` function abstracts the logical fork of a try/catch and provides an _error 22 | first callback_ reminiscent response. 23 | 24 | ```ts 25 | const [err, response] = await _.try(api.gods.create)({ name: 'Ra' }) 26 | if (err) { 27 | throw new Error('Your god is weak and could not be created') 28 | } 29 | ``` 30 | 31 | #### range 32 | The `_.range` function returns a generator that can be used for iterating. This means you'll 33 | never have to write a `for (let i)` loop again -- and you shouldn't. 34 | 35 | ```ts 36 | for (const i of _.range(0, 4)) { 37 | console.log(i) // 0, 1, 2, 3, 4 38 | } 39 | 40 | for (const i of _.range(10, 20, 2)) { 41 | console.log(i) // 10, 12, 14, 16, 18, 20 42 | } 43 | ``` 44 | 45 | #### select 46 | The `_.select` function takes a mapper and filter function and runs them together for you in 47 | a single iteration. No more writing a reduce because you need to map and filter, and you don't 48 | want to implement them separately for performance's sake. 49 | 50 | ```ts 51 | const superPoweredGodsFromEgypt = _.select( 52 | gods, 53 | g => ({ ...g, power: g.power * g.power }), 54 | g => g.culture === 'egypt' 55 | ) 56 | ``` 57 | 58 | #### defer 59 | The `_.defer` function lets you register functions to run as cleanup while running an async function. It's 60 | like a try/finally, but you can register the finally block at specific times. 61 | 62 | ```ts 63 | await _.defer(async (defer) => { 64 | await api.builds.updateStatus('in-progress') 65 | defer((err) => { 66 | api.builds.updateStatus(err ? 'failed' : 'success') 67 | }) 68 | 69 | fs.mkdir('build') 70 | defer(() => { 71 | fs.unlink('build') 72 | }) 73 | 74 | await build() 75 | }) 76 | ``` 77 | 78 | #### objectify 79 | The `_.objectify` function helps you convert a list to an object in one step. Typically, we either do this 80 | in two steps or write a reduce. 81 | 82 | ```ts 83 | const godsByCulture = _.objectify(gods, g => g.name, g => g.culture) 84 | ``` 85 | 86 | ## Love and Hate 87 | 88 | ### Lodash 89 | 90 | Lodash was incredible. When JavaScript was still maturing, it provided the power to things you couldn't 91 | easily do in the vanilla. That was last decade. In this decade, the needs are different. The goal with Radash 92 | is to provide the powerful functions you need, not the ones the runtimes now provide, and to do it with great types 93 | and source code that's easy to read and understand. 94 | 95 | #### Saying No 96 | 97 | Radash does not provide `_.map` or `_.filter` functions. They were helpful before optional chaining and nullish coalescing. 98 | Now, there really isn't a need. 99 | 100 | In the last ten years, the JavaScript community as a whole, and the Typescript community specifically, has moved 101 | closer to some key values: **deterministic is good, polymorphic is bad, strong types are everything**. A part of Lodash's 102 | charm was that it let you pass different types to a function and get other behavior based on the type. An example 103 | is the `_.map` function, which can take a collection or an object and map over either. Radash doesn't provide that 104 | kind of polymorphic behavior. 105 | 106 | Sorry, not sorry. 107 | -------------------------------------------------------------------------------- /docs/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installation" 3 | group: "Getting Started" 4 | description: "Getting radash into your project" 5 | --- 6 | 7 | ## Node 8 | 9 | ### NPM 10 | 11 | Run 12 | 13 | ``` 14 | npm install radash 15 | ``` 16 | 17 | ### Yarn 18 | 19 | Run 20 | 21 | ``` 22 | yarn add radash 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sodiray/radash/069b26cdd7d62e6ac16a0ad3baa1c9abcca420bc/docs/logo-dark.png -------------------------------------------------------------------------------- /docs/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sodiray/radash/069b26cdd7d62e6ac16a0ad3baa1c9abcca420bc/docs/logo-light.png -------------------------------------------------------------------------------- /docs/number/in-range.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: inRange 3 | description: Checks if the given number is between two numbers. The starting number is inclusive. The ending number is exclusive. The start and the end of the range can be ascending OR descending order. If end is not specified, it's set to start value. And start is set to 0. 4 | group: Number 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass the number, the start and the end (optional) of the range. The `_.inRange` function will return true if the given number is in the range. 10 | 11 | ```ts 12 | import { inRange } from 'radash' 13 | 14 | inRange(10, 0, 20) // true 15 | inRange(9.99, 0, 10) // true 16 | inRange(Math.PI, 0, 3.15) // true 17 | inRange(10, 10, 20) // true 18 | inRange(10, 0, 10) // false 19 | 20 | inRange(1, 2) // true 21 | inRange(1, 0) // false 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/number/to-float.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: toFloat 3 | description: Convert a value to a float if possible 4 | group: Number 5 | --- 6 | 7 | ## Basic usage 8 | 9 | The `_.toFloat` function will do its best to convert the given value to a float. 10 | 11 | ```ts 12 | import { toFloat } from 'radash' 13 | 14 | toFloat(0) // => 0.0 15 | toFloat(null) // => 0.0 16 | toFloat(null, 3.33) // => 3.33 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/number/to-int.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: toInt 3 | description: Convert a value to an int if possible 4 | group: Number 5 | --- 6 | 7 | ## Basic usage 8 | 9 | The `_.toInt` function will do its best to convert the given value to an int. 10 | 11 | ```ts 12 | import { toInt } from 'radash' 13 | 14 | toInt(0) // => 0 15 | toInt(null) // => 0 16 | toInt(null, 3) // => 3 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/object/assign.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: assign 3 | description: Merges two objects together recursively 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Merges two objects together recursively into a new object applying values from right to left. Recursion only applies to child object properties. 10 | 11 | ```ts 12 | import { assign } from 'radash' 13 | 14 | const ra = { 15 | name: 'Ra', 16 | power: 100 17 | } 18 | 19 | assign(ra, { name: 'Loki' }) 20 | // => { name: Loki, power: 100 } 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/object/clone.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: clone 3 | description: Creates a shallow copy of the given object/value. 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Creates a shallow copy of the given object/value. 10 | 11 | ```ts 12 | import { clone } from 'radash' 13 | 14 | const ra = { 15 | name: 'Ra', 16 | power: 100 17 | } 18 | 19 | const gods = [ra] 20 | 21 | clone(ra) // => copy of ra 22 | clone(gods) // => copy of gods 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/object/construct.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: construct 3 | description: Builds an object from key paths and values 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | The opposite of crush, given an object that was crushed into key paths and values will return the original object reconstructed. 10 | 11 | ```ts 12 | import { construct } from 'radash' 13 | 14 | const flat = { 15 | name: 'ra', 16 | power: 100, 17 | 'friend.name': 'loki', 18 | 'friend.power': 80, 19 | 'enemies.0.name': 'hathor', 20 | 'enemies.0.power': 12 21 | } 22 | 23 | construct(flat) 24 | // { 25 | // name: 'ra', 26 | // power: 100, 27 | // friend: { 28 | // name: 'loki', 29 | // power: 80 30 | // }, 31 | // enemies: [ 32 | // { 33 | // name: 'hathor', 34 | // power: 12 35 | // } 36 | // ] 37 | // } 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/object/crush.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: crush 3 | description: Flattens a deep object to a single dimension 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Flattens a deep object to a single dimension. The deep keys will be converted to a dot notation in the new object. 10 | 11 | ```ts 12 | import { crush } from 'radash' 13 | 14 | const ra = { 15 | name: 'ra', 16 | power: 100, 17 | friend: { 18 | name: 'loki', 19 | power: 80 20 | }, 21 | enemies: [ 22 | { 23 | name: 'hathor', 24 | power: 12 25 | } 26 | ] 27 | } 28 | 29 | crush(ra) 30 | // { 31 | // name: 'ra', 32 | // power: 100, 33 | // 'friend.name': 'loki', 34 | // 'friend.power': 80, 35 | // 'enemies.0.name': 'hathor', 36 | // 'enemies.0.power': 12 37 | // } 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/object/get.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: get 3 | description: Get any attribute or child attribute using a deep path 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given any value and a select function to get the desired attribute, returns the desired value or a default value if the desired value couldn't be found. 10 | 11 | ```ts 12 | import { get } from 'radash' 13 | 14 | const fish = { 15 | name: 'Bass', 16 | weight: 8, 17 | sizes: [ 18 | { 19 | maturity: 'adult', 20 | range: [7, 18], 21 | unit: 'inches' 22 | } 23 | ] 24 | } 25 | 26 | get( fish, 'sizes[0].range[1]' ) // 18 27 | get( fish, 'sizes.0.range.1' ) // 18 28 | get( fish, 'foo', 'default' ) // 'default' 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/object/invert.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: invert 3 | description: Invert the keys and values of an object 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an object returns a new object with the keys and values reversed. 10 | 11 | ```ts 12 | import { invert } from 'radash' 13 | 14 | const powersByGod = { 15 | ra: 'sun', 16 | loki: 'tricks', 17 | zeus: 'lighning' 18 | } 19 | 20 | invert(gods) // => { sun: ra, tricks: loki, lightning: zeus } 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/object/keys.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: keys 3 | description: Get all keys from an object deeply 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an object, return all of it's keys and children's keys deeply as a flat string list. 10 | 11 | ```ts 12 | import { keys } from 'radash' 13 | 14 | const ra = { 15 | name: 'ra', 16 | power: 100, 17 | friend: { 18 | name: 'loki', 19 | power: 80 20 | }, 21 | enemies: [ 22 | { 23 | name: 'hathor', 24 | power: 12 25 | } 26 | ] 27 | } 28 | 29 | keys(ra) 30 | // => [ 31 | // 'name', 32 | // 'power', 33 | // 'friend.name', 34 | // 'friend.power', 35 | // 'enemies.0.name', 36 | // 'enemies.0.power' 37 | // ] 38 | ``` 39 | 40 | This is a function you might like to use with `get`, which dynamically looks up values in an object given a string path. Using the two together you could do something like flatten a deep object. 41 | 42 | ```ts 43 | import { keys, get, objectify } from 'radash' 44 | 45 | objectify( 46 | keys(ra), 47 | key => key, 48 | key => get(ra, key) 49 | ) 50 | // => { 51 | // 'name': 'ra' 52 | // 'power': 100 53 | // 'friend.name': 'loki' 54 | // 'friend.power': 80 55 | // 'enemies.0.name': 'hathor' 56 | // 'enemies.0.power': 12 57 | // } 58 | ``` 59 | 60 | _As of v10.5.0+ you can get this behavior via the crush function_ 61 | -------------------------------------------------------------------------------- /docs/object/listify.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: listify 3 | description: Convert an object to a list 4 | group: Object 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | Given an object and a mapping function, return an array with an item for each entry in the object. 13 | 14 | ```ts 15 | import { listify } from 'radash' 16 | 17 | const fish = { 18 | marlin: { 19 | weight: 105, 20 | }, 21 | bass: { 22 | weight: 8, 23 | } 24 | } 25 | 26 | listify(fish, (key, value) => ({ ...value, name: key })) // => [{ name: 'marlin', weight: 105 }, { name: 'bass', weight: 8 }] 27 | ``` 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/object/lowerize.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: lowerize 3 | description: Convert all object keys to lower case 4 | group: Object 5 | --- 6 | 7 | 8 | 9 | 10 | ## Basic usage 11 | 12 | Convert all keys in an object to lower case. Useful to standardize attribute key casing. For example, headers. 13 | 14 | ```ts 15 | import { lowerize } from 'radash' 16 | 17 | const ra = { 18 | Mode: 'god', 19 | Power: 'sun' 20 | } 21 | 22 | lowerize(ra) // => { mode, power } 23 | ``` 24 | 25 | The `_.lowerize` function is a shortcut for `_.mapKeys(obj, k => k.toLowerCase())` 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/object/map-entries.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: mapEntries 3 | description: Map the keys and values of an object 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Iterates the entries of an object, calling the given `toEntry` callback function 10 | to generate new entries. It's a `_.mapValues` and `_.mapKeys` 11 | in one. The `toEntry` callback function should return an array with 12 | two items `[key, value]` (a.k.a the new entry). 13 | 14 | ```ts 15 | import { mapEntries } from 'radash' 16 | 17 | const ra = { 18 | name: 'Ra', 19 | power: 'sun', 20 | rank: 100, 21 | culture: 'egypt' 22 | } 23 | 24 | mapEntries(ra, (key, value) => [key.toUpperCase(), `${value}`]) // => { NAME: 'Ra', POWER: 'sun', RANK: '100', CULTURE: 'egypt' } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/object/map-keys.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: mapKeys 3 | description: Map over the keys of an object 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an object and a `toKey` callback function, returns a new object with all the keys 10 | mapped through the `toKey` function. The callback is given both the key and value for each entry. 11 | 12 | ```ts 13 | import { mapKeys } from 'radash' 14 | 15 | const ra = { 16 | mode: 'god', 17 | power: 'sun' 18 | } 19 | 20 | mapKeys(ra, key => key.toUpperCase()) // => { MODE, POWER } 21 | mapKeys(ra, (key, value) => value) // => { god: 'god', power: 'power' } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/object/map-values.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: mapValues 3 | description: Map over the keys of an object 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an object and a `toValue` callback function, returns a new object with all the values 10 | mapped through the `toValue` function. The callback is given both the value and key for each entry. 11 | 12 | ```ts 13 | import { mapValues } from 'radash' 14 | 15 | const ra = { 16 | mode: 'god', 17 | power: 'sun' 18 | } 19 | 20 | mapValues(ra, value => value.toUpperCase()) // => { mode: 'GOD', power: 'SUN' } 21 | mapValues(ra, (value, key) => key) // => { mode: 'mode', power: 'power' } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/object/omit.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: omit 3 | description: Omit unwanted attributes from an object 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an object and a list of keys in the object, returns a new object without any of the given keys. 10 | 11 | ```ts 12 | import { omit } from 'radash' 13 | 14 | const fish = { 15 | name: 'Bass', 16 | weight: 8, 17 | source: 'lake', 18 | brackish: false 19 | } 20 | 21 | omit(fish, ['name', 'source']) // => { weight, brackish } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/object/pick.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: pick 3 | description: Pick only the desired attributes from an object 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an object and a list of keys in the object, returns a new object with only the given keys. 10 | 11 | ```ts 12 | import { pick } from 'radash' 13 | 14 | const fish = { 15 | name: 'Bass', 16 | weight: 8, 17 | source: 'lake', 18 | barckish: false 19 | } 20 | 21 | pick(fish, ['name', 'source']) // => { name, source } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/object/set.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: set 3 | description: Set a value on an object using a path key 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Opposite of get, dynamically set a nested value into an object using a key path. Does not modify the given initial object. 10 | 11 | ```ts 12 | import { set } from 'radash' 13 | 14 | set({}, 'name', 'ra') 15 | // => { name: 'ra' } 16 | 17 | set({}, 'cards[0].value', 2) 18 | // => { cards: [{ value: 2 }] } 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/object/shake.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: shake 3 | description: Remove unwanted values from an object 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | A bit like `_.sift` but for objects. By default, it will return a new object with all the undefined attributes removed. You can pass a second function argument to remove any attributes by a custom condition. 10 | 11 | ```ts 12 | import { shake } from 'radash' 13 | 14 | const ra = { 15 | mode: 'god', 16 | greek: false, 17 | limit: undefined 18 | } 19 | 20 | shake(ra) // => { mode, greek } 21 | shake(ra, a => !a) // => { mode } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/object/upperize.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: upperize 3 | description: Convert all object keys to upper case 4 | group: Object 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Convert all keys in an object to upper case. 10 | 11 | ```ts 12 | import { upperize } from 'radash' 13 | 14 | const ra = { 15 | Mode: 'god', 16 | Power: 'sun' 17 | } 18 | 19 | upperize(ra) // => { MODE, POWER } 20 | ``` 21 | 22 | The `_.upperize` function is a shortcut for `_.mapKeys(obj, k => k.toUpperCase())` 23 | -------------------------------------------------------------------------------- /docs/random/draw.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: draw 3 | description: Get a random item from a list 4 | group: Random 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Draw, as in 'to draw a card from a deck', is used to get a random item from an array. 10 | 11 | ```ts 12 | import { draw } from 'radash' 13 | 14 | const fish = ['marlin', 'bass', 'trout'] 15 | 16 | draw(fish) // => a random fish 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/random/random.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: random 3 | description: 'Generate a random number' 4 | group: Random 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Genearate a number within a range. This function is meant for utility use -- not cryptographic. 10 | 11 | ```ts 12 | import { random } from 'radash' 13 | 14 | random(0, 100) // => a random number between 0 and 100 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/random/shuffle.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: shuffle 3 | description: Randomly shuffle an array 4 | group: Random 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given an array of items, return a new array with the items in a random order. 10 | 11 | ```ts 12 | import { shuffle } from 'radash' 13 | 14 | const fish = [ 15 | { 16 | name: 'Marlin', 17 | weight: 105, 18 | source: 'ocean' 19 | }, 20 | { 21 | name: 'Salmon', 22 | weight: 22, 23 | source: 'river' 24 | }, 25 | { 26 | name: 'Salmon', 27 | weight: 22, 28 | source: 'river' 29 | } 30 | ] 31 | 32 | shuffle(fish) 33 | ``` 34 | 35 | Note, this is not a cutting edge performance optimized function. This function is optimized for simplicity and best used as a utility. If performance is a priority for you, use a randomness and/or shuffle library. 36 | -------------------------------------------------------------------------------- /docs/random/uid.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: uid 3 | description: Generate a unique identifier 4 | group: Random 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Generates a unique string with optional special characters. 10 | 11 | ```ts 12 | import { uid } from 'radash' 13 | 14 | uid(7) // => UaOKdlW 15 | uid(20, '*') // => dyJdbC*NsEgcnGjTHS 16 | ``` 17 | 18 | Note, this function is optimized for simplicity and usability -- not performance or security. If you need to create universally unique or cryptographically random strings use a package specifically for that purpose. 19 | -------------------------------------------------------------------------------- /docs/series/series.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: series 3 | description: Create an ordered series object 4 | group: Random 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Sometimes you have an enum or union type, possibly a status, that has inherint order and you need to work with values as though they're ordered. The `series` function takes many values and returns an object that let's you do ordered logic on those values. 10 | 11 | ```ts 12 | import { series } from 'radash' 13 | 14 | type Weekday = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' 15 | 16 | const weekdays = series([ 17 | 'monday', 18 | 'tuesday', 19 | 'wednesday', 20 | 'thursday', 21 | 'friday' 22 | ]) 23 | 24 | weekdays.min('tuesday', 'thursday') // => 'tuesday' 25 | weekdays.max('wednesday', 'monday') // => 'wednesday' 26 | weekdays.next('wednesday') // => 'thursday' 27 | weekdays.previous('tuesday') // => 'monday' 28 | weekdays.first() // => 'monday' 29 | weekdays.last() // => 'friday' 30 | weekdays.next('friday') // => null 31 | weekdays.next('friday', weekdays.first()) // => 'monday' 32 | weekdays.spin('monday', 3) // => 'thursday' 33 | ``` 34 | 35 | ## Complex Data Types 36 | 37 | When working with objects you'll want to provide a second argument to `series`, a function that converts non-primitive values into an identity that can be checked for equality. 38 | 39 | ```ts 40 | import { series } from 'radash' 41 | 42 | type Weekday = { 43 | day: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' 44 | } 45 | 46 | const weekdays = series( 47 | [ 48 | { day: 'monday' }, 49 | { day: 'tuesday' }, 50 | { day: 'wednesday' }, 51 | { day: 'thursday' }, 52 | { day: 'friday' } 53 | ], 54 | w => w.day 55 | ) 56 | 57 | weekdays.next({ day: 'wednesday' }) // => { day: 'thursday' } 58 | weekdays.previous({ day: 'tuesday' }) // => { day: 'monday' } 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/string/camel.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: camel 3 | description: Convert a string to camel case 4 | group: String 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a string returns it in camel case format. 10 | 11 | ```ts 12 | import { camel } from 'radash' 13 | 14 | camel('green fish blue fish') // => greenFishBlueFish 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/string/capitalize.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: capitalize 3 | description: Convert a string to a capitalized format 4 | group: String 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a string returns it with the first letter upper cased and all other letters lower cased. 10 | 11 | ```ts 12 | import { capitalize } from 'radash' 13 | 14 | capitalize('green fish blue FISH') // => Green fish blue fish 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/string/dash.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: dash 3 | description: Convert a string to dash case 4 | group: String 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a string returns it in dash case format. 10 | 11 | ```ts 12 | import { dash } from 'radash' 13 | 14 | dash('green fish blue fish') // => green-fish-blue-fish 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/string/pascal.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: pascal 3 | description: Convert a string to pascal case 4 | group: String 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Formats the given string in pascal case fashion. 10 | 11 | ```ts 12 | import { pascal } from 'radash' 13 | 14 | pascal('hello world') // => 'HelloWorld' 15 | pascal('va va boom') // => 'VaVaBoom' 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/string/snake.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: snake 3 | description: Convert a string to snake case 4 | group: String 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a string returns it in snake case format. 10 | 11 | ```ts 12 | import { snake } from 'radash' 13 | 14 | snake('green fish blue fish') // => green_fish_blue_fish 15 | ``` 16 | 17 | **Warning**: In v11.0.0 a change was made to _fix_ this function so that it correctly splits numbers from neighbouring letters (`hello5` becomes `hello_5`). You can opt out of this behavior and continue with the legacy style (`hello5` becomes `hello5`) by passing the `splitOnNumber` options. 18 | 19 | ```ts 20 | snake('5green fish 2blue fish') // => 5_green_fish_2_blue_fish 21 | 22 | snake('5green fish 2blue fish', { 23 | splitOnNumber: false 24 | }) // => 5green_fish_2blue_fish 25 | ``` -------------------------------------------------------------------------------- /docs/string/template.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: template 3 | description: Template a string with values from a data object using a search expression 4 | group: String 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given a string, an object of data, and a format expression to search for, returns a string with all elements that matched the search replaced with their matching value from the data object. 10 | 11 | ```ts 12 | import { template } from 'radash' 13 | 14 | template('It is {{color}}', { color: 'blue' }) // => It is blue 15 | template('It is ', { color: 'blue' }, /<(.+?)>/g) // => It is blue 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/string/title.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: title 3 | description: Convert a string to title case 4 | group: String 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Formats the given string in title case fashion 10 | 11 | ```ts 12 | import { title } from 'radash' 13 | 14 | title('hello world') // => 'Hello World' 15 | title('va_va_boom') // => 'Va Va Boom' 16 | title('root-hook') // => 'Root Hook' 17 | title('queryItems') // => 'Query Items' 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/string/trim.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: trim 3 | description: Trim values from a string 4 | group: String 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Trims all prefix and suffix characters from the given string. Like the builtin trim function but accepts alternate (other than space) characters you would like to trim. 10 | 11 | ```ts 12 | import { trim } from 'radash' 13 | 14 | trim(' hello ') // => hello 15 | trim('__hello__', '_') // => hello 16 | trim('/repos/:owner/', '/') // => repos/:owner 17 | ``` 18 | 19 | Trim also handles more than one character to trim. 20 | 21 | ```ts 22 | trim('222__hello__111', '12_') // => hello 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/typed/is-array.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isArray 3 | description: 'Determine if a value is an Array' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is an Array. 10 | 11 | ```ts 12 | import { isArray } from 'radash' 13 | 14 | isArray('hello') // => false 15 | isArray(['hello']) // => true 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/typed/is-date.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isDate 3 | description: 'Determine if a value is a Date' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Determine if a value is a Date. Does not check that the input date is valid, only that it is a Javascript Date type. 10 | 11 | ```ts 12 | import { isDate } from 'radash' 13 | 14 | isDate(new Date()) // => true 15 | isDate(12) // => false 16 | isDate('hello') // => false 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/typed/is-empty.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isEmpty 3 | description: 'Determine if a value is empty' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is empty. 10 | 11 | ```ts 12 | import { isEmpty } from 'radash' 13 | 14 | isEmpty([]) // => true 15 | isEmpty('') // => true 16 | 17 | isEmpty('hello') // => false 18 | isEmpty(['hello']) // => false 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/typed/is-equal.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isEqual 3 | description: Determine if two values are equal 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Given two values, returns true if they are equal. 10 | 11 | ```ts 12 | import { isEqual } from 'radash' 13 | 14 | isEqual(null, null) // => true 15 | isEqual([], []) // => true 16 | 17 | isEqual('hello', 'world') // => false 18 | isEqual(22, 'abc') // => false 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/typed/is-float.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isFloat 3 | description: 'Determine if a value is a float' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is a float. 10 | 11 | ```ts 12 | import { isFloat } from 'radash' 13 | 14 | isFloat(12.233) // => true 15 | isFloat(12) // => false 16 | isFloat('hello') // => false 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/typed/is-function.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isFunction 3 | description: 'Determine if a value is a Function' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is a function. 10 | 11 | ```ts 12 | import { isFunction } from 'radash' 13 | 14 | isFunction('hello') // => false 15 | isFunction(['hello']) // => false 16 | isFunction(() => 'hello') // => true 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/typed/is-int.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isInt 3 | description: 'Determine if a value is an int' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is an int. 10 | 11 | ```ts 12 | import { isInt } from 'radash' 13 | 14 | isInt(12) // => true 15 | isInt(12.233) // => false 16 | isInt('hello') // => false 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/typed/is-number.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isNumber 3 | description: 'Determine if a value is a number' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is a number. 10 | 11 | ```ts 12 | import { isNumber } from 'radash' 13 | 14 | isNumber('hello') // => false 15 | isNumber(['hello']) // => false 16 | isNumber(12) // => true 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/typed/is-object.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isObject 3 | description: 'Determine if a value is an Object' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is an Object. 10 | 11 | ```ts 12 | import { isObject } from 'radash' 13 | 14 | isObject('hello') // => false 15 | isObject(['hello']) // => false 16 | isObject(null) // => false 17 | isObject({ say: 'hello' }) // => true 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/typed/is-primitive.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isPrimitive 3 | description: Checks if the given value is primitive 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Checks if the given value is primitive. 10 | 11 | Primitive Types: number , string , boolean , symbol, bigint, undefined, null 12 | 13 | ```ts 14 | import { isPrimitive } from 'radash' 15 | 16 | isPrimitive(22) // => true 17 | isPrimitive('hello') // => true 18 | isPrimitive(['hello']) // => false 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/typed/is-promise.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isPromise 3 | description: 'Determine if a value is a Promise' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is a Promise. This function is not _"bullet proof"_ because determining if a value is a Promise in javascript is not _"bullet proof"_. The standard/recommended method is to use `Promise.resolve` to essentially cast any value, promise or not, into an awaited value. However, this may do in a pinch. 10 | 11 | ```ts 12 | import { isPromise } from 'radash' 13 | 14 | isPromise('hello') // => false 15 | isPromise(['hello']) // => false 16 | isPromise(new Promise(res => res())) // => true 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/typed/is-string.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isString 3 | description: 'Determine if a value is a String' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is a string. 10 | 11 | ```ts 12 | import { isString } from 'radash' 13 | 14 | isString('hello') // => true 15 | isString(['hello']) // => false 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/typed/is-symbol.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: isSymbol 3 | description: 'Determine if a value is a Symbol' 4 | group: Typed 5 | --- 6 | 7 | ## Basic usage 8 | 9 | Pass in a value and get a boolean telling you if the value is a Symbol. 10 | 11 | ```ts 12 | import { isSymbol } from 'radash' 13 | 14 | isSymbol('hello') // => false 15 | isSymbol(Symbol('hello')) // => true 16 | ``` 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "testMatch": [ 6 | "**/__tests__/**/*.+(ts|tsx|js)", 7 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 8 | ], 9 | "transform": { 10 | "^.+\\.(ts|tsx)$": "ts-jest" 11 | }, 12 | "coverageThreshold": { 13 | "global": { 14 | "branches": 100, 15 | "functions": 100, 16 | "lines": 100, 17 | "statements": 100 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radash", 3 | "version": "12.1.0", 4 | "description": "Functional utility library - modern, simple, typed, powerful", 5 | "main": "dist/cjs/index.cjs", 6 | "module": "dist/esm/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | "types": "./dist/index.d.ts", 10 | "import": "./dist/esm/index.mjs", 11 | "require": "./dist/cjs/index.cjs" 12 | }, 13 | "sideEffects": false, 14 | "files": [ 15 | "dist" 16 | ], 17 | "repository": { 18 | "url": "https://github.com/rayepps/radash" 19 | }, 20 | "author": "rayepps", 21 | "private": false, 22 | "license": "MIT", 23 | "scripts": { 24 | "test": "jest --coverage", 25 | "check": "yarn lint && yarn test && yarn build", 26 | "build": "yarn tsc --noEmit && yarn rollup -c", 27 | "docs:install": "yarn && yarn add --dev next@12.3.4", 28 | "docs:build": "chiller build --ci", 29 | "lint": "tslint -p tsconfig.json", 30 | "format": "prettier --write \"src/**/*.ts\"", 31 | "format:check": "prettier --check \"src/**/*.ts\" --ignore-unknown" 32 | }, 33 | "devDependencies": { 34 | "@rollup/plugin-typescript": "^10.0.1", 35 | "@types/chai": "^4.3.3", 36 | "@types/jest": "^28.1.1", 37 | "chai": "^4.3.6", 38 | "chiller": "^1.0.0-rc.30", 39 | "esbuild": "^0.16.3", 40 | "jest": "^28.1.3", 41 | "prettier": "^2.7.1", 42 | "prettier-plugin-organize-imports": "^3.0.3", 43 | "rollup": "^3.2.5", 44 | "rollup-plugin-dts": "^5.0.0", 45 | "rollup-plugin-esbuild": "^5.0.0", 46 | "rollup-plugin-node-externals": "^5.0.2", 47 | "ts-jest": "^28.0.8", 48 | "tslint": "^6.0.0", 49 | "typescript": "^4.8.4" 50 | }, 51 | "engines": { 52 | "node": ">=14.18.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | // rollup.config.mjs 2 | import typescript from '@rollup/plugin-typescript' 3 | import dts from 'rollup-plugin-dts' 4 | import esbuild, { minify } from 'rollup-plugin-esbuild' 5 | import externals from 'rollup-plugin-node-externals' 6 | 7 | const usePreferConst = true // Use "const" instead of "var" 8 | const usePreserveModules = true // `true` -> keep modules structure, `false` -> combine everything into a single file 9 | const useStrict = true // Use "strict" 10 | const useThrowOnError = true // On error throw and exception 11 | const useSourceMap = true // Generate source map files 12 | const useEsbuild = true // `true` -> use esbuild, `false` use tsc 13 | 14 | export default [ 15 | { 16 | // .d.ts build 17 | input: 'src/index.ts', 18 | output: { 19 | file: 'dist/index.d.ts', 20 | format: 'es' 21 | }, 22 | plugins: [externals(), dts()] 23 | }, 24 | { 25 | // CJS build 26 | input: 'src/index.ts', 27 | output: { 28 | dir: 'dist/cjs', 29 | format: 'cjs', 30 | generatedCode: { 31 | constBindings: usePreferConst 32 | }, 33 | preserveModules: usePreserveModules, 34 | strict: useStrict, 35 | entryFileNames: '[name].cjs', 36 | sourcemap: useSourceMap 37 | }, 38 | plugins: [ 39 | externals(), 40 | useEsbuild 41 | ? esbuild() 42 | : typescript({ 43 | noEmitOnError: useThrowOnError, 44 | outDir: 'dist/cjs', 45 | removeComments: true 46 | }) 47 | ] 48 | }, 49 | { 50 | // ESM builds 51 | input: 'src/index.ts', 52 | output: { 53 | dir: 'dist/esm', 54 | format: 'es', 55 | generatedCode: { 56 | constBindings: usePreferConst 57 | }, 58 | preserveModules: usePreserveModules, 59 | strict: useStrict, 60 | entryFileNames: '[name].mjs', 61 | sourcemap: useSourceMap 62 | }, 63 | plugins: [ 64 | externals(), 65 | useEsbuild 66 | ? esbuild() 67 | : typescript({ 68 | noEmitOnError: useThrowOnError, 69 | outDir: 'dist/esm', 70 | removeComments: true 71 | }) 72 | ] 73 | }, 74 | { 75 | // CDN build 76 | input: 'src/index.ts', 77 | output: [ 78 | { 79 | format: 'iife', 80 | generatedCode: { 81 | constBindings: usePreferConst 82 | }, 83 | preserveModules: false, 84 | strict: useStrict, 85 | file: 'cdn/radash.js', 86 | name: 'radash', 87 | sourcemap: false 88 | }, 89 | { 90 | format: 'iife', 91 | generatedCode: { 92 | constBindings: usePreferConst 93 | }, 94 | preserveModules: false, 95 | strict: useStrict, 96 | file: 'cdn/radash.min.js', 97 | name: 'radash', 98 | sourcemap: false, 99 | plugins: [minify()] 100 | }, 101 | { 102 | format: 'es', 103 | generatedCode: { 104 | constBindings: usePreferConst 105 | }, 106 | preserveModules: false, 107 | strict: useStrict, 108 | file: 'cdn/radash.esm.js', 109 | sourcemap: false 110 | } 111 | ], 112 | plugins: [ 113 | externals(), 114 | useEsbuild 115 | ? esbuild() 116 | : typescript({ 117 | noEmitOnError: useThrowOnError, 118 | outDir: 'cdn', 119 | removeComments: true 120 | }) 121 | ] 122 | } 123 | ] -------------------------------------------------------------------------------- /src/array.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isFunction } from './typed' 2 | 3 | /** 4 | * Sorts an array of items into groups. The return value is a map where the keys are 5 | * the group ids the given getGroupId function produced and the value is an array of 6 | * each item in that group. 7 | */ 8 | export const group = ( 9 | array: readonly T[], 10 | getGroupId: (item: T) => Key 11 | ): Partial> => { 12 | return array.reduce((acc, item) => { 13 | const groupId = getGroupId(item) 14 | if (!acc[groupId]) acc[groupId] = [] 15 | acc[groupId].push(item) 16 | return acc 17 | }, {} as Record) 18 | } 19 | 20 | /** 21 | * Creates an array of grouped elements, the first of which contains the 22 | * first elements of the given arrays, the second of which contains the 23 | * second elements of the given arrays, and so on. 24 | * 25 | * Ex. const zipped = zip(['a', 'b'], [1, 2], [true, false]) // [['a', 1, true], ['b', 2, false]] 26 | */ 27 | export function zip( 28 | array1: T1[], 29 | array2: T2[], 30 | array3: T3[], 31 | array4: T4[], 32 | array5: T5[] 33 | ): [T1, T2, T3, T4, T5][] 34 | export function zip( 35 | array1: T1[], 36 | array2: T2[], 37 | array3: T3[], 38 | array4: T4[] 39 | ): [T1, T2, T3, T4][] 40 | export function zip( 41 | array1: T1[], 42 | array2: T2[], 43 | array3: T3[] 44 | ): [T1, T2, T3][] 45 | export function zip(array1: T1[], array2: T2[]): [T1, T2][] 46 | export function zip(...arrays: T[][]): T[][] { 47 | if (!arrays || !arrays.length) return [] 48 | return new Array(Math.max(...arrays.map(({ length }) => length))) 49 | .fill([]) 50 | .map((_, idx) => arrays.map(array => array[idx])) 51 | } 52 | 53 | /** 54 | * Creates an object mapping the specified keys to their corresponding values 55 | * 56 | * Ex. const zipped = zipToObject(['a', 'b'], [1, 2]) // { a: 1, b: 2 } 57 | * Ex. const zipped = zipToObject(['a', 'b'], (k, i) => k + i) // { a: 'a0', b: 'b1' } 58 | * Ex. const zipped = zipToObject(['a', 'b'], 1) // { a: 1, b: 1 } 59 | */ 60 | export function zipToObject( 61 | keys: K[], 62 | values: V | ((key: K, idx: number) => V) | V[] 63 | ): Record { 64 | if (!keys || !keys.length) { 65 | return {} as Record 66 | } 67 | 68 | const getValue = isFunction(values) 69 | ? values 70 | : isArray(values) 71 | ? (_k: K, i: number) => values[i] 72 | : (_k: K, _i: number) => values 73 | 74 | return keys.reduce((acc, key, idx) => { 75 | acc[key] = getValue(key, idx) 76 | return acc 77 | }, {} as Record) 78 | } 79 | 80 | /** 81 | * Go through a list of items, starting with the first item, 82 | * and comparing with the second. Keep the one you want then 83 | * compare that to the next item in the list with the same 84 | * 85 | * Ex. const greatest = () => boil(numbers, (a, b) => a > b) 86 | */ 87 | export const boil = ( 88 | array: readonly T[], 89 | compareFunc: (a: T, b: T) => T 90 | ) => { 91 | if (!array || (array.length ?? 0) === 0) return null 92 | return array.reduce(compareFunc) 93 | } 94 | 95 | /** 96 | * Sum all numbers in an array. Optionally provide a function 97 | * to convert objects in the array to number values. 98 | */ 99 | export function sum(array: readonly T[]): number 100 | export function sum( 101 | array: readonly T[], 102 | fn: (item: T) => number 103 | ): number 104 | export function sum( 105 | array: readonly any[], 106 | fn?: (item: T) => number 107 | ): number { 108 | return (array || []).reduce((acc, item) => acc + (fn ? fn(item) : item), 0) 109 | } 110 | 111 | /** 112 | * Get the first item in an array or a default value 113 | */ 114 | export const first = ( 115 | array: readonly T[], 116 | defaultValue: T | null | undefined = undefined 117 | ) => { 118 | return array?.length > 0 ? array[0] : defaultValue 119 | } 120 | 121 | /** 122 | * Get the last item in an array or a default value 123 | */ 124 | export const last = ( 125 | array: readonly T[], 126 | defaultValue: T | null | undefined = undefined 127 | ) => { 128 | return array?.length > 0 ? array[array.length - 1] : defaultValue 129 | } 130 | 131 | /** 132 | * Sort an array without modifying it and return 133 | * the newly sorted value 134 | */ 135 | export const sort = ( 136 | array: readonly T[], 137 | getter: (item: T) => number, 138 | desc = false 139 | ) => { 140 | if (!array) return [] 141 | const asc = (a: T, b: T) => getter(a) - getter(b) 142 | const dsc = (a: T, b: T) => getter(b) - getter(a) 143 | return array.slice().sort(desc === true ? dsc : asc) 144 | } 145 | 146 | /** 147 | * Sort an array without modifying it and return 148 | * the newly sorted value. Allows for a string 149 | * sorting value. 150 | */ 151 | export const alphabetical = ( 152 | array: readonly T[], 153 | getter: (item: T) => string, 154 | dir: 'asc' | 'desc' = 'asc' 155 | ) => { 156 | if (!array) return [] 157 | const asc = (a: T, b: T) => `${getter(a)}`.localeCompare(getter(b)) 158 | const dsc = (a: T, b: T) => `${getter(b)}`.localeCompare(getter(a)) 159 | return array.slice().sort(dir === 'desc' ? dsc : asc) 160 | } 161 | 162 | export const counting = ( 163 | list: readonly T[], 164 | identity: (item: T) => TId 165 | ): Record => { 166 | if (!list) return {} as Record 167 | return list.reduce((acc, item) => { 168 | const id = identity(item) 169 | acc[id] = (acc[id] ?? 0) + 1 170 | return acc 171 | }, {} as Record) 172 | } 173 | 174 | /** 175 | * Replace an element in an array with a new 176 | * item without modifying the array and return 177 | * the new value 178 | */ 179 | export const replace = ( 180 | list: readonly T[], 181 | newItem: T, 182 | match: (item: T, idx: number) => boolean 183 | ): T[] => { 184 | if (!list) return [] 185 | if (newItem === undefined) return [...list] 186 | for (let idx = 0; idx < list.length; idx++) { 187 | const item = list[idx] 188 | if (match(item, idx)) { 189 | return [ 190 | ...list.slice(0, idx), 191 | newItem, 192 | ...list.slice(idx + 1, list.length) 193 | ] 194 | } 195 | } 196 | return [...list] 197 | } 198 | 199 | /** 200 | * Convert an array to a dictionary by mapping each item 201 | * into a dictionary key & value 202 | */ 203 | export const objectify = ( 204 | array: readonly T[], 205 | getKey: (item: T) => Key, 206 | getValue: (item: T) => Value = item => item as unknown as Value 207 | ): Record => { 208 | return array.reduce((acc, item) => { 209 | acc[getKey(item)] = getValue(item) 210 | return acc 211 | }, {} as Record) 212 | } 213 | 214 | /** 215 | * Select performs a filter and a mapper inside of a reduce, 216 | * only iterating the list one time. 217 | * 218 | * @example 219 | * select([1, 2, 3, 4], x => x*x, x > 2) == [9, 16] 220 | */ 221 | export const select = ( 222 | array: readonly T[], 223 | mapper: (item: T, index: number) => K, 224 | condition: (item: T, index: number) => boolean 225 | ) => { 226 | if (!array) return [] 227 | return array.reduce((acc, item, index) => { 228 | if (!condition(item, index)) return acc 229 | acc.push(mapper(item, index)) 230 | return acc 231 | }, [] as K[]) 232 | } 233 | 234 | /** 235 | * Max gets the greatest value from a list 236 | * 237 | * @example 238 | * max([ 2, 3, 5]) == 5 239 | * max([{ num: 1 }, { num: 2 }], x => x.num) == { num: 2 } 240 | */ 241 | export function max(array: readonly [number, ...number[]]): number 242 | export function max(array: readonly number[]): number | null 243 | export function max( 244 | array: readonly T[], 245 | getter: (item: T) => number 246 | ): T | null 247 | export function max( 248 | array: readonly T[], 249 | getter?: (item: T) => number 250 | ): T | null { 251 | const get = getter ?? ((v: any) => v) 252 | return boil(array, (a, b) => (get(a) > get(b) ? a : b)) 253 | } 254 | 255 | /** 256 | * Min gets the smallest value from a list 257 | * 258 | * @example 259 | * min([1, 2, 3, 4]) == 1 260 | * min([{ num: 1 }, { num: 2 }], x => x.num) == { num: 1 } 261 | */ 262 | export function min(array: readonly [number, ...number[]]): number 263 | export function min(array: readonly number[]): number | null 264 | export function min( 265 | array: readonly T[], 266 | getter: (item: T) => number 267 | ): T | null 268 | export function min( 269 | array: readonly T[], 270 | getter?: (item: T) => number 271 | ): T | null { 272 | const get = getter ?? ((v: any) => v) 273 | return boil(array, (a, b) => (get(a) < get(b) ? a : b)) 274 | } 275 | 276 | /** 277 | * Splits a single list into many lists of the desired size. If 278 | * given a list of 10 items and a size of 2, it will return 5 279 | * lists with 2 items each 280 | */ 281 | export const cluster = (list: readonly T[], size: number = 2): T[][] => { 282 | const clusterCount = Math.ceil(list.length / size) 283 | return new Array(clusterCount).fill(null).map((_c: null, i: number) => { 284 | return list.slice(i * size, i * size + size) 285 | }) 286 | } 287 | 288 | /** 289 | * Given a list of items returns a new list with only 290 | * unique items. Accepts an optional identity function 291 | * to convert each item in the list to a comparable identity 292 | * value 293 | */ 294 | export const unique = ( 295 | array: readonly T[], 296 | toKey?: (item: T) => K 297 | ): T[] => { 298 | const valueMap = array.reduce((acc, item) => { 299 | const key = toKey ? toKey(item) : (item as any as string | number | symbol) 300 | if (acc[key]) return acc 301 | acc[key] = item 302 | return acc 303 | }, {} as Record) 304 | return Object.values(valueMap) 305 | } 306 | 307 | /** 308 | * Creates a generator that will produce an iteration through 309 | * the range of number as requested. 310 | * 311 | * @example 312 | * range(3) // yields 0, 1, 2, 3 313 | * range(0, 3) // yields 0, 1, 2, 3 314 | * range(0, 3, 'y') // yields y, y, y, y 315 | * range(0, 3, () => 'y') // yields y, y, y, y 316 | * range(0, 3, i => i) // yields 0, 1, 2, 3 317 | * range(0, 3, i => `y${i}`) // yields y0, y1, y2, y3 318 | * range(0, 3, obj) // yields obj, obj, obj, obj 319 | * range(0, 6, i => i, 2) // yields 0, 2, 4, 6 320 | */ 321 | export function* range( 322 | startOrLength: number, 323 | end?: number, 324 | valueOrMapper: T | ((i: number) => T) = i => i as T, 325 | step: number = 1 326 | ): Generator { 327 | const mapper = isFunction(valueOrMapper) ? valueOrMapper : () => valueOrMapper 328 | const start = end ? startOrLength : 0 329 | const final = end ?? startOrLength 330 | for (let i = start; i <= final; i += step) { 331 | yield mapper(i) 332 | if (i + step > final) break 333 | } 334 | } 335 | 336 | /** 337 | * Creates a list of given start, end, value, and 338 | * step parameters. 339 | * 340 | * @example 341 | * list(3) // 0, 1, 2, 3 342 | * list(0, 3) // 0, 1, 2, 3 343 | * list(0, 3, 'y') // y, y, y, y 344 | * list(0, 3, () => 'y') // y, y, y, y 345 | * list(0, 3, i => i) // 0, 1, 2, 3 346 | * list(0, 3, i => `y${i}`) // y0, y1, y2, y3 347 | * list(0, 3, obj) // obj, obj, obj, obj 348 | * list(0, 6, i => i, 2) // 0, 2, 4, 6 349 | */ 350 | export const list = ( 351 | startOrLength: number, 352 | end?: number, 353 | valueOrMapper?: T | ((i: number) => T), 354 | step?: number 355 | ): T[] => { 356 | return Array.from(range(startOrLength, end, valueOrMapper, step)) 357 | } 358 | 359 | /** 360 | * Given an array of arrays, returns a single 361 | * dimentional array with all items in it. 362 | */ 363 | export const flat = (lists: readonly T[][]): T[] => { 364 | return lists.reduce((acc, list) => { 365 | acc.push(...list) 366 | return acc 367 | }, []) 368 | } 369 | 370 | /** 371 | * Given two arrays, returns true if any 372 | * elements intersect 373 | */ 374 | export const intersects = ( 375 | listA: readonly T[], 376 | listB: readonly T[], 377 | identity?: (t: T) => K 378 | ): boolean => { 379 | if (!listA || !listB) return false 380 | const ident = identity ?? ((x: T) => x as unknown as K) 381 | const dictB = listB.reduce((acc, item) => { 382 | acc[ident(item)] = true 383 | return acc 384 | }, {} as Record) 385 | return listA.some(value => dictB[ident(value)]) 386 | } 387 | 388 | /** 389 | * Split an array into two array based on 390 | * a true/false condition function 391 | */ 392 | export const fork = ( 393 | list: readonly T[], 394 | condition: (item: T) => boolean 395 | ): [T[], T[]] => { 396 | if (!list) return [[], []] 397 | return list.reduce( 398 | (acc, item) => { 399 | const [a, b] = acc 400 | if (condition(item)) { 401 | return [[...a, item], b] 402 | } else { 403 | return [a, [...b, item]] 404 | } 405 | }, 406 | [[], []] as [T[], T[]] 407 | ) 408 | } 409 | 410 | /** 411 | * Given two lists of the same type, iterate the first list 412 | * and replace items matched by the matcher func in the 413 | * first place. 414 | */ 415 | export const merge = ( 416 | root: readonly T[], 417 | others: readonly T[], 418 | matcher: (item: T) => any 419 | ) => { 420 | if (!others && !root) return [] 421 | if (!others) return root 422 | if (!root) return [] 423 | if (!matcher) return root 424 | return root.reduce((acc, r) => { 425 | const matched = others.find(o => matcher(r) === matcher(o)) 426 | if (matched) acc.push(matched) 427 | else acc.push(r) 428 | return acc 429 | }, [] as T[]) 430 | } 431 | 432 | /** 433 | * Replace an item in an array by a match function condition. If 434 | * no items match the function condition, appends the new item to 435 | * the end of the list. 436 | */ 437 | export const replaceOrAppend = ( 438 | list: readonly T[], 439 | newItem: T, 440 | match: (a: T, idx: number) => boolean 441 | ) => { 442 | if (!list && !newItem) return [] 443 | if (!newItem) return [...list] 444 | if (!list) return [newItem] 445 | for (let idx = 0; idx < list.length; idx++) { 446 | const item = list[idx] 447 | if (match(item, idx)) { 448 | return [ 449 | ...list.slice(0, idx), 450 | newItem, 451 | ...list.slice(idx + 1, list.length) 452 | ] 453 | } 454 | } 455 | return [...list, newItem] 456 | } 457 | 458 | /** 459 | * If the item matching the condition already exists 460 | * in the list it will be removed. If it does not it 461 | * will be added. 462 | */ 463 | export const toggle = ( 464 | list: readonly T[], 465 | item: T, 466 | /** 467 | * Converts an item of type T item into a value that 468 | * can be checked for equality 469 | */ 470 | toKey?: null | ((item: T, idx: number) => number | string | symbol), 471 | options?: { 472 | strategy?: 'prepend' | 'append' 473 | } 474 | ) => { 475 | if (!list && !item) return [] 476 | if (!list) return [item] 477 | if (!item) return [...list] 478 | const matcher = toKey 479 | ? (x: T, idx: number) => toKey(x, idx) === toKey(item, idx) 480 | : (x: T) => x === item 481 | const existing = list.find(matcher) 482 | if (existing) return list.filter((x, idx) => !matcher(x, idx)) 483 | const strategy = options?.strategy ?? 'append' 484 | if (strategy === 'append') return [...list, item] 485 | return [item, ...list] 486 | } 487 | 488 | type Falsy = null | undefined | false | '' | 0 | 0n 489 | 490 | /** 491 | * Given a list returns a new list with 492 | * only truthy values 493 | */ 494 | export const sift = (list: readonly (T | Falsy)[]): T[] => { 495 | return (list?.filter(x => !!x) as T[]) ?? [] 496 | } 497 | 498 | /** 499 | * Like a reduce but does not require an array. 500 | * Only need a number and will iterate the function 501 | * as many times as specified. 502 | * 503 | * NOTE: This is NOT zero indexed. If you pass count=5 504 | * you will get 1, 2, 3, 4, 5 iteration in the callback 505 | * function 506 | */ 507 | export const iterate = ( 508 | count: number, 509 | func: (currentValue: T, iteration: number) => T, 510 | initValue: T 511 | ) => { 512 | let value = initValue 513 | for (let i = 1; i <= count; i++) { 514 | value = func(value, i) 515 | } 516 | return value 517 | } 518 | 519 | /** 520 | * Returns all items from the first list that 521 | * do not exist in the second list. 522 | */ 523 | export const diff = ( 524 | root: readonly T[], 525 | other: readonly T[], 526 | identity: (item: T) => string | number | symbol = (t: T) => 527 | t as unknown as string | number | symbol 528 | ): T[] => { 529 | if (!root?.length && !other?.length) return [] 530 | if (root?.length === undefined) return [...other] 531 | if (!other?.length) return [...root] 532 | const bKeys = other.reduce((acc, item) => { 533 | acc[identity(item)] = true 534 | return acc 535 | }, {} as Record) 536 | return root.filter(a => !bKeys[identity(a)]) 537 | } 538 | 539 | /** 540 | * Shift array items by n steps 541 | * If n > 0 items will shift n steps to the right 542 | * If n < 0 items will shift n steps to the left 543 | */ 544 | export function shift(arr: Array, n: number) { 545 | if (arr.length === 0) return arr 546 | 547 | const shiftNumber = n % arr.length 548 | 549 | if (shiftNumber === 0) return arr 550 | 551 | return [...arr.slice(-shiftNumber, arr.length), ...arr.slice(0, -shiftNumber)] 552 | } 553 | -------------------------------------------------------------------------------- /src/async.ts: -------------------------------------------------------------------------------- 1 | import { fork, list, range, sort } from './array' 2 | import { isArray, isPromise } from './typed' 3 | 4 | /** 5 | * An async reduce function. Works like the 6 | * built-in Array.reduce function but handles 7 | * an async reducer function 8 | */ 9 | export const reduce = async ( 10 | array: readonly T[], 11 | asyncReducer: (acc: K, item: T, index: number) => Promise, 12 | initValue?: K 13 | ): Promise => { 14 | const initProvided = initValue !== undefined 15 | if (!initProvided && array?.length < 1) { 16 | throw new Error('Cannot reduce empty array with no init value') 17 | } 18 | const iter = initProvided ? array : array.slice(1) 19 | let value: any = initProvided ? initValue : array[0] 20 | for (const [i, item] of iter.entries()) { 21 | value = await asyncReducer(value, item, i) 22 | } 23 | return value 24 | } 25 | 26 | /** 27 | * An async map function. Works like the 28 | * built-in Array.map function but handles 29 | * an async mapper function 30 | */ 31 | export const map = async ( 32 | array: readonly T[], 33 | asyncMapFunc: (item: T, index: number) => Promise 34 | ): Promise => { 35 | if (!array) return [] 36 | let result = [] 37 | let index = 0 38 | for (const value of array) { 39 | const newValue = await asyncMapFunc(value, index++) 40 | result.push(newValue) 41 | } 42 | return result 43 | } 44 | 45 | /** 46 | * Useful when for script like things where cleanup 47 | * should be done on fail or sucess no matter. 48 | * 49 | * You can call defer many times to register many 50 | * defered functions that will all be called when 51 | * the function exits in any state. 52 | */ 53 | export const defer = async ( 54 | func: ( 55 | register: ( 56 | fn: (error?: any) => any, 57 | options?: { rethrow?: boolean } 58 | ) => void 59 | ) => Promise 60 | ): Promise => { 61 | const callbacks: { 62 | fn: (error?: any) => any 63 | rethrow: boolean 64 | }[] = [] 65 | const register = ( 66 | fn: (error?: any) => any, 67 | options?: { rethrow?: boolean } 68 | ) => 69 | callbacks.push({ 70 | fn, 71 | rethrow: options?.rethrow ?? false 72 | }) 73 | const [err, response] = await tryit(func)(register) 74 | for (const { fn, rethrow } of callbacks) { 75 | const [rethrown] = await tryit(fn)(err) 76 | if (rethrown && rethrow) throw rethrown 77 | } 78 | if (err) throw err 79 | return response 80 | } 81 | 82 | type WorkItemResult = { 83 | index: number 84 | result: K 85 | error: any 86 | } 87 | 88 | /** 89 | * Support for the built-in AggregateError 90 | * is still new. Node < 15 doesn't have it 91 | * so patching here. 92 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError#browser_compatibility 93 | */ 94 | export class AggregateError extends Error { 95 | errors: Error[] 96 | constructor(errors: Error[] = []) { 97 | super() 98 | const name = errors.find(e => e.name)?.name ?? '' 99 | this.name = `AggregateError(${name}...)` 100 | this.message = `AggregateError with ${errors.length} errors` 101 | this.stack = errors.find(e => e.stack)?.stack ?? this.stack 102 | this.errors = errors 103 | } 104 | } 105 | 106 | /** 107 | * Executes many async functions in parallel. Returns the 108 | * results from all functions as an array. After all functions 109 | * have resolved, if any errors were thrown, they are rethrown 110 | * in an instance of AggregateError 111 | */ 112 | export const parallel = async ( 113 | limit: number, 114 | array: readonly T[], 115 | func: (item: T) => Promise 116 | ): Promise => { 117 | const work = array.map((item, index) => ({ 118 | index, 119 | item 120 | })) 121 | // Process array items 122 | const processor = async (res: (value: WorkItemResult[]) => void) => { 123 | const results: WorkItemResult[] = [] 124 | while (true) { 125 | const next = work.pop() 126 | if (!next) return res(results) 127 | const [error, result] = await tryit(func)(next.item) 128 | results.push({ 129 | error, 130 | result: result as K, 131 | index: next.index 132 | }) 133 | } 134 | } 135 | // Create queues 136 | const queues = list(1, limit).map(() => new Promise(processor)) 137 | // Wait for all queues to complete 138 | const itemResults = (await Promise.all(queues)) as WorkItemResult[][] 139 | const [errors, results] = fork( 140 | sort(itemResults.flat(), r => r.index), 141 | x => !!x.error 142 | ) 143 | if (errors.length > 0) { 144 | throw new AggregateError(errors.map(error => error.error)) 145 | } 146 | return results.map(r => r.result) 147 | } 148 | 149 | type PromiseValues[]> = { 150 | [K in keyof T]: T[K] extends Promise ? U : never 151 | } 152 | 153 | /** 154 | * Functionally similar to Promise.all or Promise.allSettled. If any 155 | * errors are thrown, all errors are gathered and thrown in an 156 | * AggregateError. 157 | * 158 | * @example 159 | * const [user] = await all([ 160 | * api.users.create(...), 161 | * s3.buckets.create(...), 162 | * slack.customerSuccessChannel.sendMessage(...) 163 | * ]) 164 | */ 165 | export async function all, ...Promise[]]>( 166 | promises: T 167 | ): Promise> 168 | export async function all[]>( 169 | promises: T 170 | ): Promise> 171 | /** 172 | * Functionally similar to Promise.all or Promise.allSettled. If any 173 | * errors are thrown, all errors are gathered and thrown in an 174 | * AggregateError. 175 | * 176 | * @example 177 | * const { user } = await all({ 178 | * user: api.users.create(...), 179 | * bucket: s3.buckets.create(...), 180 | * message: slack.customerSuccessChannel.sendMessage(...) 181 | * }) 182 | */ 183 | export async function all>>( 184 | promises: T 185 | ): Promise<{ [K in keyof T]: Awaited }> 186 | export async function all< 187 | T extends Record> | Promise[] 188 | >(promises: T) { 189 | const entries = isArray(promises) 190 | ? promises.map(p => [null, p] as [null, Promise]) 191 | : Object.entries(promises) 192 | 193 | const results = await Promise.all( 194 | entries.map(([key, value]) => 195 | value 196 | .then(result => ({ result, exc: null, key })) 197 | .catch(exc => ({ result: null, exc, key })) 198 | ) 199 | ) 200 | 201 | const exceptions = results.filter(r => r.exc) 202 | if (exceptions.length > 0) { 203 | throw new AggregateError(exceptions.map(e => e.exc)) 204 | } 205 | 206 | if (isArray(promises)) { 207 | return results.map(r => r.result) as T extends Promise[] 208 | ? PromiseValues 209 | : unknown 210 | } 211 | 212 | return results.reduce( 213 | (acc, item) => ({ 214 | ...acc, 215 | [item.key!]: item.result 216 | }), 217 | {} as { [K in keyof T]: Awaited } 218 | ) 219 | } 220 | 221 | /** 222 | * Retries the given function the specified number 223 | * of times. 224 | */ 225 | export const retry = async ( 226 | options: { 227 | times?: number 228 | delay?: number | null 229 | backoff?: (count: number) => number 230 | }, 231 | func: (exit: (err: any) => void) => Promise 232 | ): Promise => { 233 | const times = options?.times ?? 3 234 | const delay = options?.delay 235 | const backoff = options?.backoff ?? null 236 | for (const i of range(1, times)) { 237 | const [err, result] = (await tryit(func)((err: any) => { 238 | throw { _exited: err } 239 | })) as [any, TResponse] 240 | if (!err) return result 241 | if (err._exited) throw err._exited 242 | if (i === times) throw err 243 | if (delay) await sleep(delay) 244 | if (backoff) await sleep(backoff(i)) 245 | } 246 | // Logically, we should never reach this 247 | // code path. It makes the function meet 248 | // strict mode requirements. 249 | /* istanbul ignore next */ 250 | return undefined as unknown as TResponse 251 | } 252 | 253 | /** 254 | * Async wait 255 | */ 256 | export const sleep = (milliseconds: number) => { 257 | return new Promise(res => setTimeout(res, milliseconds)) 258 | } 259 | 260 | /** 261 | * A helper to try an async function without forking 262 | * the control flow. Returns an error first callback _like_ 263 | * array response as [Error, result] 264 | */ 265 | export const tryit = ( 266 | func: (...args: Args) => Return 267 | ) => { 268 | return ( 269 | ...args: Args 270 | ): Return extends Promise 271 | ? Promise<[Error, undefined] | [undefined, Awaited]> 272 | : [Error, undefined] | [undefined, Return] => { 273 | try { 274 | const result = func(...args) 275 | if (isPromise(result)) { 276 | return result 277 | .then(value => [undefined, value]) 278 | .catch(err => [err, undefined]) as Return extends Promise 279 | ? Promise<[Error, undefined] | [undefined, Awaited]> 280 | : [Error, undefined] | [undefined, Return] 281 | } 282 | return [undefined, result] as Return extends Promise 283 | ? Promise<[Error, undefined] | [undefined, Awaited]> 284 | : [Error, undefined] | [undefined, Return] 285 | } catch (err) { 286 | return [err as any, undefined] as Return extends Promise 287 | ? Promise<[Error, undefined] | [undefined, Awaited]> 288 | : [Error, undefined] | [undefined, Return] 289 | } 290 | } 291 | } 292 | 293 | /** 294 | * A helper to try an async function that returns undefined 295 | * if it fails. 296 | * 297 | * e.g. const result = await guard(fetchUsers)() ?? []; 298 | */ 299 | export const guard = any>( 300 | func: TFunction, 301 | shouldGuard?: (err: any) => boolean 302 | ): ReturnType extends Promise 303 | ? Promise> | undefined> 304 | : ReturnType | undefined => { 305 | const _guard = (err: any) => { 306 | if (shouldGuard && !shouldGuard(err)) throw err 307 | return undefined as any 308 | } 309 | const isPromise = (result: any): result is Promise => 310 | result instanceof Promise 311 | try { 312 | const result = func() 313 | return isPromise(result) ? result.catch(_guard) : result 314 | } catch (err) { 315 | return _guard(err) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/curry.ts: -------------------------------------------------------------------------------- 1 | export function chain( 2 | f1: (...arg: T1) => T2, 3 | f2: (arg: T2) => T3 4 | ): (...arg: T1) => T3 5 | export function chain( 6 | f1: (...arg: T1) => T2, 7 | f2: (arg: T2) => T3, 8 | f3: (arg: T3) => T4 9 | ): (...arg: T1) => T4 10 | export function chain( 11 | f1: (...arg: T1) => T2, 12 | f2: (arg: T2) => T3, 13 | f3: (arg: T3) => T4, 14 | f4: (arg: T3) => T5 15 | ): (...arg: T1) => T5 16 | export function chain( 17 | f1: (...arg: T1) => T2, 18 | f2: (arg: T2) => T3, 19 | f3: (arg: T3) => T4, 20 | f4: (arg: T3) => T5, 21 | f5: (arg: T3) => T6 22 | ): (...arg: T1) => T6 23 | export function chain( 24 | f1: (...arg: T1) => T2, 25 | f2: (arg: T2) => T3, 26 | f3: (arg: T3) => T4, 27 | f4: (arg: T3) => T5, 28 | f5: (arg: T3) => T6, 29 | f6: (arg: T3) => T7 30 | ): (...arg: T1) => T7 31 | export function chain( 32 | f1: (...arg: T1) => T2, 33 | f2: (arg: T2) => T3, 34 | f3: (arg: T3) => T4, 35 | f4: (arg: T3) => T5, 36 | f5: (arg: T3) => T6, 37 | f6: (arg: T3) => T7, 38 | f7: (arg: T3) => T8 39 | ): (...arg: T1) => T8 40 | export function chain( 41 | f1: (...arg: T1) => T2, 42 | f2: (arg: T2) => T3, 43 | f3: (arg: T3) => T4, 44 | f4: (arg: T3) => T5, 45 | f5: (arg: T3) => T6, 46 | f6: (arg: T3) => T7, 47 | f7: (arg: T3) => T8, 48 | f8: (arg: T3) => T9 49 | ): (...arg: T1) => T9 50 | export function chain( 51 | f1: (...arg: T1) => T2, 52 | f2: (arg: T2) => T3, 53 | f3: (arg: T3) => T4, 54 | f4: (arg: T3) => T5, 55 | f5: (arg: T3) => T6, 56 | f6: (arg: T3) => T7, 57 | f7: (arg: T3) => T8, 58 | f8: (arg: T3) => T9, 59 | f9: (arg: T3) => T10 60 | ): (...arg: T1) => T10 61 | export function chain< 62 | T1 extends any[], 63 | T2, 64 | T3, 65 | T4, 66 | T5, 67 | T6, 68 | T7, 69 | T8, 70 | T9, 71 | T10, 72 | T11 73 | >( 74 | f1: (...arg: T1) => T2, 75 | f2: (arg: T2) => T3, 76 | f3: (arg: T3) => T4, 77 | f4: (arg: T3) => T5, 78 | f5: (arg: T3) => T6, 79 | f6: (arg: T3) => T7, 80 | f7: (arg: T3) => T8, 81 | f8: (arg: T3) => T9, 82 | f9: (arg: T3) => T10, 83 | f10: (arg: T3) => T11 84 | ): (...arg: T1) => T11 85 | export function chain(...funcs: ((...args: any[]) => any)[]) { 86 | return (...args: any[]) => { 87 | return funcs.slice(1).reduce((acc, fn) => fn(acc), funcs[0](...args)) 88 | } 89 | } 90 | 91 | export function compose< 92 | F1Result, 93 | F1Args extends any[], 94 | F1NextArgs extends any[], 95 | LastResult 96 | >( 97 | f1: ( 98 | next: (...args: F1NextArgs) => LastResult 99 | ) => (...args: F1Args) => F1Result, 100 | last: (...args: F1NextArgs) => LastResult 101 | ): (...args: F1Args) => F1Result 102 | 103 | export function compose< 104 | F1Result, 105 | F1Args extends any[], 106 | F1NextArgs extends any[], 107 | F2Result, 108 | F2NextArgs extends any[], 109 | LastResult 110 | >( 111 | f1: ( 112 | next: (...args: F1NextArgs) => F2Result 113 | ) => (...args: F1Args) => F1Result, 114 | f2: ( 115 | next: (...args: F2NextArgs) => LastResult 116 | ) => (...args: F1NextArgs) => F2Result, 117 | last: (...args: F2NextArgs) => LastResult 118 | ): (...args: F1Args) => F1Result 119 | 120 | export function compose< 121 | F1Result, 122 | F1Args extends any[], 123 | F1NextArgs extends any[], 124 | F2NextArgs extends any[], 125 | F2Result, 126 | F3NextArgs extends any[], 127 | F3Result, 128 | LastResult 129 | >( 130 | f1: ( 131 | next: (...args: F1NextArgs) => F2Result 132 | ) => (...args: F1Args) => F1Result, 133 | f2: ( 134 | next: (...args: F2NextArgs) => F3Result 135 | ) => (...args: F1NextArgs) => F2Result, 136 | f3: ( 137 | next: (...args: F3NextArgs) => LastResult 138 | ) => (...args: F2NextArgs) => F3Result, 139 | last: (...args: F3NextArgs) => LastResult 140 | ): (...args: F1Args) => F1Result 141 | 142 | export function compose< 143 | F1Result, 144 | F1Args extends any[], 145 | F1NextArgs extends any[], 146 | F2NextArgs extends any[], 147 | F2Result, 148 | F3NextArgs extends any[], 149 | F3Result, 150 | F4NextArgs extends any[], 151 | F4Result, 152 | LastResult 153 | >( 154 | f1: ( 155 | next: (...args: F1NextArgs) => F2Result 156 | ) => (...args: F1Args) => F1Result, 157 | f2: ( 158 | next: (...args: F2NextArgs) => F3Result 159 | ) => (...args: F1NextArgs) => F2Result, 160 | f3: ( 161 | next: (...args: F3NextArgs) => F4Result 162 | ) => (...args: F2NextArgs) => F3Result, 163 | f4: ( 164 | next: (...args: F4NextArgs) => LastResult 165 | ) => (...args: F3NextArgs) => F4Result, 166 | last: (...args: F4NextArgs) => LastResult 167 | ): (...args: F1Args) => F1Result 168 | 169 | export function compose< 170 | F1Result, 171 | F1Args extends any[], 172 | F1NextArgs extends any[], 173 | F2NextArgs extends any[], 174 | F2Result, 175 | F3NextArgs extends any[], 176 | F3Result, 177 | F4NextArgs extends any[], 178 | F4Result, 179 | F5NextArgs extends any[], 180 | F5Result, 181 | LastResult 182 | >( 183 | f1: ( 184 | next: (...args: F1NextArgs) => F2Result 185 | ) => (...args: F1Args) => F1Result, 186 | f2: ( 187 | next: (...args: F2NextArgs) => F3Result 188 | ) => (...args: F1NextArgs) => F2Result, 189 | f3: ( 190 | next: (...args: F3NextArgs) => F4Result 191 | ) => (...args: F2NextArgs) => F3Result, 192 | f4: ( 193 | next: (...args: F4NextArgs) => F5Result 194 | ) => (...args: F3NextArgs) => F4Result, 195 | f5: ( 196 | next: (...args: F5NextArgs) => LastResult 197 | ) => (...args: F4NextArgs) => F5Result, 198 | last: (...args: F5NextArgs) => LastResult 199 | ): (...args: F1Args) => F1Result 200 | 201 | export function compose< 202 | F1Result, 203 | F1Args extends any[], 204 | F1NextArgs extends any[], 205 | F2NextArgs extends any[], 206 | F2Result, 207 | F3NextArgs extends any[], 208 | F3Result, 209 | F4NextArgs extends any[], 210 | F4Result, 211 | F5NextArgs extends any[], 212 | F5Result, 213 | F6NextArgs extends any[], 214 | F6Result, 215 | LastResult 216 | >( 217 | f1: ( 218 | next: (...args: F1NextArgs) => F2Result 219 | ) => (...args: F1Args) => F1Result, 220 | f2: ( 221 | next: (...args: F2NextArgs) => F3Result 222 | ) => (...args: F1NextArgs) => F2Result, 223 | f3: ( 224 | next: (...args: F3NextArgs) => F4Result 225 | ) => (...args: F2NextArgs) => F3Result, 226 | f4: ( 227 | next: (...args: F4NextArgs) => F5Result 228 | ) => (...args: F3NextArgs) => F4Result, 229 | f5: ( 230 | next: (...args: F5NextArgs) => F6Result 231 | ) => (...args: F4NextArgs) => F5Result, 232 | f6: ( 233 | next: (...args: F6NextArgs) => LastResult 234 | ) => (...args: F5NextArgs) => F6Result, 235 | last: (...args: F6NextArgs) => LastResult 236 | ): (...args: F1Args) => F1Result 237 | 238 | export function compose< 239 | F1Result, 240 | F1Args extends any[], 241 | F1NextArgs extends any[], 242 | F2NextArgs extends any[], 243 | F2Result, 244 | F3NextArgs extends any[], 245 | F3Result, 246 | F4NextArgs extends any[], 247 | F4Result, 248 | F5NextArgs extends any[], 249 | F5Result, 250 | F6NextArgs extends any[], 251 | F6Result, 252 | F7NextArgs extends any[], 253 | F7Result, 254 | LastResult 255 | >( 256 | f1: ( 257 | next: (...args: F1NextArgs) => F2Result 258 | ) => (...args: F1Args) => F1Result, 259 | f2: ( 260 | next: (...args: F2NextArgs) => F3Result 261 | ) => (...args: F1NextArgs) => F2Result, 262 | f3: ( 263 | next: (...args: F3NextArgs) => F4Result 264 | ) => (...args: F2NextArgs) => F3Result, 265 | f4: ( 266 | next: (...args: F4NextArgs) => F5Result 267 | ) => (...args: F3NextArgs) => F4Result, 268 | f5: ( 269 | next: (...args: F5NextArgs) => F6Result 270 | ) => (...args: F4NextArgs) => F5Result, 271 | f6: ( 272 | next: (...args: F6NextArgs) => F7Result 273 | ) => (...args: F5NextArgs) => F6Result, 274 | f7: ( 275 | next: (...args: F7NextArgs) => LastResult 276 | ) => (...args: F6NextArgs) => F7Result, 277 | last: (...args: F7NextArgs) => LastResult 278 | ): (...args: F1Args) => F1Result 279 | 280 | export function compose< 281 | F1Result, 282 | F1Args extends any[], 283 | F1NextArgs extends any[], 284 | F2NextArgs extends any[], 285 | F2Result, 286 | F3NextArgs extends any[], 287 | F3Result, 288 | F4NextArgs extends any[], 289 | F4Result, 290 | F5NextArgs extends any[], 291 | F5Result, 292 | F6NextArgs extends any[], 293 | F6Result, 294 | F7NextArgs extends any[], 295 | F7Result, 296 | F8NextArgs extends any[], 297 | F8Result, 298 | LastResult 299 | >( 300 | f1: ( 301 | next: (...args: F1NextArgs) => F2Result 302 | ) => (...args: F1Args) => F1Result, 303 | f2: ( 304 | next: (...args: F2NextArgs) => F3Result 305 | ) => (...args: F1NextArgs) => F2Result, 306 | f3: ( 307 | next: (...args: F3NextArgs) => F4Result 308 | ) => (...args: F2NextArgs) => F3Result, 309 | f4: ( 310 | next: (...args: F4NextArgs) => F5Result 311 | ) => (...args: F3NextArgs) => F4Result, 312 | f5: ( 313 | next: (...args: F5NextArgs) => F6Result 314 | ) => (...args: F4NextArgs) => F5Result, 315 | f6: ( 316 | next: (...args: F6NextArgs) => F7Result 317 | ) => (...args: F5NextArgs) => F6Result, 318 | f7: ( 319 | next: (...args: F7NextArgs) => LastResult 320 | ) => (...args: F6NextArgs) => F7Result, 321 | f8: ( 322 | next: (...args: F8NextArgs) => LastResult 323 | ) => (...args: F7NextArgs) => F8Result, 324 | last: (...args: F8NextArgs) => LastResult 325 | ): (...args: F1Args) => F1Result 326 | 327 | export function compose< 328 | F1Result, 329 | F1Args extends any[], 330 | F1NextArgs extends any[], 331 | F2NextArgs extends any[], 332 | F2Result, 333 | F3NextArgs extends any[], 334 | F3Result, 335 | F4NextArgs extends any[], 336 | F4Result, 337 | F5NextArgs extends any[], 338 | F5Result, 339 | F6NextArgs extends any[], 340 | F6Result, 341 | F7NextArgs extends any[], 342 | F7Result, 343 | F8NextArgs extends any[], 344 | F8Result, 345 | F9NextArgs extends any[], 346 | F9Result, 347 | LastResult 348 | >( 349 | f1: ( 350 | next: (...args: F1NextArgs) => F2Result 351 | ) => (...args: F1Args) => F1Result, 352 | f2: ( 353 | next: (...args: F2NextArgs) => F3Result 354 | ) => (...args: F1NextArgs) => F2Result, 355 | f3: ( 356 | next: (...args: F3NextArgs) => F4Result 357 | ) => (...args: F2NextArgs) => F3Result, 358 | f4: ( 359 | next: (...args: F4NextArgs) => F5Result 360 | ) => (...args: F3NextArgs) => F4Result, 361 | f5: ( 362 | next: (...args: F5NextArgs) => F6Result 363 | ) => (...args: F4NextArgs) => F5Result, 364 | f6: ( 365 | next: (...args: F6NextArgs) => F7Result 366 | ) => (...args: F5NextArgs) => F6Result, 367 | f7: ( 368 | next: (...args: F7NextArgs) => LastResult 369 | ) => (...args: F6NextArgs) => F7Result, 370 | f8: ( 371 | next: (...args: F8NextArgs) => LastResult 372 | ) => (...args: F7NextArgs) => F8Result, 373 | f9: ( 374 | next: (...args: F9NextArgs) => LastResult 375 | ) => (...args: F8NextArgs) => F9Result, 376 | last: (...args: F9NextArgs) => LastResult 377 | ): (...args: F1Args) => F1Result 378 | 379 | export function compose(...funcs: ((...args: any[]) => any)[]) { 380 | return funcs.reverse().reduce((acc, fn) => fn(acc)) 381 | } 382 | 383 | /** 384 | * This type produces the type array of TItems with all the type items 385 | * in TItemsToRemove removed from the start of the array type. 386 | * 387 | * @example 388 | * ``` 389 | * RemoveItemsInFront<[number, number], [number]> = [number] 390 | * RemoveItemsInFront<[File, number, string], [File, number]> = [string] 391 | * ``` 392 | */ 393 | type RemoveItemsInFront< 394 | TItems extends any[], 395 | TItemsToRemove extends any[] 396 | > = TItems extends [...TItemsToRemove, ...infer TRest] ? TRest : TItems 397 | 398 | export const partial = , R>( 399 | fn: (...args: T) => R, 400 | ...args: TA 401 | ) => { 402 | return (...rest: RemoveItemsInFront) => 403 | fn(...([...args, ...rest] as T)) 404 | } 405 | /** 406 | * Like partial but for unary functions that accept 407 | * a single object argument 408 | */ 409 | export const partob = >( 410 | fn: (args: T) => K, 411 | argobj: PartialArgs 412 | ) => { 413 | return (restobj: Omit): K => 414 | fn({ 415 | ...(argobj as Partial), 416 | ...(restobj as Partial) 417 | } as T) 418 | } 419 | 420 | /** 421 | * Creates a Proxy object that will dynamically 422 | * call the handler argument when attributes are 423 | * accessed 424 | */ 425 | export const proxied = ( 426 | handler: (propertyName: T) => K 427 | ): Record => { 428 | return new Proxy( 429 | {}, 430 | { 431 | get: (target, propertyName: any) => handler(propertyName) 432 | } 433 | ) 434 | } 435 | 436 | type Cache = Record 437 | 438 | const memoize = ( 439 | cache: Cache, 440 | func: (...args: TArgs) => TResult, 441 | keyFunc: ((...args: TArgs) => string) | null, 442 | ttl: number | null 443 | ) => { 444 | return function callWithMemo(...args: any): TResult { 445 | const key = keyFunc ? keyFunc(...args) : JSON.stringify({ args }) 446 | const existing = cache[key] 447 | if (existing !== undefined) { 448 | if (!existing.exp) return existing.value 449 | if (existing.exp > new Date().getTime()) { 450 | return existing.value 451 | } 452 | } 453 | const result = func(...args) 454 | cache[key] = { 455 | exp: ttl ? new Date().getTime() + ttl : null, 456 | value: result 457 | } 458 | return result 459 | } 460 | } 461 | 462 | /** 463 | * Creates a memoized function. The returned function 464 | * will only execute the source function when no value 465 | * has previously been computed. If a ttl (milliseconds) 466 | * is given previously computed values will be checked 467 | * for expiration before being returned. 468 | */ 469 | export const memo = ( 470 | func: (...args: TArgs) => TResult, 471 | options: { 472 | key?: (...args: TArgs) => string 473 | ttl?: number 474 | } = {} 475 | ) => { 476 | return memoize({}, func, options.key ?? null, options.ttl ?? null) as ( 477 | ...args: TArgs 478 | ) => TResult 479 | } 480 | 481 | export type DebounceFunction = { 482 | (...args: TArgs): void 483 | /** 484 | * Cancels the debounced function 485 | */ 486 | cancel(): void 487 | /** 488 | * Checks if there is any invocation debounced 489 | */ 490 | isPending(): boolean 491 | /** 492 | * Runs the debounced function immediately 493 | */ 494 | flush(...args: TArgs): void 495 | } 496 | 497 | export type ThrottledFunction = { 498 | (...args: TArgs): void 499 | /** 500 | * Checks if there is any invocation throttled 501 | */ 502 | isThrottled(): boolean 503 | } 504 | 505 | /** 506 | * Given a delay and a function returns a new function 507 | * that will only call the source function after delay 508 | * milliseconds have passed without any invocations. 509 | * 510 | * The debounce function comes with a `cancel` method 511 | * to cancel delayed `func` invocations and a `flush` 512 | * method to invoke them immediately 513 | */ 514 | export const debounce = ( 515 | { delay }: { delay: number }, 516 | func: (...args: TArgs) => any 517 | ) => { 518 | let timer: NodeJS.Timeout | undefined = undefined 519 | let active = true 520 | 521 | const debounced: DebounceFunction = (...args: TArgs) => { 522 | if (active) { 523 | clearTimeout(timer) 524 | timer = setTimeout(() => { 525 | active && func(...args) 526 | timer = undefined 527 | }, delay) 528 | } else { 529 | func(...args) 530 | } 531 | } 532 | debounced.isPending = () => { 533 | return timer !== undefined 534 | } 535 | debounced.cancel = () => { 536 | active = false 537 | } 538 | debounced.flush = (...args: TArgs) => func(...args) 539 | 540 | return debounced 541 | } 542 | 543 | /** 544 | * Given an interval and a function returns a new function 545 | * that will only call the source function if interval milliseconds 546 | * have passed since the last invocation 547 | */ 548 | export const throttle = ( 549 | { interval }: { interval: number }, 550 | func: (...args: TArgs) => any 551 | ) => { 552 | let ready = true 553 | let timer: NodeJS.Timeout | undefined = undefined 554 | 555 | const throttled: ThrottledFunction = (...args: TArgs) => { 556 | if (!ready) return 557 | func(...args) 558 | ready = false 559 | timer = setTimeout(() => { 560 | ready = true 561 | timer = undefined 562 | }, interval) 563 | } 564 | throttled.isThrottled = () => { 565 | return timer !== undefined 566 | } 567 | return throttled 568 | } 569 | 570 | /** 571 | * Make an object callable. Given an object and a function 572 | * the returned object will be a function with all the 573 | * objects properties. 574 | * 575 | * @example 576 | * ```typescript 577 | * const car = callable({ 578 | * wheels: 2 579 | * }, self => () => { 580 | * return 'driving' 581 | * }) 582 | * 583 | * car.wheels // => 2 584 | * car() // => 'driving' 585 | * ``` 586 | */ 587 | export const callable = < 588 | TValue, 589 | TObj extends Record, 590 | TFunc extends (...args: any) => any 591 | >( 592 | obj: TObj, 593 | fn: (self: TObj) => TFunc 594 | ): TObj & TFunc => { 595 | /* istanbul ignore next */ 596 | const FUNC = () => {} 597 | return new Proxy(Object.assign(FUNC, obj), { 598 | get: (target, key: string) => target[key], 599 | set: (target, key: string, value: any) => { 600 | ;(target as any)[key] = value 601 | return true 602 | }, 603 | apply: (target, self, args) => fn(Object.assign({}, target))(...args) 604 | }) as unknown as TObj & TFunc 605 | } 606 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | alphabetical, 3 | boil, 4 | cluster, 5 | counting, 6 | diff, 7 | first, 8 | flat, 9 | fork, 10 | group, 11 | intersects, 12 | iterate, 13 | last, 14 | list, 15 | max, 16 | merge, 17 | min, 18 | objectify, 19 | range, 20 | replace, 21 | replaceOrAppend, 22 | select, 23 | shift, 24 | sift, 25 | sort, 26 | sum, 27 | toggle, 28 | unique, 29 | zip, 30 | zipToObject 31 | } from './array' 32 | export { 33 | all, 34 | defer, 35 | guard, 36 | map, 37 | parallel, 38 | reduce, 39 | retry, 40 | sleep, 41 | tryit as try, 42 | tryit 43 | } from './async' 44 | export type { AggregateError } from './async' 45 | export { 46 | callable, 47 | chain, 48 | compose, 49 | debounce, 50 | memo, 51 | partial, 52 | partob, 53 | proxied, 54 | throttle 55 | } from './curry' 56 | export { inRange, toFloat, toInt } from './number' 57 | export { 58 | assign, 59 | clone, 60 | construct, 61 | crush, 62 | get, 63 | invert, 64 | keys, 65 | listify, 66 | lowerize, 67 | mapEntries, 68 | mapKeys, 69 | mapValues, 70 | omit, 71 | pick, 72 | set, 73 | shake, 74 | upperize 75 | } from './object' 76 | export { draw, random, shuffle, uid } from './random' 77 | export { series } from './series' 78 | export { 79 | camel, 80 | capitalize, 81 | dash, 82 | pascal, 83 | snake, 84 | template, 85 | title, 86 | trim 87 | } from './string' 88 | export { 89 | isArray, 90 | isDate, 91 | isEmpty, 92 | isEqual, 93 | isFloat, 94 | isFunction, 95 | isInt, 96 | isNumber, 97 | isObject, 98 | isPrimitive, 99 | isPromise, 100 | isString, 101 | isSymbol 102 | } from './typed' 103 | -------------------------------------------------------------------------------- /src/number.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if the given number is between zero (0) and the ending number. 0 is inclusive. 3 | * 4 | * * Numbers can be negative or positive. 5 | * * Ending number is exclusive. 6 | * 7 | * @param {number} number The number to check. 8 | * @param {number} end The end of the range. Exclusive. 9 | * @returns {boolean} Returns `true` if `number` is in the range, else `false`. 10 | */ 11 | export function inRange(number: number, end: number): boolean 12 | 13 | /** 14 | * Checks if the given number is between two numbers. 15 | * 16 | * * Numbers can be negative or positive. 17 | * * Starting number is inclusive. 18 | * * Ending number is exclusive. 19 | * * The start and the end of the range can be ascending OR descending order. 20 | * 21 | * @param {number} number The number to check. 22 | * @param {number} start The start of the range. Inclusive. 23 | * @param {number} end The end of the range. Exclusive. 24 | * @returns {boolean} Returns `true` if `number` is in the range, else `false`. 25 | */ 26 | export function inRange(number: number, start: number, end: number): boolean 27 | export function inRange(number: number, start: number, end?: number): boolean { 28 | const isTypeSafe = 29 | typeof number === 'number' && 30 | typeof start === 'number' && 31 | (typeof end === 'undefined' || typeof end === 'number') 32 | 33 | if (!isTypeSafe) { 34 | return false 35 | } 36 | 37 | if (typeof end === 'undefined') { 38 | end = start 39 | start = 0 40 | } 41 | 42 | return number >= Math.min(start, end) && number < Math.max(start, end) 43 | } 44 | 45 | export const toFloat = ( 46 | value: any, 47 | defaultValue?: T 48 | ): number | T => { 49 | const def = defaultValue === undefined ? 0.0 : defaultValue 50 | if (value === null || value === undefined) { 51 | return def 52 | } 53 | const result = parseFloat(value) 54 | return isNaN(result) ? def : result 55 | } 56 | 57 | export const toInt = ( 58 | value: any, 59 | defaultValue?: T 60 | ): number | T => { 61 | const def = defaultValue === undefined ? 0 : defaultValue 62 | if (value === null || value === undefined) { 63 | return def 64 | } 65 | const result = parseInt(value) 66 | return isNaN(result) ? def : result 67 | } 68 | -------------------------------------------------------------------------------- /src/object.ts: -------------------------------------------------------------------------------- 1 | import { objectify } from './array' 2 | import { toInt } from './number' 3 | import { isArray, isObject, isPrimitive } from './typed' 4 | 5 | type LowercasedKeys> = { 6 | [P in keyof T & string as Lowercase

]: T[P] 7 | } 8 | 9 | type UppercasedKeys> = { 10 | [P in keyof T & string as Uppercase

]: T[P] 11 | } 12 | 13 | /** 14 | * Removes (shakes out) undefined entries from an 15 | * object. Optional second argument shakes out values 16 | * by custom evaluation. 17 | */ 18 | export const shake = ( 19 | obj: T, 20 | filter: (value: any) => boolean = x => x === undefined 21 | ): Omit => { 22 | if (!obj) return {} as T 23 | const keys = Object.keys(obj) as (keyof T)[] 24 | return keys.reduce((acc, key) => { 25 | if (filter(obj[key])) { 26 | return acc 27 | } else { 28 | acc[key] = obj[key] 29 | return acc 30 | } 31 | }, {} as T) 32 | } 33 | 34 | /** 35 | * Map over all the keys of an object to return 36 | * a new object 37 | */ 38 | export const mapKeys = < 39 | TValue, 40 | TKey extends string | number | symbol, 41 | TNewKey extends string | number | symbol 42 | >( 43 | obj: Record, 44 | mapFunc: (key: TKey, value: TValue) => TNewKey 45 | ): Record => { 46 | const keys = Object.keys(obj) as TKey[] 47 | return keys.reduce((acc, key) => { 48 | acc[mapFunc(key as TKey, obj[key])] = obj[key] 49 | return acc 50 | }, {} as Record) 51 | } 52 | 53 | /** 54 | * Map over all the keys to create a new object 55 | */ 56 | export const mapValues = < 57 | TValue, 58 | TKey extends string | number | symbol, 59 | TNewValue 60 | >( 61 | obj: Record, 62 | mapFunc: (value: TValue, key: TKey) => TNewValue 63 | ): Record => { 64 | const keys = Object.keys(obj) as TKey[] 65 | return keys.reduce((acc, key) => { 66 | acc[key] = mapFunc(obj[key], key) 67 | return acc 68 | }, {} as Record) 69 | } 70 | 71 | /** 72 | * Map over all the keys to create a new object 73 | */ 74 | export const mapEntries = < 75 | TKey extends string | number | symbol, 76 | TValue, 77 | TNewKey extends string | number | symbol, 78 | TNewValue 79 | >( 80 | obj: Record, 81 | toEntry: (key: TKey, value: TValue) => [TNewKey, TNewValue] 82 | ): Record => { 83 | if (!obj) return {} as Record 84 | return Object.entries(obj).reduce((acc, [key, value]) => { 85 | const [newKey, newValue] = toEntry(key as TKey, value as TValue) 86 | acc[newKey] = newValue 87 | return acc 88 | }, {} as Record) 89 | } 90 | 91 | /** 92 | * Returns an object with { [keys]: value } 93 | * inverted as { [value]: key } 94 | */ 95 | export const invert = < 96 | TKey extends string | number | symbol, 97 | TValue extends string | number | symbol 98 | >( 99 | obj: Record 100 | ): Record => { 101 | if (!obj) return {} as Record 102 | const keys = Object.keys(obj) as TKey[] 103 | return keys.reduce((acc, key) => { 104 | acc[obj[key]] = key 105 | return acc 106 | }, {} as Record) 107 | } 108 | 109 | /** 110 | * Convert all keys in an object to lower case 111 | */ 112 | export const lowerize = >(obj: T) => 113 | mapKeys(obj, k => k.toLowerCase()) as LowercasedKeys 114 | 115 | /** 116 | * Convert all keys in an object to upper case 117 | */ 118 | export const upperize = >(obj: T) => 119 | mapKeys(obj, k => k.toUpperCase()) as UppercasedKeys 120 | 121 | /** 122 | * Creates a shallow copy of the given obejct/value. 123 | * @param {*} obj value to clone 124 | * @returns {*} shallow clone of the given value 125 | */ 126 | export const clone = (obj: T): T => { 127 | // Primitive values do not need cloning. 128 | if (isPrimitive(obj)) { 129 | return obj 130 | } 131 | 132 | // Binding a function to an empty object creates a 133 | // copy function. 134 | if (typeof obj === 'function') { 135 | return obj.bind({}) 136 | } 137 | 138 | // Access the constructor and create a new object. 139 | // This method can create an array as well. 140 | const newObj = new ((obj as object).constructor as { new (): T })() 141 | 142 | // Assign the props. 143 | Object.getOwnPropertyNames(obj).forEach(prop => { 144 | // Bypass type checking since the primitive cases 145 | // are already checked in the beginning 146 | ;(newObj as any)[prop] = (obj as any)[prop] 147 | }) 148 | 149 | return newObj 150 | } 151 | 152 | /** 153 | * Convert an object to a list, mapping each entry 154 | * into a list item 155 | */ 156 | export const listify = ( 157 | obj: Record, 158 | toItem: (key: TKey, value: TValue) => KResult 159 | ) => { 160 | if (!obj) return [] 161 | const entries = Object.entries(obj) 162 | if (entries.length === 0) return [] 163 | return entries.reduce((acc, entry) => { 164 | acc.push(toItem(entry[0] as TKey, entry[1] as TValue)) 165 | return acc 166 | }, [] as KResult[]) 167 | } 168 | 169 | /** 170 | * Pick a list of properties from an object 171 | * into a new object 172 | */ 173 | export const pick = ( 174 | obj: T, 175 | keys: TKeys[] 176 | ): Pick => { 177 | if (!obj) return {} as Pick 178 | return keys.reduce((acc, key) => { 179 | if (Object.prototype.hasOwnProperty.call(obj, key)) acc[key] = obj[key] 180 | return acc 181 | }, {} as Pick) 182 | } 183 | 184 | /** 185 | * Omit a list of properties from an object 186 | * returning a new object with the properties 187 | * that remain 188 | */ 189 | export const omit = ( 190 | obj: T, 191 | keys: TKeys[] 192 | ): Omit => { 193 | if (!obj) return {} as Omit 194 | if (!keys || keys.length === 0) return obj as Omit 195 | return keys.reduce( 196 | (acc, key) => { 197 | // Gross, I know, it's mutating the object, but we 198 | // are allowing it in this very limited scope due 199 | // to the performance implications of an omit func. 200 | // Not a pattern or practice to use elsewhere. 201 | delete acc[key] 202 | return acc 203 | }, 204 | { ...obj } 205 | ) 206 | } 207 | 208 | /** 209 | * Dynamically get a nested value from an array or 210 | * object with a string. 211 | * 212 | * @example get(person, 'friends[0].name') 213 | */ 214 | export const get = ( 215 | value: any, 216 | path: string, 217 | defaultValue?: TDefault 218 | ): TDefault => { 219 | const segments = path.split(/[\.\[\]]/g) 220 | let current: any = value 221 | for (const key of segments) { 222 | if (current === null) return defaultValue as TDefault 223 | if (current === undefined) return defaultValue as TDefault 224 | const dequoted = key.replace(/['"]/g, '') 225 | if (dequoted.trim() === '') continue 226 | current = current[dequoted] 227 | } 228 | if (current === undefined) return defaultValue as TDefault 229 | return current 230 | } 231 | 232 | /** 233 | * Opposite of get, dynamically set a nested value into 234 | * an object using a key path. Does not modify the given 235 | * initial object. 236 | * 237 | * @example 238 | * set({}, 'name', 'ra') // => { name: 'ra' } 239 | * set({}, 'cards[0].value', 2) // => { cards: [{ value: 2 }] } 240 | */ 241 | export const set = ( 242 | initial: T, 243 | path: string, 244 | value: K 245 | ): T => { 246 | if (!initial) return {} as T 247 | if (!path || value === undefined) return initial 248 | const segments = path.split(/[\.\[\]]/g).filter(x => !!x.trim()) 249 | const _set = (node: any) => { 250 | if (segments.length > 1) { 251 | const key = segments.shift() as string 252 | const nextIsNum = toInt(segments[0], null) === null ? false : true 253 | node[key] = node[key] === undefined ? (nextIsNum ? [] : {}) : node[key] 254 | _set(node[key]) 255 | } else { 256 | node[segments[0]] = value 257 | } 258 | } 259 | // NOTE: One day, when structuredClone has more 260 | // compatability use it to clone the value 261 | // https://developer.mozilla.org/en-US/docs/Web/API/structuredClone 262 | const cloned = clone(initial) 263 | _set(cloned) 264 | return cloned 265 | } 266 | 267 | /** 268 | * Merges two objects together recursivly into a new 269 | * object applying values from right to left. 270 | * Recursion only applies to child object properties. 271 | */ 272 | export const assign = >( 273 | initial: X, 274 | override: X 275 | ): X => { 276 | if (!initial || !override) return initial ?? override ?? {} 277 | 278 | return Object.entries({ ...initial, ...override }).reduce( 279 | (acc, [key, value]) => { 280 | return { 281 | ...acc, 282 | [key]: (() => { 283 | if (isObject(initial[key])) return assign(initial[key], value) 284 | // if (isArray(value)) return value.map(x => assign) 285 | return value 286 | })() 287 | } 288 | }, 289 | {} as X 290 | ) 291 | } 292 | 293 | /** 294 | * Get a string list of all key names that exist in 295 | * an object (deep). 296 | * 297 | * @example 298 | * keys({ name: 'ra' }) // ['name'] 299 | * keys({ name: 'ra', children: [{ name: 'hathor' }] }) // ['name', 'children.0.name'] 300 | */ 301 | export const keys = (value: TValue): string[] => { 302 | if (!value) return [] 303 | const getKeys = (nested: any, paths: string[]): string[] => { 304 | if (isObject(nested)) { 305 | return Object.entries(nested).flatMap(([k, v]) => 306 | getKeys(v, [...paths, k]) 307 | ) 308 | } 309 | if (isArray(nested)) { 310 | return nested.flatMap((item, i) => getKeys(item, [...paths, `${i}`])) 311 | } 312 | return [paths.join('.')] 313 | } 314 | return getKeys(value, []) 315 | } 316 | 317 | /** 318 | * Flattens a deep object to a single demension, converting 319 | * the keys to dot notation. 320 | * 321 | * @example 322 | * crush({ name: 'ra', children: [{ name: 'hathor' }] }) 323 | * // { name: 'ra', 'children.0.name': 'hathor' } 324 | */ 325 | export const crush = (value: TValue): object => { 326 | if (!value) return {} 327 | return objectify( 328 | keys(value), 329 | k => k, 330 | k => get(value, k) 331 | ) 332 | } 333 | 334 | /** 335 | * The opposite of crush, given an object that was 336 | * crushed into key paths and values will return 337 | * the original object reconstructed. 338 | * 339 | * @example 340 | * construct({ name: 'ra', 'children.0.name': 'hathor' }) 341 | * // { name: 'ra', children: [{ name: 'hathor' }] } 342 | */ 343 | export const construct = (obj: TObject): object => { 344 | if (!obj) return {} 345 | return Object.keys(obj).reduce((acc, path) => { 346 | return set(acc, path, (obj as any)[path]) 347 | }, {}) 348 | } 349 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | import { iterate } from './array' 2 | 3 | /** 4 | * Generates a random number between min and max 5 | */ 6 | export const random = (min: number, max: number) => { 7 | return Math.floor(Math.random() * (max - min + 1) + min) 8 | } 9 | 10 | /** 11 | * Draw a random item from a list. Returns 12 | * null if the list is empty 13 | */ 14 | export const draw = (array: readonly T[]): T | null => { 15 | const max = array.length 16 | if (max === 0) { 17 | return null 18 | } 19 | const index = random(0, max - 1) 20 | return array[index] 21 | } 22 | 23 | export const shuffle = (array: readonly T[]): T[] => { 24 | return array 25 | .map(a => ({ rand: Math.random(), value: a })) 26 | .sort((a, b) => a.rand - b.rand) 27 | .map(a => a.value) 28 | } 29 | 30 | export const uid = (length: number, specials: string = '') => { 31 | const characters = 32 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + specials 33 | return iterate( 34 | length, 35 | acc => { 36 | return acc + characters.charAt(random(0, characters.length - 1)) 37 | }, 38 | '' 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/series.ts: -------------------------------------------------------------------------------- 1 | import { list } from './array' 2 | 3 | /** 4 | * Creates a series object around a list of values 5 | * that should be treated with order. 6 | */ 7 | export const series = ( 8 | items: T[], 9 | toKey: (item: T) => string | symbol = item => `${item}` 10 | ) => { 11 | const { indexesByKey, itemsByIndex } = items.reduce( 12 | (acc, item, idx) => ({ 13 | indexesByKey: { 14 | ...acc.indexesByKey, 15 | [toKey(item)]: idx 16 | }, 17 | itemsByIndex: { 18 | ...acc.itemsByIndex, 19 | [idx]: item 20 | } 21 | }), 22 | { 23 | indexesByKey: {} as Record, 24 | itemsByIndex: {} as Record 25 | } 26 | ) 27 | /** 28 | * Given two values in the series, returns the 29 | * value that occurs earlier in the series 30 | */ 31 | const min = (a: T, b: T): T => { 32 | return indexesByKey[toKey(a)] < indexesByKey[toKey(b)] ? a : b 33 | } 34 | /** 35 | * Given two values in the series, returns the 36 | * value that occurs later in the series 37 | */ 38 | const max = (a: T, b: T): T => { 39 | return indexesByKey[toKey(a)] > indexesByKey[toKey(b)] ? a : b 40 | } 41 | /** 42 | * Returns the first item from the series 43 | */ 44 | const first = (): T => { 45 | return itemsByIndex[0] 46 | } 47 | /** 48 | * Returns the last item in the series 49 | */ 50 | const last = (): T => { 51 | return itemsByIndex[items.length - 1] 52 | } 53 | /** 54 | * Given an item in the series returns the next item 55 | * in the series or default if the given value is 56 | * the last item in the series 57 | */ 58 | const next = (current: T, defaultValue?: T): T => { 59 | return ( 60 | itemsByIndex[indexesByKey[toKey(current)] + 1] ?? defaultValue ?? first() 61 | ) 62 | } 63 | /** 64 | * Given an item in the series returns the previous item 65 | * in the series or default if the given value is 66 | * the first item in the series 67 | */ 68 | const previous = (current: T, defaultValue?: T): T => { 69 | return ( 70 | itemsByIndex[indexesByKey[toKey(current)] - 1] ?? defaultValue ?? last() 71 | ) 72 | } 73 | /** 74 | * A more dynamic method than next and previous that 75 | * lets you move many times in either direction. 76 | * @example series(weekdays).spin('wednesday', 3) => 'monday' 77 | * @example series(weekdays).spin('wednesday', -3) => 'friday' 78 | */ 79 | const spin = (current: T, num: number): T => { 80 | if (num === 0) return current 81 | const abs = Math.abs(num) 82 | const rel = abs > items.length ? abs % items.length : abs 83 | return list(0, rel - 1).reduce( 84 | acc => (num > 0 ? next(acc) : previous(acc)), 85 | current 86 | ) 87 | } 88 | return { 89 | min, 90 | max, 91 | first, 92 | last, 93 | next, 94 | previous, 95 | spin 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Capitalize the first word of the string 3 | * 4 | * capitalize('hello') -> 'Hello' 5 | * capitalize('va va voom') -> 'Va va voom' 6 | */ 7 | export const capitalize = (str: string): string => { 8 | if (!str || str.length === 0) return '' 9 | const lower = str.toLowerCase() 10 | return lower.substring(0, 1).toUpperCase() + lower.substring(1, lower.length) 11 | } 12 | 13 | /** 14 | * Formats the given string in camel case fashion 15 | * 16 | * camel('hello world') -> 'helloWorld' 17 | * camel('va va-VOOM') -> 'vaVaVoom' 18 | * camel('helloWorld') -> 'helloWorld' 19 | */ 20 | export const camel = (str: string): string => { 21 | const parts = 22 | str 23 | ?.replace(/([A-Z])+/g, capitalize) 24 | ?.split(/(?=[A-Z])|[\.\-\s_]/) 25 | .map(x => x.toLowerCase()) ?? [] 26 | if (parts.length === 0) return '' 27 | if (parts.length === 1) return parts[0] 28 | return parts.reduce((acc, part) => { 29 | return `${acc}${part.charAt(0).toUpperCase()}${part.slice(1)}` 30 | }) 31 | } 32 | 33 | /** 34 | * Formats the given string in snake case fashion 35 | * 36 | * snake('hello world') -> 'hello_world' 37 | * snake('va va-VOOM') -> 'va_va_voom' 38 | * snake('helloWord') -> 'hello_world' 39 | */ 40 | export const snake = ( 41 | str: string, 42 | options?: { 43 | splitOnNumber?: boolean 44 | } 45 | ): string => { 46 | const parts = 47 | str 48 | ?.replace(/([A-Z])+/g, capitalize) 49 | .split(/(?=[A-Z])|[\.\-\s_]/) 50 | .map(x => x.toLowerCase()) ?? [] 51 | if (parts.length === 0) return '' 52 | if (parts.length === 1) return parts[0] 53 | const result = parts.reduce((acc, part) => { 54 | return `${acc}_${part.toLowerCase()}` 55 | }) 56 | return options?.splitOnNumber === false 57 | ? result 58 | : result.replace(/([A-Za-z]{1}[0-9]{1})/, val => `${val[0]!}_${val[1]!}`) 59 | } 60 | 61 | /** 62 | * Formats the given string in dash case fashion 63 | * 64 | * dash('hello world') -> 'hello-world' 65 | * dash('va va_VOOM') -> 'va-va-voom' 66 | * dash('helloWord') -> 'hello-word' 67 | */ 68 | export const dash = (str: string): string => { 69 | const parts = 70 | str 71 | ?.replace(/([A-Z])+/g, capitalize) 72 | ?.split(/(?=[A-Z])|[\.\-\s_]/) 73 | .map(x => x.toLowerCase()) ?? [] 74 | if (parts.length === 0) return '' 75 | if (parts.length === 1) return parts[0] 76 | return parts.reduce((acc, part) => { 77 | return `${acc}-${part.toLowerCase()}` 78 | }) 79 | } 80 | 81 | /** 82 | * Formats the given string in pascal case fashion 83 | * 84 | * pascal('hello world') -> 'HelloWorld' 85 | * pascal('va va boom') -> 'VaVaBoom' 86 | */ 87 | export const pascal = (str: string): string => { 88 | const parts = str?.split(/[\.\-\s_]/).map(x => x.toLowerCase()) ?? [] 89 | if (parts.length === 0) return '' 90 | return parts.map(str => str.charAt(0).toUpperCase() + str.slice(1)).join('') 91 | } 92 | 93 | /** 94 | * Formats the given string in title case fashion 95 | * 96 | * title('hello world') -> 'Hello World' 97 | * title('va_va_boom') -> 'Va Va Boom' 98 | * title('root-hook') -> 'Root Hook' 99 | * title('queryItems') -> 'Query Items' 100 | */ 101 | export const title = (str: string | null | undefined): string => { 102 | if (!str) return '' 103 | return str 104 | .split(/(?=[A-Z])|[\.\-\s_]/) 105 | .map(s => s.trim()) 106 | .filter(s => !!s) 107 | .map(s => capitalize(s.toLowerCase())) 108 | .join(' ') 109 | } 110 | 111 | /** 112 | * template is used to replace data by name in template strings. 113 | * The default expression looks for {{name}} to identify names. 114 | * 115 | * Ex. template('Hello, {{name}}', { name: 'ray' }) 116 | * Ex. template('Hello, ', { name: 'ray' }, /<(.+?)>/g) 117 | */ 118 | export const template = ( 119 | str: string, 120 | data: Record, 121 | regex = /\{\{(.+?)\}\}/g 122 | ) => { 123 | return Array.from(str.matchAll(regex)).reduce((acc, match) => { 124 | return acc.replace(match[0], data[match[1]]) 125 | }, str) 126 | } 127 | 128 | /** 129 | * Trims all prefix and suffix characters from the given 130 | * string. Like the builtin trim function but accepts 131 | * other characters you would like to trim and trims 132 | * multiple characters. 133 | * 134 | * ```typescript 135 | * trim(' hello ') // => 'hello' 136 | * trim('__hello__', '_') // => 'hello' 137 | * trim('/repos/:owner/:repo/', '/') // => 'repos/:owner/:repo' 138 | * trim('222222__hello__1111111', '12_') // => 'hello' 139 | * ``` 140 | */ 141 | export const trim = ( 142 | str: string | null | undefined, 143 | charsToTrim: string = ' ' 144 | ) => { 145 | if (!str) return '' 146 | const toTrim = charsToTrim.replace(/[\W]{1}/g, '\\$&') 147 | const regex = new RegExp(`^[${toTrim}]+|[${toTrim}]+$`, 'g') 148 | return str.replace(regex, '') 149 | } 150 | -------------------------------------------------------------------------------- /src/tests/curry.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import * as _ from '..' 3 | import type { DebounceFunction } from '../curry' 4 | 5 | describe('curry module', () => { 6 | describe('compose function', () => { 7 | test('composes functions', () => { 8 | const useZero = (fn: (num: number) => number) => () => fn(0) 9 | const objectize = 10 | (fn: (obj: { num: number }) => number) => (num: number) => 11 | fn({ num }) 12 | const increment = 13 | (fn: (arg: { num: number }) => number) => 14 | ({ num }: { num: number }) => 15 | fn({ num: num + 1 }) 16 | const returnArg = (arg: 'num') => (args: { num: number }) => args[arg] 17 | 18 | const composed = _.compose( 19 | useZero, 20 | objectize, 21 | increment, 22 | increment, 23 | returnArg('num') 24 | ) 25 | 26 | const decomposed = useZero( 27 | objectize(increment(increment(returnArg('num')))) 28 | ) 29 | 30 | const expected = decomposed() 31 | const result = composed() 32 | 33 | assert.equal(result, expected) 34 | assert.equal(result, 2) 35 | }) 36 | test('composes async function', async () => { 37 | const useZero = (fn: (num: number) => Promise) => async () => 38 | fn(0) 39 | const objectize = 40 | (fn: (obj: { num: number }) => Promise) => 41 | async (num: number) => 42 | fn({ num }) 43 | const increment = 44 | (fn: (arg: { num: number }) => Promise) => 45 | async ({ num }: { num: number }) => 46 | fn({ num: num + 1 }) 47 | const returnArg = (arg: 'num') => async (args: { num: number }) => 48 | args[arg] 49 | 50 | const composed = _.compose( 51 | useZero, 52 | objectize, 53 | increment, 54 | increment, 55 | returnArg('num') 56 | ) 57 | 58 | const decomposed = useZero( 59 | objectize(increment(increment(returnArg('num')))) 60 | ) 61 | 62 | const expected = await decomposed() 63 | const result = await composed() 64 | 65 | assert.equal(result, expected) 66 | }) 67 | test('composes function type overloads', () => { 68 | const useZero = (fn: (num: number) => number) => () => fn(0) 69 | const objectize = 70 | (fn: (obj: { num: number }) => number) => (num: number) => 71 | fn({ num }) 72 | const increment = 73 | (fn: (arg: { num: number }) => number) => 74 | ({ num }: { num: number }) => 75 | fn({ num: num + 1 }) 76 | const returnArg = (arg: 'num') => (args: { num: number }) => args[arg] 77 | const returnNum = () => (num: number) => num 78 | 79 | assert.equal(_.compose(useZero, returnNum())(), 0) 80 | 81 | assert.equal(_.compose(useZero, objectize, returnArg('num'))(), 0) 82 | 83 | assert.equal( 84 | _.compose(useZero, objectize, increment, returnArg('num'))(), 85 | 1 86 | ) 87 | 88 | assert.equal( 89 | _.compose(useZero, objectize, increment, increment, returnArg('num'))(), 90 | 2 91 | ) 92 | 93 | assert.equal( 94 | _.compose( 95 | useZero, 96 | objectize, 97 | increment, 98 | increment, 99 | increment, 100 | returnArg('num') 101 | )(), 102 | 3 103 | ) 104 | 105 | assert.equal( 106 | _.compose( 107 | useZero, 108 | objectize, 109 | increment, 110 | increment, 111 | increment, 112 | increment, 113 | returnArg('num') 114 | )(), 115 | 4 116 | ) 117 | 118 | assert.equal( 119 | _.compose( 120 | useZero, 121 | objectize, 122 | increment, 123 | increment, 124 | increment, 125 | increment, 126 | increment, 127 | returnArg('num') 128 | )(), 129 | 5 130 | ) 131 | 132 | assert.equal( 133 | _.compose( 134 | useZero, 135 | objectize, 136 | increment, 137 | increment, 138 | increment, 139 | increment, 140 | increment, 141 | increment, 142 | returnArg('num') 143 | )(), 144 | 6 145 | ) 146 | 147 | assert.equal( 148 | _.compose( 149 | useZero, 150 | objectize, 151 | increment, 152 | increment, 153 | increment, 154 | increment, 155 | increment, 156 | increment, 157 | increment, 158 | returnArg('num') 159 | )(), 160 | 7 161 | ) 162 | }) 163 | }) 164 | 165 | describe('partial function', () => { 166 | test('passes single args', () => { 167 | const add = (a: number, b: number) => a + b 168 | const expected = 20 169 | const partialed = _.partial(add, 10) 170 | const result = partialed(10) 171 | assert.equal(result, expected) 172 | }) 173 | test('passes many args', () => { 174 | const add = (...nums: number[]) => nums.reduce((a, b) => a + b, 0) 175 | const expected = 10 176 | const result = _.partial(add, 2, 2, 2)(2, 2) 177 | assert.equal(result, expected) 178 | }) 179 | }) 180 | 181 | describe('partob function', () => { 182 | test('partob passes single args', () => { 183 | const add = ({ a, b }: { a: number; b: number }) => a + b 184 | const expected = 20 185 | const result = _.partob(add, { a: 10 })({ b: 10 }) 186 | assert.equal(result, expected) 187 | }) 188 | test('partob overrides inital with later', () => { 189 | const add = ({ a, b }: { a: number; b: number }) => a + b 190 | const expected = 15 191 | const result = _.partob(add, { a: 10 })({ a: 5, b: 10 } as any) 192 | assert.equal(result, expected) 193 | }) 194 | }) 195 | 196 | describe('chain function', () => { 197 | test('calls all given functions', () => { 198 | const genesis = (num: number, name: string) => 0 199 | const addFive = (num: number) => num + 5 200 | const twoX = (num: number) => num * 2 201 | const func = _.chain(genesis, addFive, twoX) 202 | const result = func(0, '') 203 | assert.equal(result, 10) 204 | }) 205 | 206 | test('calls add(1), then addFive, then twoX functions by 1', () => { 207 | const add = (y: number) => (x: number) => x + y 208 | const addFive = add(5) 209 | const twoX = (num: number) => num * 2 210 | const func = _.chain(add(1), addFive, twoX) 211 | const result = func(1) 212 | assert.equal(result, 14) 213 | }) 214 | 215 | test('calls add(2), then addFive, then twoX, then repeatX functions by 1', () => { 216 | const add = (y: number) => (x: number) => x + y 217 | const addFive = add(5) 218 | const twoX = (num: number) => num * 2 219 | const repeatX = (num: number) => 'X'.repeat(num) 220 | const func = _.chain(add(2), addFive, twoX, repeatX) 221 | const result = func(1) 222 | assert.equal(result, 'XXXXXXXXXXXXXXXX') 223 | }) 224 | 225 | test('calls addFive, then add(2), then twoX, then repeatX functions by 1', () => { 226 | const add = (y: number) => (x: number) => x + y 227 | const addFive = add(5) 228 | const twoX = (num: number) => num * 2 229 | const repeatX = (num: number) => 'X'.repeat(num) 230 | const func = _.chain(addFive, add(2), twoX, repeatX) 231 | const result = func(1) 232 | assert.equal(result, 'XXXXXXXXXXXXXXXX') 233 | }) 234 | 235 | test('calls getName, then upperCase functions as a mapper for User[]', () => { 236 | type User = { id: number; name: string } 237 | const users: User[] = [ 238 | { id: 1, name: 'John Doe' }, 239 | { id: 2, name: 'John Smith' }, 240 | { id: 3, name: 'John Wick' } 241 | ] 242 | const getName = (item: T) => item.name 243 | const upperCase: (x: string) => Uppercase = (text: string) => 244 | text.toUpperCase() as Uppercase 245 | 246 | const getUpperName = _.chain(getName, upperCase) 247 | const result = users.map(getUpperName) 248 | assert.deepEqual(result, ['JOHN DOE', 'JOHN SMITH', 'JOHN WICK']) 249 | }) 250 | }) 251 | 252 | describe('proxied function', () => { 253 | test('returns proxy that calls callback function', () => { 254 | const handler = (propertyName: string) => { 255 | if (propertyName === 'x') return 2 256 | if (propertyName === 'getName') return () => 'radash' 257 | return undefined 258 | } 259 | const proxy = _.proxied(handler) as any 260 | assert.equal(proxy.x, 2) 261 | assert.equal(proxy.getName(), 'radash') 262 | assert.isUndefined(proxy.nil) 263 | }) 264 | }) 265 | 266 | describe('memo function', () => { 267 | test('only executes function once', () => { 268 | const func = _.memo(() => new Date().getTime()) 269 | const resultA = func() 270 | const resultB = func() 271 | assert.equal(resultA, resultB) 272 | }) 273 | test('uses key to identify unique calls', () => { 274 | const func = _.memo( 275 | (arg: { user: { id: string } }) => { 276 | const ts = new Date().getTime() 277 | return `${ts}::${arg.user.id}` 278 | }, 279 | { 280 | key: arg => arg.user.id 281 | } 282 | ) 283 | const resultA = func({ user: { id: 'alpha' } }) 284 | const resultB = func({ user: { id: 'beta' } }) 285 | const resultA2 = func({ user: { id: 'alpha' } }) 286 | assert.equal(resultA, resultA2) 287 | assert.notEqual(resultB, resultA) 288 | }) 289 | test('calls function again when first value expires', async () => { 290 | const func = _.memo(() => new Date().getTime(), { 291 | ttl: 1 292 | }) 293 | const resultA = func() 294 | await new Promise(res => setTimeout(res, 100)) 295 | const resultB = func() 296 | assert.notEqual(resultA, resultB) 297 | }) 298 | test('does not call function again when first value has not expired', async () => { 299 | const func = _.memo(() => new Date().getTime(), { 300 | ttl: 1000 301 | }) 302 | const resultA = func() 303 | await new Promise(res => setTimeout(res, 100)) 304 | const resultB = func() 305 | assert.equal(resultA, resultB) 306 | }) 307 | }) 308 | 309 | describe('debounce function', () => { 310 | let func: DebounceFunction 311 | const mockFunc = jest.fn() 312 | const runFunc3Times = () => { 313 | func() 314 | func() 315 | func() 316 | } 317 | 318 | beforeEach(() => { 319 | func = _.debounce({ delay: 600 }, mockFunc) 320 | }) 321 | 322 | afterEach(() => { 323 | jest.clearAllMocks() 324 | }) 325 | 326 | test('only executes once when called rapidly', async () => { 327 | runFunc3Times() 328 | expect(mockFunc).toHaveBeenCalledTimes(0) 329 | await _.sleep(610) 330 | expect(mockFunc).toHaveBeenCalledTimes(1) 331 | }) 332 | 333 | test('does not debounce after cancel is called', () => { 334 | runFunc3Times() 335 | expect(mockFunc).toHaveBeenCalledTimes(0) 336 | func.cancel() 337 | runFunc3Times() 338 | expect(mockFunc).toHaveBeenCalledTimes(3) 339 | runFunc3Times() 340 | expect(mockFunc).toHaveBeenCalledTimes(6) 341 | }) 342 | 343 | test('executes the function immediately when the flush method is called', () => { 344 | func.flush() 345 | expect(mockFunc).toHaveBeenCalledTimes(1) 346 | }) 347 | 348 | test('continues to debounce after flush is called', async () => { 349 | runFunc3Times() 350 | expect(mockFunc).toHaveBeenCalledTimes(0) 351 | func.flush() 352 | expect(mockFunc).toHaveBeenCalledTimes(1) 353 | func() 354 | expect(mockFunc).toHaveBeenCalledTimes(1) 355 | await _.sleep(610) 356 | expect(mockFunc).toHaveBeenCalledTimes(2) 357 | func.flush() 358 | expect(mockFunc).toHaveBeenCalledTimes(3) 359 | }) 360 | 361 | test('cancels all pending invocations when the cancel method is called', async () => { 362 | const results: boolean[] = [] 363 | func() 364 | results.push(func.isPending()) 365 | results.push(func.isPending()) 366 | await _.sleep(610) 367 | results.push(func.isPending()) 368 | func() 369 | results.push(func.isPending()) 370 | await _.sleep(610) 371 | results.push(func.isPending()) 372 | assert.deepEqual(results, [true, true, false, true, false]) 373 | }) 374 | 375 | test('returns if there is any pending invocation when the pending method is called', async () => { 376 | func() 377 | func.cancel() 378 | await _.sleep(610) 379 | expect(mockFunc).toHaveBeenCalledTimes(0) 380 | }) 381 | }) 382 | 383 | describe('throttle function', () => { 384 | test('throttles!', async () => { 385 | let calls = 0 386 | const func = _.throttle({ interval: 600 }, () => calls++) 387 | func() 388 | func() 389 | func() 390 | assert.equal(calls, 1) 391 | await _.sleep(610) 392 | func() 393 | func() 394 | func() 395 | assert.equal(calls, 2) 396 | }) 397 | 398 | test('returns if the throttle is active', async () => { 399 | const results = [] 400 | const func = _.throttle({ interval: 600 }, () => {}) 401 | results.push(func.isThrottled()) 402 | func() 403 | results.push(func.isThrottled()) 404 | func() 405 | results.push(func.isThrottled()) 406 | func() 407 | results.push(func.isThrottled()) 408 | await _.sleep(610) 409 | results.push(func.isThrottled()) 410 | assert.deepEqual(results, [false, true, true, true, false]) 411 | }) 412 | }) 413 | }) 414 | 415 | describe('callable function', () => { 416 | test('makes object callable', async () => { 417 | const request = { 418 | source: 'client', 419 | body: 'ford', 420 | doors: 2 421 | } 422 | 423 | const call = _.callable(request, self => (id: string) => ({ ...self, id })) 424 | 425 | expect(call.source).toBe('client') 426 | expect(call.body).toBe('ford') 427 | expect(call.doors).toBe(2) 428 | const s = call('23') 429 | expect(s.doors).toBe(2) 430 | expect(s.id).toBe('23') 431 | 432 | call.doors = 4 433 | expect(call.doors).toBe(4) 434 | const x = call('9') 435 | expect(x.doors).toBe(4) 436 | expect(x.id).toBe('9') 437 | }) 438 | }) 439 | -------------------------------------------------------------------------------- /src/tests/number.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import * as _ from '..' 3 | 4 | describe('number module', () => { 5 | describe('inRange function', () => { 6 | test('handles nullish values', () => { 7 | assert.strictEqual(_.inRange(0, 1, null as any), false) 8 | assert.strictEqual(_.inRange(0, null as any, 1), false) 9 | assert.strictEqual(_.inRange(null as any, 0, 1), false) 10 | assert.strictEqual(_.inRange(0, undefined as any, 1), false) 11 | assert.strictEqual(_.inRange(undefined as any, 0, 1), false) 12 | 13 | assert.strictEqual(_.inRange(0, 1, undefined as any), true) 14 | }) 15 | test('handles bad input', () => { 16 | const result = _.inRange(0, 1, {} as any) 17 | assert.strictEqual(result, false) 18 | }) 19 | test('computes correctly', () => { 20 | assert.strictEqual(_.inRange(10, 0, 5), false) 21 | assert.strictEqual(_.inRange(10, 0, 20), true) 22 | assert.strictEqual(_.inRange(-10, 0, -20), true) 23 | assert.strictEqual(_.inRange(9.99, 0, 10), true) 24 | assert.strictEqual(_.inRange(Math.PI, 0, 3.15), true) 25 | }) 26 | test('handles the different syntax of number type', () => { 27 | assert.strictEqual(_.inRange(0, -1, 1), true) 28 | assert.strictEqual(_.inRange(Number(0), -1, 1), true) 29 | assert.strictEqual(_.inRange(+'0', -1, 1), true) 30 | }) 31 | test('handles two params', () => { 32 | assert.strictEqual(_.inRange(1, 2), true) 33 | assert.strictEqual(_.inRange(1.2, 2), true) 34 | assert.strictEqual(_.inRange(2, 1), false) 35 | assert.strictEqual(_.inRange(2, 2), false) 36 | assert.strictEqual(_.inRange(3.2, 2), false) 37 | assert.strictEqual(_.inRange(-1, 1), false) 38 | assert.strictEqual(_.inRange(-1, -10), true) 39 | }) 40 | test('handles the exclusive end of the range', () => { 41 | assert.strictEqual(_.inRange(1, 0, 1), false) 42 | assert.strictEqual(_.inRange(10.0, 0, 10), false) 43 | }) 44 | test('handles the inclusive start of the range', () => { 45 | assert.strictEqual(_.inRange(0, 0, 1), true) 46 | assert.strictEqual(_.inRange(10.0, 10, 20), true) 47 | }) 48 | }) 49 | 50 | describe('toFloat function', () => { 51 | test('handles null', () => { 52 | const result = _.toFloat(null) 53 | assert.strictEqual(result, 0.0) 54 | }) 55 | test('handles undefined', () => { 56 | const result = _.toFloat(undefined) 57 | assert.strictEqual(result, 0.0) 58 | }) 59 | test('uses null default', () => { 60 | const result = _.toFloat('x', null) 61 | assert.strictEqual(result, null) 62 | }) 63 | test('handles bad input', () => { 64 | const result = _.toFloat({}) 65 | assert.strictEqual(result, 0.0) 66 | }) 67 | test('converts 20.00 correctly', () => { 68 | const result = _.toFloat('20.00') 69 | assert.strictEqual(result, 20.0) 70 | }) 71 | }) 72 | 73 | describe('toInt function', () => { 74 | test('handles null', () => { 75 | const result = _.toInt(null) 76 | assert.strictEqual(result, 0) 77 | }) 78 | test('uses null default', () => { 79 | const result = _.toInt('x', null) 80 | assert.strictEqual(result, null) 81 | }) 82 | test('handles undefined', () => { 83 | const result = _.toInt(undefined) 84 | assert.strictEqual(result, 0) 85 | }) 86 | test('handles bad input', () => { 87 | const result = _.toInt({}) 88 | assert.strictEqual(result, 0) 89 | }) 90 | test('converts 20 correctly', () => { 91 | const result = _.toInt('20') 92 | assert.strictEqual(result, 20) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /src/tests/random.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import * as _ from '..' 3 | 4 | describe('random module', () => { 5 | describe('random function', () => { 6 | test('returns a number', () => { 7 | const result = _.random(0, 100) 8 | assert.isAtLeast(result, 0) 9 | assert.isAtMost(result, 100) 10 | }) 11 | }) 12 | 13 | describe('uid function', () => { 14 | test('generates the correct length string', () => { 15 | const result = _.uid(10) 16 | assert.equal(result.length, 10) 17 | }) 18 | /** 19 | * @warning This is potentially a flaky test. 20 | * We're trying to assert that given additional 21 | * special chars our function will include them 22 | * in the random selection process to generate the 23 | * uid. However, there is always a small chance that 24 | * one is never selected. If the test is flaky, increase 25 | * the size of the uid and/or the number of underscores 26 | * in the special char addition. 27 | */ 28 | test('uid generates string including special', () => { 29 | const result = _.uid( 30 | 300, 31 | '________________________________________________________________' 32 | ) 33 | assert.include(result, '_') 34 | }) 35 | }) 36 | 37 | describe('shuffle function', () => { 38 | test('returns list with same number of items', () => { 39 | const list = [1, 2, 3, 4, 5] 40 | const result = _.shuffle(list) 41 | assert.equal(list.length, result.length) 42 | }) 43 | test('returns list with same value', () => { 44 | const list = [1, 2, 3, 4, 5] 45 | const totalBefore = _.sum(list) 46 | const result = _.shuffle(list) 47 | const totalAfter = _.sum(result) 48 | assert.equal(totalBefore, totalAfter) 49 | }) 50 | test('returns copy of list without mutatuing input', () => { 51 | const list = [1, 2, 3, 4, 5] 52 | const result = _.shuffle(list) 53 | assert.notEqual(list, result) 54 | assert.deepEqual(list, [1, 2, 3, 4, 5]) 55 | }) 56 | }) 57 | 58 | describe('draw function', () => { 59 | test('returns a string from the list', () => { 60 | const letters = 'abcde' 61 | const result = _.draw(letters.split('')) 62 | assert.include(letters, result!) 63 | }) 64 | test('returns a item from the list', () => { 65 | const list = [ 66 | { id: 'a', word: 'hello' }, 67 | { id: 'b', word: 'oh' }, 68 | { id: 'c', word: 'yolo' } 69 | ] 70 | const result = _.draw(list) 71 | assert.include('abc', result!.id) 72 | }) 73 | test('returns null given empty input', () => { 74 | const list: unknown[] = [] 75 | const result = _.draw(list) 76 | assert.isNull(result) 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /src/tests/series.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import * as _ from '..' 3 | 4 | describe('series module', () => { 5 | type Weekday = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' 6 | const sut = _.series([ 7 | 'monday', 8 | 'tuesday', 9 | 'wednesday', 10 | 'thursday', 11 | 'friday' 12 | ]) 13 | 14 | describe('min function', () => { 15 | test('correctly returns min', () => { 16 | const result = sut.min('monday', 'tuesday') 17 | assert.equal(result, 'monday') 18 | }) 19 | test('correctly returns min when second arg', () => { 20 | const result = sut.min('tuesday', 'monday') 21 | assert.equal(result, 'monday') 22 | }) 23 | }) 24 | 25 | describe('max function', () => { 26 | test('correctly returns max', () => { 27 | const result = sut.max('thursday', 'tuesday') 28 | assert.equal(result, 'thursday') 29 | }) 30 | test('correctly returns max when second arg', () => { 31 | const result = sut.max('tuesday', 'thursday') 32 | assert.equal(result, 'thursday') 33 | }) 34 | }) 35 | 36 | describe('first function', () => { 37 | test('returns first item', () => { 38 | const result = sut.first() 39 | assert.equal(result, 'monday') 40 | }) 41 | }) 42 | 43 | describe('last function', () => { 44 | test('returns last item', () => { 45 | const result = sut.last() 46 | assert.equal(result, 'friday') 47 | }) 48 | }) 49 | 50 | describe('next function', () => { 51 | test('returns next item', () => { 52 | const result = sut.next('wednesday') 53 | assert.equal(result, 'thursday') 54 | }) 55 | test('returns first given last exhausted', () => { 56 | const result = sut.next('friday') 57 | assert.equal(result, 'monday') 58 | }) 59 | test('returns the given default when the last is exhausted', () => { 60 | const result = sut.next('friday', 'wednesday') 61 | assert.equal(result, 'wednesday') 62 | }) 63 | }) 64 | 65 | describe('previous function', () => { 66 | test('returns previous item', () => { 67 | const result = sut.previous('wednesday') 68 | assert.equal(result, 'tuesday') 69 | }) 70 | test('returns last given first exhausted', () => { 71 | const result = sut.previous('monday') 72 | assert.equal(result, 'friday') 73 | }) 74 | test('returns the given default when the first is exhausted', () => { 75 | const result = sut.previous('monday', 'wednesday') 76 | assert.equal(result, 'wednesday') 77 | }) 78 | }) 79 | 80 | describe('spin function', () => { 81 | test('returns current given zero', () => { 82 | const result = sut.spin('wednesday', 0) 83 | assert.equal(result, 'wednesday') 84 | }) 85 | test('returns friday given -3 starting at wednesday', () => { 86 | const result = sut.spin('wednesday', -3) 87 | assert.equal(result, 'friday') 88 | }) 89 | test('returns monday given 3 starting at wednesday', () => { 90 | const result = sut.spin('wednesday', 3) 91 | assert.equal(result, 'monday') 92 | }) 93 | test('returns monday given 13 starting at wednesday', () => { 94 | const result = sut.spin('wednesday', 13) 95 | assert.equal(result, 'monday') 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /src/tests/string.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import * as _ from '..' 3 | 4 | describe('string module', () => { 5 | describe('camel function', () => { 6 | test('returns correctly cased string', () => { 7 | const result = _.camel('hello world') 8 | assert.equal(result, 'helloWorld') 9 | }) 10 | test('returns single word', () => { 11 | const result = _.camel('hello') 12 | assert.equal(result, 'hello') 13 | }) 14 | test('returns empty string for empty input', () => { 15 | const result = _.camel(null as any) 16 | assert.equal(result, '') 17 | }) 18 | test('a word in camel case should remain in camel case', () => { 19 | const result = _.camel('helloWorld') 20 | assert.equal(result, 'helloWorld') 21 | }) 22 | }) 23 | 24 | describe('camelCase function', () => { 25 | test('returns non alphanumerics with -space and capital', () => { 26 | const result = _.camel('Exobase Starter_flash AND-go') 27 | assert.equal(result, 'exobaseStarterFlashAndGo') 28 | }) 29 | }) 30 | 31 | describe('snake function', () => { 32 | test('returns correctly cased string', () => { 33 | const result = _.snake('hello world') 34 | assert.equal(result, 'hello_world') 35 | }) 36 | test('must handle strings that are camelCase', () => { 37 | const result = _.snake('helloWorld') 38 | assert.equal(result, 'hello_world') 39 | }) 40 | test('must handle strings that are dash', () => { 41 | const result = _.snake('hello-world') 42 | assert.equal(result, 'hello_world') 43 | }) 44 | test('splits numbers that are next to letters', () => { 45 | const result = _.snake('hello-world12_19-bye') 46 | assert.equal(result, 'hello_world_12_19_bye') 47 | }) 48 | test('does not split numbers when flag is set to false', () => { 49 | const result = _.snake('hello-world12_19-bye', { 50 | splitOnNumber: false 51 | }) 52 | assert.equal(result, 'hello_world12_19_bye') 53 | }) 54 | test('returns single word', () => { 55 | const result = _.snake('hello') 56 | assert.equal(result, 'hello') 57 | }) 58 | test('returns empty string for empty input', () => { 59 | const result = _.snake(null as any) 60 | assert.equal(result, '') 61 | }) 62 | }) 63 | 64 | describe('snakeCase function', () => { 65 | test('returns non alphanumerics with _', () => { 66 | const result = _.snake('Exobase Starter_flash AND-go') 67 | assert.equal(result, 'exobase_starter_flash_and_go') 68 | }) 69 | }) 70 | 71 | describe('dash function', () => { 72 | test('returns correctly cased string', () => { 73 | const result = _.dash('hello world') 74 | assert.equal(result, 'hello-world') 75 | }) 76 | test('returns single word', () => { 77 | const result = _.dash('hello') 78 | assert.equal(result, 'hello') 79 | }) 80 | test('returns empty string for empty input', () => { 81 | const result = _.dash(null as any) 82 | assert.equal(result, '') 83 | }) 84 | test('must handle strings that are camelCase', () => { 85 | const result = _.dash('helloWorld') 86 | assert.equal(result, 'hello-world') 87 | }) 88 | test('must handle strings that are dash', () => { 89 | const result = _.dash('hello-world') 90 | assert.equal(result, 'hello-world') 91 | }) 92 | }) 93 | 94 | describe('dashCase function', () => { 95 | test('returns non alphanumerics with -', () => { 96 | const result = _.dash('Exobase Starter_flash AND-go') 97 | assert.equal(result, 'exobase-starter-flash-and-go') 98 | }) 99 | }) 100 | 101 | describe('template function', () => { 102 | test('replaces all occurrences', () => { 103 | const tmp = ` 104 | Hello my name is {{name}}. I am a {{type}}. 105 | Not sure why I am {{reason}}. 106 | 107 | Thank You - {{name}} 108 | ` 109 | const data = { 110 | name: 'Ray', 111 | type: 'template', 112 | reason: 'so beautiful' 113 | } 114 | 115 | const result = _.template(tmp, data) 116 | const expected = ` 117 | Hello my name is ${data.name}. I am a ${data.type}. 118 | Not sure why I am ${data.reason}. 119 | 120 | Thank You - ${data.name} 121 | ` 122 | 123 | assert.equal(result, expected) 124 | }) 125 | 126 | test('replaces all occurrences given template', () => { 127 | const tmp = `Hello .` 128 | const data = { 129 | name: 'Ray' 130 | } 131 | 132 | const result = _.template(tmp, data, /<(.+?)>/g) 133 | assert.equal(result, `Hello ${data.name}.`) 134 | }) 135 | }) 136 | 137 | describe('capitalize function', () => { 138 | test('handles null', () => { 139 | const result = _.capitalize(null as any) 140 | assert.equal(result, '') 141 | }) 142 | test('converts hello as Hello', () => { 143 | const result = _.capitalize('hello') 144 | assert.equal(result, 'Hello') 145 | }) 146 | test('converts hello Bob as Hello bob', () => { 147 | const result = _.capitalize('hello Bob') 148 | assert.equal(result, 'Hello bob') 149 | }) 150 | }) 151 | 152 | describe('pascal function', () => { 153 | test('returns non alphanumerics in pascal', () => { 154 | const result = _.pascal('Exobase Starter_flash AND-go') 155 | assert.equal(result, 'ExobaseStarterFlashAndGo') 156 | }) 157 | test('returns single word', () => { 158 | const result = _.pascal('hello') 159 | assert.equal(result, 'Hello') 160 | }) 161 | test('returns empty string for empty input', () => { 162 | const result = _.pascal(null as any) 163 | assert.equal(result, '') 164 | }) 165 | }) 166 | 167 | describe('title function', () => { 168 | test('returns input formatted in title case', () => { 169 | assert.equal(_.title('hello world'), 'Hello World') 170 | assert.equal(_.title('va_va_boom'), 'Va Va Boom') 171 | assert.equal(_.title('root-hook - ok!'), 'Root Hook Ok!') 172 | assert.equal(_.title('queryItems'), 'Query Items') 173 | assert.equal( 174 | _.title('queryAllItems-in_Database'), 175 | 'Query All Items In Database' 176 | ) 177 | }) 178 | test('returns empty string for bad input', () => { 179 | assert.equal(_.title(null), '') 180 | assert.equal(_.title(undefined), '') 181 | }) 182 | }) 183 | 184 | describe('trim function', () => { 185 | test('handles bad input', () => { 186 | assert.equal(_.trim(null), '') 187 | assert.equal(_.trim(undefined), '') 188 | }) 189 | test('returns input string correctly trimmed', () => { 190 | assert.equal(_.trim('\n\n\t\nhello\n\t \n', '\n\t '), 'hello') 191 | assert.equal(_.trim('hello', 'x'), 'hello') 192 | assert.equal(_.trim(' hello '), 'hello') 193 | assert.equal(_.trim(' __hello__ ', '_'), ' __hello__ ') 194 | assert.equal(_.trim('__hello__', '_'), 'hello') 195 | assert.equal(_.trim('//repos////', '/'), 'repos') 196 | assert.equal(_.trim('/repos/:owner/:repo/', '/'), 'repos/:owner/:repo') 197 | }) 198 | 199 | test('handles when char to trim is special case in regex', () => { 200 | assert.equal(_.trim('_- hello_- ', '_- '), 'hello') 201 | }) 202 | }) 203 | }) 204 | -------------------------------------------------------------------------------- /src/typed.ts: -------------------------------------------------------------------------------- 1 | export const isSymbol = (value: any): value is symbol => { 2 | return !!value && value.constructor === Symbol 3 | } 4 | 5 | export const isArray = Array.isArray 6 | 7 | export const isObject = (value: any): value is object => { 8 | return !!value && value.constructor === Object 9 | } 10 | 11 | /** 12 | * Checks if the given value is primitive. 13 | * 14 | * Primitive Types: number , string , boolean , symbol, bigint, undefined, null 15 | * 16 | * @param {*} value value to check 17 | * @returns {boolean} result 18 | */ 19 | export const isPrimitive = (value: any): boolean => { 20 | return ( 21 | value === undefined || 22 | value === null || 23 | (typeof value !== 'object' && typeof value !== 'function') 24 | ) 25 | } 26 | 27 | export const isFunction = (value: any): value is Function => { 28 | return !!(value && value.constructor && value.call && value.apply) 29 | } 30 | 31 | export const isString = (value: any): value is string => { 32 | return typeof value === 'string' || value instanceof String 33 | } 34 | 35 | export const isInt = (value: any): value is number => { 36 | return isNumber(value) && value % 1 === 0 37 | } 38 | 39 | export const isFloat = (value: any): value is number => { 40 | return isNumber(value) && value % 1 !== 0 41 | } 42 | 43 | export const isNumber = (value: any): value is number => { 44 | try { 45 | return Number(value) === value 46 | } catch { 47 | return false 48 | } 49 | } 50 | 51 | export const isDate = (value: any): value is Date => { 52 | return Object.prototype.toString.call(value) === '[object Date]' 53 | } 54 | 55 | /** 56 | * This is really a _best guess_ promise checking. You 57 | * should probably use Promise.resolve(value) to be 100% 58 | * sure you're handling it correctly. 59 | */ 60 | export const isPromise = (value: any): value is Promise => { 61 | if (!value) return false 62 | if (!value.then) return false 63 | if (!isFunction(value.then)) return false 64 | return true 65 | } 66 | 67 | export const isEmpty = (value: any) => { 68 | if (value === true || value === false) return true 69 | if (value === null || value === undefined) return true 70 | if (isNumber(value)) return value === 0 71 | if (isDate(value)) return isNaN(value.getTime()) 72 | if (isFunction(value)) return false 73 | if (isSymbol(value)) return false 74 | const length = (value as any).length 75 | if (isNumber(length)) return length === 0 76 | const size = (value as any).size 77 | if (isNumber(size)) return size === 0 78 | const keys = Object.keys(value).length 79 | return keys === 0 80 | } 81 | 82 | export const isEqual = (x: TType, y: TType): boolean => { 83 | if (Object.is(x, y)) return true 84 | if (x instanceof Date && y instanceof Date) { 85 | return x.getTime() === y.getTime() 86 | } 87 | if (x instanceof RegExp && y instanceof RegExp) { 88 | return x.toString() === y.toString() 89 | } 90 | if ( 91 | typeof x !== 'object' || 92 | x === null || 93 | typeof y !== 'object' || 94 | y === null 95 | ) { 96 | return false 97 | } 98 | const keysX = Reflect.ownKeys(x as unknown as object) as (keyof typeof x)[] 99 | const keysY = Reflect.ownKeys(y as unknown as object) 100 | if (keysX.length !== keysY.length) return false 101 | for (let i = 0; i < keysX.length; i++) { 102 | if (!Reflect.has(y as unknown as object, keysX[i])) return false 103 | if (!isEqual(x[keysX[i]], y[keysX[i]])) return false 104 | } 105 | return true 106 | } 107 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "moduleResolution": "node", 5 | "target": "es2020", 6 | "lib": ["es2020"], 7 | "esModuleInterop": true, 8 | "strict": true 9 | }, 10 | "include": [ 11 | "src/**/*.ts" 12 | ], 13 | "exclude": ["node_modules", "dist"] 14 | } --------------------------------------------------------------------------------