├── .browserslistrc ├── .changeset └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── lodash-feature-request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── add-assignee.yml │ ├── benchmark-to-comment.yml │ ├── benchmark-to-markdown.yml │ ├── canary.yml │ ├── detect-changed-packages.yml │ ├── matrix.yaml │ ├── publish.yml │ ├── rc.yml │ ├── release.yml │ ├── static.yml │ └── validate-utils-registration.yml ├── .gitignore ├── .markdownlint.jsonc ├── .markdownlintignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── BENCHMARK.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint-plugin-hidash ├── index.js ├── package.json └── rules │ └── enforce-single-function-dual-export.js ├── index.ts ├── lefthook.yml ├── package.json ├── pnpm-lock.yaml ├── scripts ├── benchmark-to-md.mjs ├── find-importing-files.mjs ├── generate-utils.mjs ├── head.ts └── pre-build.mjs ├── src ├── assign.bench.ts ├── assign.test.ts ├── assign.ts ├── before.bench.ts ├── before.test.ts ├── before.ts ├── chunk.bench.ts ├── chunk.test.ts ├── chunk.ts ├── clamp.bench.ts ├── clamp.test.ts ├── clamp.ts ├── clone.bench.ts ├── clone.test.ts ├── clone.ts ├── cloneDeep.bench.ts ├── cloneDeep.test.ts ├── cloneDeep.ts ├── debounce.test.ts ├── debounce.ts ├── delay.test.ts ├── delay.ts ├── difference.bench.ts ├── difference.test.ts ├── difference.ts ├── entries.ts ├── eq.ts ├── every.bench.ts ├── every.test.ts ├── every.ts ├── filter.bench.ts ├── filter.test.ts ├── filter.ts ├── find.bench.ts ├── find.test.ts ├── find.ts ├── findIndex.bench.ts ├── findIndex.test.ts ├── findIndex.ts ├── findLastIndex.bench.ts ├── findLastIndex.test.ts ├── findLastIndex.ts ├── first.bench.ts ├── first.test.ts ├── first.ts ├── flatten.bench.ts ├── flatten.test.ts ├── flatten.ts ├── flow.test.ts ├── flow.ts ├── get.bench.ts ├── get.test.ts ├── get.ts ├── groupBy.bench.ts ├── groupBy.test.ts ├── groupBy.ts ├── gt.bench.ts ├── gt.test.ts ├── gt.ts ├── has.bench.ts ├── has.test.ts ├── has.ts ├── identity.test.ts ├── identity.ts ├── includes.bench.ts ├── includes.test.ts ├── includes.ts ├── internal │ ├── array.ts │ ├── baseIteratee.test.ts │ ├── baseIteratee.ts │ ├── baseIteratee.type.ts │ ├── noop.test.ts │ ├── noop.ts │ ├── to-string-tags.ts │ └── types.ts ├── isArray.bench.ts ├── isArray.test.ts ├── isArray.ts ├── isEmpty.bench.ts ├── isEmpty.test.ts ├── isEmpty.ts ├── isEqual.bench.ts ├── isEqual.test.ts ├── isEqual.ts ├── isError.test.ts ├── isError.ts ├── isFunction.bench.ts ├── isFunction.test.ts ├── isFunction.ts ├── isMap.bench.ts ├── isMap.test.ts ├── isMap.ts ├── isNil.ts ├── isNull.ts ├── isNumber.bench.ts ├── isNumber.test.ts ├── isNumber.ts ├── isObject.ts ├── isPlainObject.test.ts ├── isPlainObject.ts ├── isSet.ts ├── isString.test.ts ├── isString.ts ├── isSymbol.ts ├── isUndefined.ts ├── join.test.ts ├── join.ts ├── keys.bench.ts ├── keys.test.ts ├── keys.ts ├── last.bench.ts ├── last.test.ts ├── last.ts ├── lt.bench.ts ├── lt.test.ts ├── lt.ts ├── map.bench.ts ├── map.test.ts ├── map.ts ├── mapValues.bench.ts ├── mapValues.test.ts ├── mapValues.ts ├── memoize.test.ts ├── memoize.ts ├── merge.bench.ts ├── merge.test.ts ├── merge.ts ├── omit.bench.ts ├── omit.test.ts ├── omit.ts ├── once.bench.ts ├── once.test.ts ├── once.ts ├── pick.bench.ts ├── pick.test.ts ├── pick.ts ├── pickBy.bench.ts ├── pickBy.test.ts ├── pickBy.ts ├── range.bench.ts ├── range.test.ts ├── range.ts ├── repeat.bench.ts ├── repeat.test.ts ├── repeat.ts ├── reverse.bench.ts ├── reverse.test.ts ├── reverse.ts ├── shuffle.bench.ts ├── shuffle.test.ts ├── shuffle.ts ├── size.bench.ts ├── size.test.ts ├── size.ts ├── sleep.ts ├── some.bench.ts ├── some.test.ts ├── some.ts ├── sum.bench.ts ├── sum.test.ts ├── sum.ts ├── sumBy.bench.ts ├── sumBy.test.ts ├── sumBy.ts ├── throttle.test.ts ├── throttle.ts ├── times.test.ts ├── times.ts ├── toNumber.bench.ts ├── toNumber.test.ts ├── toNumber.ts ├── toPairs.bench.ts ├── toPairs.test.ts ├── toPairs.ts ├── toString.bench.ts ├── toString.test.ts ├── toString.ts ├── transform.bench.ts ├── transform.test.ts ├── transform.ts ├── trim.bench.ts ├── trim.test.ts ├── trim.ts ├── union.bench.ts ├── union.test.ts ├── union.ts ├── uniq.bench.ts ├── uniq.test.ts ├── uniq.ts ├── uniqBy.bench.ts ├── uniqBy.test.ts ├── uniqBy.ts ├── uniqWith.bench.ts ├── uniqWith.test.ts ├── uniqWith.ts ├── unzip.bench.ts ├── unzip.test.ts ├── unzip.ts ├── values.bench.ts ├── values.test.ts ├── values.ts ├── zip.bench.ts ├── zip.test.ts └── zip.ts ├── tip.bench.ts ├── tip.kr.md ├── tip.md ├── tsconfig.json ├── vite.config.mts └── vitest.config.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | node >= 18.18.0, >= 0.1%, not dead, not op_mini all, last 3 years 2 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,ts,tsx}] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | max_line_length = 120 10 | trim_trailing_whitespace = true 11 | 12 | [*.{json,yml,yaml}] 13 | charset = utf-8 14 | end_of_line = lf 15 | indent_style = space 16 | indent_size = 4 17 | insert_final_newline = true 18 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | pnpm-lock.yaml 39 | 40 | # Optional npm cache directory 41 | .npm 42 | 43 | # Optional eslint cache 44 | .eslintcache 45 | 46 | # Optional REPL history 47 | .node_repl_history 48 | 49 | # Output of 'npm pack' 50 | *.tgz 51 | 52 | # Yarn Integrity file 53 | .yarn-integrity 54 | 55 | # dotenv environment variables file 56 | .env 57 | 58 | # next.js build output 59 | .next 60 | 61 | dist 62 | 63 | apps/docs/docs 64 | 65 | .changeset/* 66 | 67 | html/ 68 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@naverpay/eslint-config", 3 | "plugins": ["hidash"], 4 | "rules": { 5 | "@typescript-eslint/naming-convention": ["off"], 6 | "@typescript-eslint/prefer-for-of": ["off"], 7 | "hidash/enforce-single-function-dual-export": [ 8 | "error", 9 | { 10 | "include": ["src/*.ts"], 11 | "exclude": ["src/index.ts", "**/*.bench.ts", "**/*.test.ts"] 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @NaverPayDev/frontend -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/lodash-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lodash Feature request 3 | about: Suggest a new utility function or enhancement for hidash that could replace 4 | or improve upon Lodash's features. 5 | title: '' 6 | labels: 'new-feature' 7 | assignees: '' 8 | --- 9 | 10 | > [NOTICE]: Please write only the utility name as the GitHub issue title. 11 | 12 | ## **🚀 Feature Request: New Utility Proposal for `@naverpay/hidash`** 13 | 14 | ### 1. Requested Utility 15 | 16 | 20 | 21 | ### 2. Unpkg URL 22 | 23 | 24 | 25 | ### 3. Lodash Documentation 26 | 27 | 28 | 29 | ### 4. TypeScript Types (`@types/lodash`) 30 | 31 | 32 | 33 | ### 5.(Optional) Additional Considerations 34 | 35 | 42 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## This resolves 2 | -------------------------------------------------------------------------------- /.github/workflows/add-assignee.yml: -------------------------------------------------------------------------------- 1 | name: 'add assignee to pull request automatically' 2 | on: 3 | pull_request: 4 | types: [opened, ready_for_review, synchronize, reopened] 5 | branches: 6 | - '**' 7 | - '!main' 8 | jobs: 9 | ADD_ASSIGNEE_TO_PULL_REQUEST: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Add Assignee to pr 13 | uses: actions/github-script@v3 14 | with: 15 | script: | 16 | try { 17 | const result = await github.pulls.get({ 18 | owner: context.repo.owner, 19 | repo: context.repo.repo, 20 | pull_number: context.payload.number, 21 | }) 22 | 23 | console.log(result) 24 | 25 | if (result.data.assignee === null) { 26 | await github.issues.addAssignees({ 27 | owner: context.repo.owner, 28 | repo: context.repo.repo, 29 | issue_number: context.issue.number, 30 | assignees: context.actor, 31 | }) 32 | } 33 | } catch (err) { 34 | console.error(`Check Pull Request Error ${err}`) 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/benchmark-to-comment.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark Comment on PR 2 | 3 | on: 4 | pull_request_target: 5 | paths: 6 | - 'src/**' 7 | - '!src/**/*.test.ts' 8 | - '!src/**/*.spec.ts' 9 | workflow_dispatch: 10 | inputs: 11 | pr_number: 12 | description: 'PR number to run benchmark on' 13 | required: false 14 | type: string 15 | 16 | permissions: 17 | pull-requests: write 18 | 19 | jobs: 20 | benchmark: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} 26 | 27 | - name: Fetch base branch 28 | run: git fetch origin $GITHUB_BASE_REF 29 | 30 | - uses: pnpm/action-setup@v4 31 | 32 | - name: Use Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: '20' 36 | cache: 'pnpm' 37 | 38 | - name: Install dependencies 39 | run: pnpm install --frozen-lockfile 40 | 41 | - name: Run benchmarks 42 | uses: NaverPayDev/benchmark-actions@main 43 | with: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | PACKAGE_MANAGER: 'pnpm' 46 | TARGET: 'comment' 47 | SOURCE_ROOT: 'src' 48 | PR_NUMBER: ${{ github.event.inputs.pr_number }} 49 | -------------------------------------------------------------------------------- /.github/workflows/benchmark-to-markdown.yml: -------------------------------------------------------------------------------- 1 | name: Update Benchmark Results 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**' 9 | - '!src/**/*.test.ts' 10 | - '!src/**/*.spec.ts' 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: write 15 | pull-requests: read 16 | 17 | jobs: 18 | update-benchmark: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: pnpm/action-setup@v4 24 | 25 | - name: Use Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '20' 29 | cache: 'pnpm' 30 | 31 | - name: Install dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: Run benchmark 35 | uses: NaverPayDev/benchmark-actions@main 36 | with: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | PACKAGE_MANAGER: 'pnpm' 39 | TARGET: 'markdown' 40 | SOURCE_ROOT: 'src' 41 | -------------------------------------------------------------------------------- /.github/workflows/canary.yml: -------------------------------------------------------------------------------- 1 | name: Release Canary 2 | 3 | on: 4 | issue_comment: 5 | types: 6 | - created 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | canary: 12 | if: ${{ github.event.issue.pull_request && (github.event.comment.body == 'canary-publish' || github.event.comment.body == '/canary-publish')}} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Get PR branch name 16 | id: get_branch 17 | run: | 18 | PR=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" ${{ github.event.issue.pull_request.url }}) 19 | echo "::set-output name=branch::$(echo $PR | jq -r '.head.ref')" 20 | 21 | - uses: actions/checkout@v4 22 | with: 23 | ref: ${{ steps.get_branch.outputs.branch }} 24 | - uses: pnpm/action-setup@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: '22' 28 | cache: 'pnpm' 29 | 30 | - name: Install Dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: Build 34 | run: pnpm build 35 | 36 | - name: Canary Publish 37 | uses: NaverPayDev/changeset-actions/canary-publish@main 38 | with: 39 | github_token: ${{ secrets.ACTION_TOKEN }} # Add user PAT if necessary 40 | npm_tag: canary # Specify the npm tag to use for deployment 41 | npm_token: ${{ secrets.NPM_TOKEN }} # Provide the token required for npm publishing 42 | publish_script: pnpm run release:canary # Script to execute Canary deployment 43 | packages_dir: . # Directory of packages to detect changes (default: packages,share) 44 | excludes: '.github' # Files or directories to exclude from change detection 45 | version_template: '{VERSION}-canary.{DATE}-{COMMITID7}' 46 | -------------------------------------------------------------------------------- /.github/workflows/detect-changed-packages.yml: -------------------------------------------------------------------------------- 1 | name: detect changed packages 2 | 3 | on: 4 | pull_request_target: 5 | branches: ['**'] 6 | types: [opened, reopened, labeled, unlabeled, synchronize] 7 | 8 | concurrency: 9 | group: detect-${{ github.event.pull_request.number }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | detect: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }} 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v3 20 | with: 21 | token: ${{ secrets.ACTION_TOKEN }} 22 | fetch-depth: 0 23 | 24 | - uses: actions/checkout@v4 25 | - uses: pnpm/action-setup@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: '20' 29 | cache: 'pnpm' 30 | 31 | - name: install dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: 'detect changed packages' 35 | uses: NaverPayDev/changeset-actions/detect-add@main 36 | with: 37 | github_token: ${{ secrets.ACTION_TOKEN }} 38 | packages_dir: . 39 | skip_label: skip-detect-change 40 | skip_branches: main 41 | formatting_script: pnpm run markdownlint:fix 42 | -------------------------------------------------------------------------------- /.github/workflows/matrix.yaml: -------------------------------------------------------------------------------- 1 | name: CI Build Matrix 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18, 20, 22] 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | 21 | - name: Use pnpm 22 | uses: pnpm/action-setup@v4 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'pnpm' 29 | 30 | - name: Install dependencies 31 | run: | 32 | pnpm install --frozen-lockfile 33 | 34 | - name: Run Tests 35 | run: pnpm run test 36 | 37 | - name: Run Build 38 | run: pnpm run build 39 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Changesets 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | - name: Use Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '20' 19 | cache: 'pnpm' 20 | 21 | - name: Install dependencies 22 | run: pnpm install --frozen-lockfile 23 | 24 | - name: build 25 | run: pnpm run build 26 | 27 | - name: Create Release Pull Request 28 | id: changesets 29 | uses: NaverPayDev/changeset-actions/publish@main 30 | with: 31 | github_token: ${{ secrets.ACTION_TOKEN }} 32 | git_username: npayfebot 33 | git_email: npay.fe.bot@navercorp.com 34 | publish_script: pnpm release 35 | pr_title: '🚀 version changed packages' 36 | commit_message: '📦 bump changed packages version' 37 | create_github_release_tag: true 38 | formatting_script: pnpm run markdownlint:fix 39 | npm_token: ${{ secrets.NPM_TOKEN }} 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/rc.yml: -------------------------------------------------------------------------------- 1 | name: Release RC 2 | 3 | on: 4 | issue_comment: 5 | types: 6 | - created 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | permissions: 11 | contents: write # to create release 12 | 13 | jobs: 14 | canary: 15 | if: ${{ github.event.issue.pull_request && (github.event.comment.body == 'rc-publish' || github.event.comment.body == '/rc-publish')}} 16 | runs-on: ubuntu-latest 17 | env: 18 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | steps: 20 | - name: Get PR branch name 21 | id: get_branch 22 | run: | 23 | PR=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" ${{ github.event.issue.pull_request.url }}) 24 | echo "::set-output name=branch::$(echo $PR | jq -r '.head.ref')" 25 | 26 | - uses: actions/checkout@v4 27 | with: 28 | ref: ${{ steps.get_branch.outputs.branch }} 29 | fetch-depth: 0 30 | - uses: pnpm/action-setup@v4 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: '22' 34 | cache: 'pnpm' 35 | 36 | - name: Install Dependencies 37 | run: pnpm install --frozen-lockfile 38 | 39 | - name: Build 40 | run: pnpm build 41 | 42 | - name: RC Publish 43 | uses: NaverPayDev/changeset-actions/canary-publish@main 44 | with: 45 | github_token: ${{ secrets.ACTION_TOKEN }} # Add user PAT if necessary 46 | npm_tag: rc # Specify the npm tag to use for deployment 47 | npm_token: ${{ secrets.NPM_TOKEN }} # Provide the token required for npm publishing 48 | publish_script: pnpm run release:canary # Script to execute Canary deployment 49 | packages_dir: . # Directory of packages to detect changes (default: packages,share) 50 | excludes: '.github' # Files or directories to exclude from change detection 51 | version_template: '{VERSION}-rc.{DATE}-{COMMITID7}' 52 | create_release: true 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release packages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v3 16 | with: 17 | token: ${{ secrets.ACTION_TOKEN }} 18 | fetch-depth: 0 19 | 20 | - name: Use Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: '20.x' 24 | 25 | - name: Enable Corepack 26 | run: | 27 | npm install -g corepack@latest 28 | corepack enable 29 | 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: Build 34 | run: pnpm build 35 | 36 | - name: Create Release Pull Request or Publish to npm 37 | id: changesets 38 | uses: changesets/action@v1 39 | with: 40 | title: '🚀 version changed packages' 41 | commit: '📦 bump changed packages version' 42 | publish: pnpm release 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: 'pages' 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - uses: pnpm/action-setup@v4 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version: '20' 38 | cache: 'pnpm' 39 | - name: Install dependencies 40 | run: pnpm install --frozen-lockfile 41 | - name: Build 42 | run: pnpm run build 43 | - name: Test 44 | run: pnpm run test 45 | - name: Setup Pages 46 | uses: actions/configure-pages@v5 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v3 49 | with: 50 | # Upload entire repository 51 | path: './html' 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v4 55 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@naverpay/markdown-lint", 3 | "MD030": false 4 | } 5 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | .changeset 2 | CHANGELOG.md 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.20.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # changesets 2 | .changeset 3 | 4 | # markdown formatting is left to markdown-lint. 5 | **/*.md 6 | 7 | # pnpm lock yaml 8 | pnpm-lock.yaml 9 | 10 | dist/ 11 | html/ 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@naverpay/prettier-config" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NaverPayDev 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. 22 | -------------------------------------------------------------------------------- /eslint-plugin-hidash/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // eslint-disable-next-line @typescript-eslint/no-require-imports 4 | 'enforce-single-function-dual-export': require('./rules/enforce-single-function-dual-export'), 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /eslint-plugin-hidash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-hidash", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | glob: '*.{js,ts,jsx,tsx}' 6 | run: pnpm exec eslint {staged_files} 7 | prettier: 8 | glob: '*.{ts,tsx,js,mjs,cjs,jsx,json,yaml,yml}' 9 | run: pnpm exec prettier --check {staged_files} 10 | markdownlint: 11 | glob: '*.{md}' 12 | run: pnpm exec markdownlint {staged_files} 13 | commit-msg: 14 | commands: 15 | commit-helper: 16 | run: 'npx -y @naverpay/commit-helper@latest {1}' 17 | -------------------------------------------------------------------------------- /scripts/find-importing-files.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | import {execSync} from 'child_process' 4 | import fs from 'fs' 5 | import path from 'path' 6 | 7 | function isTestFile(filePath) { 8 | return filePath.endsWith('.bench.ts') || filePath.endsWith('.test.ts') 9 | } 10 | 11 | function getAllFiles(dir, fileList = []) { 12 | const files = fs.readdirSync(dir) 13 | files.forEach((file) => { 14 | const filePath = path.join(dir, file) 15 | if (filePath.endsWith('.ts') && !isTestFile(filePath)) { 16 | fileList.push(filePath) 17 | } 18 | }) 19 | return fileList 20 | } 21 | 22 | function getModifiedInternalFiles(commitId) { 23 | const output = execSync(`git diff --name-only HEAD origin/${commitId} -- src/internal/*.ts`, { 24 | encoding: 'utf8', 25 | }) 26 | 27 | return output 28 | .split('\n') 29 | .map((file) => file.trim()) 30 | .filter(Boolean) 31 | } 32 | 33 | function findImportingFiles(modifiedInternalFiles, allFiles) { 34 | const result = new Set() 35 | 36 | modifiedInternalFiles.forEach((internalFile) => { 37 | allFiles.forEach((file) => { 38 | const content = fs.readFileSync(file, 'utf8') 39 | const relativePath = 40 | './' + path.relative(path.dirname(file), internalFile).replace(/\\/g, '/').replace('.ts', '') 41 | 42 | if (content.includes(`import`) && content.includes(relativePath)) { 43 | result.add(file) 44 | } 45 | }) 46 | }) 47 | 48 | return [...result] 49 | } 50 | 51 | const rootDir = process.cwd() 52 | 53 | const [, , commitId = 'main'] = process.argv 54 | 55 | ;(async () => { 56 | const srcDir = path.resolve('src') 57 | const allFiles = getAllFiles(srcDir) 58 | const modifiedInternalFiles = getModifiedInternalFiles(commitId) 59 | 60 | if (modifiedInternalFiles.length === 0) { 61 | console.log('No modified internal files.') 62 | process.exit(0) 63 | } 64 | 65 | const importingFiles = findImportingFiles(modifiedInternalFiles, allFiles) 66 | 67 | if (importingFiles.length > 0) { 68 | console.log(importingFiles.map((importingFile) => path.relative(rootDir, importingFile)).join('\n')) 69 | } 70 | })() 71 | -------------------------------------------------------------------------------- /scripts/generate-utils.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import {fileURLToPath} from 'node:url' 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 6 | 7 | const scanUtils = () => { 8 | const srcDir = path.join(__dirname, '../src') 9 | return fs 10 | .readdirSync(srcDir) 11 | .filter((file) => file.endsWith('.ts')) 12 | .filter((file) => !file.includes('.test.') && !file.includes('.bench.')) 13 | .map((file) => file.replace('.ts', '')) 14 | .sort() 15 | } 16 | 17 | const generateModuleMap = (utils) => { 18 | const moduleEntries = utils.map((util) => ` ${util}: './src/${util}.ts'`) 19 | 20 | return `// only for vite, tsup 21 | // remember, this is not barrel file. 22 | const moduleMap = { 23 | ${moduleEntries.join(',\n')}, 24 | } as const 25 | 26 | export default moduleMap 27 | ` 28 | } 29 | 30 | const generateExports = (utils, {prefix = ''}) => { 31 | const exportEntries = utils 32 | .map( 33 | (util) => `"./${util}": { 34 | "import": { 35 | "types": "./${util}.d.mts", 36 | "default": "./${util}.mjs" 37 | }, 38 | "require": { 39 | "types": "./${util}.d.ts", 40 | "default": "./${util}.js" 41 | } 42 | }`, 43 | ) 44 | .join(',\n') 45 | 46 | return `{${prefix}${exportEntries}}` 47 | } 48 | 49 | const main = () => { 50 | const utils = scanUtils() 51 | const moduleMapContent = generateModuleMap(utils) 52 | fs.writeFileSync(path.join(__dirname, '../index.ts'), moduleMapContent) 53 | 54 | const packagePath = path.join(__dirname, '../package.json') 55 | const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8')) 56 | pkg.exports = JSON.parse( 57 | generateExports(utils, { 58 | prefix: '"./package.json": "./package.json",\n', 59 | }), 60 | ) 61 | fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 4) + '\n') 62 | } 63 | 64 | main() 65 | -------------------------------------------------------------------------------- /scripts/head.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description first === head 3 | * @see https://unpkg.com/lodash@4.17.21/first.js 4 | */ 5 | export function head(array: T[] | null | undefined): T | undefined { 6 | return array && array.length ? array[0] : undefined 7 | } 8 | 9 | export default head 10 | -------------------------------------------------------------------------------- /scripts/pre-build.mjs: -------------------------------------------------------------------------------- 1 | import {cpSync} from 'fs' 2 | import {join, dirname} from 'path' 3 | import {fileURLToPath} from 'url' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | const rootPath = join(__dirname, '..') 8 | const distPath = join(rootPath, 'dist') 9 | 10 | cpSync(distPath, rootPath, {recursive: true}) 11 | -------------------------------------------------------------------------------- /src/assign.bench.ts: -------------------------------------------------------------------------------- 1 | import _assign from 'lodash/assign' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {assign} from './assign' 5 | 6 | const largeArray = Array.from({length: 100}, (_, i) => ({[`key${i}`]: i})) 7 | const emptyObjects = Array.from({length: 100}, () => ({})) 8 | const filledObjects = Array.from({length: 100}, (_, i) => ({[`key${i}`]: i})) 9 | 10 | const mixedObjects = [...emptyObjects, ...filledObjects].sort(() => Math.random() - 0.5) 11 | 12 | const testCases = [ 13 | largeArray, 14 | Object.entries(Object.fromEntries(largeArray.map((item, index) => [`key${index}`, index]))), // convert to Object.entries 15 | emptyObjects, 16 | [...largeArray, ...emptyObjects], 17 | mixedObjects, 18 | ] 19 | 20 | const ITERATIONS = 1000 21 | 22 | describe('assign performance', () => { 23 | bench('hidash', () => { 24 | for (let i = 0; i < ITERATIONS; i++) { 25 | for (const testCase of testCases) { 26 | assign({}, ...testCase) 27 | } 28 | } 29 | }) 30 | 31 | bench('lodash', () => { 32 | for (let i = 0; i < ITERATIONS; i++) { 33 | for (const testCase of testCases) { 34 | _assign({}, ...testCase) 35 | } 36 | } 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/assign.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import isNull from './isNull' 3 | 4 | /** 5 | * @description 6 | * Assigns the values of all enumerable properties from one or more source objects to a target object. 7 | * 8 | * @param {object} target The target object to assign properties to 9 | * @param {...object} sources The source objects to assign properties from 10 | * @returns {object} The target object with assigned properties 11 | */ 12 | export function assign(target: TObject, source: TSource): TObject & TSource 13 | export function assign( 14 | target: TObject, 15 | source1: TSource1, 16 | source2: TSource2, 17 | ): TObject & TSource1 & TSource2 18 | export function assign( 19 | target: TObject, 20 | source1: TSource1, 21 | source2: TSource2, 22 | source3: TSource3, 23 | ): TObject & TSource1 & TSource2 & TSource3 24 | export function assign( 25 | target: TObject, 26 | source1: TSource1, 27 | source2: TSource2, 28 | source3: TSource3, 29 | source4: TSource4, 30 | ): TObject & TSource1 & TSource2 & TSource3 & TSource4 31 | export function assign(target: TObject): TObject 32 | export function assign(target: unknown, ...sources: any[]): any 33 | export function assign(target: unknown, ...sources: any[]) { 34 | const to = Object(target) 35 | 36 | if (sources.length === 0) { 37 | return to 38 | } 39 | 40 | for (const source of sources) { 41 | if (!isNull(source)) { 42 | for (const key of Object.keys(source)) { 43 | to[key] = source[key] 44 | } 45 | } 46 | } 47 | return to 48 | } 49 | 50 | export default assign 51 | -------------------------------------------------------------------------------- /src/before.bench.ts: -------------------------------------------------------------------------------- 1 | import _before from 'lodash/before' 2 | import {bench, describe} from 'vitest' 3 | 4 | import before from './before' 5 | 6 | const add = (a: number, b: number) => a + b 7 | 8 | const ITERATIONS = 1000 9 | 10 | describe('before performance', () => { 11 | bench('hidash', () => { 12 | const beforeFn = before(1000, add) 13 | 14 | for (let i = 0; i < ITERATIONS; i++) { 15 | beforeFn(1, 2) 16 | } 17 | }) 18 | 19 | bench('lodash', () => { 20 | const beforeFn = _before(1000, add) 21 | 22 | for (let i = 0; i < ITERATIONS; i++) { 23 | beforeFn(1, 2) 24 | } 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/before.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3 | * Creates a function that invokes `func` as long as it's called less than `initialN` times. 4 | * Subsequent calls to the new function return the result of the last `func` invocation. 5 | * This behavior is similar to lodash's `_.before` function. 6 | * 7 | * @template T - The type of the function to restrict. 8 | * @param {number} initialN - The number determining how many times `func` can be called. 9 | * `func` is invoked if called less than `initialN` times (i.e., up to `initialN - 1` times). 10 | * If `initialN` is less than or equal to 1, `func` will not be invoked. 11 | * @param {T} func - The function to restrict. 12 | * @returns {(...args: Parameters) => ReturnType} Returns the new restricted function. 13 | * @throws {TypeError} If `func` is not a function. 14 | */ 15 | export function before) => ReturnType>( 16 | initialN: number, 17 | func: T, 18 | ): (...args: Parameters) => ReturnType { 19 | // Ensure `func` is a function, otherwise throw an error. 20 | if (typeof func !== 'function') { 21 | throw new TypeError('Expected a function') 22 | } 23 | 24 | let result: ReturnType 25 | // Ensure 'n' is an integer. It tracks the remaining times `func` can be called before it's restricted. 26 | let n = Math.floor(initialN) 27 | 28 | return function (this: ThisParameterType, ...args: Parameters): ReturnType { 29 | // Decrement n. If n is still greater than 0 after decrementing, it means func can be invoked. 30 | // This allows func to be called `initialN - 1` times if initialN was originally > 1. 31 | if (--n > 0) { 32 | result = func.apply(this, args) 33 | } 34 | // If `n` has reached 1 or less (meaning `func` has been called the intended maximum times, 35 | // or was not supposed to be called at all if initialN was <=1), 36 | // disassociate `func` to prevent further calls and aid garbage collection. 37 | if (n <= 1) { 38 | // eslint-disable-next-line no-param-reassign 39 | func = undefined as unknown as T // Original func is no longer needed. 40 | } 41 | // Return the result of the last actual invocation, or undefined if never invoked. 42 | return result 43 | } 44 | } 45 | 46 | export default before 47 | -------------------------------------------------------------------------------- /src/chunk.bench.ts: -------------------------------------------------------------------------------- 1 | import {describe} from 'node:test' 2 | 3 | import _chunk from 'lodash/chunk' 4 | import {bench} from 'vitest' 5 | 6 | import {chunk} from './chunk' 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | const testCases: [any, number][] = [ 10 | [['a', 'b', 'c', 'd'], 2], 11 | [['a', 'b', 'c', 'd'], 3], 12 | [[1, 2, 3, 4, 5], 2], 13 | [[1, 2, 3, 4], 1], 14 | [[42], 3], 15 | [[1, 2, 3, 4, 5], 2.5], 16 | [[], 2], 17 | [null, 2], 18 | [undefined, 2], 19 | [[1, 2, 3, 4, 5], 0], 20 | [[1, 2, 3], -1], 21 | [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3], 22 | [['x', 'y', 'z'], 1], 23 | [[true, false, null, undefined], 2], 24 | [ 25 | [ 26 | [1, 2], 27 | [3, 4], 28 | [5, 6], 29 | ], 30 | 2, 31 | ], 32 | [[{a: 1}, {b: 2}, {c: 3}], 2], 33 | [['string'], 10], 34 | [Array.from({length: 100}, (_, i) => i), 10], 35 | [Array.from('hello'), 2], 36 | [[[null], [undefined], [true]], 1], 37 | [ 38 | [ 39 | [1, 'a'], 40 | [2, 'b'], 41 | [3, 'c'], 42 | ], 43 | 2, 44 | ], 45 | ] 46 | 47 | const ITERATIONS = 1000 48 | 49 | describe('chunk performance', () => { 50 | bench('hidash', () => { 51 | for (let i = 0; i < ITERATIONS; i++) { 52 | testCases.forEach(([input, size]) => { 53 | chunk(input, size) 54 | }) 55 | } 56 | }) 57 | 58 | bench('lodash', () => { 59 | for (let i = 0; i < ITERATIONS; i++) { 60 | testCases.forEach(([input, size]) => { 61 | _chunk(input, size) 62 | }) 63 | } 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/chunk.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3 | * Creates an array of elements split into groups the length of `size`. 4 | * If `array` can't be split evenly, the final chunk will be the remaining elements. 5 | * This function is similar to lodash's `_.chunk`. 6 | * (Original source inspiration for Lodash version: https://unpkg.com/lodash@4.17.21/chunk.js) 7 | * 8 | * @template T - The type of elements in the array. 9 | * @param {T[] | null | undefined} array - The array to process. 10 | * @param {number} [size=1] - The length of each chunk. Defaults to 1. 11 | * If `size` is less than 1, an empty array is returned. 12 | * @returns {T[][]} Returns the new array of chunks, or an empty array if the input 13 | * array is null/undefined or size is invalid. 14 | */ 15 | export function chunk(array: T[] | null | undefined, size: number = 1): T[][] { 16 | // Guard clause: if the array is null/undefined or size is less than 1, 17 | // return an empty array as no valid chunking can be performed. 18 | if (!array || size < 1) { 19 | return [] 20 | } 21 | 22 | // Determine the actual size for each chunk. 23 | // It's the floored value of `size`, but never less than 1. 24 | // (e.g., if size is 2.5, chunks will be of size 2). 25 | const absoluteSize = Math.max(Math.floor(size), 1) 26 | const length = array.length // Cache array length for minor performance optimization. 27 | 28 | // Pre-allocate the result array with the calculated number of chunks needed. 29 | const result: T[][] = new Array(Math.ceil(length / absoluteSize)) 30 | 31 | let resIndex = 0 // Index for populating the result array. 32 | // Iterate over the input array, incrementing by `absoluteSize` in each step. 33 | for (let i = 0; i < length; i += absoluteSize) { 34 | // Extract a chunk from the array using `slice` and add it to the result array. 35 | result[resIndex++] = array.slice(i, i + absoluteSize) 36 | } 37 | 38 | return result 39 | } 40 | 41 | export default chunk 42 | -------------------------------------------------------------------------------- /src/clamp.bench.ts: -------------------------------------------------------------------------------- 1 | import _clamp from 'lodash/clamp' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {clamp} from './clamp' 5 | 6 | const testCases = [ 7 | // Basic number cases 8 | [5, 0, 10], 9 | [-10, -5, 5], 10 | [10, -5, 5], 11 | [3, 0, 5], 12 | [0, -1, 1], 13 | 14 | // Edge cases 15 | [Number.MAX_VALUE, 0, 100], 16 | [Number.MIN_VALUE, -100, 0], 17 | [0, Number.MIN_VALUE, Number.MAX_VALUE], 18 | 19 | // Single bound cases 20 | [2, 5], 21 | [-2, 0], 22 | [100, 50], 23 | 24 | // Infinity cases 25 | [Infinity, -100, 100], 26 | [-Infinity, -100, 100], 27 | [0, -Infinity, Infinity], 28 | 29 | // Zero cases 30 | [0, -0, 0], 31 | [-0, -1, 1], 32 | [0, -1, 1], 33 | 34 | // Decimal cases 35 | [3.14159, 3, 4], 36 | [2.718, 2.5, 3], 37 | [1.23456, 1.2, 1.3], 38 | 39 | // Very close numbers 40 | [1.00000001, 1, 1.00000002], 41 | [0.99999999, 0.99999998, 1], 42 | 43 | // Large numbers of operations 44 | ...Array(100) 45 | .fill(0) 46 | .map((_, i) => [i, 0, 100]), 47 | 48 | // Boundary cases 49 | [5, 5, 5], 50 | [1, 1, 1], 51 | [0, 0, 0], 52 | ] as const 53 | 54 | const ITERATIONS = 1000 55 | 56 | describe('clamp performance', () => { 57 | bench('hidash', () => { 58 | for (let i = 0; i < ITERATIONS; i++) { 59 | testCases.forEach(([num, lower, upper]) => { 60 | clamp(num, lower, upper) 61 | }) 62 | } 63 | }) 64 | 65 | bench('lodash', () => { 66 | for (let i = 0; i < ITERATIONS; i++) { 67 | testCases.forEach(([num, lower, upper]) => { 68 | _clamp(num, lower, upper) 69 | }) 70 | } 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /src/clone.bench.ts: -------------------------------------------------------------------------------- 1 | import _clone from 'lodash/clone' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {clone} from './clone' 5 | 6 | const testCases = [ 7 | 42, 8 | 'hello', 9 | true, 10 | null, 11 | undefined, 12 | 13 | new Date(), 14 | /test/gi, 15 | 16 | [1, 2, 3], 17 | Array.from({length: 1000}, (_, i) => i), 18 | 19 | new Int32Array([1, 2, 3]), 20 | new Float64Array(Array.from({length: 1000}, (_, i) => i)), 21 | 22 | {a: 1, b: 2, c: 3}, 23 | Object.fromEntries(Array.from({length: 100}, (_, i) => [String(i), i])), 24 | 25 | new Set([1, 2, 3]), 26 | new Map([ 27 | [1, 2], 28 | [3, 4], 29 | ]), 30 | 31 | { 32 | date: new Date(), 33 | regexp: /test/, 34 | array: [1, 2, 3], 35 | nested: {a: 1, b: 2}, 36 | set: new Set([1, 2, 3]), 37 | }, 38 | ] 39 | 40 | const ITERATIONS = 1000 41 | 42 | describe('clone performance', () => { 43 | bench('hidash', () => { 44 | for (let i = 0; i < ITERATIONS; i++) { 45 | testCases.forEach((testCase) => { 46 | clone(testCase) 47 | }) 48 | } 49 | }) 50 | 51 | bench('lodash', () => { 52 | for (let i = 0; i < ITERATIONS; i++) { 53 | testCases.forEach((testCase) => { 54 | _clone(testCase) 55 | }) 56 | } 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/delay.test.ts: -------------------------------------------------------------------------------- 1 | import _delay from 'lodash/delay' 2 | import {describe, it, expect, vi, beforeEach, afterAll} from 'vitest' 3 | 4 | import delay from './delay' 5 | 6 | describe('delay', () => { 7 | beforeEach(() => { 8 | vi.useFakeTimers() 9 | }) 10 | 11 | it('should invoke the function after the specified delay', async () => { 12 | const mockFunc = vi.fn() 13 | const waitTime = 1000 14 | 15 | delay(mockFunc, waitTime, 'test') 16 | 17 | vi.advanceTimersByTime(waitTime) 18 | 19 | expect(mockFunc).toHaveBeenCalledWith('test') 20 | expect(mockFunc).toHaveBeenCalledTimes(1) 21 | }) 22 | 23 | it('should behave the same as lodash.delay', async () => { 24 | const hidashMock = vi.fn() 25 | const lodashMock = vi.fn() 26 | const waitTime = 1000 27 | 28 | delay(hidashMock, waitTime, 'test') 29 | _delay(lodashMock, waitTime, 'test') 30 | 31 | vi.advanceTimersByTime(waitTime) 32 | 33 | expect(hidashMock).toHaveBeenCalledWith('test') 34 | expect(lodashMock).toHaveBeenCalledWith('test') 35 | expect(hidashMock).toHaveBeenCalledTimes(1) 36 | expect(lodashMock).toHaveBeenCalledTimes(1) 37 | }) 38 | 39 | afterAll(() => { 40 | vi.useRealTimers() 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/delay.ts: -------------------------------------------------------------------------------- 1 | import toNumber from './toNumber' 2 | 3 | export function delay(func: (...args: T) => void, wait: number, ...args: T): number { 4 | if (typeof func !== 'function') { 5 | throw new TypeError('Expected a function') 6 | } 7 | const timeout = setTimeout(() => func(...args), wait) 8 | return toNumber(timeout) 9 | } 10 | 11 | export default delay 12 | -------------------------------------------------------------------------------- /src/difference.bench.ts: -------------------------------------------------------------------------------- 1 | import _difference from 'lodash/difference' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {difference} from './difference' 5 | 6 | const testCases = [ 7 | {array: [1, 2, 3], values: [[2]]}, 8 | {array: [2, 1], values: [[2, 3]]}, 9 | {array: [1, 'a', true, null], values: [[true]]}, 10 | {array: [undefined, null, 3, 'b'], values: [[3]]}, 11 | {array: [10, 20, 30, 40], values: [[20], [10, 50]]}, 12 | {array: [1, 2, 2, 3], values: [[2]]}, 13 | {array: [], values: [[]]}, 14 | {array: [1, 2, 3], values: [[]]}, 15 | { 16 | array: Array.from({length: 1000}, (_, i) => i), 17 | values: [Array.from({length: 500}, (_, i) => i * 2)], 18 | }, 19 | 20 | { 21 | array: Array.from({length: 1000}, (_, i) => (i % 2 === 0 ? i : `${i}`)), 22 | values: [Array.from({length: 100}, (_, i) => i), Array.from({length: 100}, (_, i) => `${i + 2000}`)], 23 | }, 24 | ] 25 | 26 | const ITERATIONS = 100 27 | 28 | describe('difference performance', () => { 29 | bench('hidash', () => { 30 | for (let i = 0; i < ITERATIONS; i++) { 31 | testCases.forEach(({array, values}) => { 32 | difference(array, ...values) 33 | }) 34 | } 35 | }) 36 | 37 | bench('lodash', () => { 38 | for (let i = 0; i < ITERATIONS; i++) { 39 | testCases.forEach(({array, values}) => { 40 | _difference(array, ...values) 41 | }) 42 | } 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/difference.ts: -------------------------------------------------------------------------------- 1 | export function difference(array: ArrayLike | null | undefined, ...values: ArrayLike[]): T[] { 2 | if (!array || array.length === 0) { 3 | return [] 4 | } 5 | 6 | const toExclude = new Set() 7 | const valueLength = values.length 8 | for (let i = 0; i < valueLength; i++) { 9 | const val = values[i] 10 | const valLength = val.length 11 | for (let j = 0; j < valLength; j++) { 12 | toExclude.add(val[j]) 13 | } 14 | } 15 | 16 | const result: T[] = [] 17 | for (let i = 0; i < array.length; i++) { 18 | const element = array[i] 19 | if (!toExclude.has(element)) { 20 | result.push(element) 21 | } 22 | } 23 | 24 | return result 25 | } 26 | 27 | export default difference 28 | -------------------------------------------------------------------------------- /src/entries.ts: -------------------------------------------------------------------------------- 1 | import {toPairs} from './toPairs' 2 | 3 | /** 4 | * @description 5 | * Converts a given object, array, or collection into an array of key-value pairs. 6 | * 7 | * Exactly the same as `toPairs`, but with a different name. (Alias of `toPairs`) 8 | * 9 | * @param {AnyKindOfDictionary | object | Map | Set} [object] The object to convert 10 | * @returns {[Key, Value][]} An array of key-value pairs 11 | */ 12 | export const entries = toPairs 13 | 14 | export default entries 15 | -------------------------------------------------------------------------------- /src/eq.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://unpkg.com/lodash@4.17.21/eq.js 3 | */ 4 | export function eq(value: unknown, other: unknown): boolean { 5 | // eslint-disable-next-line no-self-compare 6 | return value === other || (value !== value && other !== other) 7 | } 8 | 9 | export default eq 10 | -------------------------------------------------------------------------------- /src/every.bench.ts: -------------------------------------------------------------------------------- 1 | import _every from 'lodash/every' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {every} from './every' 5 | 6 | type List = ArrayLike 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | const testCases: [unknown, (x: any, i?: number) => boolean][] = [ 10 | // Basic arrays 11 | [[1, 2, 3] as List, (x: number) => x > 0], 12 | [[0, 1, 2] as List, Boolean], 13 | [[-1, 0, 1] as List, (x: number) => x >= -1], 14 | // Empty collections 15 | [[] as List, Boolean], 16 | [{}, Boolean], 17 | // Null & undefined 18 | [null, Boolean], 19 | [undefined, Boolean], 20 | // Objects 21 | [{a: 1, b: 2}, (x: number) => x > 0], 22 | [{a: 0, b: 1}, Boolean], 23 | // Array-like objects 24 | [{0: 'a', 1: 'b', length: 2} as List, Boolean], 25 | // Large collections 26 | [Array(1000).fill(1) as List, (x: number) => x === 1], 27 | [ 28 | Object.fromEntries( 29 | Array(1000) 30 | .fill(0) 31 | .map((_, i) => [i, 1]), 32 | ), 33 | (x: number) => x === 1, 34 | ], 35 | // Mixed types 36 | [[1, true, 'test'] as List, Boolean], 37 | [[undefined, null, false] as List, Boolean], 38 | // Complex predicates 39 | [[{}, [], ''] as List, (x: unknown) => x !== null], 40 | [Array(100).fill(1), (_, i) => (i || 0) < 100], 41 | ] 42 | 43 | const ITERATIONS = 1000 44 | 45 | describe('every performance', () => { 46 | bench('hidash', () => { 47 | for (let i = 0; i < ITERATIONS; i++) { 48 | testCases.forEach(([collection, predicate]) => { 49 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 50 | // @ts-ignore 51 | every(collection, predicate) 52 | }) 53 | } 54 | }) 55 | 56 | bench('lodash', () => { 57 | for (let i = 0; i < ITERATIONS; i++) { 58 | testCases.forEach(([collection, predicate]) => { 59 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 60 | // @ts-ignore 61 | _every(collection, predicate) 62 | }) 63 | } 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/every.test.ts: -------------------------------------------------------------------------------- 1 | import _every from 'lodash/every' 2 | import {describe, expect, it} from 'vitest' 3 | 4 | import {every} from './every' 5 | 6 | type List = ArrayLike 7 | 8 | describe('every', () => { 9 | it('should handle basic array cases', () => { 10 | const nums = [1, 2, 3] 11 | const predicate = (x: number) => x > 0 12 | expect(every(nums, predicate)).toBe(_every(nums, predicate)) 13 | }) 14 | 15 | it('should handle object cases', () => { 16 | const obj = {a: 1, b: 2} 17 | const predicate = (x: number) => x > 0 18 | expect(every(obj, predicate)).toBe(_every(obj, predicate)) 19 | }) 20 | 21 | it('should handle null/undefined', () => { 22 | expect(every(null)).toBe(_every(null)) 23 | expect(every(undefined)).toBe(_every(undefined)) 24 | }) 25 | 26 | it('should handle array-like objects', () => { 27 | const arrayLike = {0: 'a', 1: 'b', length: 2} 28 | const predicate = (x) => Boolean(x) 29 | expect(every(arrayLike, predicate)).toBe(_every(arrayLike, predicate)) 30 | }) 31 | 32 | it('should pass correct arguments to predicate', () => { 33 | const collection: List = {0: 1, 1: 2, 2: 3, length: 3} 34 | const predicateArgs: [number, number, List][] = [] 35 | 36 | every(collection, (value, index, col) => { 37 | predicateArgs.push([value, index, col]) 38 | return true 39 | }) 40 | 41 | predicateArgs.forEach(([value, index, col]) => { 42 | expect(col).toBe(collection) 43 | expect(typeof index).toBe('number') 44 | expect(typeof value).toBe('number') 45 | }) 46 | }) 47 | 48 | it('should handle empty objects', () => { 49 | expect(every({})).toBe(_every({})) 50 | expect(every({}, (v) => !!v)).toBe(_every({}, (v) => !!v)) 51 | }) 52 | 53 | it('should handle empty arrays', () => { 54 | expect(every([])).toBe(_every([])) 55 | expect(every([], (v) => !!v)).toBe(_every([], (v) => !!v)) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/every.ts: -------------------------------------------------------------------------------- 1 | import {baseIteratee} from './internal/baseIteratee' 2 | 3 | import type {ListIterateeCustom, ObjectIterateeCustom} from './internal/baseIteratee.type' 4 | import type {List} from './internal/types' 5 | 6 | export function every(collection: List | null | undefined, predicate?: ListIterateeCustom): boolean 7 | export function every( 8 | collection: T | null | undefined, 9 | predicate?: ObjectIterateeCustom, 10 | ): boolean 11 | export function every( 12 | collection: T | List | null | undefined, 13 | predicate: ListIterateeCustom | ObjectIterateeCustom = Boolean, 14 | ): boolean { 15 | if (!collection) { 16 | return true 17 | } 18 | 19 | const values = Object.values(collection) 20 | const length = values.length 21 | 22 | if (length === 0) { 23 | return true 24 | } 25 | 26 | const iteratee = baseIteratee(predicate) 27 | 28 | for (let i = 0; i < length; i++) { 29 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 30 | // @ts-ignore 31 | if (!iteratee(values[i], i, collection)) { 32 | return false 33 | } 34 | } 35 | 36 | return true 37 | } 38 | 39 | export default every 40 | -------------------------------------------------------------------------------- /src/filter.bench.ts: -------------------------------------------------------------------------------- 1 | import _filter from 'lodash/filter' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {filter} from './filter' 5 | 6 | const testCases = [ 7 | // Simple large arrays 8 | [Array(1000).fill(1), (v) => typeof v === 'number' && v > 500], 9 | [Array(1000).map((_, i) => i), (v) => typeof v === 'number' && v % 2 === 0], 10 | // Sparse arrays 11 | [ 12 | Array(1000) 13 | .fill(undefined) 14 | .map((_, i) => (i % 2 === 0 ? i : undefined)), 15 | (v) => v === undefined, 16 | ], 17 | // Large objects 18 | [ 19 | Object.fromEntries( 20 | Array(1000) 21 | .fill(0) 22 | .map((_, i) => [`key${i}`, i]), 23 | ), 24 | (v) => v > 999, 25 | ], 26 | [ 27 | Object.fromEntries( 28 | Array(1000) 29 | .fill(0) 30 | .map((_, i) => [`key${i}`, {nestedKey: i}]), 31 | ), 32 | (v) => v.nestedKey > 999, 33 | ], 34 | // Deeply nested objects 35 | [ 36 | { 37 | a: {b: {c: Array(1000).fill({nested: true})}}, 38 | d: {e: {f: Array(1000).fill({nested: false})}}, 39 | }, 40 | (v) => v.nested === true, 41 | ], 42 | // Mixed large arrays 43 | [Array(1000).map((_, i) => (i % 3 === 0 ? 'string' : i % 5 === 0 ? NaN : i)), (v) => typeof v === 'string'], 44 | // Large falsy arrays 45 | [ 46 | Array(1000) 47 | .fill(0) 48 | .map((_, i) => (i === 999 ? 1 : 0)), 49 | (v) => v === 1, 50 | ], 51 | // Large truthy arrays 52 | [ 53 | Array(1000) 54 | .fill(false) 55 | .map((_, i) => i === 999), 56 | (v) => v === true, 57 | ], 58 | ] as const 59 | 60 | const ITERATIONS = 1000 61 | 62 | describe('filter performance', () => { 63 | bench('hidash', () => { 64 | for (let i = 0; i < ITERATIONS; i++) { 65 | testCases.forEach(([collection, predicate]) => { 66 | filter(collection, predicate) 67 | }) 68 | } 69 | }) 70 | 71 | bench('lodash', () => { 72 | for (let i = 0; i < ITERATIONS; i++) { 73 | testCases.forEach(([collection, predicate]) => { 74 | _filter(collection, predicate) 75 | }) 76 | } 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /src/find.bench.ts: -------------------------------------------------------------------------------- 1 | import _find from 'lodash/find' 2 | import {describe, bench} from 'vitest' 3 | 4 | import {find} from './find' 5 | 6 | const testCases = [ 7 | [[0, 1, 0, 1, 0], (v: number) => v === 1], 8 | [new Array(10000).fill(0).map((_, i) => (i % 1000 === 0 ? 1 : 0)), (v: number) => v === 1], 9 | [ 10 | [ 11 | {name: {first: 'a', last: 'b'}}, 12 | {name: {first: 'c', last: 'd'}}, 13 | {name: {first: 'e', last: 'f'}}, 14 | {name: {first: 'a', last: 'b'}}, 15 | ], 16 | {name: {first: 'a'}}, 17 | ], 18 | [ 19 | [ 20 | {id: 1, value: 'a'}, 21 | {id: 2, value: 'b'}, 22 | {id: 3, value: 'c'}, 23 | {id: 4, value: 'd'}, 24 | ], 25 | 'value', 26 | ], 27 | [ 28 | [ 29 | {id: 1, value: 'a'}, 30 | {id: 2, value: 'b'}, 31 | {id: 3, value: 'c'}, 32 | {id: 4, value: 'd'}, 33 | ], 34 | ['value', 'b'], 35 | ], 36 | // eslint-disable-next-line no-sparse-arrays 37 | [[0, , 1, , , 3], (v: number) => v === 1], 38 | ['hello world'.split(''), (v: string) => v === 'o'], 39 | ] as const 40 | 41 | const ITERATIONS = 1000 42 | 43 | describe('find performance', () => { 44 | bench('hidash', () => { 45 | for (let i = 0; i < ITERATIONS; i++) { 46 | testCases.forEach(([array, predicate]) => { 47 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 48 | // @ts-ignore 49 | find(array, predicate) 50 | }) 51 | } 52 | }) 53 | 54 | bench('lodash', () => { 55 | for (let i = 0; i < ITERATIONS; i++) { 56 | testCases.forEach(([array, predicate]) => { 57 | _find(array, predicate) 58 | }) 59 | } 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/findIndex.bench.ts: -------------------------------------------------------------------------------- 1 | import _findIndex from 'lodash/findIndex' 2 | import {describe, bench} from 'vitest' 3 | 4 | import {findIndex} from './findIndex' 5 | 6 | const testCases = [ 7 | [[0, 1, 0, 1, 0], (v: number) => v === 1], 8 | [new Array(10000).fill(0).map((_, i) => (i % 1000 === 0 ? 1 : 0)), (v: number) => v === 1], 9 | [ 10 | [ 11 | {name: {first: 'a', last: 'b'}}, 12 | {name: {first: 'c', last: 'd'}}, 13 | {name: {first: 'e', last: 'f'}}, 14 | {name: {first: 'a', last: 'b'}}, 15 | ], 16 | {name: {first: 'a'}}, 17 | ], 18 | [ 19 | [ 20 | {id: 1, value: 'a'}, 21 | {id: 2, value: 'b'}, 22 | {id: 3, value: 'c'}, 23 | {id: 4, value: 'd'}, 24 | ], 25 | (item: {id: number; value: string}) => item.value, 26 | ], 27 | [ 28 | [ 29 | {id: 1, value: 'a'}, 30 | {id: 2, value: 'b'}, 31 | {id: 3, value: 'c'}, 32 | {id: 4, value: 'd'}, 33 | ], 34 | ['value', 'b'], 35 | ], 36 | // eslint-disable-next-line no-sparse-arrays 37 | [[0, , 1, , , 3], (v: number) => v === 1], 38 | ['hello world'.split(''), (v: string) => v === 'o'], 39 | ] 40 | 41 | const ITERATIONS = 1000 42 | 43 | describe('findIndex performance', () => { 44 | bench('hidash', () => { 45 | for (let i = 0; i < ITERATIONS; i++) { 46 | testCases.forEach(([[array, predicate]]) => { 47 | findIndex(array, predicate) 48 | }) 49 | } 50 | }) 51 | 52 | bench('lodash', () => { 53 | for (let i = 0; i < ITERATIONS; i++) { 54 | testCases.forEach(([[array, predicate]]) => { 55 | _findIndex(array, predicate) 56 | }) 57 | } 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/findIndex.ts: -------------------------------------------------------------------------------- 1 | import {baseIteratee} from './internal/baseIteratee' 2 | 3 | import type {ListIterateeCustom} from './internal/baseIteratee.type' 4 | 5 | /** 6 | * @description 7 | * Finds the **index** of the first element in an array that satisfies a given predicate. 8 | * If no element satisfies the predicate, it returns `-1`. 9 | * 10 | * Opposed to `findLastIndex`, this function starts searching from the beginning of the array. 11 | * 12 | * @param {Array} array The array to search 13 | * @param {ListIterateeCustom} [predicate] The function invoked per iteration 14 | * @param {number} [fromIndex] The index to search from (default: `0`) 15 | * @returns {number} The index of the first element that satisfies the predicate, or `-1` if none found 16 | */ 17 | export function findIndex(array: T[], predicate?: ListIterateeCustom, fromIndex?: number) { 18 | const length = array.length 19 | 20 | if (!length) { 21 | return -1 22 | } 23 | 24 | const from = fromIndex !== undefined ? fromIndex : 0 25 | const startIndex = Math.max(from >= 0 ? from : length + from, 0) 26 | 27 | const iteratee = baseIteratee(predicate) 28 | 29 | let index = startIndex - 1 30 | 31 | while (++index < length) { 32 | if (iteratee(array[index], index, array)) { 33 | return index 34 | } 35 | } 36 | 37 | return -1 38 | } 39 | 40 | export default findIndex 41 | -------------------------------------------------------------------------------- /src/findLastIndex.bench.ts: -------------------------------------------------------------------------------- 1 | import _findLastIndex from 'lodash/findLastIndex' 2 | import {describe, bench} from 'vitest' 3 | 4 | import {findLastIndex} from './findLastIndex' 5 | 6 | const testCases = [ 7 | [[0, 1, 0, 1, 0], (v: number) => v === 1], 8 | [new Array(10000).fill(0).map((_, i) => (i % 1000 === 0 ? 1 : 0)), (v: number) => v === 1], 9 | [ 10 | [ 11 | {name: {first: 'a', last: 'b'}}, 12 | {name: {first: 'c', last: 'd'}}, 13 | {name: {first: 'e', last: 'f'}}, 14 | {name: {first: 'a', last: 'b'}}, 15 | ], 16 | {name: {first: 'a'}}, 17 | ], 18 | [ 19 | [ 20 | {id: 1, value: 'a'}, 21 | {id: 2, value: 'b'}, 22 | {id: 3, value: 'c'}, 23 | {id: 4, value: 'd'}, 24 | ], 25 | (item: {id: number; value: string}) => item.value, 26 | ], 27 | [ 28 | [ 29 | {id: 1, value: 'a'}, 30 | {id: 2, value: 'b'}, 31 | {id: 3, value: 'c'}, 32 | {id: 4, value: 'd'}, 33 | ], 34 | ['value', 'b'], 35 | ], 36 | // eslint-disable-next-line no-sparse-arrays 37 | [[0, , 1, , , 3], (v: number) => v === 1], 38 | ['hello world'.split(''), (v: string) => v === 'o'], 39 | ] 40 | 41 | const ITERATIONS = 1000 42 | 43 | describe('findLastIndex performance', () => { 44 | bench('hidash', () => { 45 | for (let i = 0; i < ITERATIONS; i++) { 46 | testCases.forEach(([[array, predicate]]) => { 47 | findLastIndex(array, predicate) 48 | }) 49 | } 50 | }) 51 | 52 | bench('lodash', () => { 53 | for (let i = 0; i < ITERATIONS; i++) { 54 | testCases.forEach(([[array, predicate]]) => { 55 | _findLastIndex(array, predicate) 56 | }) 57 | } 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/findLastIndex.ts: -------------------------------------------------------------------------------- 1 | import {baseIteratee} from './internal/baseIteratee' 2 | 3 | import type {ListIterateeCustom} from './internal/baseIteratee.type' 4 | 5 | /** 6 | * @description 7 | * Finds the **last index** of an element in an array that satisfies a given predicate. 8 | * If no element satisfies the predicate, it returns `-1`. 9 | * 10 | * Opposed to `findIndex`, this function starts searching from the end of the array. 11 | * 12 | * @param {Array} array The array to search 13 | * @param {ListIterateeCustom} [predicate] The function invoked per iteration 14 | * @param {number} [fromIndex] The index to search from (default: `array.length - 1`) 15 | * @returns {number} The index of the last element that satisfies the predicate, or `-1` if none found 16 | */ 17 | export function findLastIndex(array: T[], predicate?: ListIterateeCustom, fromIndex?: number): number { 18 | const length = array.length 19 | 20 | if (!length) { 21 | return -1 22 | } 23 | 24 | const from = fromIndex !== undefined ? fromIndex : length - 1 25 | const startIndex = Math.max(from >= 0 ? from : length + from, 0) 26 | 27 | const iteratee = baseIteratee(predicate) 28 | 29 | let index = startIndex + 1 30 | 31 | while (index--) { 32 | if (iteratee(array[index], index, array)) { 33 | return index 34 | } 35 | } 36 | 37 | return -1 38 | } 39 | 40 | export default findLastIndex 41 | -------------------------------------------------------------------------------- /src/first.bench.ts: -------------------------------------------------------------------------------- 1 | import _first from 'lodash/first' 2 | import {describe, bench} from 'vitest' 3 | 4 | import {first} from './first' 5 | 6 | const testCases = [ 7 | null, 8 | undefined, 9 | [], 10 | ['a', 'b', 'c'], 11 | [1, 2, 3], 12 | new Array(3), 13 | Array(3), 14 | [null], 15 | [undefined], 16 | [false], 17 | [0], 18 | [''], 19 | ['a', 1, false, null, undefined], 20 | new Array(10000).fill(0), 21 | ] 22 | 23 | const ITERATIONS = 10000 24 | 25 | describe('first performance', () => { 26 | bench('hidash', () => { 27 | for (let i = 0; i < ITERATIONS; i++) { 28 | testCases.forEach((testCase) => { 29 | first(testCase) 30 | }) 31 | } 32 | }) 33 | 34 | bench('lodash', () => { 35 | for (let i = 0; i < ITERATIONS; i++) { 36 | testCases.forEach((testCase) => { 37 | _first(testCase) 38 | }) 39 | } 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/first.ts: -------------------------------------------------------------------------------- 1 | export function first(array: T[] | null | undefined): T | undefined { 2 | return array && array.length ? array[0] : undefined 3 | } 4 | 5 | export default first 6 | -------------------------------------------------------------------------------- /src/flatten.bench.ts: -------------------------------------------------------------------------------- 1 | import _flatten from 'lodash/flatten' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {flatten} from './flatten' 5 | 6 | type List = ArrayLike 7 | 8 | const testCases: [unknown, unknown][] = [ 9 | // Basic arrays 10 | [[1, 2, 3] as List, (x: number) => x > 0], 11 | [[0, 1, 2] as List, Boolean], 12 | [[-1, 0, 1] as List, (x: number) => x >= -1], 13 | // Empty collections 14 | [[] as List, Boolean], 15 | [{}, Boolean], 16 | // Null & undefined 17 | [null, Boolean], 18 | [undefined, Boolean], 19 | // Objects 20 | [{a: 1, b: 2}, (x: number) => x > 0], 21 | [{a: 0, b: 1}, Boolean], 22 | // Array-like objects 23 | [{0: 'a', 1: 'b', length: 2} as List, Boolean], 24 | // Large collections 25 | [Array(1000).fill(1) as List, (x: number) => x === 1], 26 | [ 27 | Object.fromEntries( 28 | Array(1000) 29 | .fill(0) 30 | .map((_, i) => [i, 1]), 31 | ), 32 | (x: number) => x === 1, 33 | ], 34 | // Mixed types 35 | [[1, true, 'test'] as List, Boolean], 36 | [[undefined, null, false] as List, Boolean], 37 | // Complex predicates 38 | [[{}, [], ''] as List, (x: unknown) => x !== null], 39 | [Array(100).fill(1), (_: unknown, i: number) => (i || 0) < 100], 40 | ] as const 41 | 42 | const ITERATIONS = 1000 43 | 44 | describe('flatten performance', () => { 45 | bench('hidash', () => { 46 | for (let i = 0; i < ITERATIONS; i++) { 47 | testCases.forEach(([collection]) => { 48 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 49 | // @ts-ignore 50 | flatten(collection) 51 | }) 52 | } 53 | }) 54 | 55 | bench('lodash', () => { 56 | for (let i = 0; i < ITERATIONS; i++) { 57 | testCases.forEach(([collection]) => { 58 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 59 | // @ts-ignore 60 | _flatten(collection) 61 | }) 62 | } 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/flatten.test.ts: -------------------------------------------------------------------------------- 1 | import _flatten from 'lodash/flatten' 2 | import {describe, test, expect} from 'vitest' 3 | 4 | import {flatten} from './flatten' 5 | 6 | describe('flatten function', () => { 7 | test('returns empty array for null and undefined', () => { 8 | expect(flatten(null)).toEqual(_flatten(null)) 9 | expect(flatten(undefined)).toEqual(_flatten(undefined)) 10 | }) 11 | 12 | test('flattens arrays a single level deep', () => { 13 | expect(flatten([1, [2, [3, [4]], 5]])).toEqual(_flatten([1, [2, [3, [4]], 5]])) 14 | expect(flatten([['a'], ['b', ['c']]])).toEqual(_flatten([['a'], ['b', ['c']]])) 15 | }) 16 | 17 | test('handles empty arrays', () => { 18 | expect(flatten([])).toEqual(_flatten([])) 19 | expect(flatten([[]])).toEqual(_flatten([[]])) 20 | }) 21 | 22 | test('handles arrays with different types', () => { 23 | expect(flatten([null])).toEqual(_flatten([null])) 24 | expect(flatten([undefined])).toEqual(_flatten([undefined])) 25 | expect(flatten([false])).toEqual(_flatten([false])) 26 | expect(flatten([0])).toEqual(_flatten([0])) 27 | expect(flatten([''])).toEqual(_flatten([''])) 28 | }) 29 | 30 | test('handles arrays with mixed types', () => { 31 | const mixedArr = [1, 'string', null, undefined, false] 32 | expect(flatten(mixedArr)).toEqual(_flatten(mixedArr)) 33 | }) 34 | 35 | test('handles sparse arrays', () => { 36 | const sparseArray = Array(3) 37 | sparseArray[1] = 'b' 38 | expect(flatten(sparseArray)).toEqual(_flatten(sparseArray)) 39 | }) 40 | 41 | test('handles large arrays efficiently', () => { 42 | const largeArray = new Array(10000).fill(0) 43 | largeArray[0] = 'first' 44 | expect(flatten(largeArray)).toEqual(_flatten(largeArray)) 45 | }) 46 | 47 | test('maintains original array reference', () => { 48 | const arr = ['a', 'b', 'c'] 49 | const result = flatten(arr) 50 | arr[0] = 'd' 51 | expect(result).toEqual(_flatten(['a', 'b', 'c'])) 52 | }) 53 | 54 | test('handles string-like objects correctly', () => { 55 | const strObj = Object('hello') 56 | expect(flatten([strObj])).toEqual(_flatten([strObj])) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/flatten.ts: -------------------------------------------------------------------------------- 1 | import isArray from './isArray' 2 | 3 | import type {List, Many} from './internal/types' 4 | 5 | function baseFlatten(array: List>, depth: number = 1, result?: T[]): T[] { 6 | const flattenResult: unknown[] = result || [] 7 | 8 | const length = array.length 9 | 10 | if (array && length > 0) { 11 | for (let i = 0; i < length; i++) { 12 | const element = array[i] 13 | if (element && isArray(element) && depth > 0) { 14 | baseFlatten(element, depth - 1, flattenResult) 15 | } else { 16 | flattenResult.push(element) 17 | } 18 | } 19 | } 20 | 21 | return flattenResult as T[] 22 | } 23 | 24 | export function flatten(array: List> | null | undefined): T[] { 25 | if (array === null || array === undefined) { 26 | return [] 27 | } 28 | return baseFlatten(array, 1) 29 | } 30 | 31 | export default flatten 32 | -------------------------------------------------------------------------------- /src/flow.test.ts: -------------------------------------------------------------------------------- 1 | import _flow from 'lodash/flow' 2 | import {describe, test, expect} from 'vitest' 3 | 4 | import {flow} from './flow' 5 | 6 | describe('flow function', () => { 7 | test('composes functions correctly', () => { 8 | const addOne = (x: number) => x + 1 9 | const double = (x: number) => x * 2 10 | 11 | const customFlow = flow(addOne, double) 12 | const lodashFlow = _flow(addOne, double) 13 | 14 | expect(customFlow(2)).toBe(lodashFlow(2)) 15 | }) 16 | 17 | test('handles single function correctly', () => { 18 | const addOne = (x: number) => x + 1 19 | 20 | const customFlow = flow(addOne) 21 | const lodashFlow = _flow(addOne) 22 | 23 | expect(customFlow(3)).toBe(lodashFlow(3)) 24 | }) 25 | 26 | test('handles multiple functions correctly', () => { 27 | const square = (x: number) => x * x 28 | const double = (x: number) => x * 2 29 | const subtractFive = (x: number) => x - 5 30 | 31 | const customFlow = flow(square, double, subtractFive) 32 | const lodashFlow = _flow(square, double, subtractFive) 33 | 34 | expect(customFlow(4)).toBe(lodashFlow(4)) 35 | }) 36 | 37 | test('works with different types', () => { 38 | const toString = (x: number) => x.toString() 39 | const repeat = (x: string) => x.repeat(3) 40 | 41 | const customFlow = flow(toString, repeat) 42 | const lodashFlow = _flow(toString, repeat) 43 | 44 | expect(customFlow(5)).toBe(lodashFlow(5)) 45 | }) 46 | 47 | test('preserves type inference', () => { 48 | const increment = (x: number) => x + 1 49 | const stringify = (x: number) => x.toString() 50 | const length = (x: string) => x.length 51 | 52 | const customFlow = flow(increment, stringify, length) 53 | const lodashFlow = _flow(increment, stringify, length) 54 | 55 | expect(customFlow(3)).toBe(lodashFlow(3)) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/flow.ts: -------------------------------------------------------------------------------- 1 | type Func = (...args: TArgs) => TResult 2 | export function flow(fn: Func): Func 3 | export function flow( 4 | fn1: Func, 5 | fn2: Func<[TResult1], TResult2>, 6 | ): Func 7 | export function flow( 8 | fn1: Func, 9 | fn2: Func<[TResult1], TResult2>, 10 | fn3: Func<[TResult2], TResult3>, 11 | ): Func 12 | export function flow( 13 | fn1: Func, 14 | fn2: Func<[TResult1], TResult2>, 15 | fn3: Func<[TResult2], TResult3>, 16 | fn4: Func<[TResult3], TResult4>, 17 | ): Func 18 | export function flow( 19 | fn1: Func, 20 | fn2: Func<[TResult1], TResult2>, 21 | fn3: Func<[TResult2], TResult3>, 22 | fn4: Func<[TResult3], TResult4>, 23 | fn5: Func<[TResult4], TResult5>, 24 | ): Func 25 | export function flow( 26 | fn1: Func, 27 | fn2: Func<[TResult1], TResult2>, 28 | fn3: Func<[TResult2], TResult3>, 29 | fn4: Func<[TResult3], TResult4>, 30 | fn5: Func<[TResult4], TResult5>, 31 | fn6: Func<[TResult5], TResult6>, 32 | ): Func 33 | export function flow( 34 | fn1: Func, 35 | fn2: Func<[TResult1], TResult2>, 36 | fn3: Func<[TResult2], TResult3>, 37 | fn4: Func<[TResult3], TResult4>, 38 | fn5: Func<[TResult4], TResult5>, 39 | fn6: Func<[TResult5], TResult6>, 40 | fn7: Func<[TResult6], TResult7>, 41 | ): Func 42 | export function flow( 43 | ...funcs: [Func, ...Func<[unknown], unknown>[]] 44 | ): Func { 45 | return (...args: TArgs) => { 46 | let result: unknown = funcs[0](...args) 47 | const funcLength = funcs.length 48 | for (let i = 1; i < funcLength; i++) { 49 | result = funcs[i](result) 50 | } 51 | return result 52 | } 53 | } 54 | 55 | export default flow 56 | -------------------------------------------------------------------------------- /src/get.bench.ts: -------------------------------------------------------------------------------- 1 | import _get from 'lodash/get' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {get} from './get' 5 | 6 | const ITERATIONS = 1000 7 | 8 | const obj = { 9 | a: [{b: {c: 3}}], 10 | x: {y: {z: 'hello'}}, 11 | e: { 12 | f: { 13 | g: 42, 14 | }, 15 | h: null, 16 | 'complex.key': { 17 | c: 'complexValue', 18 | 'escaped\\"quotes"': 'withEscapedQuotes', 19 | }, 20 | }, 21 | arr: [1, 2, {x: 'hello'}, [5, 6, 7]], 22 | 'weird.key': 'value', 23 | 123: 'number key', 24 | } 25 | const PATHS = [ 26 | ['a', 0, 'b', 'c'], 27 | ['x', 'y', 'z'], 28 | 'not.exist', 29 | ['a', '0', 'b', 'notThere'], 30 | ['e', 'f', 'g'], 31 | ['e', 'f', 'x'], 32 | ['arr', '0'], 33 | ['arr', '2', 'x'], 34 | ['arr', '4', 0], 35 | '123', 36 | ['a', 'd'], 37 | ['a', 'b', 'c'], 38 | ['arr', 2, 'x'], 39 | ['weird.key'], 40 | 'weird.key', 41 | '', 42 | ] 43 | 44 | describe('get performance', () => { 45 | bench('hidash', () => { 46 | for (let i = 0; i < ITERATIONS; i++) { 47 | PATHS.forEach((path) => { 48 | get(obj, path, 'default') 49 | }) 50 | } 51 | }) 52 | 53 | bench('lodash', () => { 54 | for (let i = 0; i < ITERATIONS; i++) { 55 | PATHS.forEach((path) => { 56 | _get(obj, path, 'default') 57 | }) 58 | } 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/get.test.ts: -------------------------------------------------------------------------------- 1 | import _get from 'lodash/get' 2 | import {describe, it, expect} from 'vitest' 3 | 4 | import {get} from './get' 5 | 6 | describe('get', () => { 7 | it('should get property', () => { 8 | const obj = { 9 | a: { 10 | b: { 11 | c: 42, 12 | }, 13 | d: null, 14 | 'complex.key': { 15 | c: 'complexValue', 16 | 'escaped\\"quotes"': 'withEscapedQuotes', 17 | }, 18 | }, 19 | arr: [1, 2, {x: 'hello'}, [5, 6, 7]], 20 | 'weird.key': 'value', 21 | 123: 'number key', 22 | } 23 | 24 | const list: { 25 | value: string | number | (string | number)[] | undefined | null 26 | options?: {default?: string} 27 | }[] = [ 28 | {value: ['a', 'b', 'c']}, 29 | {value: ['a', 'b', 'x']}, 30 | {value: ['a', 'b', 'x'], options: {default: 'default'}}, 31 | {value: ['a', 'b', 'complex.key', 'c'], options: {default: 'default'}}, 32 | {value: ['arr', 0]}, 33 | {value: ['arr', 2, 'x']}, 34 | {value: ['arr', 4, 0]}, 35 | {value: ['123']}, 36 | {value: 123}, 37 | {value: ['arr', 2, 'x']}, 38 | {value: ['weird.key']}, 39 | {value: 'weird.key'}, 40 | {value: ''}, 41 | {value: undefined}, 42 | {value: null}, 43 | ] 44 | list.forEach(({value, options: {default: d} = {}}) => { 45 | const v = value as string | symbol | (string | number)[] 46 | expect({ 47 | k: v, 48 | v: get(obj, v, d), 49 | }).toEqual({ 50 | k: v, 51 | v: _get(obj, v, d), 52 | }) 53 | }) 54 | }) 55 | 56 | it('should get property by array path', () => { 57 | const obj = {a: [{b: {c: 3}}]} 58 | expect(get(obj, ['a', 0, 'b', 'c'])).toBe(_get(obj, ['a', '0', 'b', 'c'])) 59 | }) 60 | 61 | it('should handle non-object', () => { 62 | expect(get(42, 'a', 'not found')).toBe(_get(42, 'a', 'not found')) 63 | }) 64 | 65 | it('should handle symbol keys', () => { 66 | const sym = Symbol('test') 67 | const obj = {[sym]: 'symbolValue'} 68 | expect(get(obj, sym)).toBe(_get(obj, sym)) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /src/get.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /** 3 | * @description 4 | * Gets the property value at path of object. 5 | * DefaultValue is used if it is not found at path. 6 | * excluded patterns 'a.b.c' and 'are[4][0]' 7 | * 8 | * @param {Object|Array} object - object to get. 9 | * @param {String} path - path of the property to get. 10 | * @param defaultValue - DefaultValue is used if it is not found at path. 11 | * 12 | * @returns Returns the value at path of object. 13 | */ 14 | export function get(object: unknown, path: string | symbol | (string | number)[], defaultValue?: T): T | undefined { 15 | const obj = object 16 | try { 17 | if (obj == null) { 18 | return defaultValue 19 | } 20 | if (!path) { 21 | return (obj as any)[path] ?? defaultValue 22 | } 23 | 24 | const paths: (symbol | string | number)[] = Array.isArray(path) ? path : [path] 25 | 26 | let result: any = obj 27 | 28 | for (const p of paths) { 29 | if (p in result) { 30 | result = result[p] 31 | } else { 32 | return defaultValue 33 | } 34 | } 35 | 36 | return result as T 37 | } catch { 38 | return defaultValue 39 | } 40 | } 41 | 42 | export default get 43 | -------------------------------------------------------------------------------- /src/groupBy.ts: -------------------------------------------------------------------------------- 1 | import {isArrayLike} from './internal/array' 2 | import {baseIteratee} from './internal/baseIteratee' 3 | import isPlainObject from './isPlainObject' 4 | 5 | import type {ValueIteratee} from './internal/baseIteratee.type' 6 | 7 | type PropertyName = string | number | symbol 8 | 9 | export function groupBy(collection: T[] | null | undefined, iteratee?: ValueIteratee): Record 10 | export function groupBy( 11 | collection: T | null | undefined, 12 | iteratee?: ValueIteratee, 13 | ): Record 14 | export function groupBy(collection: unknown, iteratee?: ValueIteratee): Record { 15 | if (collection == null) { 16 | return {} 17 | } 18 | 19 | const iterFn = baseIteratee(iteratee ?? ((v: unknown) => v)) 20 | const result: Record = {} 21 | 22 | if (isArrayLike(collection)) { 23 | const arr = collection as ArrayLike 24 | const arrLength = arr.length 25 | for (let i = 0; i < arrLength; i++) { 26 | const value = arr[i] 27 | const key = iterFn(value, i, arr) 28 | const stringKey = key == null ? 'undefined' : String(key) 29 | const group = result[stringKey] || (result[stringKey] = []) 30 | group.push(value) 31 | } 32 | return result 33 | } 34 | 35 | if (isPlainObject(collection)) { 36 | const values = Object.values(collection as Record) 37 | const valuesLength = values.length 38 | for (let i = 0; i < valuesLength; i++) { 39 | const value = values[i] 40 | const key = iterFn(value, i, values) 41 | const stringKey = key == null ? 'undefined' : String(key) 42 | const group = result[stringKey] || (result[stringKey] = []) 43 | group.push(value) 44 | } 45 | return result 46 | } 47 | 48 | return {} 49 | } 50 | 51 | export default groupBy 52 | -------------------------------------------------------------------------------- /src/gt.bench.ts: -------------------------------------------------------------------------------- 1 | import _gt from 'lodash/gt' 2 | import {bench, describe} from 'vitest' 3 | 4 | import gt from './gt' 5 | 6 | const testCases = [ 7 | {value: 1, other: 0}, // comparison of numbers 8 | {value: -1, other: -2}, // negative comparison 9 | {value: 0, other: 0}, // comparison of the same numbers 10 | {value: 3.14, other: 2.71}, // prime number comparison 11 | {value: '5', other: '3'}, // numeric strings comparison 12 | {value: 'a', other: 'b'}, // string comparison 13 | {value: '123', other: 456}, // mixing string and numbers 14 | {value: null, other: undefined}, // null and undefined 15 | {value: {}, other: []}, // comparison of objects and arrays 16 | {value: NaN, other: Infinity}, // comparison of special values 17 | ] 18 | 19 | const ITERATIONS = 10000 20 | 21 | describe('gt performance', () => { 22 | bench('hidash', () => { 23 | for (let i = 0; i < ITERATIONS; i++) { 24 | testCases.forEach(({value, other}) => { 25 | gt(value, other) 26 | }) 27 | } 28 | }) 29 | 30 | bench('lodash', () => { 31 | for (let i = 0; i < ITERATIONS; i++) { 32 | testCases.forEach(({value, other}) => { 33 | _gt(value, other) 34 | }) 35 | } 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/gt.ts: -------------------------------------------------------------------------------- 1 | import {isNumber} from './isNumber' 2 | import {toNumber} from './toNumber' 3 | 4 | export function gt(value: unknown, other: unknown): boolean { 5 | if (typeof value === 'string' && typeof other === 'string') { 6 | return value > other 7 | } 8 | 9 | const v = isNumber(value) ? (value as number) : toNumber(value) 10 | const o = isNumber(other) ? (other as number) : toNumber(other) 11 | 12 | return v > o 13 | } 14 | 15 | export default gt 16 | -------------------------------------------------------------------------------- /src/has.bench.ts: -------------------------------------------------------------------------------- 1 | import _has from 'lodash/has' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {has} from './has' 5 | 6 | const testCases = [ 7 | // Simple paths 8 | [{a: 1}, 'a'], 9 | [{a: {b: 2}}, 'a.b'], 10 | [{a: {b: {c: 3}}}, ['a', 'b', 'c']], 11 | // Null & undefined 12 | [null, 'a'], 13 | [undefined, 'a.b'], 14 | // Arrays 15 | [[1, 2, 3], '1'], 16 | [[1, [2, [3]]], '1.1.0'], 17 | [Array(1000).fill(1), '999'], 18 | // Nested objects 19 | [{a: {b: {c: {d: {e: 5}}}}}, 'a.b.c.d.e'], 20 | [{'a.b.c': 3}, 'a.b.c'], 21 | [{'a.b.c': 3}, ['a.b.c']], 22 | // Objects with prototype 23 | [Object.create({inherited: true}, {own: {value: true, enumerable: true}}), 'own'], 24 | [Object.create({inherited: true}, {own: {value: true, enumerable: true}}), 'inherited'], 25 | // Array-like objects 26 | [{length: 2, 0: 'a', 1: 'b'}, 'length'], 27 | [{length: 2, 0: 'a', 1: 'b'}, '0'], 28 | // Edge cases 29 | [{}, ''], 30 | [{}, []], 31 | [{}, '.'], 32 | [{}, '..'], 33 | // Deep paths 34 | [ 35 | Object.fromEntries( 36 | Array(100) 37 | .fill(0) 38 | .map((_, i) => [`level${i}`, {}]), 39 | ), 40 | Array(100) 41 | .fill(0) 42 | .map((_, i) => `level${i}`) 43 | .join('.'), 44 | ], 45 | // Symbol keys 46 | [{[Symbol('test')]: true}, [Symbol('test')]], 47 | ] as const 48 | 49 | const ITERATIONS = 1000 50 | 51 | describe('has performance', () => { 52 | bench('hidash', () => { 53 | for (let i = 0; i < ITERATIONS; i++) { 54 | testCases.forEach(([obj, path]) => { 55 | has(obj, path as string | string[]) 56 | }) 57 | } 58 | }) 59 | 60 | bench('lodash', () => { 61 | for (let i = 0; i < ITERATIONS; i++) { 62 | testCases.forEach(([obj, path]) => { 63 | _has(obj, path as string | string[]) 64 | }) 65 | } 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/has.ts: -------------------------------------------------------------------------------- 1 | import {Many, PropertyPath} from './internal/types' 2 | import isArray from './isArray' 3 | 4 | const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/ 5 | const reIsPlainProp = /^\w*$/ 6 | 7 | function isKey(value: PropertyPath, object: unknown): boolean { 8 | if (Array.isArray(value)) { 9 | return false 10 | } 11 | if (typeof value !== 'string') { 12 | return false 13 | } 14 | return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || (object != null && value in Object(object)) 15 | } 16 | 17 | function castPath(value: Many): string[] { 18 | if (Array.isArray(value)) { 19 | return value.map(String) 20 | } 21 | if (typeof value === 'string') { 22 | return value.split('.') 23 | } 24 | return [String(value)] 25 | } 26 | 27 | function hasPath(object: unknown, path: PropertyPath): boolean { 28 | if (object == null) { 29 | return false 30 | } 31 | 32 | const pathArray = isKey(path, object) ? [path as string] : castPath(path) 33 | const pathArrayLength = pathArray.length 34 | 35 | let currentObject: unknown = object 36 | 37 | for (let i = 0; i < pathArrayLength; i++) { 38 | const key = pathArray[i] 39 | 40 | if ( 41 | currentObject == null || 42 | typeof currentObject !== 'object' || 43 | typeof key !== 'string' || 44 | !Object.prototype.hasOwnProperty.call(currentObject, key) 45 | ) { 46 | return false 47 | } 48 | 49 | currentObject = (currentObject as Record)[key] 50 | 51 | if (i === pathArray.length - 1) { 52 | return true 53 | } 54 | } 55 | 56 | return true 57 | } 58 | 59 | /** 60 | * @see https://unpkg.com/browse/lodash.has@4.5.2/index.js 61 | */ 62 | export function has(object: unknown, path: PropertyPath): boolean { 63 | if (path == null || (Array.isArray(path) && !path.length)) { 64 | return false 65 | } 66 | 67 | if (typeof path === 'string') { 68 | if (!path || path === '.' || path === '..') { 69 | return false 70 | } 71 | 72 | if (!path.includes('.')) { 73 | return object != null && typeof object === 'object' && Object.prototype.hasOwnProperty.call(object, path) 74 | } 75 | } else if (isArray(path) && !path.every((segment) => typeof segment === 'string')) { 76 | return false 77 | } 78 | 79 | return hasPath(object, path) 80 | } 81 | 82 | export default has 83 | -------------------------------------------------------------------------------- /src/identity.test.ts: -------------------------------------------------------------------------------- 1 | import lodashIdentity from 'lodash/identity' 2 | import {describe, test, expect} from 'vitest' 3 | 4 | import {identity} from './identity' 5 | 6 | const testCases = [1, 'test', null, undefined, {a: 1}, [1, 2, 3]] as const 7 | 8 | describe('identity', () => { 9 | test('should return the same value', () => { 10 | testCases.forEach((value) => { 11 | expect(identity(value)).toBe(value) 12 | }) 13 | }) 14 | }) 15 | 16 | describe('identity basic functionality', () => { 17 | test('should return the same value', () => { 18 | testCases.forEach((value) => { 19 | expect(identity(value)).toBe(lodashIdentity(value)) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/identity.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://unpkg.com/lodash@4.17.21/identity.js 3 | */ 4 | export function identity(value: T): T { 5 | return value 6 | } 7 | 8 | export default identity 9 | -------------------------------------------------------------------------------- /src/includes.bench.ts: -------------------------------------------------------------------------------- 1 | import _includes from 'lodash/includes' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {includes} from './includes' 5 | 6 | const testCases = [ 7 | // Basic cases 8 | [{a: 1, b: 2}, 1], 9 | [{a: 'test', b: 'value'}, 'test'], 10 | [null, 1], 11 | [undefined, 'test'], 12 | 13 | // Arrays as objects 14 | [{0: 'a', 1: 'b', length: 2}, 'a'], 15 | [{0: 1, 1: 2, 2: 3, length: 3}, 2], 16 | 17 | // Large objects 18 | [ 19 | Object.fromEntries( 20 | Array(1000) 21 | .fill(0) 22 | .map((_, i) => [`key${i}`, i]), 23 | ), 24 | 999, 25 | ], 26 | [ 27 | Object.fromEntries( 28 | Array(1000) 29 | .fill(0) 30 | .map((_, i) => [`key${i}`, 'value']), 31 | ), 32 | 'value', 33 | ], 34 | 35 | // Edge cases 36 | [{}, 'nonexistent'], 37 | [{a: undefined}, undefined], 38 | [{a: null}, null], 39 | 40 | // Different types 41 | [{a: 1, b: '2', c: true}, true], 42 | [{a: [], b: {}, c: new Date()}, []], 43 | 44 | // With from Index 45 | [{0: 'a', 1: 'b', 2: 'a', length: 3}, 'a', 1], 46 | [{0: 1, 1: 2, 2: 1, length: 3}, 1, 2], 47 | ] as const 48 | 49 | const ITERATIONS = 1000 50 | 51 | describe('includes performance', () => { 52 | bench('hidash', () => { 53 | for (let i = 0; i < ITERATIONS; i++) { 54 | testCases.forEach(([obj, target, fromIndex]) => { 55 | includes(obj, target, fromIndex) 56 | }) 57 | } 58 | }) 59 | 60 | bench('lodash', () => { 61 | for (let i = 0; i < ITERATIONS; i++) { 62 | testCases.forEach(([obj, target, fromIndex]) => { 63 | _includes(obj, target, fromIndex) 64 | }) 65 | } 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/includes.test.ts: -------------------------------------------------------------------------------- 1 | import _includes from 'lodash/includes' 2 | import {describe, expect, it} from 'vitest' 3 | 4 | import {includes} from './includes' 5 | 6 | import type {Dictionary, NumericDictionary} from './includes' 7 | 8 | describe('includes', () => { 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | const testCases: [Dictionary | NumericDictionary | null | undefined, any, number?][] = [ 11 | // Basic object values 12 | [{a: 1, b: 2, c: 3}, 1], 13 | [{a: 'test', b: 'value'}, 'test'], 14 | 15 | // Null and undefined 16 | [null, 1], 17 | [undefined, 'test'], 18 | 19 | // Array-like objects 20 | [{0: 'a', 1: 'b'}, 'a'], 21 | [{0: 1, 1: 2, 2: 3}, 2], 22 | 23 | // With fromIndex 24 | [{0: 'a', 1: 'b', 2: 'a'}, 'a', 1], 25 | [{0: 1, 1: 2, 2: 1}, 1, -2], 26 | 27 | // Edge cases 28 | [{}, 'any'], 29 | [{a: undefined}, undefined], 30 | [{a: null}, null], 31 | 32 | // Different types 33 | [{a: 1, b: '2', c: true}, true], 34 | [{a: [], b: {}}, []], 35 | 36 | // Large objects 37 | [ 38 | Object.fromEntries( 39 | Array(100) 40 | .fill(0) 41 | .map((_, i) => [i, i]), 42 | ), 43 | 99, 44 | ], 45 | ] 46 | 47 | it.each(testCases)('should match lodash behavior for case %#', (collection, value, fromIndex) => { 48 | expect(includes(collection, value, fromIndex)).toBe(_includes(collection, value, fromIndex)) 49 | }) 50 | 51 | // Additional edge cases 52 | it('should handle empty objects like lodash', () => { 53 | const obj = {} 54 | expect(includes(obj, 'test')).toBe(_includes(obj, 'test')) 55 | }) 56 | 57 | it('should handle prototype chain like lodash', () => { 58 | const proto = {inherited: true} 59 | const obj = Object.create(proto) as Dictionary 60 | obj.own = false 61 | expect(includes(obj, true)).toBe(_includes(obj, true)) 62 | }) 63 | 64 | it('should handle numeric dictionary like lodash', () => { 65 | const numDict: NumericDictionary = {0: 'a', 2: 'c'} 66 | expect(includes(numDict, undefined, 1)).toBe(_includes(numDict, undefined, 1)) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/includes.ts: -------------------------------------------------------------------------------- 1 | import type {PropertyName} from './internal/baseIteratee.type' 2 | import type {Dictionary, NumericDictionary} from './internal/types' 3 | 4 | export function includes( 5 | collection: Dictionary | NumericDictionary | null | undefined, 6 | target: T, 7 | fromIndex?: number, 8 | ): boolean { 9 | if (!collection) { 10 | return false 11 | } 12 | 13 | const values = Object.values(collection as Record) 14 | const length = values.length 15 | 16 | if (length === 0) { 17 | return false 18 | } 19 | 20 | const start = fromIndex ? (fromIndex < 0 ? Math.max(length + fromIndex, 0) : fromIndex) : 0 21 | if (start >= length) { 22 | return false 23 | } 24 | 25 | for (let i = start; i < length; i++) { 26 | if (values[i] === target) { 27 | return true 28 | } 29 | } 30 | 31 | return false 32 | } 33 | 34 | export default includes 35 | -------------------------------------------------------------------------------- /src/internal/array.ts: -------------------------------------------------------------------------------- 1 | export function isLength(value: unknown): value is number { 2 | return typeof value === 'number' && value > -1 && value % 1 === 0 && value <= Number.MAX_SAFE_INTEGER 3 | } 4 | 5 | export function isArrayLike(value: unknown): value is {length: number} { 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | return value != null && typeof value !== 'function' && isLength((value as any).length) 8 | } 9 | -------------------------------------------------------------------------------- /src/internal/baseIteratee.type.ts: -------------------------------------------------------------------------------- 1 | import type {List} from './types' 2 | 3 | export type PropertyName = string | number | symbol 4 | export type PartialShallow = { 5 | [P in keyof T]?: T[P] extends object ? object : T[P] 6 | } 7 | export type IterateeShorthand = [PropertyName, unknown] | PartialShallow | undefined | null 8 | 9 | export type ListIterator = (value: T, index: number, collection: ArrayLike) => TResult 10 | export type ListIteratee = ListIterator | IterateeShorthand 11 | export type ListIterateeCustom = ListIterator | IterateeShorthand 12 | export type ListIteratorTypeGuard = (value: T, index: number, collection: List) => value is S 13 | 14 | export type ValueIteratee = ((value: T) => unknown) | IterateeShorthand 15 | export type ValueKeyIteratee = ((value: T, key: string) => unknown) | IterateeShorthand 16 | export type ValueKeyIterateeTypeGuard = (value: T, key: string) => value is S 17 | 18 | export type ObjectIterator = (value: T[keyof T], key: string, collection: T) => TResult 19 | export type ObjectIteratee = ObjectIterator | IterateeShorthand 20 | export type ObjectIterateeCustom = 21 | | ObjectIterator 22 | | IterateeShorthand 23 | export type ObjectIteratorTypeGuard = ( 24 | value: TObject[keyof TObject], 25 | key: string, 26 | collection: TObject, 27 | ) => value is S 28 | -------------------------------------------------------------------------------- /src/internal/noop.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from 'vitest' 2 | 3 | import {noop} from './noop' 4 | 5 | describe('noop', () => { 6 | it('should be a function', () => { 7 | expect(noop instanceof Function).toBeTruthy() 8 | }) 9 | 10 | it('should return undefined', () => { 11 | const result = noop() 12 | 13 | expect(result).toBeUndefined() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/internal/noop.ts: -------------------------------------------------------------------------------- 1 | export function noop() {} 2 | -------------------------------------------------------------------------------- /src/internal/to-string-tags.ts: -------------------------------------------------------------------------------- 1 | export const STRING_TAG = '[string String]' 2 | 3 | export const STRING_OBJECT_TAG = '[object String]' 4 | 5 | export const WEAK_MAP_TAG = '[object WeakMap]' 6 | 7 | export const WEAK_SET_TAG = '[object WeakSet]' 8 | 9 | export const FUNCTION_TAG = '[object Function]' 10 | 11 | export const NUMBER_OBJECT_TAG = '[object Number]' 12 | 13 | export const SYMBOL_TAG = '[object Symbol]' 14 | 15 | export const DATE_TAG = '[object Date]' 16 | 17 | export const REGEXP_TAG = '[object RegExp]' 18 | 19 | export const MAP_TAG = '[object Map]' 20 | 21 | export const SET_TAG = '[object Set]' 22 | -------------------------------------------------------------------------------- /src/internal/types.ts: -------------------------------------------------------------------------------- 1 | export type Collection = ArrayLike | Record 2 | 3 | export type List = T[] | ArrayLike 4 | 5 | export type Dictionary = Record 6 | export type NumericDictionary = Record 7 | export type AnyKindOfDictionary = Dictionary | NumericDictionary 8 | export type Many = T | Many[] 9 | 10 | export type PropertyPath = Many 11 | 12 | type EmptyObject = {[K in keyof T]?: never} 13 | export type EmptyObjectOf = EmptyObject extends T ? EmptyObject : never 14 | -------------------------------------------------------------------------------- /src/isArray.bench.ts: -------------------------------------------------------------------------------- 1 | import _isArray from 'lodash/isArray' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {isArray} from './isArray' 5 | 6 | const testCases = [ 7 | [], 8 | [1, 2, 3], 9 | {}, 10 | 'string', 11 | 21, 12 | true, 13 | null, 14 | undefined, 15 | Symbol('symbol'), 16 | function () {}, 17 | new Date(), 18 | new Error(), 19 | /regex/, 20 | new Set(), 21 | new Map(), 22 | ] 23 | 24 | const ITERATIONS = 10000 25 | 26 | describe('isArray performance', () => { 27 | bench('hidash', () => { 28 | for (let i = 0; i < ITERATIONS; i++) { 29 | testCases.forEach((testCase) => { 30 | isArray(testCase) 31 | }) 32 | } 33 | }) 34 | 35 | bench('lodash', () => { 36 | for (let i = 0; i < ITERATIONS; i++) { 37 | testCases.forEach((testCase) => { 38 | _isArray(testCase) 39 | }) 40 | } 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/isArray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://unpkg.com/lodash.isarray@4.0.0/index.js 3 | */ 4 | export function isArray(value: unknown): value is unknown[] { 5 | return Array.isArray(value) 6 | } 7 | 8 | export default isArray 9 | -------------------------------------------------------------------------------- /src/isEmpty.bench.ts: -------------------------------------------------------------------------------- 1 | import _isEmpty from 'lodash/isEmpty' 2 | import {bench, describe} from 'vitest' 3 | 4 | import isEmpty from './isEmpty' 5 | 6 | const testCases = [ 7 | null, 8 | undefined, 9 | '', 10 | 'hello', 11 | [], 12 | [1, 2, 3], 13 | {}, 14 | {key: 'value'}, 15 | 0, 16 | 1, 17 | true, 18 | false, 19 | new Date(), 20 | new Map(), 21 | new Set(), 22 | NaN, 23 | Infinity, 24 | ] 25 | 26 | const ITERATIONS = 10000 27 | 28 | describe('isEmpty performance', () => { 29 | bench('hidash', () => { 30 | for (let i = 0; i < ITERATIONS; i++) { 31 | testCases.forEach((testCase) => { 32 | isEmpty(testCase) 33 | }) 34 | } 35 | }) 36 | 37 | bench('lodash', () => { 38 | for (let i = 0; i < ITERATIONS; i++) { 39 | testCases.forEach((testCase) => { 40 | _isEmpty(testCase) 41 | }) 42 | } 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/isEmpty.ts: -------------------------------------------------------------------------------- 1 | import {isArrayLike} from './internal/array' 2 | 3 | import type {EmptyObjectOf} from './internal/types' 4 | 5 | /** 6 | * @see https://unpkg.com/browse/lodash.isempty@4.4.0/index.js 7 | */ 8 | export function isEmpty(value: string): value is '' 9 | export function isEmpty(value: Map | Set | ArrayLike | null | undefined): boolean 10 | export function isEmpty(value: T | null | undefined): value is EmptyObjectOf | null | undefined 11 | export function isEmpty(value?: unknown): boolean 12 | export function isEmpty(value: unknown): boolean { 13 | if (value == null) { 14 | return true 15 | } 16 | 17 | const typeOf = typeof value 18 | if (typeOf === 'number' || typeOf === 'boolean' || typeOf === 'function') { 19 | return true 20 | } 21 | if (isArrayLike(value)) { 22 | return !(value as ArrayLike).length 23 | } 24 | 25 | const type = Object.prototype.toString.call(value) 26 | 27 | if (type === '[object Date]') { 28 | return true 29 | } 30 | 31 | if (type === '[object Map]' || type === '[object Set]') { 32 | return !(value as Map | Set).size 33 | } 34 | 35 | if (type === '[object Object]') { 36 | for (const key in value as object) { 37 | if (Object.prototype.hasOwnProperty.call(value, key)) { 38 | return false 39 | } 40 | } 41 | return true 42 | } 43 | 44 | return false 45 | } 46 | 47 | export default isEmpty 48 | -------------------------------------------------------------------------------- /src/isEqual.bench.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-new-wrappers */ 2 | import _isEqual from 'lodash/isEqual' 3 | import {bench, describe} from 'vitest' 4 | 5 | import {isEqual} from './isEqual' 6 | 7 | const testCases = [ 8 | [[{a: {name: 'jane'}}], [{a: {name: 'jane'}}]], 9 | [NaN, NaN], 10 | [new String('1'), new String('1')], 11 | [{a: [1, 2, {b: 3}]}, {a: [1, 2, {b: 3}]}], 12 | [ 13 | [1, 2, {x: 4, y: [5, 6]}], 14 | [1, 2, {x: 4, y: [5, 6]}], 15 | ], 16 | [{a: 1}, {a: 1}], 17 | [ 18 | [1, 2, 3], 19 | [1, 2, 3], 20 | ], 21 | [{a: {b: {c: 3}}}, {a: {b: {c: 3}}}], 22 | 23 | [ 24 | [{a: 1}, [{b: 2}]], 25 | [{a: 1}, [{b: 2}]], 26 | ], 27 | [{a: {b: {c: {d: {e: {f: {g: 7}}}}}}}, {a: {b: {c: {d: {e: {f: {g: 7}}}}}}}], 28 | [{a: [1, {b: [2, {c: [3, {d: [4, {e: 5}]}]}]}]}, {a: [1, {b: [2, {c: [3, {d: [4, {e: 5}]}]}]}]}], 29 | [ 30 | Array.from({length: 1000}, (_, i) => ({value: i, nested: {value: i * 2}})), 31 | Array.from({length: 1000}, (_, i) => ({value: i, nested: {value: i * 2}})), 32 | ], 33 | ] 34 | 35 | const ITERATIONS = 1000 36 | 37 | describe('isEqual performance', () => { 38 | bench('hidash', () => { 39 | for (let i = 0; i < ITERATIONS; i++) { 40 | testCases.forEach(([value, other]) => { 41 | isEqual(value, other) 42 | }) 43 | } 44 | }) 45 | 46 | bench('lodash', () => { 47 | for (let i = 0; i < ITERATIONS; i++) { 48 | testCases.forEach(([value, other]) => { 49 | _isEqual(value, other) 50 | }) 51 | } 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/isEqual.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-new-wrappers */ 2 | import _isEqual from 'lodash/isEqual' 3 | import {describe, it, expect} from 'vitest' 4 | 5 | import {isEqual} from './isEqual' 6 | 7 | describe('isEqual', () => { 8 | it.concurrent.each([ 9 | [1, 1], 10 | ['abc', 'abc'], 11 | [true, true], 12 | [{a: 1}, {a: 1}], 13 | [ 14 | [1, 2, 3], 15 | [1, 2, 3], 16 | ], 17 | [[{a: 1}], [{a: 1}]], 18 | [[{person: {name: 'jane'}}], [{person: {name: 'jane'}}]], 19 | [new String('1'), new String(1)], 20 | [new Boolean(), new Boolean()], 21 | [!!1, !!2], 22 | [{}, {}], 23 | [NaN, NaN], 24 | [[], []], 25 | [null, null], 26 | [undefined, undefined], 27 | ])('should be equal %o and %o', (a, b) => { 28 | expect(isEqual(a, b)).toEqual(_isEqual(a, b)) 29 | }) 30 | 31 | it.concurrent.each([ 32 | ['a', 'b'], 33 | [1, 2], 34 | [true, false], 35 | [ 36 | [1, 2, 3], 37 | [1, 2], 38 | ], 39 | [{a: 1}, {b: 1}], 40 | [{a: 1}, {a: 2}], 41 | [[{person: {name: 'jane'}}], [{person: {name: 'jane', age: 28}}]], 42 | [!!1, !!0], 43 | [new String(1), new String(2)], 44 | ])('should not be equal %o and %o', (a, b) => { 45 | expect(isEqual(a, b)).toEqual(_isEqual(a, b)) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/isEqual.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 4 | * Performs a **deep equality comparison** between two values. 5 | * Handles special cases such as `NaN` and object references. 6 | * 7 | * This function recursively compares the properties of objects and elements of arrays, 8 | * ensuring that all nested values are equal. 9 | * 10 | * @param {unknown} value The first value to compare 11 | * @param {unknown} other The second value to compare 12 | * @returns {boolean} `true` if the values are equal, `false` otherwise 13 | * 14 | * * @example 15 | * isEqual({ a: 1 }, { a: 1 }); // true 16 | * isEqual([1, 2], [1, 2]); // true 17 | * isEqual(NaN, NaN); // true 18 | * isEqual({ a: { b: 2 } }, { a: { b: 2 } }); // true (deep comparison) 19 | */ 20 | export function isEqual(value: unknown, other: unknown) { 21 | if (typeof value === 'number' && typeof other === 'number') { 22 | if (isNaN(value) && isNaN(other)) { 23 | return true 24 | } 25 | } 26 | 27 | if (value === other) { 28 | return true 29 | } 30 | 31 | if (typeof value === 'object' && value !== null && typeof other === 'object' && other !== null) { 32 | const valueObject = value as Record 33 | const otherObject = other as Record 34 | if (Object.getPrototypeOf(valueObject) !== Object.getPrototypeOf(otherObject)) { 35 | return false 36 | } 37 | 38 | const valueKeys = Object.keys(valueObject) 39 | const otherKeys = Object.keys(otherObject) 40 | 41 | if (valueKeys.length !== otherKeys.length) { 42 | return false 43 | } 44 | 45 | for (const key of valueKeys) { 46 | if (!isEqual(valueObject[key], otherObject[key])) { 47 | return false 48 | } 49 | } 50 | 51 | return true 52 | } 53 | 54 | return false 55 | } 56 | 57 | export default isEqual 58 | -------------------------------------------------------------------------------- /src/isError.ts: -------------------------------------------------------------------------------- 1 | export function isError(value: unknown): value is Error { 2 | if (!value || typeof value !== 'object') { 3 | return false 4 | } 5 | 6 | const proto = Object.getPrototypeOf(value) 7 | return proto !== null && ((proto.constructor && proto.constructor.name === 'Error') || proto instanceof Error) 8 | } 9 | 10 | export default isError 11 | -------------------------------------------------------------------------------- /src/isFunction.bench.ts: -------------------------------------------------------------------------------- 1 | import _isFunction from 'lodash/isFunction' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {isFunction} from './isFunction' 5 | 6 | const testCases = [ 7 | function () {}, 8 | async function () {}, 9 | function* () {}, 10 | new Proxy(function () {}, {}), 11 | {}, 12 | [], 13 | 42, 14 | 'string', 15 | true, 16 | Symbol('symbol'), 17 | null, 18 | undefined, 19 | class MyClass {}, 20 | new Proxy({}, {}), 21 | Reflect, 22 | ] 23 | 24 | const ITERATIONS = 1000 25 | 26 | describe('isFunction performance', () => { 27 | bench('hidash', () => { 28 | for (let i = 0; i < ITERATIONS; i++) { 29 | testCases.forEach((testCase) => { 30 | isFunction(testCase) 31 | }) 32 | } 33 | }) 34 | 35 | bench('lodash', () => { 36 | for (let i = 0; i < ITERATIONS; i++) { 37 | testCases.forEach((testCase) => { 38 | _isFunction(testCase) 39 | }) 40 | } 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/isFunction.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, it, expect} from 'vitest' 2 | 3 | import {noop} from './internal/noop' 4 | import isFunction from './isFunction' 5 | 6 | describe('isFunction', () => { 7 | it('should return true for normal functions', () => { 8 | expect(isFunction(function () {})).toBe(true) 9 | expect(isFunction(noop)).toBe(true) 10 | }) 11 | 12 | it('should return true for async functions', () => { 13 | expect(isFunction(async function () {})).toBe(true) 14 | expect(isFunction(async () => {})).toBe(true) 15 | }) 16 | 17 | it('should return true for generator functions', () => { 18 | function* generatorFunc() {} 19 | expect(isFunction(generatorFunc)).toBe(true) 20 | }) 21 | 22 | it('should return true for Proxy-wrapped functions', () => { 23 | const proxyFunc = new Proxy(noop, {}) 24 | expect(isFunction(proxyFunc)).toBe(true) 25 | }) 26 | 27 | it('should return false for objects', () => { 28 | expect(isFunction({})).toBe(false) 29 | expect(isFunction([])).toBe(false) 30 | expect(isFunction(Object.create(null))).toBe(false) 31 | }) 32 | 33 | it('should return false for primitives', () => { 34 | expect(isFunction(42)).toBe(false) 35 | expect(isFunction('string')).toBe(false) 36 | expect(isFunction(true)).toBe(false) 37 | expect(isFunction(Symbol('symbol'))).toBe(false) 38 | expect(isFunction(null)).toBe(false) 39 | expect(isFunction(undefined)).toBe(false) 40 | }) 41 | 42 | it('should return false for class constructors', () => { 43 | class MyClass {} 44 | expect(isFunction(MyClass)).toBe(true) 45 | }) 46 | 47 | it('should return false for non-function proxies', () => { 48 | const proxyObject = new Proxy({}, {}) 49 | expect(isFunction(proxyObject)).toBe(false) 50 | }) 51 | 52 | it('should return false for Reflect', () => { 53 | expect(isFunction(Reflect)).toBe(false) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/isFunction.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import isObject from './isObject' 3 | 4 | /** 5 | * @description 6 | * Checks if the provided value is a function. 7 | * 8 | * @param {unknown} value The value to check 9 | * @returns {boolean} `true` if the value is a function, `false` otherwise 10 | */ 11 | export function isFunction(value: unknown): value is (...args: any[]) => any { 12 | if (!isObject(value)) { 13 | return false 14 | } 15 | return typeof value === 'function' 16 | } 17 | 18 | export default isFunction 19 | -------------------------------------------------------------------------------- /src/isMap.bench.ts: -------------------------------------------------------------------------------- 1 | import _isMap from 'lodash/isMap' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {isMap} from './isMap' 5 | 6 | const testCases = [new Map(), {}, [], new Set(), null, undefined, 21, 'string', false, Symbol('symbol')] 7 | 8 | const ITERATIONS = 1000 9 | 10 | describe('isMap performance', () => { 11 | bench('hidash', () => { 12 | for (let i = 0; i < ITERATIONS; i++) { 13 | testCases.forEach((testCase) => { 14 | isMap(testCase) 15 | }) 16 | } 17 | }) 18 | 19 | bench('lodash', () => { 20 | for (let i = 0; i < ITERATIONS; i++) { 21 | testCases.forEach((testCase) => { 22 | _isMap(testCase) 23 | }) 24 | } 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/isMap.test.ts: -------------------------------------------------------------------------------- 1 | import _isMap from 'lodash/isMap' 2 | import {describe, expect, it} from 'vitest' 3 | 4 | import {isMap} from './isMap' 5 | 6 | describe('isMap function', () => { 7 | it('should return true for Map instances', () => { 8 | const map = new Map() 9 | expect(isMap(map)).toBe(true) 10 | expect(isMap(map)).toBe(_isMap(map)) 11 | }) 12 | 13 | it('should return false for values that are not Map instances', () => { 14 | expect(isMap({})).toBe(false) 15 | expect(isMap([])).toBe(false) 16 | expect(isMap(new Set())).toBe(false) 17 | expect(isMap(null)).toBe(false) 18 | expect(isMap(undefined)).toBe(false) 19 | expect(isMap(21)).toBe(false) 20 | expect(isMap('string')).toBe(false) 21 | expect(isMap(false)).toBe(false) 22 | expect(isMap(Symbol('symbol'))).toBe(false) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/isMap.ts: -------------------------------------------------------------------------------- 1 | import {MAP_TAG} from './internal/to-string-tags' 2 | 3 | /** 4 | * @description 5 | * Checks if the provided value is a `Map` object. 6 | * 7 | * @param {unknown} value The value to check 8 | * @returns {boolean} `true` if the value is a `Map`, `false` otherwise 9 | */ 10 | export function isMap(value: unknown): value is Map { 11 | return Object.prototype.toString.call(value) === MAP_TAG 12 | } 13 | 14 | export default isMap 15 | -------------------------------------------------------------------------------- /src/isNil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://unpkg.com/browse/lodash.isnil@4.0.0/index.js 3 | */ 4 | export function isNil(value: unknown): value is null | undefined { 5 | return value == null 6 | } 7 | 8 | export default isNil 9 | -------------------------------------------------------------------------------- /src/isNull.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://unpkg.com/lodash@4.17.21/isNull.js 3 | * 4 | * @param value 5 | * @returns boolean 6 | */ 7 | export function isNull(value: unknown): value is null { 8 | return value === null 9 | } 10 | 11 | export default isNull 12 | -------------------------------------------------------------------------------- /src/isNumber.bench.ts: -------------------------------------------------------------------------------- 1 | import _isNumber from 'lodash/isNumber' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {isNumber} from './isNumber' 5 | 6 | const testCases = [ 7 | 0, 8 | 1, 9 | -1, 10 | 0.1, 11 | -0.1, 12 | Infinity, 13 | -Infinity, 14 | NaN, 15 | '123', 16 | '-123', 17 | '0', 18 | '3.14', 19 | '-3.14', 20 | 'string', 21 | null, 22 | undefined, 23 | {}, 24 | [], 25 | [1], 26 | true, 27 | false, 28 | ] 29 | 30 | const ITERATIONS = 1000 31 | 32 | describe('isNumber performance', () => { 33 | bench('hidash', () => { 34 | for (let i = 0; i < ITERATIONS; i++) { 35 | testCases.forEach((testCase) => { 36 | isNumber(testCase) 37 | }) 38 | } 39 | }) 40 | bench('lodash', () => { 41 | for (let i = 0; i < ITERATIONS; i++) { 42 | testCases.forEach((testCase) => { 43 | _isNumber(testCase) 44 | }) 45 | } 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/isNumber.test.ts: -------------------------------------------------------------------------------- 1 | import _isNumber from 'lodash/isNumber' 2 | import {expect, describe, it} from 'vitest' 3 | 4 | import {isNumber} from './isNumber' 5 | 6 | describe('basic number validation', () => { 7 | it('handles integers', () => { 8 | const testCases = [0, 1, -1, 999999] 9 | testCases.forEach((num) => { 10 | expect(isNumber(num)).toBe(_isNumber(num)) 11 | }) 12 | }) 13 | 14 | it('handles decimals', () => { 15 | const testCases = [0.1, -0.1, 3.14, -3.14] 16 | testCases.forEach((num) => { 17 | expect(isNumber(num)).toBe(_isNumber(num)) 18 | }) 19 | }) 20 | 21 | it('handles special numeric values', () => { 22 | const testCases = [Infinity, -Infinity, NaN] 23 | testCases.forEach((num) => { 24 | expect(isNumber(num)).toBe(_isNumber(num)) 25 | }) 26 | }) 27 | }) 28 | 29 | describe('string numbers', () => { 30 | it('handles numeric strings', () => { 31 | const testCases = ['123', '-123', '0', '3.14', '-3.14'] 32 | testCases.forEach((str) => { 33 | expect(isNumber(str)).toBe(_isNumber(str)) 34 | }) 35 | }) 36 | 37 | it('handles invalid strings', () => { 38 | const testCases = ['', 'abc', '12px', '1.2.3'] 39 | testCases.forEach((str) => { 40 | expect(isNumber(str)).toBe(_isNumber(str)) 41 | }) 42 | }) 43 | }) 44 | 45 | describe('other types', () => { 46 | it('handles null and undefined', () => { 47 | const testCases = [null, undefined] 48 | testCases.forEach((value) => { 49 | expect(isNumber(value)).toBe(_isNumber(value)) 50 | }) 51 | }) 52 | 53 | it('handles objects and arrays', () => { 54 | const testCases = [{}, [], [1]] 55 | testCases.forEach((value) => { 56 | expect(isNumber(value)).toBe(_isNumber(value)) 57 | }) 58 | }) 59 | 60 | it('handles booleans', () => { 61 | const testCases = [true, false] 62 | testCases.forEach((value) => { 63 | expect(isNumber(value)).toBe(_isNumber(value)) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/isNumber.ts: -------------------------------------------------------------------------------- 1 | import {NUMBER_OBJECT_TAG} from './internal/to-string-tags' 2 | 3 | export function isNumber(value: unknown): value is number { 4 | return ( 5 | typeof value === 'number' || 6 | (typeof value === 'object' && value !== null && Object.prototype.toString.call(value) === NUMBER_OBJECT_TAG) 7 | ) 8 | } 9 | 10 | export default isNumber 11 | -------------------------------------------------------------------------------- /src/isObject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://unpkg.com/browse/lodash.isobject@3.0.2/index.js 3 | */ 4 | export function isObject(value: unknown): value is object { 5 | const type = typeof value 6 | return !!value && (type === 'object' || type === 'function') 7 | } 8 | 9 | export default isObject 10 | -------------------------------------------------------------------------------- /src/isPlainObject.ts: -------------------------------------------------------------------------------- 1 | export function isPlainObject(value: unknown): value is object { 2 | if (!(value && typeof value === 'object')) { 3 | return false 4 | } 5 | 6 | if (Object.prototype.toString.call(value) !== '[object Object]') { 7 | return false 8 | } 9 | 10 | const proto = Object.getPrototypeOf(Object(value)) 11 | if (proto === null) { 12 | return true 13 | } 14 | 15 | const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor 16 | return ( 17 | typeof Ctor === 'function' && 18 | Ctor instanceof Ctor && 19 | Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object) 20 | ) 21 | } 22 | 23 | export default isPlainObject 24 | -------------------------------------------------------------------------------- /src/isSet.ts: -------------------------------------------------------------------------------- 1 | import {SET_TAG} from './internal/to-string-tags' 2 | 3 | /** 4 | * @description 5 | * Checks if the provided value is a `Set` object. 6 | * 7 | * @param {unknown} value The value to check 8 | * @returns {boolean} `true` if the value is a `Set`, `false` otherwise 9 | */ 10 | export function isSet(value: unknown): value is Set { 11 | return Object.prototype.toString.call(value) === SET_TAG 12 | } 13 | 14 | export default isSet 15 | -------------------------------------------------------------------------------- /src/isString.test.ts: -------------------------------------------------------------------------------- 1 | import _isString from 'lodash/isString' 2 | import {expect, describe, it} from 'vitest' 3 | 4 | import {isString} from './isString' 5 | 6 | describe('basic string validation', () => { 7 | it('handles plain strings', () => { 8 | const testCases = ['hello', 'world', '123', ''] 9 | testCases.forEach((str) => { 10 | expect(isString(str)).toBe(_isString(str)) 11 | }) 12 | }) 13 | 14 | it('handles strings with special characters', () => { 15 | const testCases = ['@#$', 'hello\nworld', ' ', '🔥'] 16 | testCases.forEach((str) => { 17 | expect(isString(str)).toBe(_isString(str)) 18 | }) 19 | }) 20 | }) 21 | 22 | describe('non-string values', () => { 23 | it('handles numbers', () => { 24 | const testCases = [123, -123, 0, 3.14, NaN, Infinity] 25 | testCases.forEach((num) => { 26 | expect(isString(num)).toBe(_isString(num)) 27 | }) 28 | }) 29 | 30 | it('handles objects and arrays', () => { 31 | const testCases = [{}, [], [1], {key: 'value'}] 32 | testCases.forEach((value) => { 33 | expect(isString(value)).toBe(_isString(value)) 34 | }) 35 | }) 36 | 37 | it('handles booleans', () => { 38 | const testCases = [true, false] 39 | testCases.forEach((bool) => { 40 | expect(isString(bool)).toBe(_isString(bool)) 41 | }) 42 | }) 43 | 44 | it('handles null and undefined', () => { 45 | const testCases = [null, undefined] 46 | testCases.forEach((value) => { 47 | expect(isString(value)).toBe(_isString(value)) 48 | }) 49 | }) 50 | }) 51 | 52 | describe('special cases', () => { 53 | it('handles string objects', () => { 54 | // eslint-disable-next-line no-new-wrappers 55 | const testCases = [new String('hello'), new String('')] 56 | testCases.forEach((strObj) => { 57 | expect(isString(strObj)).toBe(_isString(strObj)) 58 | }) 59 | }) 60 | 61 | it('handles symbols', () => { 62 | // eslint-disable-next-line symbol-description 63 | const testCases = [Symbol('hello'), Symbol()] 64 | testCases.forEach((sym) => { 65 | expect(isString(sym)).toBe(_isString(sym)) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/isString.ts: -------------------------------------------------------------------------------- 1 | import {STRING_OBJECT_TAG} from './internal/to-string-tags' 2 | import isArray from './isArray' 3 | 4 | export function isString(value: unknown): value is string { 5 | return ( 6 | typeof value === 'string' || 7 | (!isArray(value) && 8 | typeof value === 'object' && 9 | value !== null && 10 | Object.prototype.toString.call(value) === STRING_OBJECT_TAG) 11 | ) 12 | } 13 | 14 | export default isString 15 | -------------------------------------------------------------------------------- /src/isSymbol.ts: -------------------------------------------------------------------------------- 1 | import {SYMBOL_TAG} from './internal/to-string-tags' 2 | 3 | export function isSymbol(value: unknown): value is symbol { 4 | return ( 5 | typeof value === 'symbol' || 6 | (typeof value === 'object' && value !== null && Object.prototype.toString.call(value) === SYMBOL_TAG) 7 | ) 8 | } 9 | 10 | export default isSymbol 11 | -------------------------------------------------------------------------------- /src/isUndefined.ts: -------------------------------------------------------------------------------- 1 | export function isUndefined(value: unknown): value is undefined { 2 | return value === undefined 3 | } 4 | 5 | export default isUndefined 6 | -------------------------------------------------------------------------------- /src/join.test.ts: -------------------------------------------------------------------------------- 1 | import _join from 'lodash/join' 2 | import {describe, expect, it} from 'vitest' 3 | 4 | import {join} from './join' 5 | 6 | describe('join', () => { 7 | describe('should match lodash behavior', () => { 8 | it('joins array elements with default separator', () => { 9 | const arrays = [ 10 | [1, 2, 3], 11 | ['a', 'b', 'c'], 12 | [true, false, true], 13 | [null, undefined, NaN], 14 | [Infinity, -Infinity, 0], 15 | ['hello', '', 'world'], 16 | ] 17 | 18 | arrays.forEach((array) => { 19 | expect(join(array)).toBe(_join(array)) 20 | }) 21 | }) 22 | 23 | it('joins with custom separator', () => { 24 | const testCases = [ 25 | [[1, 2, 3], '-'], 26 | [['a', 'b', 'c'], '|'], 27 | [[true, false, true], '&'], 28 | [[null, undefined, NaN], ':'], 29 | [[], ','], 30 | ] as const 31 | 32 | testCases.forEach(([array, separator]) => { 33 | expect(join(array, separator)).toBe(_join(array, separator)) 34 | }) 35 | }) 36 | 37 | it('handles empty arrays and various types', () => { 38 | const arrays = [[], [undefined], [null], [NaN], [0], ['']] 39 | 40 | arrays.forEach((array) => { 41 | expect(join(array)).toBe(_join(array)) 42 | }) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/join.ts: -------------------------------------------------------------------------------- 1 | export function join(array: ArrayLike | null | undefined, separator?: string): string { 2 | if (array) { 3 | return Array.prototype.join.call(array, separator) 4 | } else { 5 | return '' 6 | } 7 | } 8 | 9 | export default join 10 | -------------------------------------------------------------------------------- /src/keys.bench.ts: -------------------------------------------------------------------------------- 1 | import _keys from 'lodash/keys' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {keys} from './keys' 5 | 6 | const testCases = [ 7 | // null & undefined 8 | null, 9 | undefined, 10 | 11 | // Arrays 12 | [], 13 | [1, 2, 3], 14 | Array(1000).fill(1), 15 | [...Array(1000).keys()], 16 | 17 | // Objects 18 | {}, 19 | {a: 1, b: 2}, 20 | Object.create(null), 21 | Object.create( 22 | {}, 23 | { 24 | a: {value: 1, enumerable: true}, 25 | b: {value: 2, enumerable: false}, 26 | }, 27 | ), 28 | 29 | // Array-like objects 30 | {length: 2, 0: 'a', 1: 'b'}, 31 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 32 | // @ts-ignore 33 | {length: 1000, ...Array(1000).fill(1)}, 34 | 35 | // String objects 36 | Object(''), 37 | Object('hello'), 38 | 39 | // Other cases 40 | new Date(), 41 | new Map([ 42 | ['a', 1], 43 | ['b', 2], 44 | ]), 45 | new Set([1, 2, 3]), 46 | 47 | // Objects with prototype 48 | Object.create( 49 | {inherited: true}, 50 | { 51 | own: {value: true, enumerable: true}, 52 | }, 53 | ), 54 | 55 | // Large object 56 | { 57 | ...Object.fromEntries( 58 | Array(1000) 59 | .fill(0) 60 | .map((_, i) => [`key${i}`, i]), 61 | ), 62 | }, 63 | 64 | // Object with symbols 65 | {[Symbol('test')]: 'value', regular: 'key'}, 66 | 67 | // Sparse array 68 | Object.assign(Array(1000), {1: 'a', 500: 'b', 999: 'c'}), 69 | ] 70 | 71 | const ITERATIONS = 1000 72 | 73 | describe('keys performance', () => { 74 | bench('hidash', () => { 75 | for (let i = 0; i < ITERATIONS; i++) { 76 | testCases.forEach((testCase) => { 77 | keys(testCase) 78 | }) 79 | } 80 | }) 81 | 82 | bench('lodash', () => { 83 | for (let i = 0; i < ITERATIONS; i++) { 84 | testCases.forEach((testCase) => { 85 | _keys(testCase) 86 | }) 87 | } 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /src/keys.ts: -------------------------------------------------------------------------------- 1 | import {isArrayLike} from './internal/array' 2 | 3 | const nativeKeys = Object.keys 4 | const isArray = Array.isArray 5 | 6 | /** 7 | * @see https://unpkg.com/browse/lodash.keys@4.2.0/index.js 8 | */ 9 | export function keys(object: unknown): string[] { 10 | if (object == null) { 11 | return [] 12 | } 13 | 14 | const obj = Object(object) 15 | 16 | if (isArray(obj)) { 17 | const plainKeys = nativeKeys(obj) 18 | const len = obj.length 19 | if (plainKeys.length === len) { 20 | const result = new Array(len) 21 | let idx = 0 22 | while (idx < len) { 23 | result[idx] = String(idx++) 24 | } 25 | return result 26 | } 27 | const result = new Array(plainKeys.length) 28 | let idx = 0 29 | while (idx < len) { 30 | result[idx] = String(idx++) 31 | } 32 | for (const key of plainKeys) { 33 | const keyNum = +key 34 | if (isNaN(keyNum) || keyNum >= len) { 35 | result[idx++] = key 36 | } 37 | } 38 | return result 39 | } 40 | 41 | if (isArrayLike(obj)) { 42 | return nativeKeys(obj) 43 | } 44 | 45 | return nativeKeys(obj) 46 | } 47 | 48 | export default keys 49 | -------------------------------------------------------------------------------- /src/last.bench.ts: -------------------------------------------------------------------------------- 1 | import _last from 'lodash/last' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {last} from './last' 5 | 6 | const testCases = [ 7 | null, 8 | undefined, 9 | ['a', 'b', 'c'], 10 | [1, 2, 3], 11 | [], 12 | new Array(3), 13 | [null], 14 | [undefined], 15 | [false], 16 | [0], 17 | [''], 18 | [1, 'string', null, undefined, false], 19 | new Array(1000).fill(0).map((_, i) => i), 20 | Object('hello'), 21 | ] 22 | 23 | const ITERATIONS = 1000 24 | 25 | describe('last performance', () => { 26 | bench('hidash', () => { 27 | for (let i = 0; i < ITERATIONS; i++) { 28 | testCases.forEach((testCase) => { 29 | last(testCase) 30 | }) 31 | } 32 | }) 33 | 34 | bench('lodash', () => { 35 | for (let i = 0; i < ITERATIONS; i++) { 36 | testCases.forEach((testCase) => { 37 | _last(testCase) 38 | }) 39 | } 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/last.ts: -------------------------------------------------------------------------------- 1 | export function last(array: T[] | null | undefined): T | undefined { 2 | return array && array.length ? array[array.length - 1] : undefined 3 | } 4 | 5 | export default last 6 | -------------------------------------------------------------------------------- /src/lt.bench.ts: -------------------------------------------------------------------------------- 1 | import _lt from 'lodash/lt' 2 | import {bench, describe} from 'vitest' 3 | 4 | import lt from './lt' 5 | 6 | const testCases = [ 7 | {value: 1, other: 0}, // comparison of numbers 8 | {value: -1, other: -2}, // negative comparison 9 | {value: 0, other: 0}, // comparison of the same numbers 10 | {value: 3.14, other: 2.71}, // prime number comparison 11 | {value: '5', other: '3'}, // numeric strings comparison 12 | {value: 'a', other: 'b'}, // string comparison 13 | {value: '123', other: 456}, // mixing string and numbers 14 | {value: null, other: undefined}, // null and undefined 15 | {value: {}, other: []}, // comparison of objects and arrays 16 | {value: NaN, other: Infinity}, // comparison of special values 17 | ] 18 | 19 | const ITERATIONS = 10000 20 | 21 | describe('lt performance', () => { 22 | bench('hidash', () => { 23 | for (let i = 0; i < ITERATIONS; i++) { 24 | testCases.forEach(({value, other}) => { 25 | lt(value, other) 26 | }) 27 | } 28 | }) 29 | bench('lodash', () => { 30 | for (let i = 0; i < ITERATIONS; i++) { 31 | testCases.forEach(({value, other}) => { 32 | _lt(value, other) 33 | }) 34 | } 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/lt.ts: -------------------------------------------------------------------------------- 1 | import {isNumber} from './isNumber' 2 | import {toNumber} from './toNumber' 3 | 4 | export function lt(value: unknown, other: unknown): boolean { 5 | if (typeof value === 'string' && typeof other === 'string') { 6 | return value < other 7 | } 8 | 9 | const v = isNumber(value) ? (value as number) : toNumber(value) 10 | const o = isNumber(other) ? (other as number) : toNumber(other) 11 | 12 | return v < o 13 | } 14 | 15 | export default lt 16 | -------------------------------------------------------------------------------- /src/map.bench.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import _map from 'lodash/map' 3 | import {bench, describe} from 'vitest' 4 | 5 | import map from './map' 6 | 7 | const testCases: [ArrayLike | null | undefined, any][] = [ 8 | [[1, 2, 3, 4], (x: any) => x * 2], 9 | [['a', 'b', 'c'], (x: any) => x.toUpperCase()], 10 | [[true, false, null, undefined], (x: any) => !!x], 11 | [[{a: 1}, {b: 2}], (x: any) => JSON.stringify(x)], 12 | [Array.from({length: 100}, (_, i) => i), (x: any) => x + 1], 13 | [Array.from('benchmark'), (x: any) => `${x}_test`], 14 | [null, (x: any) => x], 15 | [undefined, (x: any) => x], 16 | [[], (x: any) => x], 17 | ] 18 | 19 | const ITERATIONS = 1000 20 | 21 | describe('map performance', () => { 22 | bench('hidash', () => { 23 | for (let i = 0; i < ITERATIONS; i++) { 24 | testCases.forEach(([input, iteratee]) => { 25 | map(input, iteratee) 26 | }) 27 | } 28 | }) 29 | 30 | bench('lodash', () => { 31 | for (let i = 0; i < ITERATIONS; i++) { 32 | testCases.forEach(([input, iteratee]) => { 33 | _map(input, iteratee) 34 | }) 35 | } 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/map.ts: -------------------------------------------------------------------------------- 1 | import {isArrayLike} from './internal/array' 2 | import {baseIteratee} from './internal/baseIteratee' 3 | import isArray from './isArray' 4 | 5 | import type {ListIterator} from './internal/baseIteratee.type' 6 | 7 | function arrayMap(array: ArrayLike | null | undefined, iteratee: ListIterator): R[] { 8 | if (!array || array.length === 0) { 9 | return [] 10 | } 11 | 12 | const length = array.length 13 | const result: R[] = new Array(length) 14 | 15 | for (let index = 0; index < length; index++) { 16 | result[index] = iteratee(array[index], index, array) 17 | } 18 | 19 | return result 20 | } 21 | 22 | function baseMap(array: ArrayLike | null | undefined, iteratee: ListIterator): R[] { 23 | if (!array || array.length === 0) { 24 | return [] 25 | } 26 | 27 | const length = array.length 28 | const result: R[] = isArrayLike(array) ? Array(array.length) : [] 29 | 30 | for (let index = 0; index < length; index++) { 31 | result[index] = iteratee(array[index], index, array) 32 | } 33 | 34 | return result 35 | } 36 | 37 | export function map(collection: ArrayLike | null | undefined, iteratee: ListIterator): R[] { 38 | const mapper = isArray(collection) ? arrayMap : baseMap 39 | // ensure type matching 40 | return mapper(collection, baseIteratee(iteratee)) 41 | } 42 | 43 | export default map 44 | -------------------------------------------------------------------------------- /src/memoize.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | type AnyFunc = (...args: any[]) => any 3 | 4 | interface MemoizedFunction { 5 | (...args: Parameters): ReturnType 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | cache: Map> 8 | } 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | class MemoizeCache extends Map {} 12 | 13 | /** 14 | * @descriptoin https://unpkg.com/browse/lodash.memoize@4.1.2/index.js 15 | */ 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | export function memoize(func: T, resolver?: (...args: Parameters) => any): MemoizedFunction { 18 | if (typeof func !== 'function' || (resolver != null && typeof resolver !== 'function')) { 19 | throw new TypeError('Expected a function') 20 | } 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | const memoized = function (this: any, ...args: Parameters): ReturnType { 24 | const key = resolver ? resolver.apply(this, args) : args[0] 25 | const cache = memoized.cache 26 | 27 | if (cache.has(key)) { 28 | return cache.get(key)! 29 | } 30 | const result = func.apply(this, args) 31 | cache.set(key, result) 32 | return result 33 | } as MemoizedFunction 34 | 35 | memoized.cache = new MemoizeCache>() 36 | return memoized 37 | } 38 | 39 | memoize.Cache = MemoizeCache 40 | 41 | export default memoize 42 | -------------------------------------------------------------------------------- /src/merge.bench.ts: -------------------------------------------------------------------------------- 1 | import _merge from 'lodash/merge' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {merge} from './merge' 5 | 6 | // simplifies test cases and creates new objects for each test 7 | function getTestCases() { 8 | return [ 9 | // merge default objects 10 | [ 11 | {a: 1, b: 2}, 12 | {c: 3, d: 4}, 13 | ], 14 | 15 | // nested objects 16 | [{a: {b: {c: 1}}}, {a: {b: {d: 2}}}], 17 | 18 | // merge arrays 19 | [{arr: [1, 2, {x: 1}]}, {arr: [3, 4, {y: 2}]}], 20 | 21 | // multi-source (simplification) 22 | [{a: 1}, {b: 2}, {c: 3}], 23 | 24 | // special objects 25 | [{date: new Date(2023, 0, 1)}, {date: new Date(2024, 0, 1)}], 26 | 27 | // medium-sized objects 28 | [ 29 | Object.fromEntries( 30 | Array(10) 31 | .fill(0) 32 | .map((_, i) => [`key${i}`, i]), 33 | ), 34 | Object.fromEntries( 35 | Array(10) 36 | .fill(0) 37 | .map((_, i) => [`key${i}`, i + 10]), 38 | ), 39 | ], 40 | 41 | // shallow superposition structure 42 | [ 43 | {a: {b: 1}, c: {d: 2}}, 44 | {a: {e: 3}, c: {f: 4}}, 45 | ], 46 | 47 | // a small arrangement 48 | [{arr: [1, 2, 3]}, {arr: [4, 5, 6]}], 49 | 50 | // undefined handling 51 | [ 52 | {a: 1, b: 2}, 53 | {a: undefined, c: 3}, 54 | ], 55 | 56 | // basic type mix 57 | [ 58 | {num: 1, str: 'test'}, 59 | {num: 2, bool: true}, 60 | ], 61 | ] 62 | } 63 | 64 | const ITERATIONS = 100 65 | 66 | describe('merge performance', () => { 67 | bench('hidash', () => { 68 | for (let i = 0; i < ITERATIONS; i++) { 69 | getTestCases().forEach((testCase) => { 70 | const [first, ...rest] = testCase 71 | merge(structuredClone(first), ...rest.map((item) => structuredClone(item))) 72 | }) 73 | } 74 | }) 75 | 76 | bench('lodash', () => { 77 | for (let i = 0; i < ITERATIONS; i++) { 78 | getTestCases().forEach((testCase) => { 79 | const [first, ...rest] = testCase 80 | _merge(structuredClone(first), ...rest.map((item) => structuredClone(item))) 81 | }) 82 | } 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/omit.bench.ts: -------------------------------------------------------------------------------- 1 | import _omit from 'lodash/omit' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {omit} from './omit' 5 | 6 | const testCases = [ 7 | // Basic objects 8 | [{a: 1, b: 2, c: 3}, ['a', 'b']], 9 | [{x: 1, y: 2, z: 3, w: 4}, ['x', 'z']], 10 | 11 | // Nested objects 12 | [{a: {b: 2, c: 3}, d: 4}, ['a']], 13 | [{x: {y: {z: 1}}}, ['x.y']], 14 | 15 | // Arrays 16 | [{arr: [1, 2, 3], b: 2}, ['arr']], 17 | [{a: [{b: 1}, {b: 2}]}, ['a.0']], 18 | 19 | // Null & undefined 20 | [null, ['a']], 21 | [undefined, ['a', 'b']], 22 | 23 | // Large objects 24 | [ 25 | Object.fromEntries( 26 | Array(100) 27 | .fill(0) 28 | .map((_, i) => [`key${i}`, i]), 29 | ), 30 | ['key1', 'key50'], 31 | ], 32 | 33 | // Deep paths 34 | [ 35 | { 36 | a: {b: {c: {d: {e: 5}}}}, 37 | }, 38 | ['a.b.c'], 39 | ], 40 | 41 | // Special characters 42 | [{'a.b': 1, 'x.y': 2}, ['a.b']], 43 | [{'a.b.c': 3}, ['a.b.c']], 44 | 45 | // Edge cases 46 | [{}, []], 47 | [{a: 1}, ['nonexistent']], 48 | [{a: undefined, b: null}, ['a', 'b']], 49 | ] as const 50 | 51 | const ITERATIONS = 1000 52 | 53 | describe('omit performance', () => { 54 | bench('hidash', () => { 55 | for (let i = 0; i < ITERATIONS; i++) { 56 | testCases.forEach(([obj, paths]) => { 57 | omit(obj, ...paths) 58 | }) 59 | } 60 | }) 61 | 62 | bench('lodash', () => { 63 | for (let i = 0; i < ITERATIONS; i++) { 64 | testCases.forEach(([obj, paths]) => { 65 | _omit(obj, ...paths) 66 | }) 67 | } 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/omit.test.ts: -------------------------------------------------------------------------------- 1 | import _omit from 'lodash/omit' 2 | import {describe, it, expect} from 'vitest' 3 | 4 | import omit from './omit' 5 | 6 | describe('omit', () => { 7 | it('should match lodash omit behavior with basic objects', () => { 8 | const obj = {a: 1, b: '2', c: 3} 9 | 10 | expect(omit(obj, 'a', 'c')).toEqual(_omit(obj, 'a', 'c')) 11 | expect(omit(obj, 'b')).toEqual(_omit(obj, 'b')) 12 | expect(omit(obj)).toEqual(_omit(obj)) 13 | }) 14 | 15 | it('should handle nested objects', () => { 16 | const obj = { 17 | a: {b: 2, c: 3}, 18 | d: 4, 19 | e: {f: 5}, 20 | } 21 | 22 | expect(omit(obj, 'a', 'e')).toEqual(_omit(obj, 'a', 'e')) 23 | expect(omit(obj, 'd')).toEqual(_omit(obj, 'd')) 24 | }) 25 | 26 | it('should handle null/undefined input', () => { 27 | expect(omit(null, 'a')).toEqual(_omit(null, 'a')) 28 | expect(omit(undefined, 'a')).toEqual(_omit(undefined, 'a')) 29 | }) 30 | 31 | it('should handle array paths', () => { 32 | const obj = {a: 1, b: 2, c: 3, d: 4} 33 | 34 | expect(omit(obj, ['a', 'b'])).toEqual(_omit(obj, ['a', 'b'])) 35 | expect(omit(obj, ['b', 'd'])).toEqual(_omit(obj, ['b', 'd'])) 36 | }) 37 | 38 | it('should handle symbol keys', () => { 39 | const sym = Symbol('test') 40 | const obj = {[sym]: 1, b: 2} 41 | 42 | expect(omit(obj, sym)).toEqual(_omit(obj, sym)) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/omit.ts: -------------------------------------------------------------------------------- 1 | import {PropertyPath} from './internal/types' 2 | 3 | export function omit(object: T | null | undefined, ...paths: PropertyPath[]): T { 4 | if (object == null) { 5 | return {} as T 6 | } 7 | 8 | const result = {...object} 9 | const flattenedPaths: PropertyPath[] = [] 10 | 11 | for (let i = 0; i < paths.length; i++) { 12 | const path = paths[i] 13 | if (Array.isArray(path)) { 14 | for (let j = 0; j < path.length; j++) { 15 | flattenedPaths.push(path[j]) 16 | } 17 | } else { 18 | flattenedPaths.push(path) 19 | } 20 | } 21 | 22 | const size = flattenedPaths.length 23 | for (let i = 0; i < size; i++) { 24 | delete result[flattenedPaths[i] as keyof T] 25 | } 26 | 27 | return result 28 | } 29 | 30 | export default omit 31 | -------------------------------------------------------------------------------- /src/once.bench.ts: -------------------------------------------------------------------------------- 1 | import _once from 'lodash/once' 2 | import {bench, describe} from 'vitest' 3 | 4 | import once from './once' 5 | 6 | const testCase = () => 'result' 7 | 8 | const ITERATIONS = 1000 9 | 10 | describe('once performance', () => { 11 | bench('hidash', () => { 12 | const hidashOnce = once(testCase) 13 | for (let i = 0; i < ITERATIONS; i++) { 14 | hidashOnce() 15 | } 16 | }) 17 | bench('lodash', () => { 18 | const lodashOnce = _once(testCase) 19 | for (let i = 0; i < ITERATIONS; i++) { 20 | lodashOnce() 21 | } 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/once.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, it, expect, vi} from 'vitest' 2 | 3 | import once from './once' 4 | 5 | describe('once', () => { 6 | it('should execute function only once', () => { 7 | const spy = vi.fn().mockReturnValue('result') 8 | const limited = once(spy) 9 | 10 | expect(limited()).toBe('result') 11 | expect(limited()).toBe('result') 12 | expect(limited()).toBe('result') 13 | expect(spy).toHaveBeenCalledTimes(1) 14 | }) 15 | 16 | it('should preserve first result', () => { 17 | let counter = 0 18 | const incrementer = once(() => ++counter) 19 | 20 | expect(incrementer()).toBe(1) 21 | expect(incrementer()).toBe(1) 22 | expect(incrementer()).toBe(1) 23 | expect(counter).toBe(1) 24 | }) 25 | 26 | it('should preserve this context', () => { 27 | const context = {value: 'test'} 28 | const getFn = once(function (this: typeof context) { 29 | return this.value 30 | }) 31 | 32 | expect(getFn.call(context)).toBe('test') 33 | expect(getFn.call({value: 'changed'})).toBe('test') 34 | }) 35 | 36 | it('should preserve arguments from first call', () => { 37 | const spy = vi.fn() 38 | const limited = once(spy) 39 | 40 | limited(1, 'test') 41 | limited(2, 'other') 42 | limited(3, 'another') 43 | 44 | expect(spy).toHaveBeenCalledTimes(1) 45 | expect(spy).toHaveBeenCalledWith(1, 'test') 46 | }) 47 | 48 | it('should handle async functions', async () => { 49 | const asyncFn = vi.fn().mockResolvedValue('result') 50 | const limited = once(asyncFn) 51 | 52 | const result1 = await limited() 53 | const result2 = await limited() 54 | 55 | expect(result1).toBe('result') 56 | expect(result2).toBe('result') 57 | expect(asyncFn).toHaveBeenCalledTimes(1) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/once.ts: -------------------------------------------------------------------------------- 1 | import {before} from './before' 2 | 3 | /** 4 | * Creates a function that is invoked only once. 5 | * Subsequent calls to the new function return the result of the first invocation. 6 | * Internally uses `before(2, func)` to allow the original function to be called only once. 7 | * 8 | * @template T - The type of the function to restrict. 9 | * @param {T} func - The function to invoke once. 10 | * @returns {(...args: Parameters) => ReturnType} A new function that calls `func` at most once. 11 | * 12 | * @example 13 | * const initialize = once(() => { 14 | * console.log('Initialized!'); 15 | * return 'result'; 16 | * }); 17 | * 18 | * initialize(); // Logs: 'Initialized!' and returns 'result' 19 | * initialize(); // Returns 'result' without logging 20 | */ 21 | export function once) => ReturnType>( 22 | func: T, 23 | ): (...args: Parameters) => ReturnType { 24 | return before(2, func) 25 | } 26 | 27 | export default once 28 | -------------------------------------------------------------------------------- /src/pick.bench.ts: -------------------------------------------------------------------------------- 1 | import _pick from 'lodash/pick' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {pick} from './pick' 5 | 6 | const testCases = [ 7 | [{a: 1, b: '2', c: 3}, ['a', 'c']], 8 | [{a: 1, b: '2'}, ['x', 'y']], 9 | [{a: undefined, b: 2}, ['a', 'b']], 10 | [{a: 1, b: 2}, ['a', 'b', 'c']], 11 | [null, ['a']], 12 | [undefined, ['a']], 13 | ] as const 14 | 15 | const ITERATIONS = 10000 16 | 17 | describe('pick performance', () => { 18 | bench('hidash', () => { 19 | for (let i = 0; i < ITERATIONS; i++) { 20 | testCases.forEach(([obj, keys]) => { 21 | pick(obj, keys) 22 | }) 23 | } 24 | }) 25 | 26 | bench('lodash', () => { 27 | for (let i = 0; i < ITERATIONS; i++) { 28 | testCases.forEach(([obj, keys]) => { 29 | _pick(obj, keys) 30 | }) 31 | } 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/pick.test.ts: -------------------------------------------------------------------------------- 1 | import _pick from 'lodash/pick' 2 | import {describe, it, expect} from 'vitest' 3 | 4 | import {pick} from './pick' 5 | 6 | describe('pick', () => { 7 | it('should match lodash pick results', () => { 8 | const obj = {a: 1, b: '2', c: 3} 9 | 10 | expect(pick(obj, ['a', 'c'])).toEqual(pick(obj, ['a', 'c'])) 11 | }) 12 | 13 | it('should return an empty object when no keys match', () => { 14 | const obj = {a: 1, b: '2'} 15 | 16 | expect(pick(obj, ['x', 'y'])).toEqual(_pick(obj, ['x', 'y'])) 17 | }) 18 | 19 | it('should handle null or undefined input gracefully', () => { 20 | expect(pick(null, ['a'])).toEqual(_pick(null, ['a'])) 21 | expect(pick(undefined, ['a'])).toEqual(_pick(undefined, ['a'])) 22 | }) 23 | 24 | it('should pick keys even when values are undefined', () => { 25 | const obj = {a: undefined, b: 2} 26 | 27 | expect(pick(obj, ['a', 'b'])).toEqual(_pick(obj, ['a', 'b'])) 28 | }) 29 | 30 | it('should ignore non-existent keys', () => { 31 | const obj = {a: 1, b: 2} 32 | 33 | expect(pick(obj, ['a', 'b', 'c'])).toEqual(_pick(obj, ['a', 'b', 'c'])) 34 | }) 35 | 36 | it('should preserve type inference', () => { 37 | interface TestObj { 38 | a: number 39 | b: string 40 | c: boolean 41 | } 42 | const obj: TestObj = {a: 42, b: 'hello', c: true} 43 | 44 | expect(pick(obj, ['a', 'c'])).toEqual(_pick(obj, ['a', 'c'])) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/pick.ts: -------------------------------------------------------------------------------- 1 | import flatten from './flatten' 2 | 3 | import type {Many} from './internal/types' 4 | 5 | export function pick(object: T, ...params: Many[]): Pick { 6 | if (object == null) { 7 | return {} as Pick 8 | } 9 | 10 | const keys = flatten(params) 11 | const result: Partial = {} 12 | 13 | for (const key of keys) { 14 | if (key in object) { 15 | result[key] = object[key] 16 | } 17 | } 18 | 19 | return result as Pick 20 | } 21 | 22 | export default pick 23 | -------------------------------------------------------------------------------- /src/pickBy.bench.ts: -------------------------------------------------------------------------------- 1 | import _pickBy from 'lodash/pickBy' 2 | import {bench, describe} from 'vitest' 3 | 4 | import pickBy from './pickBy' 5 | 6 | const testCases = [ 7 | // simple large objects 8 | [ 9 | Object.fromEntries( 10 | Array(1000) 11 | .fill(0) 12 | .map((_, i) => [`key${i}`, i]), 13 | ), 14 | (v) => v > 500, // test that leave only values greater than 500 15 | ], 16 | // sparse large objects 17 | [ 18 | Object.fromEntries( 19 | Array(1000) 20 | .fill(undefined) 21 | .map((_, i) => [`key${i}`, i % 2 === 0 ? i : undefined]), 22 | ), 23 | (v) => v !== undefined, // test that leave only values not undefined 24 | ], 25 | // filter booleans 26 | [ 27 | Object.fromEntries( 28 | Array(1000) 29 | .fill(0) 30 | .map((_, i) => [`key${i}`, i % 2 === 0]), 31 | ), 32 | (v) => typeof v === 'boolean' && v === true, // test that leave only true 33 | ], 34 | // nested objects 35 | [ 36 | { 37 | a: {b: {c: Array(1000).fill({nested: true})}}, 38 | d: {e: {f: Array(1000).fill({nested: false})}}, 39 | }, 40 | (v) => v.nested === true, // test that leaves only values where nested is true 41 | ], 42 | // filter only numbers 43 | [ 44 | {a: 1, b: 2, c: 3, d: 4}, 45 | (v) => typeof v === 'number' && v % 2 === 0, // test to leave only even values 46 | ], 47 | // empty object case 48 | [ 49 | {}, 50 | (v) => !!v, // test to leave only truthy values, also test if it is dealing with an empty object 51 | ], 52 | // filter specific values 53 | [ 54 | {a: null, b: 0, c: false, d: true}, 55 | (v) => v === true, // test that leave only true 56 | ], 57 | ] as const 58 | 59 | const ITERATIONS = 1000 60 | 61 | describe('pickBy performance', () => { 62 | bench('hidash', () => { 63 | for (let i = 0; i < ITERATIONS; i++) { 64 | testCases.forEach(([collection, predicate]) => { 65 | pickBy(collection, predicate) 66 | }) 67 | } 68 | }) 69 | 70 | bench('lodash', () => { 71 | for (let i = 0; i < ITERATIONS; i++) { 72 | testCases.forEach(([collection, predicate]) => { 73 | _pickBy(collection, predicate) 74 | }) 75 | } 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /src/range.bench.ts: -------------------------------------------------------------------------------- 1 | import _range from 'lodash/range' 2 | import {bench, describe} from 'vitest' 3 | 4 | import range from './range' 5 | 6 | const ITERATIONS = 100 7 | const step = 3 8 | 9 | const testCases: [number, number | undefined, number | undefined][] = Array.from({length: 1000}, (_, i) => { 10 | return [0, i, step] 11 | }) 12 | 13 | describe('range performance', () => { 14 | bench('hidash', () => { 15 | for (let i = 0; i < ITERATIONS; i++) { 16 | testCases.forEach(([a, b, c]) => { 17 | range(a, b, c) 18 | }) 19 | } 20 | }) 21 | 22 | bench('lodash', () => { 23 | for (let i = 0; i < ITERATIONS; i++) { 24 | testCases.forEach(([a, b, c]) => { 25 | _range(a, b, c) 26 | }) 27 | } 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/range.test.ts: -------------------------------------------------------------------------------- 1 | import _range from 'lodash/range' 2 | import {describe, test, expect} from 'vitest' 3 | 4 | import range from './range' 5 | 6 | const lodashOfficialDocumentCases: [number, number | undefined, number | undefined, number[]][] = [ 7 | [4, undefined, undefined, [0, 1, 2, 3]], 8 | [-4, undefined, undefined, [0, -1, -2, -3]], 9 | [1, 5, undefined, [1, 2, 3, 4]], 10 | [0, 20, 5, [0, 5, 10, 15]], 11 | [0, -4, -1, [0, -1, -2, -3]], 12 | [1, 4, 0, [1, 1, 1]], 13 | [0, undefined, undefined, []], 14 | ] 15 | 16 | describe('basic functionality (lodash official webpage)', () => { 17 | test.each(lodashOfficialDocumentCases)('range(%i, %s, %s) = %j', (a, b, c, expected) => { 18 | expect(range(a, b, c)).toStrictEqual(expected) 19 | }) 20 | }) 21 | 22 | describe('same functionality', () => { 23 | test.each(lodashOfficialDocumentCases)('range(%i, %s, %s) = %j', (a, b, c) => { 24 | expect(range(a, b, c)).toStrictEqual(_range(a, b, c)) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/repeat.bench.ts: -------------------------------------------------------------------------------- 1 | import _repeat from 'lodash/repeat' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {repeat} from './repeat' 5 | 6 | const testCases: [string, number][] = [ 7 | ['abc', 100000], 8 | ['abc', 1000000], 9 | ['a'.repeat(100000), 5], 10 | ['abcd'.repeat(1000), 10000], 11 | ['test', 0], 12 | ['', 1000000], 13 | ['negative', -5], 14 | ['default', NaN], 15 | ] 16 | 17 | const ITERATIONS = 1000 18 | 19 | describe('repeat performance', () => { 20 | bench('hidash', () => { 21 | for (let i = 0; i < ITERATIONS; i++) { 22 | testCases.forEach(([str, n]) => { 23 | repeat(str, n) 24 | }) 25 | } 26 | }) 27 | 28 | bench('lodash', () => { 29 | for (let i = 0; i < ITERATIONS; i++) { 30 | testCases.forEach(([str, n]) => { 31 | _repeat(str, n) 32 | }) 33 | } 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/repeat.test.ts: -------------------------------------------------------------------------------- 1 | import _repeat from 'lodash/repeat' 2 | import {describe, it, expect} from 'vitest' 3 | 4 | import {repeat} from './repeat' 5 | 6 | describe('repeat', () => { 7 | it.concurrent.each([ 8 | ['*', 3], 9 | ['abc', 2], 10 | ['abc', 0], 11 | ['abc', NaN], 12 | ['abc', Infinity], 13 | ['abc', -1], 14 | ['abc', 1.11], 15 | ['', 3], 16 | ['abc', Number.MAX_SAFE_INTEGER + 1], // Exceeds max safe integer 17 | ])('should repeat the string %o given by n = %d', (str, n) => { 18 | const expected = _repeat(str, n) 19 | expect(repeat(str, n)).toBe(expected) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/repeat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 4 | * Repeats a string `n` times using an efficient algorithm. 5 | * 6 | * @param {string} str The string to repeat 7 | * @param {number} n The number of times to repeat the string 8 | * @returns {string} The string that repeats `str` by `n` 9 | */ 10 | export function repeat(str: string, n: number): string { 11 | // Early type checking avoids unnecessary processing 12 | if (typeof n !== 'number') { 13 | return str 14 | } 15 | 16 | // Simplified validation: combined edge case checks reduce branching 17 | if (str === '' || n <= 0 || n > Number.MAX_SAFE_INTEGER || !Number.isFinite(n) || isNaN(n)) { 18 | return '' 19 | } 20 | 21 | let result = '' 22 | let current = str 23 | let num = Math.floor(n) 24 | 25 | while (num > 0) { 26 | // 1. No string concatenation in loop condition checks 27 | // 2. Simplified bitwise check 28 | if (num % 2 === 1) { 29 | result = result + current 30 | } 31 | current = current + current 32 | num = Math.floor(num / 2) 33 | } 34 | 35 | return result 36 | } 37 | 38 | export default repeat 39 | -------------------------------------------------------------------------------- /src/reverse.bench.ts: -------------------------------------------------------------------------------- 1 | import _reverse from 'lodash/reverse' 2 | import {bench, describe} from 'vitest' 3 | 4 | import reverse from './reverse' 5 | 6 | const testCases = [ 7 | [1, 2, 3, 4, 5], 8 | ['a', 'b', 'c'], 9 | [true, false, true], 10 | [null, undefined, 0], 11 | [], 12 | [1, 'a', null, undefined, true], 13 | Array.from({length: 1000}, (_, i) => i), 14 | ] 15 | 16 | const ITERATIONS = 1000 17 | 18 | describe('reverse performance', () => { 19 | bench('hidash', () => { 20 | for (let i = 0; i < ITERATIONS; i++) { 21 | testCases.forEach((testCase) => { 22 | reverse(testCase) 23 | }) 24 | } 25 | }) 26 | bench('lodash', () => { 27 | for (let i = 0; i < ITERATIONS; i++) { 28 | testCases.forEach((testCase) => { 29 | _reverse(testCase) 30 | }) 31 | } 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/reverse.test.ts: -------------------------------------------------------------------------------- 1 | import _reverse from 'lodash/reverse' 2 | import {describe, expect, it} from 'vitest' 3 | 4 | import reverse from './reverse' 5 | 6 | describe('reverse', () => { 7 | describe('Basic functionality', () => { 8 | const testCases = [[[1, 2, 3, 4, 5]], [['a', 'b', 'c']], [[true, false, true]], [[null, undefined, 0]], [[]]] 9 | 10 | testCases.forEach(([array], index) => { 11 | it(`should match lodash output: basic case ${index + 1}`, () => { 12 | const input = [...array] 13 | expect(reverse(input)).toEqual(_reverse([...array])) 14 | }) 15 | }) 16 | }) 17 | 18 | describe('In-place mutation', () => { 19 | it('should mutate the original array', () => { 20 | const array = [1, 2, 3] 21 | 22 | const result = reverse(array) 23 | 24 | expect(result).toBe(array) 25 | expect(result).toEqual([3, 2, 1]) 26 | 27 | const array2 = [1, 2, 3] 28 | 29 | const _result = _reverse(array2) 30 | 31 | expect(_result).toBe(array2) 32 | expect(_result).toEqual([3, 2, 1]) 33 | }) 34 | }) 35 | 36 | describe('Edge cases', () => { 37 | it('should handle empty arrays', () => { 38 | const array = [] 39 | expect(reverse(array)).toEqual([]) 40 | expect(reverse(array)).toEqual(_reverse(array)) 41 | }) 42 | 43 | it('should handle single-element arrays', () => { 44 | const array = [42] 45 | expect(reverse(array)).toEqual([42]) 46 | expect(reverse(array)).toEqual(_reverse(array)) 47 | }) 48 | 49 | it('should handle arrays with mixed types', () => { 50 | const array = [1, 'a', null, undefined, true] 51 | expect(reverse(array)).toEqual([true, undefined, null, 'a', 1]) 52 | expect(reverse(array)).toEqual(_reverse(array)) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/reverse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Reverses the order of elements in an array `in place` and returns the reversed array. 3 | * 4 | * @template T 5 | * @param {T[]} array - The array to reverse. 6 | * @returns {T[]} The same array with elements in reversed order. 7 | */ 8 | export function reverse(array: T[]): T[] { 9 | return Array.prototype.reverse.call(array) 10 | } 11 | 12 | export default reverse 13 | -------------------------------------------------------------------------------- /src/shuffle.bench.ts: -------------------------------------------------------------------------------- 1 | import _shuffle from 'lodash/shuffle' 2 | import {bench, describe} from 'vitest' 3 | 4 | import shuffle from './shuffle' 5 | 6 | const testCases = [ 7 | null, 8 | undefined, 9 | '', 10 | 'hello', 11 | [], 12 | [1, 2, 3], 13 | {}, 14 | {key: 'value'}, 15 | new Map(), 16 | new Set(), 17 | new Map([ 18 | ['a', 1], 19 | ['b', 2], 20 | ]), 21 | new Set([1, 2, 3]), 22 | {a: 1, b: 2, c: 3}, 23 | Array(1000).fill(0), 24 | 'a'.repeat(1000), 25 | new Array(1000) 26 | .fill(0) 27 | .map((_, i) => [`key${i}`, i]) 28 | .reduce((acc, [k, v]) => ({...acc, [k]: v}), {}), 29 | ] as const 30 | 31 | const ITERATIONS = 1000 32 | 33 | describe('shuffle performance', () => { 34 | bench('hidash', () => { 35 | for (let i = 0; i < ITERATIONS; i++) { 36 | testCases.forEach((testCase) => { 37 | shuffle(testCase) 38 | }) 39 | } 40 | }) 41 | 42 | bench('lodash', () => { 43 | for (let i = 0; i < ITERATIONS; i++) { 44 | testCases.forEach((testCase) => { 45 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 46 | // @ts-ignore 47 | _shuffle(testCase) 48 | }) 49 | } 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/shuffle.ts: -------------------------------------------------------------------------------- 1 | import {isArrayLike} from './internal/array' 2 | import isArray from './isArray' 3 | 4 | import type {Collection} from './internal/types' 5 | 6 | export function shuffle(collection: Collection | null | undefined): T[] { 7 | if (collection == null) { 8 | return [] 9 | } 10 | let result: T[] 11 | if (collection instanceof Map) { 12 | result = [] 13 | const entries = Array.from(collection.entries()) 14 | const entriesLength = entries.length 15 | for (let i = 0; i < entriesLength; i++) { 16 | result.push(entries[i][0] as unknown as T) 17 | result.push(entries[i][1] as T) 18 | } 19 | } else if (collection instanceof Set) { 20 | result = Array.from(collection) 21 | } else if (typeof collection === 'string' || isArray(collection)) { 22 | result = [...collection] as T[] 23 | } else if (typeof collection === 'object') { 24 | if (isArrayLike(collection)) { 25 | result = Array.from(collection as ArrayLike) 26 | } else { 27 | result = Object.values(collection) as T[] 28 | } 29 | } else { 30 | return [] 31 | } 32 | let index = result.length 33 | while (index > 0) { 34 | const rand = Math.floor(Math.random() * index--) 35 | const temp = result[index] 36 | result[index] = result[rand] 37 | result[rand] = temp 38 | } 39 | return result 40 | } 41 | 42 | export default shuffle 43 | -------------------------------------------------------------------------------- /src/size.bench.ts: -------------------------------------------------------------------------------- 1 | import _size from 'lodash/size' 2 | import {bench, describe} from 'vitest' 3 | 4 | import size from './size' 5 | 6 | const testCases = [ 7 | null, 8 | undefined, 9 | '', 10 | 'hello', 11 | [], 12 | [1, 2, 3], 13 | {}, 14 | {key: 'value'}, 15 | new Map(), 16 | new Set(), 17 | new Map([ 18 | ['a', 1], 19 | ['b', 2], 20 | ]), 21 | new Set([1, 2, 3]), 22 | {a: 1, b: 2, c: 3}, 23 | Array(1000).fill(0), 24 | 'a'.repeat(1000), 25 | new Array(1000) 26 | .fill(0) 27 | .map((_, i) => [`key${i}`, i]) 28 | .reduce((acc, [k, v]) => ({...acc, [k]: v}), {}), 29 | ] 30 | 31 | const ITERATIONS = 10000 32 | 33 | describe('size performance', () => { 34 | bench('hidash', () => { 35 | for (let i = 0; i < ITERATIONS; i++) { 36 | testCases.forEach((testCase) => { 37 | size(testCase) 38 | }) 39 | } 40 | }) 41 | 42 | bench('lodash', () => { 43 | for (let i = 0; i < ITERATIONS; i++) { 44 | testCases.forEach((testCase) => { 45 | size(testCase) 46 | }) 47 | } 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/size.ts: -------------------------------------------------------------------------------- 1 | import {isArrayLike} from './internal/array' 2 | import {MAP_TAG, SET_TAG} from './internal/to-string-tags' 3 | 4 | /** 5 | * @see https://unpkg.com/lodash.size@4.2.0/index.js 6 | */ 7 | export function size(collection: unknown): number { 8 | if (collection == null) { 9 | return 0 10 | } 11 | if (isArrayLike(collection)) { 12 | return collection.length 13 | } 14 | 15 | const type = Object.prototype.toString.call(collection) 16 | if (type === MAP_TAG || type === SET_TAG) { 17 | return (collection as Map | Set).size 18 | } 19 | 20 | if (typeof collection === 'object') { 21 | return Object.keys(collection as object).length 22 | } 23 | return 0 24 | } 25 | 26 | export default size 27 | -------------------------------------------------------------------------------- /src/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, ms)) 3 | } 4 | 5 | export default sleep 6 | -------------------------------------------------------------------------------- /src/some.bench.ts: -------------------------------------------------------------------------------- 1 | import _some from 'lodash/some' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {some} from './some' 5 | 6 | const testCases = [ 7 | // simple large arrays 8 | [Array(1000).fill(1), (v) => typeof v === 'number' && v > 500], 9 | [Array(1000).map((_, i) => i), (v) => typeof v === 'number' && v % 2 === 0], 10 | // sparse arrays 11 | [ 12 | Array(1000) 13 | .fill(undefined) 14 | .map((_, i) => (i % 2 === 0 ? i : undefined)), 15 | (v) => v === undefined, 16 | ], 17 | // large objects 18 | [ 19 | Object.fromEntries( 20 | Array(1000) 21 | .fill(0) 22 | .map((_, i) => [`key${i}`, i]), 23 | ), 24 | (v) => v > 999, 25 | ], 26 | [ 27 | Object.fromEntries( 28 | Array(1000) 29 | .fill(0) 30 | .map((_, i) => [`key${i}`, {nestedKey: i}]), 31 | ), 32 | (v) => v.nestedKey > 999, 33 | ], 34 | // deeply nested objects 35 | [ 36 | { 37 | a: {b: {c: Array(1000).fill({nested: true})}}, 38 | d: {e: {f: Array(1000).fill({nested: false})}}, 39 | }, 40 | (v) => v.nested === true, 41 | ], 42 | // mixed large arrays 43 | [Array(1000).map((_, i) => (i % 3 === 0 ? 'string' : i % 5 === 0 ? NaN : i)), (v) => typeof v === 'string'], 44 | // large falsy arrays 45 | [ 46 | Array(1000) 47 | .fill(0) 48 | .map((_, i) => (i === 999 ? 1 : 0)), 49 | (v) => v === 1, 50 | ], 51 | // large truthy arrays 52 | [ 53 | Array(1000) 54 | .fill(false) 55 | .map((_, i) => i === 999), 56 | (v) => v === true, 57 | ], 58 | ] as const 59 | 60 | const ITERATIONS = 1000 61 | 62 | describe('some performance', () => { 63 | bench('hidash', () => { 64 | for (let i = 0; i < ITERATIONS; i++) { 65 | testCases.forEach(([collection, predicate]) => { 66 | some(collection, predicate) 67 | }) 68 | } 69 | }) 70 | 71 | bench('lodash', () => { 72 | for (let i = 0; i < ITERATIONS; i++) { 73 | testCases.forEach(([collection, predicate]) => { 74 | _some(collection, predicate) 75 | }) 76 | } 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /src/some.ts: -------------------------------------------------------------------------------- 1 | import {isArrayLike} from './internal/array' 2 | import {baseIteratee} from './internal/baseIteratee' 3 | import isPlainObject from './isPlainObject' 4 | import keys from './keys' 5 | 6 | import type {ListIterateeCustom, ObjectIterateeCustom} from './internal/baseIteratee.type' 7 | import type {List} from './internal/types' 8 | 9 | /** 10 | * @description 11 | * Checks if **any** element in a collection satisfies a given predicate. 12 | * If no predicate is provided, it checks if the collection is empty. 13 | * 14 | * This function is similar to `Array.prototype.some` but works with various types of collections. 15 | * 16 | * @param {List | object} [collection] The collection to iterate over 17 | * @param {ListIterateeCustom | ObjectIterateeCustom} [predicate] The function invoked per iteration 18 | * @returns {boolean} `true` if any element satisfies the predicate, `false` otherwise 19 | */ 20 | export function some(collection: List | null | undefined, predicate?: ListIterateeCustom): boolean 21 | export function some( 22 | collection: T | null | undefined, 23 | predicate?: ObjectIterateeCustom, 24 | ): boolean 25 | export function some( 26 | collection: unknown, 27 | predicate?: ListIterateeCustom | ObjectIterateeCustom, 28 | ) { 29 | if (!predicate && keys(collection).length === 0) { 30 | return false 31 | } 32 | 33 | const iteratee = baseIteratee(predicate) 34 | 35 | if (isPlainObject(collection)) { 36 | for (const key in collection) { 37 | if ( 38 | Object.prototype.hasOwnProperty.call(collection, key) && 39 | iteratee(collection[key as keyof typeof collection], 0, []) 40 | ) { 41 | return true 42 | } 43 | } 44 | } 45 | 46 | if (isArrayLike(collection)) { 47 | const arrayLike = collection as ArrayLike 48 | 49 | const collectionLength = collection.length 50 | for (let i = 0; i < collectionLength; i++) { 51 | if (iteratee(arrayLike[i], i, arrayLike)) { 52 | return true 53 | } 54 | } 55 | } 56 | 57 | return false 58 | } 59 | 60 | export default some 61 | -------------------------------------------------------------------------------- /src/sum.bench.ts: -------------------------------------------------------------------------------- 1 | import _sum from 'lodash/sum' 2 | import {bench, describe} from 'vitest' 3 | 4 | import sum from './sum' 5 | 6 | const testCases = [ 7 | // Empty array 8 | [[]], 9 | // Simple arrays 10 | [[1, 2, 3, 4, 5]], 11 | [[0.1, 0.2, 0.3]], 12 | // Negative numbers 13 | [[-1, -2, -3, -4, -5]], 14 | // Mixed positive and negative 15 | [[1, -2, 3, -4, 5]], 16 | // Large arrays 17 | [Array(1000).fill(1)], 18 | [Array(1000).map((_, i) => i)], 19 | // Decimal numbers 20 | [[0.1, 0.01, 0.001, 0.0001]], 21 | // Large numbers 22 | [[Number.MAX_SAFE_INTEGER, 1, 2, 3]], 23 | // Small numbers 24 | [[Number.MIN_SAFE_INTEGER, -1, -2, -3]], 25 | // Arrays with zeros 26 | [[0, 0, 0, 0, 0]], 27 | [[1, 0, 2, 0, 3]], 28 | // Single element 29 | [[42]], 30 | // Repeated numbers 31 | [[1, 1, 1, 1, 1]], 32 | // Scientific notation 33 | [[1e5, 1e4, 1e3]], 34 | // Various lengths 35 | [Array(10).fill(1)], 36 | [Array(100).fill(1)], 37 | [Array(1000).fill(1)], 38 | // Random numbers 39 | [Array(100).map(() => Math.random() * 100)], 40 | // Edge cases with Number limits 41 | [[Number.MAX_VALUE, 1]], 42 | [[Number.MIN_VALUE, -1]], 43 | ] as const 44 | 45 | const ITERATIONS = 1000 46 | 47 | describe('sum performance', () => { 48 | bench('hidash', () => { 49 | for (let i = 0; i < ITERATIONS; i++) { 50 | testCases.forEach(([arr]) => { 51 | sum(arr) 52 | }) 53 | } 54 | }) 55 | 56 | bench('lodash', () => { 57 | for (let i = 0; i < ITERATIONS; i++) { 58 | testCases.forEach(([arr]) => { 59 | _sum(arr) 60 | }) 61 | } 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/sum.test.ts: -------------------------------------------------------------------------------- 1 | import _sum from 'lodash/sum' 2 | import {describe, it, expect} from 'vitest' 3 | 4 | import sum from './sum' 5 | 6 | describe('sum', () => { 7 | it('number array check', () => { 8 | expect(sum([])).toBe(0) 9 | expect(sum([])).toEqual(_sum([])) 10 | expect(sum([4, 2, 8, 6])).toBe(20) 11 | expect(sum([4, 2, 8, 6])).toEqual(_sum([4, 2, 8, 6])) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/sum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://unpkg.com/lodash.sum lodash.sum is only compatible with number arrays 3 | */ 4 | export function sum(elements: number[]): number { 5 | let result = 0 6 | const elementLength = elements.length 7 | for (let i = 0; i < elementLength; i++) { 8 | result += elements[i] 9 | } 10 | return result 11 | } 12 | 13 | export default sum 14 | -------------------------------------------------------------------------------- /src/sumBy.test.ts: -------------------------------------------------------------------------------- 1 | import _sumBy from 'lodash/sumBy' 2 | import {describe, it, expect} from 'vitest' 3 | 4 | import sumBy from './sumBy' 5 | 6 | describe('sumBy', () => { 7 | const elementOneDepth = [{n: 2}, {n: 4}, {n: 6}, {n: 8}] 8 | const elementTwoDepth = [{n: {m: 2}}, {n: {m: 4}}, {n: {m: 6}}, {n: {m: 8}}] 9 | 10 | it('sumBy by function ', () => { 11 | expect( 12 | sumBy(elementOneDepth, function ({n}) { 13 | return n 14 | }), 15 | ).toBe(20) 16 | 17 | expect( 18 | sumBy(elementOneDepth, function ({n}) { 19 | return n 20 | }), 21 | ).toBe( 22 | _sumBy(elementOneDepth, function ({n}) { 23 | return n 24 | }), 25 | ) 26 | 27 | expect( 28 | sumBy(elementTwoDepth, function ({n: {m}}) { 29 | return m 30 | }), 31 | ).toBe(20) 32 | 33 | expect( 34 | sumBy(elementTwoDepth, function ({n: {m}}) { 35 | return m 36 | }), 37 | ).toBe( 38 | _sumBy(elementTwoDepth, function ({n: {m}}) { 39 | return m 40 | }), 41 | ) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/sumBy.ts: -------------------------------------------------------------------------------- 1 | import {baseIteratee} from './internal/baseIteratee' 2 | 3 | import type {ValueIteratee} from './internal/baseIteratee.type' 4 | 5 | export function sumBy(elements: T[], iteratee: ValueIteratee): number { 6 | const len = elements.length 7 | let sum = 0 8 | 9 | const iterateeFn = baseIteratee(iteratee) 10 | 11 | for (let i = 0; i < len; i++) { 12 | sum += iterateeFn(elements[i], i, elements) as number 13 | } 14 | 15 | return sum 16 | } 17 | 18 | export default sumBy 19 | -------------------------------------------------------------------------------- /src/times.test.ts: -------------------------------------------------------------------------------- 1 | import _times from 'lodash/times' 2 | import {describe, it, expect} from 'vitest' 3 | 4 | import {times} from './times' 5 | 6 | describe('times function', () => { 7 | it('matches lodash for basic cases', () => { 8 | const testCases = [0, 1, 2, 5, 10, 50] 9 | testCases.forEach((n) => { 10 | expect(times(n)).toStrictEqual(_times(n)) 11 | expect(times(n, (i) => i * 2)).toStrictEqual(_times(n, (i) => i * 2)) 12 | expect(times(n, (i) => `str-${i}`)).toStrictEqual(_times(n, (i) => `str-${i}`)) 13 | }) 14 | }) 15 | 16 | it('returns empty array for invalid input', () => { 17 | const invalidCases = [-1, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, 1.5] 18 | invalidCases.forEach((n) => { 19 | expect(times(n)).toStrictEqual([]) 20 | }) 21 | }) 22 | 23 | it('handles large but valid values', () => { 24 | const largeN = 10000 25 | const arr = times(largeN, (i) => i) 26 | expect(arr.length).toBe(largeN) 27 | expect(arr[largeN - 1]).toBe(largeN - 1) 28 | }) 29 | 30 | it('uses identity as default iteratee', () => { 31 | expect(times(3)).toStrictEqual([0, 1, 2]) 32 | expect(_times(3)).toStrictEqual([0, 1, 2]) 33 | }) 34 | 35 | it('handles custom iteratee that returns objects', () => { 36 | const n = 5 37 | const arr = times(n, (i) => ({index: i, val: i * 10})) 38 | expect(arr).toHaveLength(5) 39 | expect(arr[0]).toEqual({index: 0, val: 0}) 40 | expect(arr[4]).toEqual({index: 4, val: 40}) 41 | }) 42 | 43 | it('handles empty arrays correctly', () => { 44 | expect(times(0)).toStrictEqual([]) 45 | expect(times(-100)).toStrictEqual([]) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/times.ts: -------------------------------------------------------------------------------- 1 | export function times(n: number): number[] 2 | export function times(n: number, iteratee: (num: number) => TResult): TResult[] 3 | export function times(n: number, iteratee?: (num: number) => TResult): (number | TResult)[] { 4 | if (!Number.isInteger(n) || n < 1 || n >= Number.MAX_SAFE_INTEGER) { 5 | return [] 6 | } 7 | 8 | const nTimes = n 9 | const result = new Array(nTimes) 10 | 11 | if (iteratee) { 12 | for (let i = 0; i < nTimes; i++) { 13 | result[i] = iteratee(i) 14 | } 15 | } else { 16 | for (let i = 0; i < nTimes; i++) { 17 | result[i] = i 18 | } 19 | } 20 | 21 | return result 22 | } 23 | 24 | export default times 25 | -------------------------------------------------------------------------------- /src/toNumber.bench.ts: -------------------------------------------------------------------------------- 1 | import _toNumber from 'lodash/toNumber' 2 | import {bench, describe} from 'vitest' 3 | 4 | import toNumber from './toNumber' 5 | 6 | // includes various data types and edge cases 7 | const testCases = [ 8 | null, 9 | undefined, 10 | '', 11 | '123', 12 | ' 456 ', 13 | '0x1f', 14 | '0b101', 15 | '0o17', 16 | 'hello', 17 | [], 18 | [42], 19 | [1, 2], 20 | {valueOf: () => 42}, 21 | true, 22 | false, 23 | 0, 24 | 1, 25 | NaN, 26 | Infinity, 27 | new Date(), 28 | new Map(), 29 | new Set(), 30 | ] 31 | 32 | const ITERATIONS = 10000 33 | 34 | describe('toNumber performance', () => { 35 | bench('hidash', () => { 36 | for (let i = 0; i < ITERATIONS; i++) { 37 | testCases.forEach((testCase) => { 38 | toNumber(testCase) 39 | }) 40 | } 41 | }) 42 | 43 | bench('lodash', () => { 44 | for (let i = 0; i < ITERATIONS; i++) { 45 | testCases.forEach((testCase) => { 46 | _toNumber(testCase) 47 | }) 48 | } 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/toNumber.ts: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | import isSymbol from './isSymbol' 3 | 4 | function baseTrim(string: string) { 5 | return string ? string.trim() : string 6 | } 7 | 8 | const NAN = NaN // Number.isNaN(0/0) true 9 | 10 | const reIsBadHex = /^[-+]0x[0-9a-f]+$/i 11 | const reIsBinary = /^0b[01]+$/i 12 | const reIsOctal = /^0o[0-7]+$/i 13 | const freeParseInt = Number.parseInt 14 | 15 | export function toNumber(value: unknown): number { 16 | if (typeof value === 'number') { 17 | return value 18 | } 19 | 20 | if (typeof value === 'boolean') { 21 | return value ? 1 : 0 22 | } 23 | 24 | if (value === null) { 25 | return 0 26 | } 27 | 28 | if (value === undefined) { 29 | return NAN 30 | } 31 | 32 | if (typeof value === 'string') { 33 | const trimmedValue = baseTrim(value) 34 | 35 | if (reIsBinary.test(trimmedValue)) { 36 | return freeParseInt(trimmedValue.slice(2), 2) 37 | } 38 | 39 | if (reIsOctal.test(trimmedValue)) { 40 | return freeParseInt(trimmedValue.slice(2), 8) 41 | } 42 | 43 | if (reIsBadHex.test(trimmedValue)) { 44 | return NAN 45 | } 46 | 47 | const result = +trimmedValue 48 | return isNaN(result) ? NAN : result 49 | } 50 | 51 | if (isSymbol(value)) { 52 | return NAN 53 | } 54 | 55 | if (Array.isArray(value)) { 56 | if (value.length === 0) { 57 | return 0 58 | } 59 | if (value.length === 1) { 60 | return toNumber(value[0]) 61 | } 62 | return NAN 63 | } 64 | 65 | if (isObject(value)) { 66 | const primitiveValue = value.valueOf() 67 | if (typeof primitiveValue === 'number') { 68 | return primitiveValue 69 | } 70 | 71 | if (typeof primitiveValue === 'string') { 72 | return toNumber(primitiveValue) 73 | } 74 | } 75 | 76 | return NAN 77 | } 78 | 79 | export default toNumber 80 | -------------------------------------------------------------------------------- /src/toPairs.bench.ts: -------------------------------------------------------------------------------- 1 | import _toPairs from 'lodash/toPairs' 2 | import {bench, describe} from 'vitest' 3 | 4 | import toPairs from './toPairs' 5 | 6 | const testCases = [ 7 | 'a'.repeat(100), 8 | Array.from({length: 100}, (_, i) => i), 9 | Array.from({length: 100}, (_, i) => [`key${i}`, {value: i, nested: {child: i * 2}}]), 10 | new Map(Array.from({length: 100}, (_, i) => [`key${i}`, `value${i}`])), 11 | new Set(Array.from({length: 100}, (_, i) => i)), 12 | ] 13 | 14 | const ITERATIONS = 1000 15 | 16 | describe('toPairs performance', () => { 17 | bench('hidash', () => { 18 | for (let i = 0; i < ITERATIONS; i++) { 19 | testCases.forEach((testCase) => { 20 | toPairs(testCase) 21 | }) 22 | } 23 | }) 24 | 25 | bench('lodash', () => { 26 | for (let i = 0; i < ITERATIONS; i++) { 27 | testCases.forEach((testCase) => { 28 | _toPairs(testCase) 29 | }) 30 | } 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/toPairs.ts: -------------------------------------------------------------------------------- 1 | import {AnyKindOfDictionary} from './internal/types' 2 | import isArray from './isArray' 3 | import isEmpty from './isEmpty' 4 | import isMap from './isMap' 5 | import isPlainObject from './isPlainObject' 6 | import isSet from './isSet' 7 | import isString from './isString' 8 | 9 | /** 10 | * @description 11 | * Converts a given object, array, or collection into an array of key-value pairs. 12 | * 13 | * @param {AnyKindOfDictionary | object | Map | Set} [object] The object to convert 14 | * @returns {[Key, Value][]} An array of key-value pairs 15 | * 16 | * @example 17 | * toPairs({ a: 1, b: 2 }) // [['a', 1], ['b', 2]] 18 | * toPairs([1, 2, 3]) // [['0', 1], ['1', 2], ['2', 3]] 19 | * toPairs(new Map([['a', 1], ['b', 2]])) // [['a', 1], ['b', 2]] 20 | * toPairs(new Set([1, 2, 3])) // [[1, 1], [2, 2], [3, 3]] 21 | * toPairs('abc') // [['0', 'a'], ['1', 'b'], ['2', 'c']] 22 | * toPairs(null) // [], but type error 23 | */ 24 | export function toPairs( 25 | object?: AnyKindOfDictionary | object | Map | Set, 26 | ): [Key, Value][] { 27 | /** 28 | * Compared to Lodash, this implementation: 29 | * - Directly handles multiple types (object, array, string, Map, Set, null/undefined) in a single optimized function. 30 | * - Minimizes unnecessary iterations and conversions for better performance. 31 | * - Uses early returns for empty or nullish values. 32 | * - Leverages TypeScript for enhanced type safety. 33 | */ 34 | if (!object) { 35 | return [] 36 | } 37 | 38 | if (isString(object)) { 39 | return object.split('').map((char, index) => [String(index) as unknown as Key, char as unknown as Value]) 40 | } 41 | 42 | if (isArray(object)) { 43 | return object.map((value, index) => [String(index) as unknown as Key, value as Value]) 44 | } 45 | 46 | if (isEmpty(object)) { 47 | return [] 48 | } 49 | 50 | if (isPlainObject(object)) { 51 | return Object.entries(object) as [Key, Value][] 52 | } 53 | 54 | if (isMap(object)) { 55 | return [...(object as unknown as Map).entries()] 56 | } 57 | 58 | if (isSet(object)) { 59 | return [...(object as unknown as Set).entries()] as unknown as [Key, Value][] 60 | } 61 | 62 | return [] 63 | } 64 | 65 | export default toPairs 66 | -------------------------------------------------------------------------------- /src/toString.bench.ts: -------------------------------------------------------------------------------- 1 | import _toString from 'lodash/toString' 2 | import {bench, describe} from 'vitest' 3 | 4 | import toString from './toString' 5 | 6 | const testCases = [ 7 | {value: 123}, 8 | {value: -0}, 9 | {value: ''}, 10 | {value: 'hello'}, 11 | {value: true}, 12 | {value: Symbol('test')}, 13 | 14 | // null, undefined 15 | {value: null}, 16 | {value: undefined}, 17 | 18 | // array 19 | {value: []}, 20 | {value: [1, 2, 3]}, 21 | {value: ['a', 'b', 'c']}, 22 | {value: [1, null, 'mixed', undefined]}, 23 | 24 | // objects 25 | {value: {}}, 26 | {value: {toString: () => 'custom'}}, 27 | 28 | // special values 29 | {value: NaN}, 30 | {value: Infinity}, 31 | {value: -Infinity}, 32 | 33 | // nested structures 34 | { 35 | value: [ 36 | [1, 2], 37 | [3, 4], 38 | ], 39 | }, 40 | {value: {a: {b: {c: 1}}}}, 41 | {value: [1, {a: 2}, [3, {b: 4}]]}, 42 | ] 43 | 44 | const ITERATIONS = 10000 45 | 46 | describe('toString performance', () => { 47 | bench('hidash', () => { 48 | for (let i = 0; i < ITERATIONS; i++) { 49 | testCases.forEach(({value}) => { 50 | toString(value) 51 | }) 52 | } 53 | }) 54 | 55 | bench('lodash', () => { 56 | for (let i = 0; i < ITERATIONS; i++) { 57 | testCases.forEach(({value}) => { 58 | _toString(value) 59 | }) 60 | } 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/toString.test.ts: -------------------------------------------------------------------------------- 1 | import _toString from 'lodash/toString' 2 | import {describe, expect, it} from 'vitest' 3 | 4 | import toString from './toString' 5 | 6 | describe('toString', () => { 7 | describe('should match lodash behavior', () => { 8 | it('primitive values', () => { 9 | const values = [null, undefined, '', 'hello', 0, -0, 42, -42, true, false, NaN, Infinity, -Infinity] 10 | 11 | values.forEach((value) => { 12 | expect(toString(value)).toBe(_toString(value)) 13 | }) 14 | }) 15 | 16 | it('arrays', () => { 17 | const arrays = [ 18 | [], 19 | [1, 2, 3], 20 | ['a', 'b', 'c'], 21 | [null, undefined], 22 | [true, false], 23 | [1, [2, [3, 4]], 5], 24 | [1, 'hello', null, undefined, [2, 3]], 25 | ] 26 | 27 | arrays.forEach((arr) => { 28 | expect(toString(arr)).toBe(_toString(arr)) 29 | }) 30 | }) 31 | 32 | it('objects', () => { 33 | const objects = [{}, {toString: () => 'custom'}] 34 | 35 | objects.forEach((obj) => { 36 | expect(toString(obj)).toBe(_toString(obj)) 37 | }) 38 | }) 39 | 40 | it('symbols', () => { 41 | const symbols = [Symbol('test'), Symbol(''), Symbol.for('test')] 42 | 43 | symbols.forEach((symbol) => { 44 | expect(toString(symbol)).toBe(_toString(symbol)) 45 | }) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/toString.ts: -------------------------------------------------------------------------------- 1 | import isArray from './isArray' 2 | import isString from './isString' 3 | import isSymbol from './isSymbol' 4 | 5 | export function toString(value: unknown): string { 6 | if (value == null) { 7 | return '' 8 | } 9 | 10 | if (isString(value)) { 11 | return value 12 | } 13 | 14 | if (isArray(value)) { 15 | if (value.length === 0) { 16 | return '' 17 | } 18 | return ( 19 | value 20 | /** 21 | * @see lodash why this way? 22 | */ 23 | .map((item) => (item === null ? 'null' : item === undefined ? 'undefined' : toString(item))) 24 | .join(',') 25 | ) 26 | } 27 | 28 | if (isSymbol(value)) { 29 | return value.toString() 30 | } 31 | 32 | if (typeof value === 'object') { 33 | if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) { 34 | return value.toString() 35 | } 36 | 37 | return Object.prototype.toString.call(value) 38 | } 39 | 40 | if (typeof value === 'number' && Object.is(value, -0)) { 41 | return '-0' 42 | } 43 | 44 | return String(value) 45 | } 46 | 47 | export default toString 48 | -------------------------------------------------------------------------------- /src/transform.ts: -------------------------------------------------------------------------------- 1 | import isArray from './isArray' 2 | 3 | type Keys = T extends readonly unknown[] ? number : keyof T 4 | type ValueOf = T extends readonly (infer U)[] ? U : T[keyof T] 5 | 6 | export function transform, R>( 7 | object: T | null | undefined, 8 | iteratee: (acc: R, value: ValueOf, key: Keys, obj: T) => boolean | void, 9 | accumulator?: R, 10 | ): R { 11 | const obj = Object(object) as T 12 | const isArr = isArray(obj) 13 | const acc: R = accumulator !== undefined ? accumulator : ((isArr ? [] : {}) as unknown as R) 14 | 15 | if (isArr) { 16 | const arr = obj as unknown[] 17 | const size = arr.length 18 | for (let i = 0; i < size; i++) { 19 | const key = i as Keys 20 | const value = arr[i] as ValueOf 21 | const result = iteratee(acc, value, key, obj) 22 | if (result === false) { 23 | break 24 | } 25 | } 26 | } else { 27 | const objKeys = Object.keys(obj) as Keys[] 28 | const rec = obj as Record 29 | for (let i = 0; i < objKeys.length; i++) { 30 | const key = objKeys[i] 31 | const value = rec[key as keyof typeof rec] as ValueOf 32 | const result = iteratee(acc, value, key, obj) 33 | if (result === false) { 34 | break 35 | } 36 | } 37 | } 38 | 39 | return acc 40 | } 41 | 42 | export default transform 43 | -------------------------------------------------------------------------------- /src/trim.bench.ts: -------------------------------------------------------------------------------- 1 | import _trim from 'lodash/trim' 2 | import {bench, describe} from 'vitest' 3 | 4 | import trim from './trim' 5 | 6 | const testCases = [ 7 | {value: ' hello '}, 8 | {value: '\tworld\t'}, 9 | {value: '\ntrim me\n'}, 10 | {value: ' no-space'}, 11 | {value: 'space-only '}, 12 | {value: ' mixed spaces \t \n'}, 13 | {value: ''}, 14 | {value: ' '}, 15 | 16 | {value: ['--hello--', '-']}, 17 | {value: ['__world__', '_']}, 18 | {value: ['##test##', '#']}, 19 | {value: ['!!important!!', '!']}, 20 | {value: ['abcdeabc', 'abc']}, 21 | {value: ['---test--', '-']}, 22 | {value: ['xxtrimxx', 'x']}, 23 | 24 | {value: [null, undefined]}, 25 | {value: ['$$$hello$$$', '$']}, 26 | {value: ['@@@world@@@', '@']}, 27 | {value: ['###javascript###', '#']}, 28 | {value: ['***code***', '*']}, 29 | ] 30 | 31 | const ITERATIONS = 10000 32 | 33 | describe('trim performance', () => { 34 | bench('hidash', () => { 35 | for (let i = 0; i < ITERATIONS; i++) { 36 | testCases.forEach(({value}) => { 37 | Array.isArray(value) ? trim(value[0], value[1]) : trim(value as string) 38 | }) 39 | } 40 | }) 41 | 42 | bench('lodash', () => { 43 | for (let i = 0; i < ITERATIONS; i++) { 44 | testCases.forEach(({value}) => { 45 | Array.isArray(value) ? _trim(value[0], value[1]) : _trim(value as string) 46 | }) 47 | } 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/trim.test.ts: -------------------------------------------------------------------------------- 1 | import _trim from 'lodash/trim' 2 | import {describe, expect, it} from 'vitest' 3 | 4 | import trim from './trim' 5 | 6 | describe('trim', () => { 7 | describe('should match lodash behavior', () => { 8 | it('trims whitespace by default', () => { 9 | const values = [ 10 | ' hello ', 11 | '\tworld\t', 12 | '\ntrim me\n', 13 | ' no-space', 14 | 'space-only ', 15 | ' mixed spaces \t \n', 16 | '', 17 | ' ', 18 | ] 19 | 20 | values.forEach((value) => { 21 | expect(trim(value)).toBe(_trim(value)) 22 | }) 23 | }) 24 | 25 | it('trims custom characters', () => { 26 | const testCases = [ 27 | ['--hello--', '-', 'hello'], 28 | ['__world__', '_', 'world'], 29 | ['##test##', '#', 'test'], 30 | ['!!important!!', '!', 'important'], 31 | ['abcdeabc', 'abc', 'de'], 32 | ['---test--', '-', 'test'], 33 | ['xxtrimxx', 'x', 'trim'], 34 | ['-_-abc-_-', '_-', 'abc'], 35 | ] 36 | 37 | testCases.forEach(([input, chars, expected]) => { 38 | expect(trim(input, chars)).toBe(_trim(input, chars)) 39 | expect(trim(input, chars)).toBe(expected) 40 | }) 41 | }) 42 | 43 | it('handles empty strings and undefined inputs', () => { 44 | const values = [undefined, null, '', ' '] 45 | 46 | values.forEach((value) => { 47 | expect(trim(value)).toBe(_trim(value)) 48 | }) 49 | }) 50 | 51 | it('works with complex strings', () => { 52 | const values = [ 53 | ['$$$hello$$$', '$', 'hello'], 54 | ['@@@world@@@', '@', 'world'], 55 | ['###javascript###', '#', 'javascript'], 56 | ['***code***', '*', 'code'], 57 | ] 58 | 59 | values.forEach(([input, chars, expected]) => { 60 | expect(trim(input, chars)).toBe(_trim(input, chars)) 61 | expect(trim(input, chars)).toBe(expected) 62 | }) 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/trim.ts: -------------------------------------------------------------------------------- 1 | export function trim(input: unknown = '', chars?: string, guard?: unknown): string { 2 | const str = input == null ? '' : String(input) 3 | 4 | // remove leading and trailing spaces 5 | if (str && (guard != null || chars === undefined)) { 6 | return str.trim() 7 | } 8 | 9 | if (!str || !chars) { 10 | return str 11 | } 12 | 13 | return trimSinglePass(str, chars) 14 | } 15 | 16 | function trimSinglePass(str: string, chars: string): string { 17 | let start = 0 18 | let end = str.length - 1 19 | const charSet = new Set(chars) 20 | while (start <= end && (charSet.has(str[start]) || charSet.has(str[end]))) { 21 | if (charSet.has(str[start])) { 22 | start++ 23 | } 24 | if (charSet.has(str[end])) { 25 | end-- 26 | } 27 | } 28 | 29 | return start <= end ? str.slice(start, end + 1) : '' 30 | } 31 | 32 | export default trim 33 | -------------------------------------------------------------------------------- /src/union.bench.ts: -------------------------------------------------------------------------------- 1 | import _union from 'lodash/union' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {union} from './union' 5 | 6 | const testCases = [ 7 | [1, 2, 1, 4, 1, 3], 8 | [true, false, true, true, false], 9 | ['a', 'b', 'a', 'c', 'b'], 10 | [null, undefined, null, 1, undefined], 11 | [0, -0, 0, NaN, NaN], 12 | [Infinity, -Infinity, Infinity], 13 | ['', 'hello', ''], 14 | 15 | [[1], [2], [1]], 16 | [ 17 | [1, 2], 18 | [2, 1], 19 | [1, 2], 20 | ], 21 | 22 | [{id: 1}, {id: 2}, {id: 1}], 23 | [{toString: () => 'a'}, {toString: () => 'b'}, {toString: () => 'a'}], 24 | 25 | Array.from({length: 1000}, (_, i) => i % 100), 26 | ] 27 | 28 | const ITERATIONS = 1000 29 | 30 | describe('union performance', () => { 31 | bench('hidash', () => { 32 | for (let i = 0; i < ITERATIONS; i++) { 33 | testCases.forEach((testCase) => { 34 | union(testCase) 35 | }) 36 | } 37 | }) 38 | 39 | bench('lodash', () => { 40 | for (let i = 0; i < ITERATIONS; i++) { 41 | testCases.forEach((testCase) => { 42 | _union(testCase) 43 | }) 44 | } 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/union.test.ts: -------------------------------------------------------------------------------- 1 | import _union from 'lodash/union' 2 | import {describe, expect, it} from 'vitest' 3 | 4 | import {union} from './union' 5 | 6 | describe('union', () => { 7 | describe('should match lodash behavior', () => { 8 | it('unions arrays of primitives', () => { 9 | const arrays = [ 10 | [ 11 | [1, 2, 3], 12 | [3, 4, 5], 13 | [5, 6], 14 | ], 15 | [ 16 | ['a', 'b'], 17 | ['b', 'c'], 18 | ], 19 | [ 20 | [true, false], 21 | [false, true, true], 22 | ], 23 | [ 24 | [null, undefined], 25 | [undefined, null], 26 | ], 27 | ] 28 | 29 | arrays.forEach(([...args]) => { 30 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 31 | // @ts-ignore 32 | expect(union(...args)).toEqual(_union(...args)) 33 | }) 34 | }) 35 | 36 | it('handles empty and undefined inputs', () => { 37 | const inputs = [ 38 | [undefined, null], 39 | [[], undefined], 40 | [[], null], 41 | [undefined, []], 42 | [null, []], 43 | ] 44 | 45 | inputs.forEach(([...args]) => { 46 | expect(union(...args)).toEqual(_union(...args)) 47 | }) 48 | }) 49 | 50 | it('works with complex nested arrays', () => { 51 | const testCases = [ 52 | [[1, [2, 3]], [3, [4, 5]], [[5, 6]]], 53 | [ 54 | ['x', ['y']], 55 | ['y', ['z']], 56 | ], 57 | [[[true, false]], [[false, true]]], 58 | ] 59 | 60 | testCases.forEach(([...args]) => { 61 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 62 | // @ts-ignore 63 | expect(union(...args)).toEqual(_union(...args)) 64 | }) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/union.ts: -------------------------------------------------------------------------------- 1 | export function union(...arrays: (T[] | null | undefined)[]): T[] { 2 | const result: T[] = [] 3 | const seen = new Map() 4 | const arrayLength = arrays.length 5 | 6 | for (let i = 0; i < arrayLength; i++) { 7 | const array = arrays[i] 8 | if (array) { 9 | const length = array.length 10 | for (let j = 0; j < length; j++) { 11 | const item = array[j] 12 | if (!seen.has(item)) { 13 | seen.set(item, true) 14 | result.push(item) 15 | } 16 | } 17 | } 18 | } 19 | 20 | return result 21 | } 22 | 23 | export default union 24 | -------------------------------------------------------------------------------- /src/uniq.bench.ts: -------------------------------------------------------------------------------- 1 | import _uniq from 'lodash/uniq' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {uniq} from './uniq' 5 | 6 | const testCases = [ 7 | [1, 2, 1, 4, 1, 3], 8 | [true, false, true, true, false], 9 | ['a', 'b', 'a', 'c', 'b'], 10 | [null, undefined, null, 1, undefined], 11 | [0, -0, 0, NaN, NaN], 12 | [Infinity, -Infinity, Infinity], 13 | ['', 'hello', ''], 14 | 15 | [[1], [2], [1]], 16 | [ 17 | [1, 2], 18 | [2, 1], 19 | [1, 2], 20 | ], 21 | 22 | [{id: 1}, {id: 2}, {id: 1}], 23 | [{toString: () => 'a'}, {toString: () => 'b'}, {toString: () => 'a'}], 24 | 25 | Array.from({length: 1000}, (_, i) => i % 100), 26 | ] 27 | 28 | const ITERATIONS = 1000 29 | 30 | describe('uniq performance', () => { 31 | bench('hidash', () => { 32 | for (let i = 0; i < ITERATIONS; i++) { 33 | testCases.forEach((testCase) => { 34 | uniq(testCase) 35 | }) 36 | } 37 | }) 38 | 39 | bench('lodash', () => { 40 | for (let i = 0; i < ITERATIONS; i++) { 41 | testCases.forEach((testCase) => { 42 | _uniq(testCase) 43 | }) 44 | } 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/uniq.ts: -------------------------------------------------------------------------------- 1 | import {List} from './internal/types' 2 | 3 | export function uniq(array: List | null | undefined): T[] { 4 | if (!array || !('length' in array)) { 5 | return [] 6 | } 7 | 8 | const length = array.length 9 | 10 | if (length <= 1) { 11 | return Array.from(array) 12 | } 13 | 14 | if (length < 200) { 15 | return Array.from(new Set(Array.from(array))) 16 | } 17 | 18 | const seen = new Map() 19 | const result: T[] = [] 20 | 21 | for (let i = 0; i < length; i++) { 22 | const value = array[i] 23 | 24 | if (!seen.has(value)) { 25 | seen.set(value, true) 26 | result.push(value) 27 | } 28 | } 29 | 30 | return result 31 | } 32 | 33 | export default uniq 34 | -------------------------------------------------------------------------------- /src/uniqBy.bench.ts: -------------------------------------------------------------------------------- 1 | import _uniqBy from 'lodash/uniqBy' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {uniqBy} from './uniqBy' 5 | 6 | interface TestCase { 7 | array: unknown[] 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | iteratee: (value: any) => unknown 10 | } 11 | 12 | const testCases: TestCase[] = [ 13 | { 14 | array: [2.1, 1.2, 2.3, 1.4, 2.5], 15 | iteratee: Math.floor, 16 | }, 17 | { 18 | array: [-2.1, -1.2, -2.3, -1.4], 19 | iteratee: Math.abs, 20 | }, 21 | { 22 | array: [ 23 | {id: 1, name: 'a'}, 24 | {id: 2, name: 'a'}, 25 | {id: 3, name: 'b'}, 26 | ], 27 | iteratee: (obj: {name: string}) => obj.name, 28 | }, 29 | { 30 | array: [{user: {id: 1, name: 'John'}}, {user: {id: 2, name: 'Jane'}}, {user: {id: 1, name: 'Johnny'}}], 31 | iteratee: (obj: {user: {id: number}}) => obj.user.id, 32 | }, 33 | { 34 | array: [{data: [1, 2]}, {data: [2, 3]}, {data: [1, 2]}], 35 | iteratee: (obj: {data: number[]}) => JSON.stringify(obj.data), 36 | }, 37 | { 38 | array: [null, undefined, null], 39 | iteratee: (x: unknown) => x, 40 | }, 41 | { 42 | array: [NaN, NaN, NaN], 43 | iteratee: (x: unknown) => x, 44 | }, 45 | { 46 | array: Array.from({length: 1000}, (_, i) => ({value: i % 100})), 47 | iteratee: (obj: {value: number}) => obj.value, 48 | }, 49 | ] 50 | 51 | const ITERATIONS = 1000 52 | 53 | describe('uniqBy performance', () => { 54 | bench('hidash', () => { 55 | for (let i = 0; i < ITERATIONS; i++) { 56 | testCases.forEach((testCase) => { 57 | uniqBy(testCase.array, testCase.iteratee) 58 | }) 59 | } 60 | }) 61 | 62 | bench('lodash', () => { 63 | for (let i = 0; i < ITERATIONS; i++) { 64 | testCases.forEach((testCase) => { 65 | _uniqBy(testCase.array, testCase.iteratee) 66 | }) 67 | } 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/uniqBy.ts: -------------------------------------------------------------------------------- 1 | import {baseIteratee} from './internal/baseIteratee' 2 | import {List} from './internal/types' 3 | 4 | import type {ValueIteratee} from './internal/baseIteratee.type' 5 | 6 | /** 7 | * @description 8 | * Creates a duplicate-free version of an array. Uniqueness of elements is determined by 9 | * the result of passing each element to the `iteratee` function. 10 | * The order of result values is determined by the order they occur in the original array. 11 | * This function is similar to lodash's `uniqBy`. 12 | * 13 | * @param {List | null | undefined} array - The array to inspect. If null, undefined, or not an array-like object, an empty array is returned. 14 | * @param {ValueIteratee} iteratee - The function invoked per element to generate the criterion for uniqueness. 15 | * It receives three arguments: (value: T, index: number, collection: List). 16 | * @returns {T[]} Returns the new array of unique elements. 17 | */ 18 | export function uniqBy(array: List | null | undefined, iteratee: ValueIteratee): T[] { 19 | // Handle cases where the input is null, undefined, or not an array-like object. 20 | if (!array || !('length' in array)) { 21 | return [] 22 | } 23 | 24 | // A Map to store encountered computed values to track uniqueness. 25 | // The key is the computed value from the iteratee, and the value is boolean (true if seen). 26 | const seen = new Map() 27 | const result: T[] = [] 28 | 29 | // Normalize the iteratee using `baseIteratee`. 30 | // This allows `iteratee` to be a function, a property name string, or other shorthands. 31 | const iterateeFn = baseIteratee(iteratee) 32 | const arrayLength = array.length 33 | 34 | for (let i = 0; i < arrayLength; i++) { 35 | const value = array[i] 36 | // Compute the criterion for uniqueness using the iteratee function. 37 | const computed = iterateeFn(value, i, array) 38 | 39 | // If the computed criterion has not been seen before, 40 | // mark it as seen and add the original value to the result array. 41 | if (!seen.has(computed)) { 42 | seen.set(computed, true) 43 | result.push(value) 44 | } 45 | } 46 | 47 | return result 48 | } 49 | 50 | export default uniqBy 51 | -------------------------------------------------------------------------------- /src/uniqWith.ts: -------------------------------------------------------------------------------- 1 | import uniq from './uniq' 2 | 3 | export function uniqWith(array: T[] | null | undefined, comparator?: (a: T, b: T) => boolean): T[] { 4 | if (!Array.isArray(array)) { 5 | return [] 6 | } 7 | 8 | const length = array.length 9 | 10 | if (!comparator && length > 200) { 11 | return [...new Set(array)] 12 | } 13 | 14 | if (!comparator) { 15 | return uniq(array) 16 | } 17 | 18 | const result: T[] = [] 19 | 20 | let i = -1 21 | 22 | while (++i < length) { 23 | let isDuplicate = false 24 | const item = array[i] 25 | 26 | for (const existing of result) { 27 | if (comparator(existing, item)) { 28 | isDuplicate = true 29 | break 30 | } 31 | } 32 | 33 | if (!isDuplicate) { 34 | result.push(item) 35 | } 36 | } 37 | 38 | return result 39 | } 40 | 41 | export default uniqWith 42 | -------------------------------------------------------------------------------- /src/unzip.bench.ts: -------------------------------------------------------------------------------- 1 | import _unzip from 'lodash/unzip' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {unzip} from './unzip' 5 | 6 | const testCases = [ 7 | [ 8 | ['a', 1, true], 9 | ['b', 2, false], 10 | ['c', 3, true], 11 | ], 12 | [Array(1000).fill(['a', 1, true])], 13 | [ 14 | [ 15 | {id: 1, name: 'John'}, 16 | {age: 30, city: 'New York'}, 17 | ], 18 | [ 19 | {id: 2, name: 'Jane'}, 20 | {age: 25, city: 'London'}, 21 | ], 22 | ], 23 | [ 24 | [ 25 | Object.fromEntries( 26 | Array(1000) 27 | .fill(0) 28 | .map((_, i) => [`key${i}`, `value${i}`]), 29 | ), 30 | Object.fromEntries( 31 | Array(1000) 32 | .fill(0) 33 | .map((_, i) => [`key${i}`, i]), 34 | ), 35 | ], 36 | ], 37 | [ 38 | ['string', new Set([1, 2, 3])], 39 | [ 40 | 123, 41 | new Map([ 42 | ['a', 1], 43 | ['b', 2], 44 | ]), 45 | ], 46 | [true, /regex/], 47 | [null, BigInt(123)], 48 | [undefined, () => {}], 49 | [Symbol('test'), new Date()], 50 | ], 51 | [[1, 'a'], [[2, 'b']], [[[3, 'c']]], [[[[4, 'd']]]]], 52 | [Array(100).fill(['a', 1, true]), Array(50).fill(['b', 2, false]), Array(25).fill(['c', 3, true])], 53 | [[], [null], [undefined], [null, undefined]], 54 | ] 55 | 56 | const ITERATIONS = 1000 57 | 58 | describe('unzip performance', () => { 59 | bench('hidash', () => { 60 | for (let i = 0; i < ITERATIONS; i++) { 61 | for (const testCase of testCases) { 62 | unzip(testCase) 63 | } 64 | } 65 | }) 66 | 67 | bench('lodash', () => { 68 | for (let i = 0; i < ITERATIONS; i++) { 69 | for (const testCase of testCases) { 70 | _unzip(testCase) 71 | } 72 | } 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/values.bench.ts: -------------------------------------------------------------------------------- 1 | import _values from 'lodash/values' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {values} from './values' 5 | 6 | const testCases = [ 7 | 'hello world', 8 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 9 | {a: 'juj', b: 'jhb'}, 10 | {a: {name: 'juj'}, b: {addr: 'Seoul'}}, 11 | 1, 12 | true, 13 | new (class { 14 | constructor() { 15 | this.a = 'a' 16 | this.b = 'b' 17 | } 18 | })(), 19 | Symbol(1), 20 | new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), 21 | new Map([ 22 | ['a', 'juj'], 23 | ['b', 'jhb'], 24 | ]), 25 | 'a'.repeat(1000), 26 | Array.from({length: 1000}, (_, i) => i), 27 | ] 28 | 29 | const ITERATIONS = 1000 30 | 31 | describe('values performance', () => { 32 | bench('hidash', () => { 33 | for (let i = 0; i < ITERATIONS; i++) { 34 | for (const testCase of testCases) { 35 | values(testCase) 36 | } 37 | } 38 | }) 39 | 40 | bench('lodash', () => { 41 | for (let i = 0; i < ITERATIONS; i++) { 42 | for (const testCase of testCases) { 43 | _values(testCase) 44 | } 45 | } 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/values.ts: -------------------------------------------------------------------------------- 1 | import isArray from './isArray' 2 | import isFunction from './isFunction' 3 | import isMap from './isMap' 4 | import {isNull} from './isNull' 5 | import isObject from './isObject' 6 | import isSet from './isSet' 7 | import isString from './isString' 8 | 9 | /** 10 | * @description 11 | * Returns an array of the values of the given object. 12 | * 13 | * @param {AnyKindOfDictionary | object | Map | Set} [obj] The object to get values from 14 | * @returns {Value[]} An array of the values of the object 15 | * 16 | * @example 17 | * values({ a: 1, b: 2 }) // [1, 2] 18 | * values([1, 2, 3]) // [1, 2, 3] 19 | * values(new Map([['a', 1], ['b', 2]])) // [1, 2] 20 | * values(new Set([1, 2, 3])) // [1, 2, 3] 21 | * values('abc') // ['a', 'b', 'c'] 22 | * values(null) // [] 23 | */ 24 | export function values(obj: T): unknown[] { 25 | if (isNull(obj)) { 26 | return [] 27 | } 28 | 29 | if (isString(obj)) { 30 | return [...obj] 31 | } 32 | 33 | if (isArray(obj)) { 34 | return [...obj] 35 | } 36 | 37 | if (isSet(obj) || isMap(obj)) { 38 | return [] 39 | } 40 | 41 | if (isObject(obj) || isFunction(obj)) { 42 | return Object.keys(obj).map((key) => (obj as Record)[key]) 43 | } 44 | 45 | return [] 46 | } 47 | 48 | export default values 49 | -------------------------------------------------------------------------------- /src/zip.bench.ts: -------------------------------------------------------------------------------- 1 | import _zip from 'lodash/zip' 2 | import {bench, describe} from 'vitest' 3 | 4 | import {zip} from './zip' 5 | 6 | const testCases = [ 7 | [ 8 | ['a', 'b', 'c'], 9 | [1, 2, 3], 10 | [true, false, true], 11 | ], 12 | [Array(1000).fill('a'), Array(1000).fill(1), Array(1000).fill(true)], 13 | [ 14 | [ 15 | {id: 1, name: 'John'}, 16 | {id: 2, name: 'Jane'}, 17 | ], 18 | [ 19 | {age: 30, city: 'New York'}, 20 | {age: 25, city: 'London'}, 21 | ], 22 | ], 23 | [ 24 | [ 25 | Object.fromEntries( 26 | Array(1000) 27 | .fill(0) 28 | .map((_, i) => [`key${i}`, `value${i}`]), 29 | ), 30 | ], 31 | [ 32 | Object.fromEntries( 33 | Array(1000) 34 | .fill(0) 35 | .map((_, i) => [`key${i}`, i]), 36 | ), 37 | ], 38 | ], 39 | [ 40 | ['string', 123, true, null, undefined, Symbol('test'), () => {}, new Date()], 41 | [ 42 | new Set([1, 2, 3]), 43 | new Map([ 44 | ['a', 1], 45 | ['b', 2], 46 | ]), 47 | /regex/, 48 | BigInt(123), 49 | ], 50 | ], 51 | [ 52 | [1, [2, [3, [4]]]], 53 | ['a', ['b', ['c', ['d']]]], 54 | ], 55 | [Array(100).fill('a'), Array(50).fill(1), Array(25).fill(true)], 56 | [[], [null], [undefined], [null, undefined]], 57 | ] 58 | 59 | const ITERATIONS = 1000 60 | 61 | describe('zip performance', () => { 62 | bench('hidash', () => { 63 | for (let i = 0; i < ITERATIONS; i++) { 64 | for (const testCase of testCases) { 65 | zip(...testCase) 66 | } 67 | } 68 | }) 69 | 70 | bench('lodash', () => { 71 | for (let i = 0; i < ITERATIONS; i++) { 72 | for (const testCase of testCases) { 73 | _zip(...testCase) 74 | } 75 | } 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "lib": ["ESNext", "dom"], 7 | "declaration": true, 8 | "rootDir": "./src", 9 | "strict": true, 10 | "moduleResolution": "Bundler", 11 | "baseUrl": "./", 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "**/*.test.ts", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import {createViteConfig} from '@naverpay/pite' 2 | 3 | export default createViteConfig({ 4 | entry: ['!./src/**/*.bench.ts', '!./src/**/*.test.ts', './src/**/*.ts'], 5 | outputs: [ 6 | { 7 | format: 'cjs', 8 | dist: 'dist', 9 | }, 10 | { 11 | format: 'es', 12 | dist: 'dist', 13 | }, 14 | ], 15 | publint: { 16 | severity: 'off', 17 | }, 18 | includeRequiredPolyfill: [ 19 | 'es.array.push', // https://bugs.chromium.org/p/v8/issues/detail?id=12681 20 | 'es.array.includes', // https://bugzilla.mozilla.org/show_bug.cgi?id=1767541 21 | 'es.array.reduce', // https://issues.chromium.org/issues/40672866 22 | 'es.string.trim', // https://github.com/zloirock/core-js/issues/480#issuecomment-457494016 safari bug 23 | 'es.regexp.flags', // https://github.com/zloirock/core-js/commit/9017066b4cb367c6609e4473d43d6e6dad8031a5#diff-59f90be4cf68f9d13d2dce1818780ae968bf48328da4014b47138adf527ec0fcR1066 24 | 'es.array.reverse', // https://bugs.webkit.org/show_bug.cgi?id=188794 25 | ], 26 | }) 27 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | // vitest.config.ts 2 | import {defineConfig} from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | benchmark: { 7 | reporters: ['default'], 8 | outputJson: 'benchmark-result.json', 9 | }, 10 | coverage: { 11 | provider: 'istanbul', // or 'v8' 12 | reporter: ['text', 'json', 'html'], 13 | }, 14 | reporters: ['default', 'html'], 15 | }, 16 | }) 17 | --------------------------------------------------------------------------------