├── .nvmrc ├── .gitignore ├── tsconfig.node.json ├── jest.config.js ├── tsconfig.json ├── .github └── workflows │ ├── verify.yml │ └── deploy.yml ├── package.json ├── benchmarks ├── fastIsEqual.benchmark.ts └── results.txt ├── README.md └── src ├── index.ts └── __tests__ └── fastIsEqual.test.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | tmp/ -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "moduleResolution": "Node", 6 | "target": "ES2020", 7 | "sourceMap": true 8 | } 9 | } -------------------------------------------------------------------------------- /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 | collectCoverageFrom: [ 11 | 'src/index.ts' 12 | ] 13 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": [ 14 | "node_modules", 15 | "dist", 16 | "benchmarks", 17 | "src/__tests__/**" 18 | ] 19 | } -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | branches: [ main, public-package ] 5 | pull_request: 6 | branches: [ main, public-package ] 7 | jobs: 8 | ci: 9 | runs-on: ubuntu-24.04 10 | strategy: 11 | matrix: 12 | node-version: [20.x, 22.x, 24.x] 13 | steps: 14 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm ci 20 | 21 | - name: build 22 | run: npm run build 23 | 24 | - name: test 25 | run: npm test 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [ public-package ] 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-24.04 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 14 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 15 | with: 16 | node-version: '24.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Update npm 19 | run: npm install -g npm@latest # ensure npm 11.5.1 or later 20 | - run: npm ci 21 | - name: Verify 22 | run: npm test 23 | - name: Build 24 | run: npm run build 25 | - name: Publish 26 | if: ${{ success() }} 27 | run: npm publish --access public -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@localnerve/fast-is-equal", 3 | "version": "2.0.3", 4 | "description": "Blazing-fast equality checks, minus the baggage. A lean, standalone alternative to Lodash's isEqual—because speed matters.", 5 | "keywords": [ 6 | "lodash", 7 | "isEqual", 8 | "typescript", 9 | "react", 10 | "react-native" 11 | ], 12 | "homepage": "https://github.com/localnerve/fast-is-equal#readme", 13 | "bugs": { 14 | "url": "https://github.com/localnerve/fast-is-equal/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/localnerve/fast-is-equal.git" 19 | }, 20 | "license": "MIT", 21 | "author": "Jairaj Jangle (https://github.com/JairajJangle)", 22 | "type": "module", 23 | "main": "dist/index.js", 24 | "types": "dist/index.d.ts", 25 | "scripts": { 26 | "test": "jest", 27 | "build": "rimraf dist && tsc", 28 | "prepublishOnly": "npm run build", 29 | "benchmark": "ts-node --project tsconfig.node.json benchmarks/fastIsEqual.benchmark.ts" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "devDependencies": { 35 | "@types/jest": "^30.0.0", 36 | "@types/lodash": "^4.17.21", 37 | "jest": "^30.2.0", 38 | "lodash": "^4.17.21", 39 | "rimraf": "^6.1.2", 40 | "ts-jest": "^29.4.6", 41 | "ts-node": "^10.9.2", 42 | "tslib": "^2.8.1", 43 | "typescript": "^5.9.3" 44 | }, 45 | "funding": [ 46 | { 47 | "type": "individual", 48 | "url": "https://www.paypal.com/paypalme/jairajjangle001/usd" 49 | }, 50 | { 51 | "type": "individual", 52 | "url": "https://liberapay.com/FutureJJ/donate" 53 | }, 54 | { 55 | "type": "individual", 56 | "url": "https://ko-fi.com/futurejj" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /benchmarks/fastIsEqual.benchmark.ts: -------------------------------------------------------------------------------- 1 | import { performance } from 'node:perf_hooks'; 2 | import { fastIsEqual } from '../src/index.ts'; 3 | import isEqual from 'lodash/isEqual.js'; 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-is-equal 2 | 3 | > A fork of JairajJangle/fast-is-equal that works with modern bundlers without plugins (full ESNext) 4 | 5 | Blazing-fast equality checks, minus the baggage. A lean, standalone alternative to Lodash’s `isEqual` - because speed matters. 6 | 7 | ![](https://img.shields.io/npm/v/@localnerve/fast-is-equal/latest.svg) 8 | ![](https://github.com/localnerve/fast-is-equal/workflows/Verify/badge.svg) 9 | ![](https://img.shields.io/npm/dt/@localnerve/fast-is-equal.svg) 10 | ![](https://img.shields.io/npm/l/@localnerve/fast-is-equal.svg) 11 | 12 | 13 | ## Installation 14 | 15 | Using npm: 16 | 17 | ```bash 18 | npm install fast-is-equal 19 | ``` 20 | 21 | ## Usage 22 | ```typescript 23 | import { fastIsEqual } from 'fast-is-equal'; 24 | 25 | console.log(fastIsEqual(1, 1)); // true 26 | console.log(fastIsEqual({ a: 1 }, { a: 1 })); // true 27 | console.log(fastIsEqual([1, 2], [1, 3])); // false 28 | ``` 29 | 30 | ## Features 31 | - Lightweight and dependency-free. 32 | - Handles primitives, objects, arrays, Maps, Sets, circular references, and more. 33 | - Optimized for performance (see benchmarks). 34 | 35 | ## Benchmarks 36 | `fast-is-equal` outperforms Lodash’s `isEqual` in most cases. Run `npm run benchmark` locally to compare: 37 | ```bash 38 | Performance Comparison: fastIsEqual vs Lodash isEqual 39 | Iterations per test case: 1,000,000 40 | ════════════════════════════════════════════════════════════════════════════════ 41 | Test 1: Numbers 42 | fastIsEqual: 0.000005 ms 43 | Lodash isEqual: 0.000005 ms 44 | ➡️ Speed: 1.11x faster 45 | ──────────────────────────────────────────────────────────────────────────────── 46 | Test 2: Strings 47 | fastIsEqual: 0.000006 ms 48 | Lodash isEqual: 0.000008 ms 49 | ✅ Speed: 1.39x faster 50 | ──────────────────────────────────────────────────────────────────────────────── 51 | Test 3: Booleans 52 | fastIsEqual: 0.000005 ms 53 | Lodash isEqual: 0.000005 ms 54 | ➡️ Speed: 1.01x faster 55 | ──────────────────────────────────────────────────────────────────────────────── 56 | Test 4: NaN 57 | fastIsEqual: 0.000009 ms 58 | Lodash isEqual: 0.000012 ms 59 | ✅ Speed: 1.36x faster 60 | ──────────────────────────────────────────────────────────────────────────────── 61 | Test 5: Large Numbers 62 | fastIsEqual: 0.000006 ms 63 | Lodash isEqual: 0.000006 ms 64 | ⚠️ Speed: 0.99x slower 65 | ──────────────────────────────────────────────────────────────────────────────── 66 | Test 6: Negative Zero 67 | fastIsEqual: 0.000005 ms 68 | Lodash isEqual: 0.000005 ms 69 | ➡️ Speed: 1.00x faster 70 | ──────────────────────────────────────────────────────────────────────────────── 71 | Test 7: Empty Objects 72 | fastIsEqual: 0.000127 ms 73 | Lodash isEqual: 0.000222 ms 74 | ✅ Speed: 1.75x faster 75 | ──────────────────────────────────────────────────────────────────────────────── 76 | Test 8: Single Property Object 77 | fastIsEqual: 0.000100 ms 78 | Lodash isEqual: 0.000264 ms 79 | 🚀 Speed: 2.64x faster 80 | ──────────────────────────────────────────────────────────────────────────────── 81 | Test 9: Simple Object (equal) 82 | fastIsEqual: 0.000131 ms 83 | Lodash isEqual: 0.000304 ms 84 | 🚀 Speed: 2.32x faster 85 | ──────────────────────────────────────────────────────────────────────────────── 86 | Test 10: Simple Object (unequal) 87 | fastIsEqual: 0.000100 ms 88 | Lodash isEqual: 0.000287 ms 89 | 🚀 Speed: 2.87x faster 90 | ──────────────────────────────────────────────────────────────────────────────── 91 | Test 11: Object with null prototype 92 | fastIsEqual: 0.000196 ms 93 | Lodash isEqual: 0.000310 ms 94 | ✅ Speed: 1.59x faster 95 | ──────────────────────────────────────────────────────────────────────────────── 96 | Test 12: Nested Object (equal) 97 | fastIsEqual: 0.000385 ms 98 | Lodash isEqual: 0.000868 ms 99 | 🚀 Speed: 2.26x faster 100 | ──────────────────────────────────────────────────────────────────────────────── 101 | Test 13: Nested Object (unequal) 102 | fastIsEqual: 0.000282 ms 103 | Lodash isEqual: 0.000847 ms 104 | 🚀 Speed: 3.00x faster 105 | ──────────────────────────────────────────────────────────────────────────────── 106 | Test 14: Deeply Nested (5 levels) 107 | fastIsEqual: 0.000674 ms 108 | Lodash isEqual: 0.001482 ms 109 | 🚀 Speed: 2.20x faster 110 | ──────────────────────────────────────────────────────────────────────────────── 111 | Test 15: Empty Arrays 112 | fastIsEqual: 0.000021 ms 113 | Lodash isEqual: 0.000083 ms 114 | 🚀 Speed: 3.93x faster 115 | ──────────────────────────────────────────────────────────────────────────────── 116 | Test 16: Single Element Array 117 | fastIsEqual: 0.000022 ms 118 | Lodash isEqual: 0.000091 ms 119 | 🚀 Speed: 4.07x faster 120 | ──────────────────────────────────────────────────────────────────────────────── 121 | Test 17: Array of Primitives (equal) 122 | fastIsEqual: 0.000024 ms 123 | Lodash isEqual: 0.000095 ms 124 | 🚀 Speed: 3.97x faster 125 | ──────────────────────────────────────────────────────────────────────────────── 126 | Test 18: Array of Primitives (unequal) 127 | fastIsEqual: 0.000023 ms 128 | Lodash isEqual: 0.000103 ms 129 | 🚀 Speed: 4.38x faster 130 | ──────────────────────────────────────────────────────────────────────────────── 131 | Test 19: Large Array of Numbers (100) 132 | fastIsEqual: 0.000392 ms 133 | Lodash isEqual: 0.000486 ms 134 | ➡️ Speed: 1.24x faster 135 | ──────────────────────────────────────────────────────────────────────────────── 136 | Test 20: Array of Strings 137 | fastIsEqual: 0.000032 ms 138 | Lodash isEqual: 0.000106 ms 139 | 🚀 Speed: 3.33x faster 140 | ──────────────────────────────────────────────────────────────────────────────── 141 | Test 21: Mixed Type Array 142 | fastIsEqual: 0.000031 ms 143 | Lodash isEqual: 0.000103 ms 144 | 🚀 Speed: 3.32x faster 145 | ──────────────────────────────────────────────────────────────────────────────── 146 | Test 22: Sparse Array 147 | fastIsEqual: 0.000036 ms 148 | Lodash isEqual: 0.000111 ms 149 | 🚀 Speed: 3.12x faster 150 | ──────────────────────────────────────────────────────────────────────────────── 151 | Test 23: Array of Objects (equal) 152 | fastIsEqual: 0.000268 ms 153 | Lodash isEqual: 0.000644 ms 154 | 🚀 Speed: 2.41x faster 155 | ──────────────────────────────────────────────────────────────────────────────── 156 | Test 24: Uint8Array 157 | fastIsEqual: 0.000059 ms 158 | Lodash isEqual: 0.000674 ms 159 | 🚀 Speed: 11.34x faster 160 | ──────────────────────────────────────────────────────────────────────────────── 161 | Test 25: Float32Array 162 | fastIsEqual: 0.000060 ms 163 | Lodash isEqual: 0.000680 ms 164 | 🚀 Speed: 11.30x faster 165 | ──────────────────────────────────────────────────────────────────────────────── 166 | Test 26: Large TypedArray (1000) 167 | fastIsEqual: 0.000944 ms 168 | Lodash isEqual: 0.013165 ms 169 | 🚀 Speed: 13.95x faster 170 | ──────────────────────────────────────────────────────────────────────────────── 171 | Test 27: ArrayBuffer (small) 172 | fastIsEqual: 0.000092 ms 173 | Lodash isEqual: 0.001263 ms 174 | 🚀 Speed: 13.74x faster 175 | ──────────────────────────────────────────────────────────────────────────────── 176 | Test 28: Dates (equal) 177 | fastIsEqual: 0.000023 ms 178 | Lodash isEqual: 0.000198 ms 179 | 🚀 Speed: 8.63x faster 180 | ──────────────────────────────────────────────────────────────────────────────── 181 | Test 29: RegExp (equal) 182 | fastIsEqual: 0.000042 ms 183 | Lodash isEqual: 0.000417 ms 184 | 🚀 Speed: 9.81x faster 185 | ──────────────────────────────────────────────────────────────────────────────── 186 | Test 30: RegExp (unequal flags) 187 | fastIsEqual: 0.000042 ms 188 | Lodash isEqual: 0.000431 ms 189 | 🚀 Speed: 10.25x faster 190 | ──────────────────────────────────────────────────────────────────────────────── 191 | Test 31: Circular Reference 192 | fastIsEqual: 0.000136 ms 193 | Lodash isEqual: 0.000507 ms 194 | 🚀 Speed: 3.72x faster 195 | ──────────────────────────────────────────────────────────────────────────────── 196 | Test 32: Mutual Circular 197 | fastIsEqual: 0.000275 ms 198 | Lodash isEqual: 0.000839 ms 199 | 🚀 Speed: 3.04x faster 200 | ──────────────────────────────────────────────────────────────────────────────── 201 | Test 33: Empty Map 202 | fastIsEqual: 0.000058 ms 203 | Lodash isEqual: 0.000684 ms 204 | 🚀 Speed: 11.84x faster 205 | ──────────────────────────────────────────────────────────────────────────────── 206 | Test 34: Map with primitives 207 | fastIsEqual: 0.000092 ms 208 | Lodash isEqual: 0.001487 ms 209 | 🚀 Speed: 16.09x faster 210 | ──────────────────────────────────────────────────────────────────────────────── 211 | Test 35: Map (unequal) 212 | fastIsEqual: 0.000092 ms 213 | Lodash isEqual: 0.001406 ms 214 | 🚀 Speed: 15.29x faster 215 | ──────────────────────────────────────────────────────────────────────────────── 216 | Test 36: Large Map (50 entries) 217 | fastIsEqual: 0.001059 ms 218 | Lodash isEqual: 0.025756 ms 219 | 🚀 Speed: 24.32x faster 220 | ──────────────────────────────────────────────────────────────────────────────── 221 | Test 37: Empty Set 222 | fastIsEqual: 0.000058 ms 223 | Lodash isEqual: 0.000691 ms 224 | 🚀 Speed: 11.96x faster 225 | ──────────────────────────────────────────────────────────────────────────────── 226 | Test 38: Set of numbers 227 | fastIsEqual: 0.000087 ms 228 | Lodash isEqual: 0.000958 ms 229 | 🚀 Speed: 10.96x faster 230 | ──────────────────────────────────────────────────────────────────────────────── 231 | Test 39: Set (unequal) 232 | fastIsEqual: 0.000087 ms 233 | Lodash isEqual: 0.000939 ms 234 | 🚀 Speed: 10.84x faster 235 | ──────────────────────────────────────────────────────────────────────────────── 236 | Test 40: Set of strings 237 | fastIsEqual: 0.000082 ms 238 | Lodash isEqual: 0.000940 ms 239 | 🚀 Speed: 11.51x faster 240 | ──────────────────────────────────────────────────────────────────────────────── 241 | Test 41: Large Set (100 items) 242 | fastIsEqual: 0.000673 ms 243 | Lodash isEqual: 0.037564 ms 244 | 🚀 Speed: 55.84x faster 245 | ──────────────────────────────────────────────────────────────────────────────── 246 | Test 42: Object vs Array 247 | fastIsEqual: 0.000009 ms 248 | Lodash isEqual: 0.000033 ms 249 | 🚀 Speed: 3.62x faster 250 | ──────────────────────────────────────────────────────────────────────────────── 251 | Test 43: Map vs Set 252 | fastIsEqual: 0.000018 ms 253 | Lodash isEqual: 0.000485 ms 254 | 🚀 Speed: 26.52x faster 255 | ──────────────────────────────────────────────────────────────────────────────── 256 | Test 44: String vs Number 257 | fastIsEqual: 0.000007 ms 258 | Lodash isEqual: 0.000006 ms 259 | ⚠️ Speed: 0.95x slower 260 | ──────────────────────────────────────────────────────────────────────────────── 261 | Test 45: Boolean vs Number 262 | fastIsEqual: 0.000006 ms 263 | Lodash isEqual: 0.000006 ms 264 | ⚠️ Speed: 0.99x slower 265 | ──────────────────────────────────────────────────────────────────────────────── 266 | Test 46: User Object 267 | fastIsEqual: 0.000188 ms 268 | Lodash isEqual: 0.000360 ms 269 | ✅ Speed: 1.91x faster 270 | ──────────────────────────────────────────────────────────────────────────────── 271 | Test 47: API Response 272 | fastIsEqual: 0.000672 ms 273 | Lodash isEqual: 0.001423 ms 274 | 🚀 Speed: 2.12x faster 275 | ──────────────────────────────────────────────────────────────────────────────── 276 | Test 48: Config Object 277 | fastIsEqual: 0.000242 ms 278 | Lodash isEqual: 0.000491 ms 279 | 🚀 Speed: 2.03x faster 280 | ──────────────────────────────────────────────────────────────────────────────── 281 | Test 49: State Object 282 | fastIsEqual: 0.000432 ms 283 | Lodash isEqual: 0.000810 ms 284 | ✅ Speed: 1.88x faster 285 | ──────────────────────────────────────────────────────────────────────────────── 286 | ════════════════════════════════════════════════════════════════════════════════ 287 | SUMMARY 288 | ════════════════════════════════════════════════════════════════════════════════ 289 | Overall Performance: 290 | Average fastIsEqual time: 0.000172 ms 291 | Average Lodash isEqual time: 0.002013 ms 292 | fastIsEqual is 11.73x faster on average 293 | 294 | Win Rate: 295 | fastIsEqual wins: 46/49 (93.9%) 296 | Lodash wins: 3/49 (6.1%) 297 | 298 | 🏆 Top 10 Best Performance Gains: 299 | 1. Large Set (100 items): 55.84x faster 300 | 2. Map vs Set: 26.52x faster 301 | 3. Large Map (50 entries): 24.32x faster 302 | 4. Map with primitives: 16.09x faster 303 | 5. Map (unequal): 15.29x faster 304 | 6. Large TypedArray (1000): 13.95x faster 305 | 7. ArrayBuffer (small): 13.74x faster 306 | 8. Empty Set: 11.96x faster 307 | 9. Empty Map: 11.84x faster 308 | 10. Set of strings: 11.51x faster 309 | 310 | ⚠️ Cases where Lodash performed better: 311 | 1. String vs Number: 0.95x 312 | 2. Large Numbers: 0.99x 313 | 3. Boolean vs Number: 0.99x 314 | 315 | ════════════════════════════════════════════════════════════════════════════════ 316 | ✨ Done in 108.11s. 317 | ``` 318 | 319 | ## License 320 | MIT 321 | ``` 322 | MIT License 323 | 324 | Copyright (c) 2025 Jairaj Jangle 325 | 326 | Permission is hereby granted, free of charge, to any person obtaining a copy 327 | of this software and associated documentation files (the "Software"), to deal 328 | in the Software without restriction, including without limitation the rights 329 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 330 | copies of the Software, and to permit persons to whom the Software is 331 | furnished to do so, subject to the following conditions: 332 | 333 | The above copyright notice and this permission notice shall be included in all 334 | copies or substantial portions of the Software. 335 | 336 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 337 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 338 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 339 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 340 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 341 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 342 | SOFTWARE. 343 | ``` 344 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | }); --------------------------------------------------------------------------------