├── .nvmrc ├── .gitignore ├── .github ├── assets │ ├── upi.png │ ├── paypal_donate.png │ └── Jairaj_Jangle_Google_Pay_UPI_QR_Code.jpg ├── FUNDING.yml ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── tsconfig.node.json ├── lefthook.yml ├── jest.config.js ├── release.config.js ├── tsconfig.json ├── LICENSE ├── CHANGELOG.md ├── package.json ├── CONTRIBUTING.md ├── README.md ├── benchmarks ├── fastIsEqual.benchmark.ts └── results.txt └── src ├── index.ts └── __tests__ └── fastIsEqual.test.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ -------------------------------------------------------------------------------- /.github/assets/upi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JairajJangle/fast-is-equal/HEAD/.github/assets/upi.png -------------------------------------------------------------------------------- /.github/assets/paypal_donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JairajJangle/fast-is-equal/HEAD/.github/assets/paypal_donate.png -------------------------------------------------------------------------------- /.github/assets/Jairaj_Jangle_Google_Pay_UPI_QR_Code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JairajJangle/fast-is-equal/HEAD/.github/assets/Jairaj_Jangle_Google_Pay_UPI_QR_Code.jpg -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "moduleResolution": "Node", 6 | "target": "ES2020", 7 | "sourceMap": true 8 | } 9 | } -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | test: 5 | run: yarn test 6 | types: 7 | files: git diff --name-only @{push} 8 | glob: "*.{js,ts,jsx,tsx}" 9 | run: npx tsc --noEmit 10 | commit-msg: 11 | parallel: true 12 | commands: 13 | commitlint: 14 | run: npx commitlint --edit 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | roots: ['/src'], 6 | testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], 7 | moduleFileExtensions: ['ts', 'js', 'json', 'node'], 8 | collectCoverage: true, 9 | coverageDirectory: 'coverage', 10 | coverageReporters: ['text', 'json-summary'], 11 | collectCoverageFrom: [ 12 | 'src/index.ts' 13 | ] 14 | }; -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | 'main' 4 | ], 5 | plugins: [ 6 | '@semantic-release/commit-analyzer', // Analyzes commits for version bumping 7 | '@semantic-release/release-notes-generator', // Generates release notes 8 | '@semantic-release/changelog', // Generates the changelog 9 | [ 10 | '@semantic-release/npm', 11 | { 12 | npmPublish: true 13 | } 14 | ], 15 | '@semantic-release/github', // Handles GitHub releases 16 | [ 17 | '@semantic-release/git', 18 | { 19 | assets: ['package.json', 'CHANGELOG.md'], 20 | message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}' 21 | } 22 | ] 23 | ] 24 | }; -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ["https://www.paypal.com/paypalme/jairajjangle001/usd", "https://github.com/JairajJangle/fast-is-equal/blob/master/.github/Jairaj_Jangle_Google_Pay_UPI_QR_Code.jpg"] 4 | liberapay: FutureJJ 5 | ko_fi: futurejj 6 | 7 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 8 | patreon: # Replace with a single Patreon username 9 | open_collective: # Replace with a single Open Collective username 10 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 11 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 12 | issuehunt: # Replace with a single IssueHunt username 13 | otechie: # Replace with a single Otechie username 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "sourceMap": true, 19 | "jsx": "react-jsx", 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "benchmarks", 27 | "src/**/*.test.ts", 28 | "src/**/*.spec.ts", 29 | "tests/**/*" 30 | ] 31 | } -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v6 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v4 15 | with: 16 | path: | 17 | **/node_modules 18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }} 19 | restore-keys: | 20 | ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 21 | ${{ runner.os }}-yarn- 22 | 23 | - name: Install dependencies 24 | if: steps.yarn-cache.outputs.cache-hit != 'true' 25 | run: | 26 | yarn install --cwd example --frozen-lockfile 27 | yarn install --frozen-lockfile 28 | shell: bash 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jairaj Jangle 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.2.4](https://github.com/JairajJangle/fast-is-equal/compare/v1.2.3...v1.2.4) (2025-12-07) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **merge:** apply maintenance updates (deps, tests, ci) and trigger patch release ([3d9898e](https://github.com/JairajJangle/fast-is-equal/commit/3d9898ec2c8808d27f506dd376720e992fa0895c)) 7 | 8 | ## [1.2.3](https://github.com/JairajJangle/fast-is-equal/compare/v1.2.2...v1.2.3) (2025-05-31) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * improve type checking and add support for Symbol, BigInt, and DataView ([4f66af2](https://github.com/JairajJangle/fast-is-equal/commit/4f66af2deb8a7e4f4ef70343003c647c8041aeaa)) 14 | 15 | ## [1.2.2](https://github.com/JairajJangle/fast-is-equal/compare/v1.2.1...v1.2.2) (2025-05-29) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * updated docs ([534d10e](https://github.com/JairajJangle/fast-is-equal/commit/534d10eb5e0b35ae4a368c05933375cbb0274630)) 21 | 22 | ## [1.2.1](https://github.com/JairajJangle/fast-is-equal/compare/v1.2.0...v1.2.1) (2025-05-29) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **perf:** greatly optimized performance by implementing the below improvements ([41c391f](https://github.com/JairajJangle/fast-is-equal/commit/41c391f327a69c46967a3a3531c5f024d8b00189)) 28 | * **perf:** optimize fastIsEqual for better performance on equal objects and primitives ([c42094f](https://github.com/JairajJangle/fast-is-equal/commit/c42094f1c8df421841b7f82d02e870342270b387)) 29 | 30 | # [1.2.0](https://github.com/JairajJangle/fast-is-equal/compare/v1.1.0...v1.2.0) (2025-05-23) 31 | 32 | 33 | ### Features 34 | 35 | * **trigger release:** force release ([13cea85](https://github.com/JairajJangle/fast-is-equal/commit/13cea8576ae3ae753ec93a443c3562a665e82b7b)) 36 | 37 | # [1.1.0](https://github.com/JairajJangle/fast-is-equal/compare/v1.0.4...v1.1.0) (2025-05-22) 38 | 39 | 40 | ### Features 41 | 42 | * update benchmark results and improve performance ([a50d999](https://github.com/JairajJangle/fast-is-equal/commit/a50d999b5a092e5226afbb09dac5da9d0722ff1a)) 43 | * update benchmark results and improve performance ([7138dcb](https://github.com/JairajJangle/fast-is-equal/commit/7138dcb9c7640861dd83adcec01bd5d96d81e18e)) 44 | 45 | # 1.0.0 (2025-05-22) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * docs: updated readme ([9342620](https://github.com/JairajJangle/fast-is-equal/commit/9342620e60a0e887081f5fa186893bd047ebd878)) 51 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | # TODO: Configure lint job in later release 12 | # lint: 13 | # runs-on: ubuntu-latest 14 | # steps: 15 | # - name: Checkout 16 | # uses: actions/checkout@v4 17 | 18 | # - name: Setup Node.js 19 | # uses: actions/setup-node@v4 20 | # with: 21 | # node-version: "lts/*" 22 | # cache: 'yarn' 23 | 24 | # - name: Install dependencies 25 | # run: yarn install 26 | 27 | # - name: Lint files 28 | # run: yarn lint 29 | 30 | # - name: Typecheck files 31 | # run: yarn typecheck 32 | 33 | test: 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | node-version: [22, 24] 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v5 41 | 42 | - name: Setup Node.js ${{ matrix.node-version }} 43 | uses: actions/setup-node@v6 44 | with: 45 | node-version: ${{ matrix.node-version }} 46 | cache: 'yarn' 47 | 48 | - name: Install dependencies 49 | run: yarn install 50 | 51 | - name: Run unit tests 52 | run: yarn test:cov 53 | 54 | - name: Update Coverage Badge 55 | # GitHub actions: default branch variable 56 | # https://stackoverflow.com/questions/64781462/github-actions-default-branch-variable 57 | if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 58 | uses: we-cli/coverage-badge-action@main 59 | 60 | build: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v5 65 | 66 | - name: Setup Node.js 67 | uses: actions/setup-node@v6 68 | with: 69 | node-version: "lts/*" 70 | cache: 'yarn' 71 | 72 | - name: Install dependencies 73 | run: yarn install 74 | 75 | - name: Build package 76 | run: yarn build 77 | 78 | publish-npm: 79 | needs: [test, build] 80 | runs-on: ubuntu-latest 81 | permissions: 82 | contents: write # To publish a GitHub release 83 | issues: write # To comment on released issues 84 | pull-requests: write # To comment on released pull requests 85 | id-token: write # To enable use of OIDC for npm provenance 86 | if: github.ref == 'refs/heads/main' 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v5 90 | with: 91 | fetch-depth: 0 92 | 93 | - name: Setup Node.js 94 | uses: actions/setup-node@v6 95 | with: 96 | node-version: "lts/*" 97 | registry-url: 'https://registry.npmjs.org/' 98 | cache: 'yarn' 99 | 100 | - name: Install dependencies 101 | run: yarn install 102 | 103 | - name: Verify the integrity of provenance attestations and registry signatures 104 | run: yarn audit 105 | 106 | - name: Release 107 | run: yarn semantic-release 108 | env: 109 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 110 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-is-equal", 3 | "version": "1.2.4", 4 | "description": "Blazing-fast equality checks, minus the baggage. A lean, standalone alternative to Lodash's isEqual—because speed matters.", 5 | "keywords": [ 6 | "deep-equal", 7 | "equality", 8 | "compare", 9 | "lodash", 10 | "isEqual", 11 | "fast", 12 | "performance", 13 | "typescript", 14 | "javascript", 15 | "react", 16 | "react-native", 17 | "vue", 18 | "angular", 19 | "object-comparison", 20 | "array-comparison", 21 | "deep-comparison", 22 | "utility", 23 | "lightweight", 24 | "zero-dependencies", 25 | "circular-references", 26 | "map", 27 | "set", 28 | "immutable", 29 | "benchmark", 30 | "speed", 31 | "efficient", 32 | "alternative", 33 | "replacement" 34 | ], 35 | "homepage": "https://github.com/JairajJangle/fast-is-equal#readme", 36 | "bugs": { 37 | "url": "https://github.com/JairajJangle/fast-is-equal/issues" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/JairajJangle/fast-is-equal.git" 42 | }, 43 | "license": "MIT", 44 | "author": "Jairaj Jangle (https://github.com/JairajJangle)", 45 | "publishConfig": { 46 | "registry": "https://registry.npmjs.org/", 47 | "provenance": true 48 | }, 49 | "main": "dist/index.js", 50 | "types": "dist/index.d.ts", 51 | "scripts": { 52 | "test": "jest", 53 | "test:cov": "jest --coverage --maxWorkers=2 --coverageReporters=\"json-summary\" --coverageReporters=\"html\"", 54 | "build": "rimraf dist && tsc", 55 | "prepublishOnly": "npm run build", 56 | "benchmark": "ts-node --project tsconfig.node.json benchmarks/fastIsEqual.benchmark.ts" 57 | }, 58 | "files": [ 59 | "dist" 60 | ], 61 | "devDependencies": { 62 | "@commitlint/config-conventional": "^20.2.0", 63 | "@evilmartians/lefthook": "^2.0.8", 64 | "@release-it/conventional-changelog": "^10.0.2", 65 | "@semantic-release/changelog": "^6.0.3", 66 | "@semantic-release/git": "^10.0.1", 67 | "@semantic-release/github": "^12.0.2", 68 | "@semantic-release/npm": "^13.1.2", 69 | "@types/jest": "^30.0.0", 70 | "@types/lodash": "^4.17.21", 71 | "commitlint": "^20.2.0", 72 | "jest": "^30.2.0", 73 | "lodash": "^4.17.21", 74 | "rimraf": "^6.1.2", 75 | "semantic-release": "^25.0.2", 76 | "ts-jest": "^29.4.6", 77 | "ts-node": "^10.9.2", 78 | "tslib": "^2.8.1", 79 | "typescript": "^5.9.3" 80 | }, 81 | "commitlint": { 82 | "extends": [ 83 | "@commitlint/config-conventional" 84 | ] 85 | }, 86 | "release-it": { 87 | "git": { 88 | "commitMessage": "chore: release ${version}", 89 | "tagName": "v${version}" 90 | }, 91 | "npm": { 92 | "publish": true 93 | }, 94 | "github": { 95 | "release": true 96 | }, 97 | "plugins": { 98 | "@release-it/conventional-changelog": { 99 | "preset": "angular" 100 | } 101 | } 102 | }, 103 | "funding": [ 104 | { 105 | "type": "individual", 106 | "url": "https://www.paypal.com/paypalme/jairajjangle001/usd" 107 | }, 108 | { 109 | "type": "individual", 110 | "url": "https://liberapay.com/FutureJJ/donate" 111 | }, 112 | { 113 | "type": "individual", 114 | "url": "https://ko-fi.com/futurejj" 115 | } 116 | ], 117 | "packageManager": "yarn@1.22.22" 118 | } 119 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. 6 | 7 | ## Development Workflow 8 | 9 | This project is a lightweight TypeScript library with zero runtime dependencies. It uses **Yarn 1.22.22** for package management. 10 | 11 | To get started with the project, follow these steps: 12 | 13 | 1. **Ensure Node.js is installed**: Use Node.js 22 or higher (as specified in `.nvmrc`). Install via [nvm](https://github.com/nvm-sh/nvm) if needed: 14 | 15 | ```sh 16 | nvm install 22 17 | nvm use 22 18 | ``` 19 | 20 | 2. **Install dependencies**: Install dependencies using Yarn: 21 | 22 | ```sh 23 | yarn install 24 | ``` 25 | 26 | > **Important**: This project uses Yarn 1.22.22. While npm may work for basic operations, we recommend using Yarn for consistency with the project setup. 27 | 28 | ### Making Changes 29 | 30 | The library source code is located in the `src/` directory. The main implementation is in `src/index.ts`. 31 | 32 | Any changes you make to the TypeScript source code will need to be built before they can be tested: 33 | 34 | ```sh 35 | yarn build 36 | ``` 37 | 38 | Make sure your code passes TypeScript type checking and tests. Run the following to verify: 39 | 40 | ```sh 41 | yarn typecheck 42 | yarn test 43 | ``` 44 | 45 | ### Running Benchmarks 46 | 47 | This project includes comprehensive benchmarks comparing performance against Lodash's `isEqual`. To run benchmarks locally: 48 | 49 | ```sh 50 | yarn benchmark 51 | ``` 52 | 53 | Benchmark results are saved to `benchmarks/results.txt`. If you're making performance-related changes, please include updated benchmark results in your pull request. 54 | 55 | ### Commit Message Convention 56 | 57 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 58 | 59 | - `fix`: bug fixes, e.g. fix incorrect comparison for typed arrays 60 | - `feat`: new features, e.g. add support for new data type 61 | - `refactor`: code refactor, e.g. optimize circular reference detection 62 | - `docs`: changes into documentation, e.g. update README with new examples 63 | - `test`: adding or updating tests, e.g. add test cases for edge cases 64 | - `chore`: tooling changes, e.g. update CI config 65 | - `perf`: performance improvements, e.g. optimize Map comparison 66 | 67 | Our pre-commit hooks (via Lefthook) verify that your commit message matches this format when committing. 68 | 69 | ### Linting and Tests 70 | 71 | We use [TypeScript](https://www.typescriptlang.org/) for type checking and [Jest](https://jestjs.io/) for testing. 72 | 73 | Our pre-commit hooks verify that the type checker and tests pass when committing. 74 | 75 | ### Publishing to npm 76 | 77 | We use [semantic-release](https://github.com/semantic-release/semantic-release) for automated releases. Releases are automatically published to npm when changes are merged to the main branch, based on the conventional commit messages. 78 | 79 | Manual releases can be triggered using the GitHub Actions workflow. 80 | 81 | ### Scripts 82 | 83 | The `package.json` file contains various scripts for common tasks: 84 | 85 | - `yarn typecheck`: Type-check files with TypeScript (note: this script needs to be added) 86 | - `yarn test`: Run unit tests with Jest 87 | - `yarn build`: Build the TypeScript source to JavaScript in the `dist/` directory 88 | - `yarn benchmark`: Run performance benchmarks against Lodash's `isEqual` 89 | 90 | ### Project Structure 91 | 92 | ``` 93 | fast-is-equal/ 94 | ├── src/ # TypeScript source code 95 | │ └── index.ts # Main implementation 96 | ├── benchmarks/ # Performance benchmarks 97 | ├── dist/ # Built JavaScript output (generated) 98 | ├── coverage/ # Test coverage reports (generated) 99 | ├── .github/ # GitHub Actions workflows and assets 100 | ├── jest.config.js # Jest configuration 101 | ├── tsconfig.json # TypeScript configuration 102 | └── package.json # Package metadata and scripts 103 | ``` 104 | 105 | ### Sending a Pull Request 106 | 107 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 108 | 109 | When you're sending a pull request: 110 | 111 | - Prefer small pull requests focused on one change 112 | - Verify that tests are passing 113 | - Add tests for your changes when applicable 114 | - If making performance improvements, include benchmark results 115 | - Review the documentation to make sure it looks good 116 | - Follow the pull request template when opening a pull request (if available) 117 | - For pull requests that change the API or implementation significantly, discuss with maintainers first by opening an issue 118 | 119 | ### Performance Considerations 120 | 121 | This library is optimized for speed. When contributing: 122 | 123 | - **Benchmark your changes**: Run `yarn benchmark` before and after your changes 124 | - **Avoid unnecessary allocations**: Minimize object/array creation in hot paths 125 | - **Consider edge cases**: Ensure optimizations don't break correctness 126 | - **Profile if needed**: For significant changes, consider profiling with Node.js inspector 127 | 128 | ### Testing Guidelines 129 | 130 | - Add test cases for new features or bug fixes 131 | - Include edge cases in your tests 132 | - Ensure tests are deterministic and don't rely on timing 133 | - Test coverage should remain high (aim for >95%) 134 | 135 | ### Getting Help 136 | 137 | - 💬 Open an issue for bugs or feature requests 138 | - 📖 Check existing issues and pull requests before creating new ones 139 | - 🤝 Be respectful and constructive in all interactions 140 | 141 | --- 142 | 143 | Thank you for contributing to fast-is-equal! 🚀 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-is-equal 2 | 3 | ⚡️Blazing-fast equality checks, minus the baggage. A lean, standalone alternative to Lodash's `isEqual` - because speed matters. 4 | 5 | [![npm version](https://img.shields.io/npm/v/fast-is-equal)](https://badge.fury.io/js/fast-is-equal) [![License](https://img.shields.io/github/license/JairajJangle/fast-is-equal)](https://github.com/JairajJangle/fast-is-equal/blob/main/LICENSE) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/fast-is-equal) [![Workflow Status](https://github.com/JairajJangle/fast-is-equal/actions/workflows/ci.yml/badge.svg)](https://github.com/JairajJangle/fast-is-equal/actions/workflows/ci.yml) [![Coverage](https://raw.githubusercontent.com/JairajJangle/fast-is-equal/gh-pages/badges/coverage.svg)](https://github.com/JairajJangle/fast-is-equal/actions/workflows/ci.yml) [![React Compatibility](https://img.shields.io/badge/React-Compatible-61DAFB?logo=react)](https://github.com/JairajJangle/fast-is-equal) [![React Native Compatibility](https://img.shields.io/badge/React%20Native-Compatible-61DAFB?logo=react)](https://github.com/JairajJangle/fast-is-equal) [![Angular Compatibility](https://img.shields.io/badge/Angular-Compatible-DD0031?logo=angular)](https://github.com/JairajJangle/fast-is-equal) [![Vue Compatibility](https://img.shields.io/badge/Vue-Compatible-4FC08D?logo=vue.js)](https://github.com/JairajJangle/fast-is-equal) [![Svelte Compatibility](https://img.shields.io/badge/Svelte-Compatible-FF3E00?logo=svelte)](https://github.com/JairajJangle/fast-is-equal) [![Modern JS Frameworks](https://img.shields.io/badge/Modern%20JS%20Frameworks-Compatible-F7DF1E?logo=javascript)](https://github.com/JairajJangle/fast-is-equal) 6 | 7 | ## Why fast-is-equal? 8 | 9 | - 🚀 **Lightning Speed**: Up to **55.84x faster** than Lodash's `isEqual` (average **11.73x faster** across 49 test cases). 10 | - 🪶 **Lightweight**: Dependency-free, minimal footprint. 11 | - 🔄 **Versatile**: Handles primitives, objects, arrays, Maps, Sets, typed arrays, circular references, and more. 12 | - 🏆 **Proven**: Outperforms Lodash in **93.9%** of benchmark cases. 13 | 14 | ## Installation 15 | 16 | Using yarn: 17 | 18 | ```bash 19 | yarn add fast-is-equal 20 | ``` 21 | 22 | Using npm: 23 | 24 | ```bash 25 | npm install fast-is-equal 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```typescript 31 | import { fastIsEqual } from 'fast-is-equal'; 32 | 33 | console.log(fastIsEqual(1, 1)); // true 34 | console.log(fastIsEqual({ a: 1 }, { a: 1 })); // true 35 | console.log(fastIsEqual([1, 2], [1, 3])); // false 36 | ``` 37 | 38 | ## Performance Benchmarks 39 | 40 | `fast-is-equal` was tested against Lodash's `isEqual` across **49 diverse test cases** with **1,000,000 iterations each**. The results speak for themselves: 41 | 42 | ### Key Highlights 43 | 44 | - **Average Speed**: `fastIsEqual` is **11.73x faster** (0.000172 ms vs. 0.002013 ms). 45 | - **Win Rate**: Outperforms Lodash in **46/49 cases (93.9%)**. 46 | - **Peak Performance**: Up to **55.84x faster** for large Sets. 47 | 48 | ### Top 10 Performance Gains 49 | 50 | | Test Case | fastIsEqual (ms) | Lodash isEqual (ms) | Speed Boost | 51 | | ----------------------- | ---------------- | ------------------- | ------------ | 52 | | Large Set (100 items) | 0.000673 | 0.037564 | **55.84x** 🚀 | 53 | | Map vs Set | 0.000018 | 0.000485 | **26.52x** 🚀 | 54 | | Large Map (50 entries) | 0.001059 | 0.025756 | **24.32x** 🚀 | 55 | | Map with primitives | 0.000092 | 0.001487 | **16.09x** 🚀 | 56 | | Map (unequal) | 0.000092 | 0.001406 | **15.29x** 🚀 | 57 | | Large TypedArray (1000) | 0.000944 | 0.013165 | **13.95x** 🚀 | 58 | | ArrayBuffer (small) | 0.000092 | 0.001263 | **13.74x** 🚀 | 59 | | Empty Set | 0.000058 | 0.000691 | **11.96x** 🚀 | 60 | | Empty Map | 0.000058 | 0.000684 | **11.84x** 🚀 | 61 | | Set of strings | 0.000082 | 0.000940 | **11.51x** 🚀 | 62 | 63 | ### Performance Across Categories 64 | 65 | - **Primitives**: Competitive performance with smart optimizations for edge cases like NaN 66 | - **Objects**: 1.59x–2.87x faster, with best gains on simple and nested structures 67 | - **Arrays**: 1.24x–4.38x faster, excelling at primitive arrays and sparse arrays 68 | - **TypedArrays**: 11.30x–13.95x faster, dramatically outperforming on all variants 69 | - **Special Objects**: 8.63x–10.25x faster for Dates and RegExp 70 | - **Collections**: 10.84x–55.84x faster for Maps and Sets, with exceptional gains on large collections 71 | - **Circular References**: 3.04x–3.72x faster with optimized cycle detection 72 | 73 | ### Detailed Benchmark Results 74 | 75 | Run `yarn benchmark` or `npm run benchmark` to test locally. Full results available in [benchmarks/results.txt](benchmarks/results.txt). 76 | 77 | #### Edge Cases Where Lodash Wins 78 | 79 | Only 3 cases where Lodash marginally outperforms (by less than 5%): 80 | 81 | - String vs Number: 0.95x slower 82 | - Large Numbers: 0.99x slower 83 | - Boolean vs Number: 0.99x slower 84 | 85 | These represent cross-type comparisons with negligible real-world impact. 86 | 87 | ## Features 88 | 89 | - **Dependency-Free**: No bloat, just performance. 90 | - **Comprehensive**: Supports all JavaScript types, including edge cases like circular references and typed arrays. 91 | - **Optimized**: Fine-tuned for real-world use cases (e.g., API responses, state objects). 92 | 93 | ## License 94 | 95 | MIT 96 | 97 | ## 🙏 Support the project 98 | 99 |

LiberPay_Donation_Button           Paypal_Donation_Button           Paypal_Donation_Button

100 | -------------------------------------------------------------------------------- /benchmarks/fastIsEqual.benchmark.ts: -------------------------------------------------------------------------------- 1 | import { fastIsEqual } from '../src/index'; 2 | import isEqual from 'lodash/isEqual'; 3 | import { performance } from 'perf_hooks'; 4 | 5 | // ANSI color codes 6 | const RED = '\x1b[31m'; 7 | const GREEN = '\x1b[32m'; 8 | const YELLOW = '\x1b[33m'; 9 | const WHITE = '\x1b[37m'; 10 | const RESET = '\x1b[0m'; 11 | const BOLD = '\x1b[1m'; 12 | 13 | // Test cases 14 | const testCases = [ 15 | // Primitives 16 | { label: 'Numbers', a: 42, b: 42 }, 17 | { label: 'Strings', a: 'hello', b: 'hello' }, 18 | { label: 'Booleans', a: true, b: true }, 19 | { label: 'NaN', a: NaN, b: NaN }, 20 | { label: 'Large Numbers', a: 9007199254740991, b: 9007199254740991 }, 21 | { label: 'Negative Zero', a: -0, b: +0 }, 22 | 23 | // Simple Objects 24 | { label: 'Empty Objects', a: {}, b: {} }, 25 | { label: 'Single Property Object', a: { id: 123 }, b: { id: 123 } }, 26 | { label: 'Simple Object (equal)', a: { x: 1, y: 2 }, b: { x: 1, y: 2 } }, 27 | { label: 'Simple Object (unequal)', a: { x: 1, y: 2 }, b: { x: 1, y: 3 } }, 28 | { label: 'Object with null prototype', a: Object.create(null), b: Object.create(null) }, 29 | 30 | // Nested Objects 31 | { label: 'Nested Object (equal)', a: { x: { y: { z: 1 } } }, b: { x: { y: { z: 1 } } } }, 32 | { label: 'Nested Object (unequal)', a: { x: { y: { z: 1 } } }, b: { x: { y: { z: 2 } } } }, 33 | { 34 | label: 'Deeply Nested (5 levels)', 35 | a: { a: { b: { c: { d: { e: 'value' } } } } }, 36 | b: { a: { b: { c: { d: { e: 'value' } } } } } 37 | }, 38 | 39 | // Arrays 40 | { label: 'Empty Arrays', a: [], b: [] }, 41 | { label: 'Single Element Array', a: [1], b: [1] }, 42 | { label: 'Array of Primitives (equal)', a: [1, 2, 3], b: [1, 2, 3] }, 43 | { label: 'Array of Primitives (unequal)', a: [1, 2, 3], b: [1, 2, 4] }, 44 | { label: 'Large Array of Numbers (100)', a: Array(100).fill(42), b: Array(100).fill(42) }, 45 | { label: 'Array of Strings', a: ['a', 'b', 'c', 'd'], b: ['a', 'b', 'c', 'd'] }, 46 | { label: 'Mixed Type Array', a: [1, 'two', true, null], b: [1, 'two', true, null] }, 47 | { label: 'Sparse Array', a: [1, , , 4], b: [1, , , 4] }, 48 | { label: 'Array of Objects (equal)', a: [{ x: 1 }, { y: 2 }], b: [{ x: 1 }, { y: 2 }] }, 49 | 50 | // TypedArrays 51 | { label: 'Uint8Array', a: new Uint8Array([1, 2, 3, 4]), b: new Uint8Array([1, 2, 3, 4]) }, 52 | { label: 'Float32Array', a: new Float32Array([1.1, 2.2, 3.3]), b: new Float32Array([1.1, 2.2, 3.3]) }, 53 | { 54 | label: 'Large TypedArray (1000)', 55 | a: new Int32Array(1000).fill(99), 56 | b: new Int32Array(1000).fill(99) 57 | }, 58 | 59 | // ArrayBuffer 60 | { 61 | label: 'ArrayBuffer (small)', 62 | a: new Uint8Array([1, 2, 3, 4]).buffer, 63 | b: new Uint8Array([1, 2, 3, 4]).buffer 64 | }, 65 | 66 | // Special Objects 67 | { label: 'Dates (equal)', a: new Date('2024-01-01'), b: new Date('2024-01-01') }, 68 | { label: 'RegExp (equal)', a: /test/gi, b: /test/gi }, 69 | { label: 'RegExp (unequal flags)', a: /test/g, b: /test/i }, 70 | 71 | // Circular References 72 | { 73 | label: 'Circular Reference', 74 | a: (() => { const obj: any = {}; obj.self = obj; return obj; })(), 75 | b: (() => { const obj: any = {}; obj.self = obj; return obj; })(), 76 | }, 77 | { 78 | label: 'Mutual Circular', 79 | a: (() => { const a: any = { name: 'a' }; const b = { ref: a }; a.ref = b; return a; })(), 80 | b: (() => { const a: any = { name: 'a' }; const b = { ref: a }; a.ref = b; return a; })(), 81 | }, 82 | 83 | // Maps 84 | { label: 'Empty Map', a: new Map(), b: new Map() }, 85 | { label: 'Map with primitives', a: new Map([[1, 'one'], [2, 'two']]), b: new Map([[1, 'one'], [2, 'two']]) }, 86 | { label: 'Map (unequal)', a: new Map([[1, 'one'], [2, 'two']]), b: new Map([[1, 'one'], [3, 'three']]) }, 87 | { 88 | label: 'Large Map (50 entries)', 89 | a: new Map(Array.from({ length: 50 }, (_, i) => [i, `value${i}`])), 90 | b: new Map(Array.from({ length: 50 }, (_, i) => [i, `value${i}`])) 91 | }, 92 | 93 | // Sets 94 | { label: 'Empty Set', a: new Set(), b: new Set() }, 95 | { label: 'Set of numbers', a: new Set([1, 2, 3]), b: new Set([1, 2, 3]) }, 96 | { label: 'Set (unequal)', a: new Set([1, 2, 3]), b: new Set([1, 2, 4]) }, 97 | { label: 'Set of strings', a: new Set(['a', 'b', 'c']), b: new Set(['a', 'b', 'c']) }, 98 | { 99 | label: 'Large Set (100 items)', 100 | a: new Set(Array.from({ length: 100 }, (_, i) => i)), 101 | b: new Set(Array.from({ length: 100 }, (_, i) => i)) 102 | }, 103 | 104 | // Mixed types (should fail fast) 105 | { label: 'Object vs Array', a: {}, b: [] }, 106 | { label: 'Map vs Set', a: new Map(), b: new Set() }, 107 | { label: 'String vs Number', a: '42', b: 42 }, 108 | { label: 'Boolean vs Number', a: true, b: 1 }, 109 | 110 | // Real-world-like objects 111 | { 112 | label: 'User Object', 113 | a: { id: 1, name: 'John', email: 'john@example.com', active: true }, 114 | b: { id: 1, name: 'John', email: 'john@example.com', active: true } 115 | }, 116 | { 117 | label: 'API Response', 118 | a: { status: 200, data: { users: [{ id: 1 }, { id: 2 }], total: 2 }, timestamp: 1234567890 }, 119 | b: { status: 200, data: { users: [{ id: 1 }, { id: 2 }], total: 2 }, timestamp: 1234567890 } 120 | }, 121 | { 122 | label: 'Config Object', 123 | a: { debug: false, port: 3000, host: 'localhost', features: ['auth', 'api', 'ui'] }, 124 | b: { debug: false, port: 3000, host: 'localhost', features: ['auth', 'api', 'ui'] } 125 | }, 126 | { 127 | label: 'State Object', 128 | a: { 129 | counter: 0, 130 | items: [], 131 | loading: false, 132 | error: null, 133 | metadata: { version: '1.0.0', lastUpdated: null } 134 | }, 135 | b: { 136 | counter: 0, 137 | items: [], 138 | loading: false, 139 | error: null, 140 | metadata: { version: '1.0.0', lastUpdated: null } 141 | } 142 | } 143 | ]; 144 | 145 | // Number of iterations to average the performance 146 | const iterations = 1_000_000; 147 | 148 | // Function to measure performance of a given equality function 149 | function measurePerformance(fn: any, a: any, b: any) { 150 | const start = performance.now(); 151 | for (let i = 0; i < iterations; i++) { 152 | fn(a, b); 153 | } 154 | const end = performance.now(); 155 | return (end - start) / iterations; // Return average time per iteration in ms 156 | } 157 | 158 | // Run the performance comparison 159 | console.log(`${BOLD}Performance Comparison: fastIsEqual vs Lodash isEqual${RESET}`); 160 | console.log(`Iterations per test case: ${iterations.toLocaleString()}`); 161 | console.log('═'.repeat(80)); 162 | 163 | let totalCustomTime = 0; 164 | let totalLodashTime = 0; 165 | let customWins = 0; 166 | let lodashWins = 0; 167 | 168 | const results: Array<{ label: string, speedup: number, customTime: number, lodashTime: number; }> = []; 169 | 170 | testCases.forEach((testCase, index) => { 171 | const { label, a, b } = testCase; 172 | 173 | // Measure custom isEqual performance 174 | const customTime = measurePerformance(fastIsEqual, a, b); 175 | totalCustomTime += customTime; 176 | 177 | // Measure Lodash isEqual performance 178 | const lodashTime = measurePerformance(isEqual, a, b); 179 | totalLodashTime += lodashTime; 180 | 181 | // Calculate speed multiplier 182 | const speedMultiplier = lodashTime / customTime; 183 | 184 | // Track wins 185 | if (customTime < lodashTime) { 186 | customWins++; 187 | } else { 188 | lodashWins++; 189 | } 190 | 191 | results.push({ label, speedup: speedMultiplier, customTime, lodashTime }); 192 | 193 | // Determine color based on speed multiplier 194 | let color = WHITE; 195 | let emoji = ''; 196 | if (speedMultiplier < 0.9) { 197 | color = RED; 198 | emoji = '❌'; 199 | } else if (speedMultiplier < 1) { 200 | color = YELLOW; 201 | emoji = '⚠️ '; 202 | } else if (speedMultiplier > 2) { 203 | color = GREEN; 204 | emoji = '🚀'; 205 | } else if (speedMultiplier > 1.25) { 206 | color = GREEN; 207 | emoji = '✅'; 208 | } else { 209 | emoji = '➡️ '; 210 | } 211 | 212 | // Output results with colors 213 | console.log(`${BOLD}Test ${index + 1}:${RESET} ${label}`); 214 | console.log(` fastIsEqual: ${customTime.toFixed(6)} ms`); 215 | console.log(` Lodash isEqual: ${lodashTime.toFixed(6)} ms`); 216 | console.log(`${color} ${emoji} Speed: ${speedMultiplier.toFixed(2)}x${speedMultiplier >= 1 ? ' faster' : ' slower'}${RESET}`); 217 | console.log('─'.repeat(80)); 218 | }); 219 | 220 | // Sort results by speedup for summary 221 | const topPerformers = results 222 | .sort((a, b) => b.speedup - a.speedup) 223 | .slice(0, 10); 224 | 225 | const worstPerformers = results 226 | .filter(r => r.speedup < 1) 227 | .sort((a, b) => a.speedup - b.speedup); 228 | 229 | // Calculate and print summary 230 | console.log('═'.repeat(80)); 231 | console.log(`${BOLD}SUMMARY${RESET}`); 232 | console.log('═'.repeat(80)); 233 | 234 | const averageCustomTime = totalCustomTime / testCases.length; 235 | const averageLodashTime = totalLodashTime / testCases.length; 236 | const averageSpeedMultiplier = averageLodashTime / averageCustomTime; 237 | 238 | console.log(`${BOLD}Overall Performance:${RESET}`); 239 | console.log(` Average fastIsEqual time: ${averageCustomTime.toFixed(6)} ms`); 240 | console.log(` Average Lodash isEqual time: ${averageLodashTime.toFixed(6)} ms`); 241 | console.log(` ${GREEN}${BOLD}fastIsEqual is ${averageSpeedMultiplier.toFixed(2)}x faster on average${RESET}`); 242 | console.log(); 243 | console.log(`${BOLD}Win Rate:${RESET}`); 244 | console.log(` fastIsEqual wins: ${GREEN}${customWins}/${testCases.length}${RESET} (${(customWins / testCases.length * 100).toFixed(1)}%)`); 245 | console.log(` Lodash wins: ${RED}${lodashWins}/${testCases.length}${RESET} (${(lodashWins / testCases.length * 100).toFixed(1)}%)`); 246 | 247 | console.log(); 248 | console.log(`${BOLD}🏆 Top 10 Best Performance Gains:${RESET}`); 249 | topPerformers.forEach((result, i) => { 250 | console.log(` ${i + 1}. ${result.label}: ${GREEN}${result.speedup.toFixed(2)}x faster${RESET}`); 251 | }); 252 | 253 | if (worstPerformers.length > 0) { 254 | console.log(); 255 | console.log(`${BOLD}⚠️ Cases where Lodash performed better:${RESET}`); 256 | worstPerformers.forEach((result, i) => { 257 | console.log(` ${i + 1}. ${result.label}: ${YELLOW}${result.speedup.toFixed(2)}x${RESET}`); 258 | }); 259 | } 260 | 261 | console.log(); 262 | console.log('═'.repeat(80)); -------------------------------------------------------------------------------- /benchmarks/results.txt: -------------------------------------------------------------------------------- 1 | Performance Comparison: fastIsEqual vs Lodash isEqual 2 | Iterations per test case: 1,000,000 3 | ════════════════════════════════════════════════════════════════════════════════ 4 | Test 1: Numbers 5 | fastIsEqual: 0.000005 ms 6 | Lodash isEqual: 0.000005 ms 7 | ➡️ Speed: 1.11x faster 8 | ──────────────────────────────────────────────────────────────────────────────── 9 | Test 2: Strings 10 | fastIsEqual: 0.000006 ms 11 | Lodash isEqual: 0.000008 ms 12 | ✅ Speed: 1.39x faster 13 | ──────────────────────────────────────────────────────────────────────────────── 14 | Test 3: Booleans 15 | fastIsEqual: 0.000005 ms 16 | Lodash isEqual: 0.000005 ms 17 | ➡️ Speed: 1.01x faster 18 | ──────────────────────────────────────────────────────────────────────────────── 19 | Test 4: NaN 20 | fastIsEqual: 0.000009 ms 21 | Lodash isEqual: 0.000012 ms 22 | ✅ Speed: 1.36x faster 23 | ──────────────────────────────────────────────────────────────────────────────── 24 | Test 5: Large Numbers 25 | fastIsEqual: 0.000006 ms 26 | Lodash isEqual: 0.000006 ms 27 | ⚠️ Speed: 0.99x slower 28 | ──────────────────────────────────────────────────────────────────────────────── 29 | Test 6: Negative Zero 30 | fastIsEqual: 0.000005 ms 31 | Lodash isEqual: 0.000005 ms 32 | ➡️ Speed: 1.00x faster 33 | ──────────────────────────────────────────────────────────────────────────────── 34 | Test 7: Empty Objects 35 | fastIsEqual: 0.000127 ms 36 | Lodash isEqual: 0.000222 ms 37 | ✅ Speed: 1.75x faster 38 | ──────────────────────────────────────────────────────────────────────────────── 39 | Test 8: Single Property Object 40 | fastIsEqual: 0.000100 ms 41 | Lodash isEqual: 0.000264 ms 42 | 🚀 Speed: 2.64x faster 43 | ──────────────────────────────────────────────────────────────────────────────── 44 | Test 9: Simple Object (equal) 45 | fastIsEqual: 0.000131 ms 46 | Lodash isEqual: 0.000304 ms 47 | 🚀 Speed: 2.32x faster 48 | ──────────────────────────────────────────────────────────────────────────────── 49 | Test 10: Simple Object (unequal) 50 | fastIsEqual: 0.000100 ms 51 | Lodash isEqual: 0.000287 ms 52 | 🚀 Speed: 2.87x faster 53 | ──────────────────────────────────────────────────────────────────────────────── 54 | Test 11: Object with null prototype 55 | fastIsEqual: 0.000196 ms 56 | Lodash isEqual: 0.000310 ms 57 | ✅ Speed: 1.59x faster 58 | ──────────────────────────────────────────────────────────────────────────────── 59 | Test 12: Nested Object (equal) 60 | fastIsEqual: 0.000385 ms 61 | Lodash isEqual: 0.000868 ms 62 | 🚀 Speed: 2.26x faster 63 | ──────────────────────────────────────────────────────────────────────────────── 64 | Test 13: Nested Object (unequal) 65 | fastIsEqual: 0.000282 ms 66 | Lodash isEqual: 0.000847 ms 67 | 🚀 Speed: 3.00x faster 68 | ──────────────────────────────────────────────────────────────────────────────── 69 | Test 14: Deeply Nested (5 levels) 70 | fastIsEqual: 0.000674 ms 71 | Lodash isEqual: 0.001482 ms 72 | 🚀 Speed: 2.20x faster 73 | ──────────────────────────────────────────────────────────────────────────────── 74 | Test 15: Empty Arrays 75 | fastIsEqual: 0.000021 ms 76 | Lodash isEqual: 0.000083 ms 77 | 🚀 Speed: 3.93x faster 78 | ──────────────────────────────────────────────────────────────────────────────── 79 | Test 16: Single Element Array 80 | fastIsEqual: 0.000022 ms 81 | Lodash isEqual: 0.000091 ms 82 | 🚀 Speed: 4.07x faster 83 | ──────────────────────────────────────────────────────────────────────────────── 84 | Test 17: Array of Primitives (equal) 85 | fastIsEqual: 0.000024 ms 86 | Lodash isEqual: 0.000095 ms 87 | 🚀 Speed: 3.97x faster 88 | ──────────────────────────────────────────────────────────────────────────────── 89 | Test 18: Array of Primitives (unequal) 90 | fastIsEqual: 0.000023 ms 91 | Lodash isEqual: 0.000103 ms 92 | 🚀 Speed: 4.38x faster 93 | ──────────────────────────────────────────────────────────────────────────────── 94 | Test 19: Large Array of Numbers (100) 95 | fastIsEqual: 0.000392 ms 96 | Lodash isEqual: 0.000486 ms 97 | ➡️ Speed: 1.24x faster 98 | ──────────────────────────────────────────────────────────────────────────────── 99 | Test 20: Array of Strings 100 | fastIsEqual: 0.000032 ms 101 | Lodash isEqual: 0.000106 ms 102 | 🚀 Speed: 3.33x faster 103 | ──────────────────────────────────────────────────────────────────────────────── 104 | Test 21: Mixed Type Array 105 | fastIsEqual: 0.000031 ms 106 | Lodash isEqual: 0.000103 ms 107 | 🚀 Speed: 3.32x faster 108 | ──────────────────────────────────────────────────────────────────────────────── 109 | Test 22: Sparse Array 110 | fastIsEqual: 0.000036 ms 111 | Lodash isEqual: 0.000111 ms 112 | 🚀 Speed: 3.12x faster 113 | ──────────────────────────────────────────────────────────────────────────────── 114 | Test 23: Array of Objects (equal) 115 | fastIsEqual: 0.000268 ms 116 | Lodash isEqual: 0.000644 ms 117 | 🚀 Speed: 2.41x faster 118 | ──────────────────────────────────────────────────────────────────────────────── 119 | Test 24: Uint8Array 120 | fastIsEqual: 0.000059 ms 121 | Lodash isEqual: 0.000674 ms 122 | 🚀 Speed: 11.34x faster 123 | ──────────────────────────────────────────────────────────────────────────────── 124 | Test 25: Float32Array 125 | fastIsEqual: 0.000060 ms 126 | Lodash isEqual: 0.000680 ms 127 | 🚀 Speed: 11.30x faster 128 | ──────────────────────────────────────────────────────────────────────────────── 129 | Test 26: Large TypedArray (1000) 130 | fastIsEqual: 0.000944 ms 131 | Lodash isEqual: 0.013165 ms 132 | 🚀 Speed: 13.95x faster 133 | ──────────────────────────────────────────────────────────────────────────────── 134 | Test 27: ArrayBuffer (small) 135 | fastIsEqual: 0.000092 ms 136 | Lodash isEqual: 0.001263 ms 137 | 🚀 Speed: 13.74x faster 138 | ──────────────────────────────────────────────────────────────────────────────── 139 | Test 28: Dates (equal) 140 | fastIsEqual: 0.000023 ms 141 | Lodash isEqual: 0.000198 ms 142 | 🚀 Speed: 8.63x faster 143 | ──────────────────────────────────────────────────────────────────────────────── 144 | Test 29: RegExp (equal) 145 | fastIsEqual: 0.000042 ms 146 | Lodash isEqual: 0.000417 ms 147 | 🚀 Speed: 9.81x faster 148 | ──────────────────────────────────────────────────────────────────────────────── 149 | Test 30: RegExp (unequal flags) 150 | fastIsEqual: 0.000042 ms 151 | Lodash isEqual: 0.000431 ms 152 | 🚀 Speed: 10.25x faster 153 | ──────────────────────────────────────────────────────────────────────────────── 154 | Test 31: Circular Reference 155 | fastIsEqual: 0.000136 ms 156 | Lodash isEqual: 0.000507 ms 157 | 🚀 Speed: 3.72x faster 158 | ──────────────────────────────────────────────────────────────────────────────── 159 | Test 32: Mutual Circular 160 | fastIsEqual: 0.000275 ms 161 | Lodash isEqual: 0.000839 ms 162 | 🚀 Speed: 3.04x faster 163 | ──────────────────────────────────────────────────────────────────────────────── 164 | Test 33: Empty Map 165 | fastIsEqual: 0.000058 ms 166 | Lodash isEqual: 0.000684 ms 167 | 🚀 Speed: 11.84x faster 168 | ──────────────────────────────────────────────────────────────────────────────── 169 | Test 34: Map with primitives 170 | fastIsEqual: 0.000092 ms 171 | Lodash isEqual: 0.001487 ms 172 | 🚀 Speed: 16.09x faster 173 | ──────────────────────────────────────────────────────────────────────────────── 174 | Test 35: Map (unequal) 175 | fastIsEqual: 0.000092 ms 176 | Lodash isEqual: 0.001406 ms 177 | 🚀 Speed: 15.29x faster 178 | ──────────────────────────────────────────────────────────────────────────────── 179 | Test 36: Large Map (50 entries) 180 | fastIsEqual: 0.001059 ms 181 | Lodash isEqual: 0.025756 ms 182 | 🚀 Speed: 24.32x faster 183 | ──────────────────────────────────────────────────────────────────────────────── 184 | Test 37: Empty Set 185 | fastIsEqual: 0.000058 ms 186 | Lodash isEqual: 0.000691 ms 187 | 🚀 Speed: 11.96x faster 188 | ──────────────────────────────────────────────────────────────────────────────── 189 | Test 38: Set of numbers 190 | fastIsEqual: 0.000087 ms 191 | Lodash isEqual: 0.000958 ms 192 | 🚀 Speed: 10.96x faster 193 | ──────────────────────────────────────────────────────────────────────────────── 194 | Test 39: Set (unequal) 195 | fastIsEqual: 0.000087 ms 196 | Lodash isEqual: 0.000939 ms 197 | 🚀 Speed: 10.84x faster 198 | ──────────────────────────────────────────────────────────────────────────────── 199 | Test 40: Set of strings 200 | fastIsEqual: 0.000082 ms 201 | Lodash isEqual: 0.000940 ms 202 | 🚀 Speed: 11.51x faster 203 | ──────────────────────────────────────────────────────────────────────────────── 204 | Test 41: Large Set (100 items) 205 | fastIsEqual: 0.000673 ms 206 | Lodash isEqual: 0.037564 ms 207 | 🚀 Speed: 55.84x faster 208 | ──────────────────────────────────────────────────────────────────────────────── 209 | Test 42: Object vs Array 210 | fastIsEqual: 0.000009 ms 211 | Lodash isEqual: 0.000033 ms 212 | 🚀 Speed: 3.62x faster 213 | ──────────────────────────────────────────────────────────────────────────────── 214 | Test 43: Map vs Set 215 | fastIsEqual: 0.000018 ms 216 | Lodash isEqual: 0.000485 ms 217 | 🚀 Speed: 26.52x faster 218 | ──────────────────────────────────────────────────────────────────────────────── 219 | Test 44: String vs Number 220 | fastIsEqual: 0.000007 ms 221 | Lodash isEqual: 0.000006 ms 222 | ⚠️ Speed: 0.95x slower 223 | ──────────────────────────────────────────────────────────────────────────────── 224 | Test 45: Boolean vs Number 225 | fastIsEqual: 0.000006 ms 226 | Lodash isEqual: 0.000006 ms 227 | ⚠️ Speed: 0.99x slower 228 | ──────────────────────────────────────────────────────────────────────────────── 229 | Test 46: User Object 230 | fastIsEqual: 0.000188 ms 231 | Lodash isEqual: 0.000360 ms 232 | ✅ Speed: 1.91x faster 233 | ──────────────────────────────────────────────────────────────────────────────── 234 | Test 47: API Response 235 | fastIsEqual: 0.000672 ms 236 | Lodash isEqual: 0.001423 ms 237 | 🚀 Speed: 2.12x faster 238 | ──────────────────────────────────────────────────────────────────────────────── 239 | Test 48: Config Object 240 | fastIsEqual: 0.000242 ms 241 | Lodash isEqual: 0.000491 ms 242 | 🚀 Speed: 2.03x faster 243 | ──────────────────────────────────────────────────────────────────────────────── 244 | Test 49: State Object 245 | fastIsEqual: 0.000432 ms 246 | Lodash isEqual: 0.000810 ms 247 | ✅ Speed: 1.88x faster 248 | ──────────────────────────────────────────────────────────────────────────────── 249 | ════════════════════════════════════════════════════════════════════════════════ 250 | SUMMARY 251 | ════════════════════════════════════════════════════════════════════════════════ 252 | Overall Performance: 253 | Average fastIsEqual time: 0.000172 ms 254 | Average Lodash isEqual time: 0.002013 ms 255 | fastIsEqual is 11.73x faster on average 256 | 257 | Win Rate: 258 | fastIsEqual wins: 46/49 (93.9%) 259 | Lodash wins: 3/49 (6.1%) 260 | 261 | 🏆 Top 10 Best Performance Gains: 262 | 1. Large Set (100 items): 55.84x faster 263 | 2. Map vs Set: 26.52x faster 264 | 3. Large Map (50 entries): 24.32x faster 265 | 4. Map with primitives: 16.09x faster 266 | 5. Map (unequal): 15.29x faster 267 | 6. Large TypedArray (1000): 13.95x faster 268 | 7. ArrayBuffer (small): 13.74x faster 269 | 8. Empty Set: 11.96x faster 270 | 9. Empty Map: 11.84x faster 271 | 10. Set of strings: 11.51x faster 272 | 273 | ⚠️ Cases where Lodash performed better: 274 | 1. String vs Number: 0.95x 275 | 2. Large Numbers: 0.99x 276 | 3. Boolean vs Number: 0.99x 277 | 278 | ════════════════════════════════════════════════════════════════════════════════ 279 | ✨ Done in 108.11s. -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Pre-defined constants to avoid repeated string comparisons 2 | const TYPEOF_OBJECT = 'object'; 3 | const TYPEOF_FUNCTION = 'function'; 4 | const TYPEOF_NUMBER = 'number'; 5 | const TYPEOF_STRING = 'string'; 6 | const TYPEOF_BOOLEAN = 'boolean'; 7 | const TYPEOF_SYMBOL = 'symbol'; 8 | const TYPEOF_BIGINT = 'bigint'; 9 | 10 | // Inline NaN check for maximum speed 11 | const isNaN = Number.isNaN; 12 | 13 | // Cache for constructor checks 14 | const dateConstructor = Date; 15 | const regExpConstructor = RegExp; 16 | const mapConstructor = Map; 17 | const setConstructor = Set; 18 | const arrayBufferConstructor = ArrayBuffer; 19 | const promiseConstructor = Promise; 20 | const errorConstructor = Error; 21 | const dataViewConstructor = DataView; 22 | 23 | export function fastIsEqual(a: any, b: any) { 24 | // Fast path for strict equality 25 | if (a === b) return true; 26 | 27 | // Handle null/undefined early with single comparison 28 | if (a == null || b == null) return false; 29 | 30 | // Get types once 31 | const typeA = typeof a; 32 | 33 | // Type mismatch = not equal (avoid second typeof if possible) 34 | if (typeA === TYPEOF_NUMBER) { 35 | // Optimize number comparison - avoid typeof b when possible 36 | return typeof b === TYPEOF_NUMBER && isNaN(a) && isNaN(b); 37 | } 38 | 39 | if (typeA === TYPEOF_STRING || typeA === TYPEOF_BOOLEAN || typeA === TYPEOF_FUNCTION || typeA === TYPEOF_SYMBOL || typeA === TYPEOF_BIGINT) { 40 | return false; // We know a !== b from first check 41 | } 42 | 43 | // Now check if b is also object 44 | if (typeof b !== TYPEOF_OBJECT) return false; 45 | 46 | // At this point, we know both are objects 47 | 48 | // Array check using fastest method 49 | const aIsArray = Array.isArray(a); 50 | if (aIsArray !== Array.isArray(b)) return false; 51 | 52 | // Constructor check 53 | const aCtor = a.constructor; 54 | if (aCtor !== b.constructor) return false; 55 | 56 | // Fast path for arrays - highly optimized 57 | if (aIsArray) { 58 | const len = a.length; 59 | if (len !== b.length) return false; 60 | 61 | // Empty arrays 62 | if (len === 0) return true; 63 | 64 | // Small arrays - unroll loop with minimal overhead 65 | if (len < 8) { 66 | for (let i = 0; i < len; i++) { 67 | // Sparse array check 68 | const hasA = i in a; 69 | if (hasA !== (i in b)) return false; 70 | if (!hasA) continue; 71 | 72 | const elemA = a[i]; 73 | const elemB = b[i]; 74 | 75 | // Fast path for identical elements 76 | if (elemA === elemB) continue; 77 | 78 | // Null check 79 | if (elemA == null || elemB == null) return false; 80 | 81 | // Type check 82 | const elemTypeA = typeof elemA; 83 | if (elemTypeA !== typeof elemB) return false; 84 | 85 | // Number special case 86 | if (elemTypeA === TYPEOF_NUMBER) { 87 | if (!(isNaN(elemA) && isNaN(elemB))) return false; 88 | continue; 89 | } 90 | 91 | // Primitive comparison 92 | if (elemTypeA !== TYPEOF_OBJECT && elemTypeA !== TYPEOF_FUNCTION) { 93 | return false; 94 | } 95 | 96 | // Need deep comparison - use minimal visited map 97 | if (!deepEqual(elemA, elemB, new Map())) return false; 98 | } 99 | return true; 100 | } 101 | 102 | // Large arrays - use deep equal 103 | return deepEqual(a, b, new Map()); 104 | } 105 | 106 | // Handle built-in types inline for common cases 107 | if (aCtor === dateConstructor) { 108 | return a.getTime() === b.getTime(); 109 | } 110 | 111 | if (aCtor === regExpConstructor) { 112 | return a.source === b.source && a.flags === b.flags; 113 | } 114 | 115 | // For all other objects, use deep comparison 116 | return deepEqual(a, b, new Map()); 117 | } 118 | 119 | function deepEqual(valA: any, valB: any, visited: Map): boolean { 120 | // Fast equality check 121 | if (valA === valB) return true; 122 | 123 | // Null check 124 | if (valA == null || valB == null) return false; 125 | 126 | // Type check 127 | const typeA = typeof valA; 128 | if (typeA !== typeof valB) return false; 129 | 130 | // Primitive types 131 | if (typeA === TYPEOF_NUMBER) { 132 | return isNaN(valA) && isNaN(valB); 133 | } 134 | 135 | if (typeA !== TYPEOF_OBJECT && typeA !== TYPEOF_FUNCTION) { 136 | return false; 137 | } 138 | 139 | // Check visited - optimized with single lookup 140 | const visitedVal = visited.get(valA); 141 | if (visitedVal !== undefined) return visitedVal === valB; 142 | if (visited.has(valB)) return false; 143 | 144 | // Constructor check 145 | const ctorA = valA.constructor; 146 | if (ctorA !== valB.constructor) return false; 147 | 148 | // Date - inline comparison 149 | if (ctorA === dateConstructor) { 150 | return valA.getTime() === valB.getTime(); 151 | } 152 | 153 | // RegExp - inline comparison 154 | if (ctorA === regExpConstructor) { 155 | return valA.source === valB.source && valA.flags === valB.flags; 156 | } 157 | 158 | // Promise and Error - reference equality only 159 | if (ctorA === promiseConstructor || ctorA === errorConstructor) { 160 | return false; 161 | } 162 | 163 | // Arrays - optimized 164 | if (Array.isArray(valA)) { 165 | const len = valA.length; 166 | if (len !== valB.length) return false; 167 | 168 | // Mark visited early 169 | visited.set(valA, valB); 170 | visited.set(valB, valA); 171 | 172 | // Empty arrays 173 | if (len === 0) return true; 174 | 175 | // Optimized loop - check primitives first for early exit 176 | for (let i = 0; i < len; i++) { 177 | // Sparse array handling 178 | const hasA = i in valA; 179 | if (hasA !== (i in valB)) return false; 180 | if (!hasA) continue; 181 | 182 | const elemA = valA[i]; 183 | const elemB = valB[i]; 184 | 185 | if (elemA !== elemB && !deepEqual(elemA, elemB, visited)) { 186 | return false; 187 | } 188 | } 189 | return true; 190 | } 191 | 192 | // Map - optimized 193 | if (ctorA === mapConstructor) { 194 | const mapA = valA as Map; 195 | const mapB = valB as Map; 196 | 197 | if (mapA.size !== mapB.size) return false; 198 | 199 | // Empty maps 200 | if (mapA.size === 0) return true; 201 | 202 | visited.set(valA, valB); 203 | visited.set(valB, valA); 204 | 205 | // Optimized iteration 206 | for (const [key, valueA] of mapA) { 207 | // Fast primitive key path 208 | const keyType = typeof key; 209 | if (keyType !== TYPEOF_OBJECT && keyType !== TYPEOF_FUNCTION) { 210 | if (!mapB.has(key)) return false; 211 | const valueB = mapB.get(key); 212 | if (valueA !== valueB && !deepEqual(valueA, valueB, visited)) { 213 | return false; 214 | } 215 | } else { 216 | // Complex key - need full search 217 | let found = false; 218 | for (const [keyB, valueB] of mapB) { 219 | if (deepEqual(key, keyB, visited) && deepEqual(valueA, valueB, visited)) { 220 | found = true; 221 | break; 222 | } 223 | } 224 | if (!found) return false; 225 | } 226 | } 227 | return true; 228 | } 229 | 230 | // Set - highly optimized 231 | if (ctorA === setConstructor) { 232 | const setA = valA as Set; 233 | const setB = valB as Set; 234 | 235 | if (setA.size !== setB.size) return false; 236 | 237 | // Empty sets 238 | if (setA.size === 0) return true; 239 | 240 | // Early visited check 241 | visited.set(valA, valB); 242 | visited.set(valB, valA); 243 | 244 | // For equal sets, we can optimize by checking if all primitives exist first 245 | let hasPrimitives = false; 246 | let hasObjects = false; 247 | 248 | // First pass - categorize and check primitives 249 | for (const val of setA) { 250 | const valType = typeof val; 251 | if (valType === TYPEOF_OBJECT || valType === TYPEOF_FUNCTION) { 252 | hasObjects = true; 253 | } else { 254 | hasPrimitives = true; 255 | if (!setB.has(val)) return false; // Fast fail for primitives 256 | } 257 | } 258 | 259 | // If only primitives, we're done 260 | if (!hasObjects) return true; 261 | 262 | // For objects, create arrays for matching 263 | const objectsA: any[] = []; 264 | const objectsB: any[] = []; 265 | 266 | for (const val of setA) { 267 | const valType = typeof val; 268 | if (valType === TYPEOF_OBJECT || valType === TYPEOF_FUNCTION) { 269 | objectsA.push(val); 270 | } 271 | } 272 | 273 | for (const val of setB) { 274 | const valType = typeof val; 275 | if (valType === TYPEOF_OBJECT || valType === TYPEOF_FUNCTION) { 276 | objectsB.push(val); 277 | } 278 | } 279 | 280 | // Match objects 281 | const used = new Uint8Array(objectsB.length); 282 | for (const valA of objectsA) { 283 | let found = false; 284 | for (let j = 0; j < objectsB.length; j++) { 285 | if (!used[j]) { 286 | const newVisited = new Map(visited); 287 | if (deepEqual(valA, objectsB[j], newVisited)) { 288 | used[j] = 1; 289 | found = true; 290 | break; 291 | } 292 | } 293 | } 294 | if (!found) return false; 295 | } 296 | 297 | return true; 298 | } 299 | 300 | // ArrayBuffer - optimized 301 | if (ctorA === arrayBufferConstructor) { 302 | const bufA = valA as ArrayBuffer; 303 | const bufB = valB as ArrayBuffer; 304 | 305 | const byteLength = bufA.byteLength; 306 | if (byteLength !== bufB.byteLength) return false; 307 | 308 | const viewA = new Uint8Array(bufA); 309 | const viewB = new Uint8Array(bufB); 310 | 311 | // Unroll loop for better performance on larger buffers 312 | let i = 0; 313 | const unrollEnd = byteLength - 7; 314 | 315 | for (; i < unrollEnd; i += 8) { 316 | if (viewA[i] !== viewB[i] || 317 | viewA[i + 1] !== viewB[i + 1] || 318 | viewA[i + 2] !== viewB[i + 2] || 319 | viewA[i + 3] !== viewB[i + 3] || 320 | viewA[i + 4] !== viewB[i + 4] || 321 | viewA[i + 5] !== viewB[i + 5] || 322 | viewA[i + 6] !== viewB[i + 6] || 323 | viewA[i + 7] !== viewB[i + 7]) { 324 | return false; 325 | } 326 | } 327 | 328 | // Handle remaining bytes 329 | for (; i < byteLength; i++) { 330 | if (viewA[i] !== viewB[i]) return false; 331 | } 332 | 333 | return true; 334 | } 335 | 336 | // DataView - optimized 337 | if (ctorA === dataViewConstructor) { 338 | const viewA = valA as DataView; 339 | const viewB = valB as DataView; 340 | if (viewA.byteLength !== viewB.byteLength || viewA.byteOffset !== viewB.byteOffset) { 341 | return false; 342 | } 343 | // Compare the underlying buffer data 344 | for (let i = 0; i < viewA.byteLength; i++) { 345 | if (viewA.getUint8(i) !== viewB.getUint8(i)) return false; 346 | } 347 | return true; 348 | } 349 | 350 | // TypedArrays 351 | if (ArrayBuffer.isView(valA)) { 352 | const arrA = valA as any; 353 | const arrB = valB as any; 354 | const len = arrA.length; 355 | if (len !== arrB.length) return false; 356 | 357 | // Small typed arrays 358 | if (len < 16) { 359 | for (let i = 0; i < len; i++) { 360 | if (arrA[i] !== arrB[i]) return false; 361 | } 362 | return true; 363 | } 364 | 365 | // Large typed arrays - unroll loop 366 | let i = 0; 367 | const unrollLen = len - 3; 368 | for (; i < unrollLen; i += 4) { 369 | if (arrA[i] !== arrB[i] || 370 | arrA[i + 1] !== arrB[i + 1] || 371 | arrA[i + 2] !== arrB[i + 2] || 372 | arrA[i + 3] !== arrB[i + 3]) { 373 | return false; 374 | } 375 | } 376 | // Handle remaining 377 | for (; i < len; i++) { 378 | if (arrA[i] !== arrB[i]) return false; 379 | } 380 | return true; 381 | } 382 | 383 | // Plain objects - highly optimized 384 | visited.set(valA, valB); 385 | visited.set(valB, valA); 386 | 387 | // Get keys efficiently 388 | const keysA = Object.keys(valA); 389 | const keysALen = keysA.length; 390 | 391 | // Quick length check 392 | if (keysALen !== Object.keys(valB).length) return false; 393 | 394 | // Empty objects - check symbols 395 | if (keysALen === 0) { 396 | const checkSymbols = Object.getOwnPropertySymbols !== undefined; 397 | if (checkSymbols) { 398 | const symbolsA = Object.getOwnPropertySymbols(valA); 399 | if (symbolsA.length !== Object.getOwnPropertySymbols(valB).length) { 400 | return false; 401 | } 402 | // Check symbol properties 403 | for (let i = 0; i < symbolsA.length; i++) { 404 | const sym = symbolsA[i]; 405 | if (!(sym in valB) || !deepEqual(valA[sym], valB[sym], visited)) { 406 | return false; 407 | } 408 | } 409 | } 410 | return true; 411 | } 412 | 413 | // Optimized property checking - batch primitive checks 414 | for (let i = 0; i < keysALen; i++) { 415 | const key = keysA[i]; 416 | // Use in operator for fastest check 417 | if (!(key in valB)) return false; 418 | 419 | const propA = valA[key]; 420 | const propB = valB[key]; 421 | 422 | // Quick primitive equality check 423 | if (propA !== propB) { 424 | // Only do deep comparison if needed 425 | const propTypeA = typeof propA; 426 | if (propTypeA === TYPEOF_OBJECT || propTypeA === TYPEOF_FUNCTION) { 427 | if (!deepEqual(propA, propB, visited)) return false; 428 | } else if (propTypeA === TYPEOF_NUMBER) { 429 | if (!(isNaN(propA) && isNaN(propB))) return false; 430 | } else { 431 | return false; 432 | } 433 | } 434 | } 435 | 436 | // Check for symbols only if likely to have them 437 | const checkSymbols = Object.getOwnPropertySymbols !== undefined; 438 | if (checkSymbols) { 439 | const symbolsA = Object.getOwnPropertySymbols(valA); 440 | if (symbolsA.length > 0) { 441 | if (symbolsA.length !== Object.getOwnPropertySymbols(valB).length) { 442 | return false; 443 | } 444 | // Check symbol properties 445 | for (let i = 0; i < symbolsA.length; i++) { 446 | const sym = symbolsA[i]; 447 | if (!(sym in valB) || !deepEqual(valA[sym], valB[sym], visited)) { 448 | return false; 449 | } 450 | } 451 | } 452 | } 453 | 454 | return true; 455 | } 456 | -------------------------------------------------------------------------------- /src/__tests__/fastIsEqual.test.ts: -------------------------------------------------------------------------------- 1 | import { fastIsEqual } from '../index'; 2 | 3 | describe('fastIsEqual', () => { 4 | describe('Primitive Types', () => { 5 | describe('Basic primitives', () => { 6 | it('should return true for identical primitives', () => { 7 | expect(fastIsEqual(1, 1)).toBe(true); 8 | expect(fastIsEqual('a', 'a')).toBe(true); 9 | expect(fastIsEqual(true, true)).toBe(true); 10 | }); 11 | 12 | it('should return false for different primitives', () => { 13 | expect(fastIsEqual(1, 2)).toBe(false); 14 | expect(fastIsEqual('a', 'b')).toBe(false); 15 | expect(fastIsEqual(true, false)).toBe(false); 16 | }); 17 | 18 | it('should return false for different types', () => { 19 | expect(fastIsEqual(1, '1')).toBe(false); 20 | expect(fastIsEqual({}, [])).toBe(false); 21 | expect(fastIsEqual(new Map(), new Set())).toBe(false); 22 | }); 23 | 24 | it('should handle number comparison with non-number efficiently', () => { 25 | expect(fastIsEqual(42, '42')).toBe(false); 26 | expect(fastIsEqual(NaN, 'NaN')).toBe(false); 27 | }); 28 | 29 | it('should efficiently handle primitive type mismatches', () => { 30 | expect(fastIsEqual('string', true)).toBe(false); 31 | expect(fastIsEqual(true, 42)).toBe(false); 32 | expect(fastIsEqual(() => { }, 'function')).toBe(false); 33 | }); 34 | }); 35 | 36 | describe('Special numeric values', () => { 37 | it('should return true for NaN and NaN', () => { 38 | expect(fastIsEqual(NaN, NaN)).toBe(true); 39 | }); 40 | 41 | it('should handle -0 and +0', () => { 42 | expect(fastIsEqual(-0, +0)).toBe(true); 43 | }); 44 | 45 | it('should handle -0 === +0 correctly', () => { 46 | expect(fastIsEqual(-0, +0)).toBe(true); 47 | expect(fastIsEqual(-0, 0)).toBe(true); 48 | }); 49 | 50 | it('should handle Infinity', () => { 51 | expect(fastIsEqual(Infinity, Infinity)).toBe(true); 52 | expect(fastIsEqual(-Infinity, -Infinity)).toBe(true); 53 | expect(fastIsEqual(Infinity, -Infinity)).toBe(false); 54 | }); 55 | }); 56 | 57 | describe('Null and undefined', () => { 58 | it('should return true for null and null', () => { 59 | expect(fastIsEqual(null, null)).toBe(true); 60 | }); 61 | 62 | it('should return true for undefined and undefined', () => { 63 | expect(fastIsEqual(undefined, undefined)).toBe(true); 64 | }); 65 | 66 | it('should return false for null and undefined', () => { 67 | expect(fastIsEqual(null, undefined)).toBe(false); 68 | }); 69 | }); 70 | 71 | describe('Symbols', () => { 72 | it('should return true for identical symbols', () => { 73 | const sym = Symbol('test'); 74 | expect(fastIsEqual(sym, sym)).toBe(true); 75 | }); 76 | 77 | it('should return false for different symbols', () => { 78 | const sym1 = Symbol('test'); 79 | const sym2 = Symbol('test'); 80 | expect(fastIsEqual(sym1, sym2)).toBe(false); 81 | }); 82 | 83 | it('should handle objects with symbol properties', () => { 84 | const sym = Symbol('test'); 85 | const obj1 = { [sym]: 'value' }; 86 | const obj2 = { [sym]: 'value' }; 87 | expect(fastIsEqual(obj1, obj2)).toBe(true); 88 | }); 89 | }); 90 | 91 | describe('BigInt', () => { 92 | it('should handle BigInt values', () => { 93 | expect(fastIsEqual(BigInt(123), BigInt(123))).toBe(true); 94 | expect(fastIsEqual(BigInt(123), BigInt(124))).toBe(false); 95 | expect(fastIsEqual(BigInt(123), 123)).toBe(false); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('Objects', () => { 101 | describe('Plain objects', () => { 102 | it('should return true for empty objects', () => { 103 | const obj1 = {}; 104 | const obj2 = {}; 105 | expect(fastIsEqual(obj1, obj2)).toBe(true); 106 | }); 107 | 108 | it('should return true for identical objects', () => { 109 | const obj = { a: 1, b: { c: 2 } }; 110 | expect(fastIsEqual(obj, obj)).toBe(true); 111 | }); 112 | 113 | it('should return true for deeply equal objects', () => { 114 | const obj1 = { a: 1, b: { c: 2 } }; 115 | const obj2 = { a: 1, b: { c: 2 } }; 116 | expect(fastIsEqual(obj1, obj2)).toBe(true); 117 | }); 118 | 119 | it('should return false for objects with different keys', () => { 120 | const obj1 = { a: 1 }; 121 | const obj2 = { b: 1 }; 122 | expect(fastIsEqual(obj1, obj2)).toBe(false); 123 | }); 124 | 125 | it('should return false for objects with different numbers of keys', () => { 126 | const obj1 = { a: 1 }; 127 | const obj2 = { a: 1, b: 2 }; 128 | expect(fastIsEqual(obj1, obj2)).toBe(false); 129 | }); 130 | 131 | it('should return false for objects with different values', () => { 132 | const obj1 = { a: 1 }; 133 | const obj2 = { a: 2 }; 134 | expect(fastIsEqual(obj1, obj2)).toBe(false); 135 | }); 136 | 137 | it('should return true for objects with matching NaN values', () => { 138 | const obj1 = { a: NaN }; 139 | const obj2 = { a: NaN }; 140 | expect(fastIsEqual(obj1, obj2)).toBe(true); 141 | }); 142 | 143 | it('should handle objects with numeric string keys correctly', () => { 144 | const obj1 = { '0': 'a', '1': 'b', '2': 'c' }; 145 | const obj2 = { '2': 'c', '0': 'a', '1': 'b' }; 146 | expect(fastIsEqual(obj1, obj2)).toBe(true); 147 | }); 148 | 149 | it('should handle objects with exactly 8 properties', () => { 150 | const obj1 = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 }; 151 | const obj2 = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 }; 152 | expect(fastIsEqual(obj1, obj2)).toBe(true); 153 | }); 154 | 155 | it('should return false comparing object to a symbol', () => { 156 | const sym = Symbol('test'); 157 | const obj = {}; 158 | expect(fastIsEqual(obj, sym)).toBe(false); 159 | }); 160 | }); 161 | 162 | describe('Objects with special prototypes', () => { 163 | it('should handle objects with null prototype', () => { 164 | const obj1 = Object.create(null); 165 | obj1.a = 1; 166 | const obj2 = Object.create(null); 167 | obj2.a = 1; 168 | expect(fastIsEqual(obj1, obj2)).toBe(true); 169 | }); 170 | 171 | it('should handle null prototype objects with symbols', () => { 172 | const sym = Symbol('test'); 173 | const obj1 = Object.create(null); 174 | obj1[sym] = { nested: true }; 175 | const obj2 = Object.create(null); 176 | obj2[sym] = { nested: true }; 177 | expect(fastIsEqual(obj1, obj2)).toBe(true); 178 | }); 179 | }); 180 | 181 | describe('Objects with symbol properties', () => { 182 | it('should handle empty objects with only symbol properties', () => { 183 | const sym = Symbol('test'); 184 | const obj1 = { [sym]: 'value' }; 185 | const obj2 = { [sym]: 'value' }; 186 | expect(fastIsEqual(obj1, obj2)).toBe(true); 187 | }); 188 | 189 | it('should correctly handle objects with only non-enumerable symbol properties', () => { 190 | const sym1 = Symbol('test'); 191 | const sym2 = Symbol('test2'); 192 | 193 | const obj1 = {}; 194 | Object.defineProperty(obj1, sym1, { value: 'a', enumerable: false }); 195 | Object.defineProperty(obj1, sym2, { value: 'b', enumerable: false }); 196 | 197 | const obj2 = {}; 198 | Object.defineProperty(obj2, sym1, { value: 'a', enumerable: false }); 199 | Object.defineProperty(obj2, sym2, { value: 'b', enumerable: false }); 200 | 201 | expect(fastIsEqual(obj1, obj2)).toBe(true); 202 | }); 203 | }); 204 | 205 | describe('Objects with property descriptors', () => { 206 | it('should handle non-enumerable properties', () => { 207 | const obj1 = {}; 208 | Object.defineProperty(obj1, 'hidden', { value: 'secret', enumerable: false }); 209 | const obj2 = {}; 210 | Object.defineProperty(obj2, 'hidden', { value: 'secret', enumerable: false }); 211 | expect(fastIsEqual(obj1, obj2)).toBe(true); 212 | }); 213 | }); 214 | }); 215 | 216 | describe('Arrays', () => { 217 | describe('Basic arrays', () => { 218 | it('should return true for identical arrays, same ref', () => { 219 | const arr = [1, 2, 3]; 220 | expect(fastIsEqual(arr, arr)).toBe(true); 221 | }); 222 | 223 | it('should return true for identical arrays', () => { 224 | const arr1 = [1, 2, 3]; 225 | const arr2 = [1, 2, 3]; 226 | expect(fastIsEqual(arr1, arr2)).toBe(true); 227 | }); 228 | 229 | it('should return true for deeply equal arrays', () => { 230 | const arr1 = [1, [2, 3]]; 231 | const arr2 = [1, [2, 3]]; 232 | expect(fastIsEqual(arr1, arr2)).toBe(true); 233 | }); 234 | 235 | it('should return false for arrays with different lengths', () => { 236 | const arr1 = [1, 2]; 237 | const arr2 = [1, 2, 3]; 238 | expect(fastIsEqual(arr1, arr2)).toBe(false); 239 | }); 240 | 241 | it('should return false for arrays with different elements', () => { 242 | const arr1 = [1, 2]; 243 | const arr2 = [1, 3]; 244 | expect(fastIsEqual(arr1, arr2)).toBe(false); 245 | }); 246 | 247 | it('should return true for empty arrays', () => { 248 | const arr1 = new Array(); 249 | const arr2 = new Array(); 250 | expect(fastIsEqual(arr1, arr2)).toBe(true); 251 | }); 252 | }); 253 | 254 | describe('Special cases small arrays', () => { 255 | it('should return true for two NaNs in a small array', () => { 256 | const arr1 = [1, 2, NaN, 3]; 257 | const arr2 = [1, 2, NaN, 3]; 258 | expect(fastIsEqual(arr1, arr2)).toBe(true); 259 | }); 260 | 261 | it('should return false for different symbols, different content', () => { 262 | const arr1 = [Symbol('one')]; 263 | const arr2 = [Symbol('two')]; 264 | expect(fastIsEqual(arr1, arr2)).toBe(false); 265 | }); 266 | 267 | it('should return false for different symbols, same content', () => { 268 | const arr1 = [Symbol('one')]; 269 | const arr2 = [Symbol('one')]; 270 | expect(fastIsEqual(arr1, arr2)).toBe(false); 271 | }); 272 | 273 | it('should return false if it contains mismatching nullish', () => { 274 | const arr1 = [1, 2, null, 3]; 275 | const arr2 = [1, 2, undefined, 3]; 276 | expect(fastIsEqual(arr1, arr2)).toBe(false); 277 | }); 278 | 279 | it('should return true if it contains matching nullish', () => { 280 | const arr1 = [1, 2, undefined, 3]; 281 | const arr2 = [1, 2, undefined, 3]; 282 | const arr3 = [1, 2, null, 3]; 283 | const arr4 = [1, 2, null, 3]; 284 | expect(fastIsEqual(arr1, arr2)).toBe(true); 285 | expect(fastIsEqual(arr3, arr4)).toBe(true); 286 | }); 287 | 288 | it('should return false if it contains mismatching types', () => { 289 | const arr1 = [1, 2, Symbol('test'), 3]; 290 | const arr2 = [1, 2, {}, 3]; 291 | expect(fastIsEqual(arr1, arr2)).toBe(false); 292 | }); 293 | 294 | it('should return true for contained empty arrays', () => { 295 | const arr1 = [[]]; 296 | const arr2 = [[]]; 297 | expect(fastIsEqual(arr1, arr2)).toBe(true); 298 | }); 299 | }); 300 | 301 | describe('Special cases arrays n > 8', () => { 302 | it('should return false if it contains mismatching nullish', () => { 303 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, null, 10]; 304 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, undefined, 10]; 305 | expect(fastIsEqual(arr1, arr2)).toBe(false); 306 | }); 307 | 308 | it('should return true if it contains matching nullish', () => { 309 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, undefined, 10]; 310 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, undefined, 10]; 311 | const arr3 = [1, 2, 3, 4, 5, 6, 7, 8, null, 10]; 312 | const arr4 = [1, 2, 3, 4, 5, 6, 7, 8, null, 10]; 313 | expect(fastIsEqual(arr1, arr2)).toBe(true); 314 | expect(fastIsEqual(arr3, arr4)).toBe(true); 315 | }); 316 | 317 | it('should return false if it contains mismatching types', () => { 318 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, Symbol('test'), 10]; 319 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, {}, 10]; 320 | expect(fastIsEqual(arr1, arr2)).toBe(false); 321 | }); 322 | 323 | it('should return false for contained array length mismatch', () => { 324 | const arr1 = [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]; 325 | const arr2 = [[1, 2, 3, 4, 5, 6, 7, 8, 9]]; 326 | expect(fastIsEqual(arr1, arr2)).toBe(false); 327 | }); 328 | 329 | it('shoulld return true for matching contained sparse arrays', () => { 330 | const arr1 = [[1, , 3, , 5, , 7, , 9, , 11, , 13, , 15]]; 331 | const arr2 = [[1, , 3, , 5, , 7, , 9, , 11, , 13, , 15]]; 332 | expect(fastIsEqual(arr1, arr2)).toBe(true); 333 | }); 334 | 335 | it('shoulld return false for different contained sparse arrays', () => { 336 | const arr1 = [[1, , 3, , 5, , 7, , 9, , 11, , 13, , 15]]; 337 | const arr2 = [[1, , 3, , 5, , 7, , 9, , 11, 12, 13, , 15]]; 338 | expect(fastIsEqual(arr1, arr2)).toBe(false); 339 | }); 340 | 341 | it('should return true for two NaNs', () => { 342 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, NaN, 10]; 343 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, NaN, 10]; 344 | expect(fastIsEqual(arr1, arr2)).toBe(true); 345 | }); 346 | 347 | it('should return false for different symbols, different content', () => { 348 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, Symbol('one'), 10]; 349 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, Symbol('two'), 10]; 350 | expect(fastIsEqual(arr1, arr2)).toBe(false); 351 | }); 352 | 353 | it('should return true for matching dates', () => { 354 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, new Date('2023-01-01'), 10]; 355 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, new Date('2023-01-01'), 10]; 356 | expect(fastIsEqual(arr1, arr2)).toBe(true); 357 | }); 358 | 359 | it('should return false for different dates', () => { 360 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, new Date('2023-01-01'), 10]; 361 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, new Date('2023-01-02'), 10]; 362 | expect(fastIsEqual(arr1, arr2)).toBe(false); 363 | }); 364 | 365 | it('should return true for matching regular expressions', () => { 366 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, /^[\d]+$/, 10]; 367 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, /^[\d]+$/, 10]; 368 | expect(fastIsEqual(arr1, arr2)).toBe(true); 369 | }); 370 | 371 | it('should return false for different regular expressions', () => { 372 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, /^[\d]+$/, 10]; 373 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, /^[\d]*$/, 10]; 374 | expect(fastIsEqual(arr1, arr2)).toBe(false); 375 | }); 376 | 377 | it('should return false for objects with different symbol keys, different descriptions', () => { 378 | const symb1 = Symbol('one'); 379 | const symb2 = Symbol('two'); 380 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb1]: 'one' }, 10]; 381 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb2]: 'two' }, 10]; 382 | expect(fastIsEqual(arr1, arr2)).toBe(false); 383 | }); 384 | 385 | it('should return false for objects with different symbol keys, same descriptions', () => { 386 | const symb1 = Symbol('one'); 387 | const symb2 = Symbol('one'); 388 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb1]: 'one' }, 10]; 389 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb2]: 'one' }, 10]; 390 | expect(fastIsEqual(arr1, arr2)).toBe(false); 391 | }); 392 | 393 | it('should return true for objects with the same symbol properties', () => { 394 | const symb1 = Symbol('one'); 395 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb1]: 'one' }, 10]; 396 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb1]: 'one' }, 10]; 397 | expect(fastIsEqual(arr1, arr2)).toBe(true); 398 | }); 399 | 400 | it('should return false for objects with the mismatching symbol properties', () => { 401 | const symb1 = Symbol('one'); 402 | const symb2 = Symbol('one'); 403 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb1]: 'one' }, 10]; 404 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb1]: 'one', [symb2]: 'two' }, 10]; 405 | expect(fastIsEqual(arr1, arr2)).toBe(false); 406 | }); 407 | 408 | it('should return true for objects with the same symbol properties, same content, and other props', () => { 409 | const symb1 = Symbol('one'); 410 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb1]: 'one', hello: 'world' }, 10]; 411 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, { [symb1]: 'one', hello: 'world' }, 10]; 412 | expect(fastIsEqual(arr1, arr2)).toBe(true); 413 | }); 414 | 415 | it('should return false for objects with different symbol properties and matching other props', () => { 416 | const symb1 = Symbol('one'); 417 | const symb2 = Symbol('one'); 418 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, { hello: 'world', [symb1]: 'one' }, 10]; 419 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, { hello: 'world', [symb2]: 'one' }, 10]; 420 | expect(fastIsEqual(arr1, arr2)).toBe(false); 421 | }); 422 | 423 | it('should return false for objects with same symbol properties, but too many, matching other props', () => { 424 | const symb1 = Symbol('one'); 425 | const symb2 = Symbol('one'); 426 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, { hello: 'world', [symb1]: 'one' }, 10]; 427 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, { hello: 'world', [symb1]: 'one', [symb2]: 'one' }, 10]; 428 | expect(fastIsEqual(arr1, arr2)).toBe(false); 429 | }); 430 | }); 431 | 432 | describe('Arrays with objects', () => { 433 | it('should return true with equal objects', () => { 434 | const arr1 = [1, 2, { one: 'two' }, 3]; 435 | const arr2 = [1, 2, { one: 'two' }, 3]; 436 | expect(fastIsEqual(arr1, arr2)).toBe(true); 437 | }); 438 | 439 | it('should return false with equal objects but different other values', () => { 440 | const arr1 = [1, 2, { one: 'two' }, 3]; 441 | const arr2 = [1, 2, { one: 'two' }, 4]; 442 | expect(fastIsEqual(arr1, arr2)).toBe(false); 443 | }); 444 | 445 | it('should return false with different objects', () => { 446 | const arr1 = [1, 2, { one: 'two' }, 3]; 447 | const arr2 = [1, 2, { one: 'three' }, 3]; 448 | expect(fastIsEqual(arr1, arr2)).toBe(false); 449 | }); 450 | 451 | it('should return true with equal objects at the end', () => { 452 | const arr1 = [1, 2, { one: 'two' }, 3, { four: 'five' }]; 453 | const arr2 = [1, 2, { one: 'two' }, 3, { four: 'five' }]; 454 | expect(fastIsEqual(arr1, arr2)).toBe(true); 455 | }); 456 | 457 | it('should return false with different objects at the end', () => { 458 | const arr1 = [1, 2, { one: 'two' }, 3, { four: 'five' }]; 459 | const arr2 = [1, 2, { one: 'two' }, 3, { four: 'six' }]; 460 | expect(fastIsEqual(arr1, arr2)).toBe(false); 461 | }); 462 | }); 463 | 464 | describe('Array optimization boundaries', () => { 465 | it('should handle arrays of exactly 8 elements (boundary case)', () => { 466 | const arr1 = [1, 2, 3, 4, 5, 6, 7, 8]; 467 | const arr2 = [1, 2, 3, 4, 5, 6, 7, 8]; 468 | expect(fastIsEqual(arr1, arr2)).toBe(true); 469 | }); 470 | 471 | it('should early exit on first few elements difference in large arrays', () => { 472 | const arr1 = new Array(1000).fill(1); 473 | const arr2 = new Array(1000).fill(1); 474 | arr2[2] = 2; 475 | expect(fastIsEqual(arr1, arr2)).toBe(false); 476 | }); 477 | }); 478 | 479 | describe('Sparse arrays', () => { 480 | it('should handle sparse arrays correctly', () => { 481 | const arr1 = [1, , 3]; // sparse array with hole 482 | const arr2 = [1, undefined, 3]; 483 | expect(fastIsEqual(arr1, arr2)).toBe(false); 484 | }); 485 | 486 | it('should handle sparse arrays in small array optimization path', () => { 487 | const arr1 = [1, , 3, , 5]; 488 | const arr2 = [1, , 3, , 5]; 489 | expect(fastIsEqual(arr1, arr2)).toBe(true); 490 | }); 491 | }); 492 | 493 | describe('Arrays with circular references', () => { 494 | it('should handle arrays with circular references', () => { 495 | const arr1: any[] = [1, 2]; 496 | const arr2: any[] = [1, 2]; 497 | arr1.push(arr1); 498 | arr2.push(arr2); 499 | expect(fastIsEqual(arr1, arr2)).toBe(true); 500 | }); 501 | 502 | it('should handle deeply nested circular references', () => { 503 | const arr1: any = [1, { a: [] }]; 504 | arr1[1].a.push(arr1); 505 | arr1.push(arr1[1]); 506 | 507 | const arr2: any = [1, { a: [] }]; 508 | arr2[1].a.push(arr2); 509 | arr2.push(arr2[1]); 510 | 511 | expect(fastIsEqual(arr1, arr2)).toBe(true); 512 | }); 513 | }); 514 | }); 515 | 516 | describe('Built-in Objects', () => { 517 | describe('Date objects', () => { 518 | it('should return true for identical dates', () => { 519 | const date = new Date(); 520 | expect(fastIsEqual(date, date)).toBe(true); 521 | }); 522 | 523 | it('should return true for dates with the same timestamp', () => { 524 | const date1 = new Date('2023-01-01'); 525 | const date2 = new Date('2023-01-01'); 526 | expect(fastIsEqual(date1, date2)).toBe(true); 527 | }); 528 | 529 | it('should return false for dates with different timestamps', () => { 530 | const date1 = new Date('2023-01-01'); 531 | const date2 = new Date('2023-01-02'); 532 | expect(fastIsEqual(date1, date2)).toBe(false); 533 | }); 534 | }); 535 | 536 | describe('RegExp objects', () => { 537 | it('should return true for identical regexes', () => { 538 | const regex = /a/g; 539 | expect(fastIsEqual(regex, regex)).toBe(true); 540 | }); 541 | 542 | it('should return true for regexes with the same pattern and flags', () => { 543 | const regex1 = /a/g; 544 | const regex2 = /a/g; 545 | expect(fastIsEqual(regex1, regex2)).toBe(true); 546 | }); 547 | 548 | it('should return false for regexes with different patterns', () => { 549 | const regex1 = /a/g; 550 | const regex2 = /b/g; 551 | expect(fastIsEqual(regex1, regex2)).toBe(false); 552 | }); 553 | 554 | it('should return false for regexes with different flags', () => { 555 | const regex1 = /a/g; 556 | const regex2 = /a/i; 557 | expect(fastIsEqual(regex1, regex2)).toBe(false); 558 | }); 559 | }); 560 | 561 | describe('Error objects', () => { 562 | it('should return true for identical Error instances', () => { 563 | const err = new Error('test'); 564 | expect(fastIsEqual(err, err)).toBe(true); 565 | }); 566 | 567 | it('should return false for different Error instances with same message', () => { 568 | const err1 = new Error('test'); 569 | const err2 = new Error('test'); 570 | expect(fastIsEqual(err1, err2)).toBe(false); 571 | }); 572 | }); 573 | 574 | describe('Promise objects', () => { 575 | it('should return false for different promises', () => { 576 | const p1 = Promise.resolve(1); 577 | const p2 = Promise.resolve(1); 578 | expect(fastIsEqual(p1, p2)).toBe(false); 579 | }); 580 | 581 | it('should handle promises with additional properties', () => { 582 | const p1 = Promise.resolve(1); 583 | const p2 = Promise.resolve(1); 584 | (p1 as any).customProp = 'test'; 585 | (p2 as any).customProp = 'test'; 586 | expect(fastIsEqual(p1, p2)).toBe(false); 587 | }); 588 | }); 589 | 590 | describe('Function objects', () => { 591 | it('should return true for the same function reference', () => { 592 | const func = () => { }; 593 | expect(fastIsEqual(func, func)).toBe(true); 594 | }); 595 | 596 | it('should return false for different functions', () => { 597 | const func1 = () => { }; 598 | const func2 = () => { }; 599 | expect(fastIsEqual(func1, func2)).toBe(false); 600 | }); 601 | 602 | it('should handle functions with properties', () => { 603 | const func1 = () => { }; 604 | func1.customProp = 'value'; 605 | const func2 = () => { }; 606 | func2.customProp = 'value'; 607 | expect(fastIsEqual(func1, func2)).toBe(false); 608 | }); 609 | }); 610 | }); 611 | 612 | describe('Collections', () => { 613 | describe('Map objects', () => { 614 | it('should return true for identical maps', () => { 615 | const map = new Map([['a', 1]]); 616 | expect(fastIsEqual(map, map)).toBe(true); 617 | }); 618 | 619 | it('should return true for maps with the same key-value pairs', () => { 620 | const map1 = new Map([['a', 1]]); 621 | const map2 = new Map([['a', 1]]); 622 | expect(fastIsEqual(map1, map2)).toBe(true); 623 | }); 624 | 625 | it('should return false for maps with different key-value pairs', () => { 626 | const map1 = new Map([['a', 1]]); 627 | const map2 = new Map([['a', 2]]); 628 | expect(fastIsEqual(map1, map2)).toBe(false); 629 | }); 630 | 631 | it('should return false for maps with different sizes', () => { 632 | const map1 = new Map([['a', 1]]); 633 | const map2 = new Map([['a', 1], ['b', 2]]); 634 | expect(fastIsEqual(map1, map2)).toBe(false); 635 | }); 636 | 637 | it('should handle empty maps', () => { 638 | expect(fastIsEqual(new Map(), new Map())).toBe(true); 639 | }); 640 | 641 | it('should handle maps with object keys', () => { 642 | const key1 = { id: 1 }; 643 | const key2 = { id: 1 }; 644 | const map1 = new Map([[key1, 'value']]); 645 | const map2 = new Map([[key2, 'value']]); 646 | expect(fastIsEqual(map1, map2)).toBe(true); 647 | }); 648 | 649 | it('should handle maps with NaN keys', () => { 650 | const map1 = new Map([[NaN, 'value']]); 651 | const map2 = new Map([[NaN, 'value']]); 652 | expect(fastIsEqual(map1, map2)).toBe(true); 653 | }); 654 | 655 | it('should handle maps with undefined values', () => { 656 | const map1 = new Map([['key', undefined]]); 657 | const map2 = new Map([['key', undefined]]); 658 | expect(fastIsEqual(map1, map2)).toBe(true); 659 | }); 660 | 661 | it('should handle maps with circular references between keys and values', () => { 662 | const key1: any = { id: 1 }; 663 | const value1: any = { data: key1 }; 664 | key1.ref = value1; 665 | 666 | const key2: any = { id: 1 }; 667 | const value2: any = { data: key2 }; 668 | key2.ref = value2; 669 | 670 | const map1 = new Map([[key1, value1]]); 671 | const map2 = new Map([[key2, value2]]); 672 | 673 | expect(fastIsEqual(map1, map2)).toBe(true); 674 | }); 675 | 676 | it('should return false for maps with completely disperate objects, primitive keys', () => { 677 | const map1 = new Map([['one', { name: 'test' }]]); 678 | const map2 = new Map([['two', { last: 12 }]]); 679 | expect(fastIsEqual(map1, map2)).toBe(false); 680 | }); 681 | 682 | it ('should return true for maps with matching objects, primitive keys', () => { 683 | const map1 = new Map([['one', { name: 'test' }]]); 684 | const map2 = new Map([['one', { name: 'test' }]]); 685 | expect(fastIsEqual(map1, map2)).toBe(true); 686 | }); 687 | 688 | it('should return false for maps with completely disperate objects, object keys', () => { 689 | const map1 = new Map([[{ key: 'one' }, { name: 'test' }]]); 690 | const map2 = new Map([[{ key: 'two' }, { last: 12 }]]); 691 | expect(fastIsEqual(map1, map2)).toBe(false); 692 | }); 693 | 694 | it('should return true for maps with matching objects, object keys', () => { 695 | const map1 = new Map([[{ key: 'one' }, { name: 'test' }]]); 696 | const map2 = new Map([[{ key: 'one' }, { name: 'test' }]]); 697 | expect(fastIsEqual(map1, map2)).toBe(true); 698 | }); 699 | }); 700 | 701 | describe('Set objects', () => { 702 | it('should return true for identical sets', () => { 703 | const set = new Set([1, 2]); 704 | expect(fastIsEqual(set, set)).toBe(true); 705 | }); 706 | 707 | it('should return true for sets with the same elements', () => { 708 | const set1 = new Set([1, 2]); 709 | const set2 = new Set([1, 2]); 710 | expect(fastIsEqual(set1, set2)).toBe(true); 711 | }); 712 | 713 | it('should return false for sets with different elements', () => { 714 | const set1 = new Set([1, 2]); 715 | const set2 = new Set([1, 3]); 716 | expect(fastIsEqual(set1, set2)).toBe(false); 717 | }); 718 | 719 | it('should return false for sets with different sizes', () => { 720 | const set1 = new Set([1, 2]); 721 | const set2 = new Set([1]); 722 | expect(fastIsEqual(set1, set2)).toBe(false); 723 | }); 724 | 725 | it('should return true for sets with equal objects', () => { 726 | const obj1 = { a: 1 }; 727 | const obj2 = { a: 1 }; 728 | const set1 = new Set([obj1]); 729 | const set2 = new Set([obj2]); 730 | expect(fastIsEqual(set1, set2)).toBe(true); 731 | }); 732 | 733 | it('should handle sets with nested structures', () => { 734 | const set1 = new Set([{ a: { b: 1 } }, [1, 2]]); 735 | const set2 = new Set([[1, 2], { a: { b: 1 } }]); 736 | expect(fastIsEqual(set1, set2)).toBe(true); 737 | }); 738 | 739 | it('should handle sets with mixed primitive and complex values', () => { 740 | const obj1 = { a: 1 }; 741 | const obj2 = { a: 1 }; 742 | const set1 = new Set([1, 'hello', obj1, true]); 743 | const set2 = new Set([true, obj2, 1, 'hello']); 744 | expect(fastIsEqual(set1, set2)).toBe(true); 745 | }); 746 | 747 | it('should handle sets where >70% are primitives (optimization path)', () => { 748 | const set1 = new Set([1, 2, 3, 4, 5, 6, 7, { a: 1 }, { b: 2 }]); 749 | const set2 = new Set([7, 6, 5, 4, 3, 2, 1, { b: 2 }, { a: 1 }]); 750 | expect(fastIsEqual(set1, set2)).toBe(true); 751 | }); 752 | 753 | it('should handle sets with duplicate-looking but different objects', () => { 754 | const obj1a = { x: { y: 1 } }; 755 | const obj1b = { x: { y: 1 } }; 756 | const obj2a = { x: { y: 1 } }; 757 | const obj2b = { x: { y: 1 } }; 758 | 759 | const set1 = new Set([obj1a, obj1b]); 760 | const set2 = new Set([obj2a, obj2b]); 761 | 762 | expect(fastIsEqual(set1, set2)).toBe(true); 763 | }); 764 | 765 | it('should handle sets containing self-referential objects', () => { 766 | const obj1: any = { name: 'test' }; 767 | obj1.self = obj1; 768 | const obj2: any = { name: 'test' }; 769 | obj2.self = obj2; 770 | 771 | const set1 = new Set([obj1, 'primitive']); 772 | const set2 = new Set(['primitive', obj2]); 773 | 774 | expect(fastIsEqual(set1, set2)).toBe(true); 775 | }); 776 | 777 | it('should handle sets with many similar objects efficiently', () => { 778 | const createObj = (n: number) => ({ a: 1, b: 2, c: 3, id: n }); 779 | const set1 = new Set(Array.from({ length: 100 }, (_, i) => createObj(i))); 780 | const set2 = new Set(Array.from({ length: 100 }, (_, i) => createObj(i))); 781 | 782 | expect(fastIsEqual(set1, set2)).toBe(true); 783 | }); 784 | 785 | it('should return false for one empty set', () => { 786 | const set1 = new Set(); 787 | const set2 = new Set([1, 2, 3]); 788 | expect(fastIsEqual(set1, set2)).toBe(false); 789 | }); 790 | 791 | it('should return true for matching empty sets', () => { 792 | const set1 = new Set(); 793 | const set2 = new Set(); 794 | expect(fastIsEqual(set1, set2)).toBe(true); 795 | }); 796 | 797 | it('should handle sets of completely disperate objects', () => { 798 | const set1 = new Set([{ name: 'test' }]); 799 | const set2 = new Set([{ last: 12 }]); 800 | expect(fastIsEqual(set1, set2)).toBe(false); 801 | }); 802 | }); 803 | }); 804 | 805 | describe('Binary Data Types', () => { 806 | describe('ArrayBuffer', () => { 807 | it('should return true for empty ArrayBuffers', () => { 808 | const buffer1 = new ArrayBuffer(); 809 | const buffer2 = new ArrayBuffer(); 810 | expect(fastIsEqual(buffer1, buffer2)).toBe(true); 811 | }); 812 | 813 | it('should return false for ArrayBuffers of different lengths', () => { 814 | const buffer1 = new ArrayBuffer(4); 815 | const buffer2 = new ArrayBuffer(3); 816 | expect(fastIsEqual(buffer1, buffer2)).toBe(false); 817 | }); 818 | 819 | it('should return false for different TypedArray views', () => { 820 | const arr1 = new Uint8Array([1, 2, 3]); 821 | const arr2 = new Uint16Array([1, 2, 3]); 822 | expect(fastIsEqual(arr1, arr2)).toBe(false); 823 | }); 824 | 825 | it('should return false for different array buffers', () => { 826 | const arr1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); 827 | const arr2 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -11, -12]); 828 | expect(fastIsEqual(arr1.buffer, arr2.buffer)).toBe(false); 829 | }); 830 | 831 | it('should return false for different larger array buffers', () => { 832 | const arr1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); 833 | const arr2 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, -16, 17]); 834 | expect(fastIsEqual(arr1, arr2)).toBe(false); 835 | }); 836 | 837 | it('should return false for different larger array buffers > 16', () => { 838 | const arr1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); 839 | const arr2 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, -17]); 840 | expect(fastIsEqual(arr1, arr2)).toBe(false); 841 | }); 842 | 843 | it('should return true for matching larger array buffers > 16', () => { 844 | const arr1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); 845 | const arr2 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); 846 | expect(fastIsEqual(arr1, arr2)).toBe(true); 847 | }); 848 | 849 | it('should handle ArrayBuffer comparison', () => { 850 | const buffer1 = new ArrayBuffer(8); 851 | const buffer2 = new ArrayBuffer(8); 852 | new Uint8Array(buffer1).set([1, 2, 3, 4]); 853 | new Uint8Array(buffer2).set([1, 2, 3, 4]); 854 | expect(fastIsEqual(buffer1, buffer2)).toBe(true); 855 | }); 856 | 857 | it('should handle ArrayBuffer with non-4-byte-aligned size', () => { 858 | const buffer1 = new ArrayBuffer(33); // 8 * 4 + 1 859 | const buffer2 = new ArrayBuffer(33); 860 | new Uint8Array(buffer1).fill(42); 861 | new Uint8Array(buffer2).fill(42); 862 | expect(fastIsEqual(buffer1, buffer2)).toBe(true); 863 | }); 864 | 865 | it('should handle small ArrayBuffer (< 32 bytes)', () => { 866 | const buffer1 = new ArrayBuffer(16); 867 | const buffer2 = new ArrayBuffer(16); 868 | new Uint8Array(buffer1).set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); 869 | new Uint8Array(buffer2).set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); 870 | expect(fastIsEqual(buffer1, buffer2)).toBe(true); 871 | }); 872 | 873 | it('should handle ArrayBuffer of exactly 32 bytes', () => { 874 | const buffer1 = new ArrayBuffer(32); 875 | const buffer2 = new ArrayBuffer(32); 876 | const view1 = new Uint8Array(buffer1); 877 | const view2 = new Uint8Array(buffer2); 878 | 879 | for (let i = 0; i < 32; i++) { 880 | view1[i] = i; 881 | view2[i] = i; 882 | } 883 | 884 | expect(fastIsEqual(buffer1, buffer2)).toBe(true); 885 | }); 886 | 887 | it('should handle ArrayBuffer with different views correctly', () => { 888 | const buffer1 = new ArrayBuffer(8); 889 | const view1 = new DataView(buffer1); 890 | view1.setInt32(0, 42); 891 | view1.setInt32(4, 100); 892 | 893 | const buffer2 = new ArrayBuffer(8); 894 | const view2 = new DataView(buffer2); 895 | view2.setInt32(0, 42); 896 | view2.setInt32(4, 200); 897 | 898 | expect(fastIsEqual(buffer1, buffer2)).toBe(false); 899 | }); 900 | }); 901 | 902 | describe('TypedArrays', () => { 903 | it('should return true for empty TypedArrays', () => { 904 | const arr1 = new Uint8Array([]); 905 | const arr2 = new Uint8Array([]); 906 | expect(fastIsEqual(arr1, arr2)).toBe(true); 907 | }); 908 | 909 | it('should return false for different TypedArray lengths', () => { 910 | const arr1 = new Uint8Array([1, 2, 3]); 911 | const arr2 = new Uint8Array([1, 2]); 912 | expect(fastIsEqual(arr1, arr2)).toBe(false); 913 | }); 914 | 915 | it('should return true for identical TypedArrays', () => { 916 | const arr1 = new Uint8Array([1, 2, 3]); 917 | const arr2 = new Uint8Array([1, 2, 3]); 918 | expect(fastIsEqual(arr1, arr2)).toBe(true); 919 | }); 920 | 921 | it('should return false for TypedArrays with different values', () => { 922 | const arr1 = new Uint8Array([1, 2, 3]); 923 | const arr2 = new Uint8Array([1, 2, 4]); 924 | expect(fastIsEqual(arr1, arr2)).toBe(false); 925 | }); 926 | 927 | it('should return false for different TypedArray types', () => { 928 | const arr1 = new Uint8Array([1, 2, 3]); 929 | const arr2 = new Int8Array([1, 2, 3]); 930 | expect(fastIsEqual(arr1, arr2)).toBe(false); 931 | }); 932 | 933 | it('should return false for different TypedArray constructors with same values', () => { 934 | const arr1 = new Uint16Array([1, 2, 3]); 935 | const arr2 = new Uint32Array([1, 2, 3]); 936 | expect(fastIsEqual(arr1, arr2)).toBe(false); 937 | }); 938 | 939 | it('should handle typed arrays with length exactly 16', () => { 940 | const arr1 = new Float32Array(16).fill(3.14); 941 | const arr2 = new Float32Array(16).fill(3.14); 942 | expect(fastIsEqual(arr1, arr2)).toBe(true); 943 | }); 944 | 945 | it('should handle typed arrays with non-multiple-of-4 length', () => { 946 | const arr1 = new Int32Array([1, 2, 3, 4, 5, 6, 7]); 947 | const arr2 = new Int32Array([1, 2, 3, 4, 5, 6, 7]); 948 | expect(fastIsEqual(arr1, arr2)).toBe(true); 949 | }); 950 | 951 | it('should handle TypedArrays with different buffer offsets', () => { 952 | const buffer = new ArrayBuffer(16); 953 | const arr1 = new Uint8Array(buffer, 4, 4); 954 | const arr2 = new Uint8Array(buffer, 8, 4); 955 | arr1.set([1, 2, 3, 4]); 956 | arr2.set([1, 2, 3, 4]); 957 | expect(fastIsEqual(arr1, arr2)).toBe(true); 958 | }); 959 | }); 960 | 961 | describe('DataView', () => { 962 | it('should handle DataView comparison', () => { 963 | const buffer1 = new ArrayBuffer(8); 964 | const view1 = new DataView(buffer1); 965 | view1.setInt32(0, 42); 966 | view1.setFloat32(4, 3.14); 967 | 968 | const buffer2 = new ArrayBuffer(8); 969 | const view2 = new DataView(buffer2); 970 | view2.setInt32(0, 42); 971 | view2.setFloat32(4, 3.14); 972 | 973 | expect(fastIsEqual(view1, view2)).toBe(true); 974 | }); 975 | 976 | it('should handle DataView with different values', () => { 977 | const view1 = new DataView(new ArrayBuffer(8)); 978 | const view2 = new DataView(new ArrayBuffer(8)); 979 | view1.setInt32(0, 42); 980 | view2.setInt32(0, 43); 981 | 982 | expect(fastIsEqual(view1, view2)).toBe(false); 983 | }); 984 | 985 | it('should handle DataView with different byte lengths', () => { 986 | const view1 = new DataView(new ArrayBuffer(8)); 987 | const view2 = new DataView(new ArrayBuffer(16)); 988 | 989 | expect(fastIsEqual(view1, view2)).toBe(false); 990 | }); 991 | }); 992 | }); 993 | 994 | describe('Circular References', () => { 995 | it('should return true for circular references', () => { 996 | const obj1: any = {}; 997 | obj1.self = obj1; 998 | const obj2: any = {}; 999 | obj2.self = obj2; 1000 | expect(fastIsEqual(obj1, obj2)).toBe(true); 1001 | }); 1002 | 1003 | it('should return false for different circular references', () => { 1004 | const obj1: any = {}; 1005 | obj1.self = obj1; 1006 | const obj2: any = { self: {} }; 1007 | expect(fastIsEqual(obj1, obj2)).toBe(false); 1008 | }); 1009 | 1010 | it('should handle mutual circular references', () => { 1011 | const obj1: any = { a: {} }; 1012 | const obj2: any = { a: {} }; 1013 | obj1.a.b = obj1; 1014 | obj2.a.b = obj2; 1015 | expect(fastIsEqual(obj1, obj2)).toBe(true); 1016 | }); 1017 | 1018 | it('should handle different circular reference structures', () => { 1019 | const obj1: any = { a: { b: {} } }; 1020 | obj1.a.b.c = obj1.a; 1021 | const obj2: any = { a: { b: {} } }; 1022 | obj2.a.b.c = obj2; 1023 | expect(fastIsEqual(obj1, obj2)).toBe(false); 1024 | }); 1025 | }); 1026 | }); --------------------------------------------------------------------------------