├── .eslintignore ├── .eslintrc.json ├── .github └── pull_request_template.md ├── .gitignore ├── .npmignore ├── .prettierrc ├── .yarnrc.yml ├── README.md ├── package.json ├── src ├── index.spec.ts └── index.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .prettierrc.ts 4 | .eslintrc.json 5 | env.d.ts 6 | vite.config.ts -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | // By extending from a plugin config, we can get recommended rules without having to add them manually. 4 | "eslint:recommended", 5 | "plugin:import/recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | // This disables the formatting rules in ESLint that Prettier is going to be responsible for handling. 8 | // Make sure it's always the last config, so it gets the chance to override other configs. 9 | "eslint-config-prettier" 10 | ], 11 | 12 | "settings": { 13 | // Tells eslint how to resolve imports 14 | "import/resolver": { 15 | "node": { 16 | "paths": ["src"], 17 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 18 | } 19 | } 20 | }, 21 | "parserOptions": { 22 | "sourceType": "module" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull request template 2 | 3 | ***REMOVE THIS - START*** 4 | 5 | 1. Title should be something descriptive about what you're changes do. (It will default to whatever you put as your commit message.) 6 | 2. Make sure to point to `dev` branch; 7 | 3. Mark your pull request as `DRAFT` until it will be ready to review; 8 | 4. Before marking a PR ready to review, make sure that: 9 | 10 | a. You have done your changes in a separate branch. Branches MUST have descriptive names that start with either the `fix/` or `feature/` prefixes. Good examples are: `fix/signin-issue` or `feature/issue-templates`. 11 | 12 | b. `npm test` doesn't throw any error. 13 | 14 | 5. Describe your changes, link to the issue and add images if relevant under each #TODO next comments; 15 | 6. MAKE SURE TO CLEAN ALL THIS 6 POINTS BEFORE SUBMITTING 16 | 17 | ***REMOVE THIS - END*** 18 | 19 | ## Describe your changes 20 | TODO: Add a short summary of your changes and impact: 21 | 22 | ## Link to issue this resolves 23 | TODO: Add your issue no. or link to the issue. Example: 24 | Fixes: #100 25 | 26 | ## Screenshot of changes(if relevant) 27 | TODO: Add images: 28 | 29 | 🙏🙏 !! THANK YOU !! 🚀🚀 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .yarn/* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120, 7 | "bracketSpacing": true 8 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generic Binary Search Library 2 | 3 | A versatile and efficient TypeScript library for performing various binary search operations on arrays of any type. 4 | 5 | ## Quick Examples 6 | 7 | ```typescript 8 | import { binaryFind, binaryFindClosest, binaryFindGte } from 'generic-binary-search'; 9 | 10 | const products = [ 11 | { price: 10.99, name: 'Basic Widget' }, 12 | { price: 24.99, name: 'Standard Widget' }, 13 | { price: 49.99, name: 'Premium Widget' }, 14 | { price: 99.99, name: 'Deluxe Widget' }, 15 | ]; 16 | 17 | // Find exact match 18 | const exactMatch = binaryFind(products, 24.99, (product) => product.price); 19 | console.log(exactMatch); // Output: { price: 24.99, name: 'Standard Widget' } 20 | 21 | // Find closest match 22 | const closestMatch = binaryFindClosest(products, 30, (product) => product.price); 23 | console.log(closestMatch); // Output: { price: 24.99, name: 'Standard Widget' } 24 | 25 | // Find first item greater than or equal to 26 | const gteMatch = binaryFindGte(products, 50, (product) => product.price); 27 | console.log(gteMatch); // Output: { price: 99.99, name: 'Deluxe Widget' } 28 | ``` 29 | 30 | ## Features 31 | 32 | - Generic implementation: Works with arrays of any type 33 | - Customizable value extraction: Provide a function to extract numeric values from your items 34 | - Multiple search operations: 35 | - Exact match 36 | - Closest match 37 | - Greater than / Less than 38 | - Greater than or equal / Less than or equal 39 | - Find all occurrences 40 | - Find items within a range 41 | - Find item containing a value (for range-based data) 42 | 43 | ## Rationale 44 | 45 | Binary search is a powerful algorithm for quickly finding elements in sorted arrays. However, most implementations are limited to simple arrays of numbers. This library provides a generic approach, allowing you to use binary search on arrays of any type by specifying a function to extract numeric values from your items. 46 | 47 | This flexibility makes the library useful for a wide range of applications, from simple number arrays to complex objects with multiple fields. 48 | 49 | ## Installation 50 | 51 | ```bash 52 | npm install generic-binary-search 53 | 54 | # or 55 | 56 | yarn add generic-binary-search 57 | ``` 58 | 59 | ## Usage 60 | 61 | Here are examples of how to use each function in the library with simple objects, ordered by most common usage: 62 | 63 | ### 1. Basic Binary Search (Exact Match) 64 | 65 | ```typescript 66 | import { binaryFind, binaryFindIndex } from 'generic-binary-search'; 67 | 68 | const employees = [ 69 | { salary: 30000, name: 'Alice' }, 70 | { salary: 45000, name: 'Bob' }, 71 | { salary: 60000, name: 'Charlie' }, 72 | { salary: 75000, name: 'David' }, 73 | ]; 74 | 75 | // Find the employee with salary 60000 76 | const employee = binaryFind(employees, 60000, (emp) => emp.salary); 77 | console.log(employee); // Output: { salary: 60000, name: 'Charlie' } 78 | 79 | // Find the index of the employee with salary 45000 80 | const index = binaryFindIndex(employees, 45000, (emp) => emp.salary); 81 | console.log(index); // Output: 1 82 | ``` 83 | 84 | ### 2. Closest Match 85 | 86 | ```typescript 87 | import { binaryFindClosest, binaryFindClosestIndex } from 'generic-binary-search'; 88 | 89 | const temperatures = [ 90 | { celsius: 15.5, location: 'Mountain' }, 91 | { celsius: 22.3, location: 'Beach' }, 92 | { celsius: 28.7, location: 'Desert' }, 93 | { celsius: 35.1, location: 'Volcano' }, 94 | ]; 95 | 96 | // Find the location with temperature closest to 25°C 97 | const closestTemp = binaryFindClosest(temperatures, 25, (temp) => temp.celsius); 98 | console.log(closestTemp); // Output: { celsius: 22.3, location: 'Beach' } 99 | 100 | // Find the index of the location with temperature closest to 30°C 101 | const closestIndex = binaryFindClosestIndex(temperatures, 30, (temp) => temp.celsius); 102 | console.log(closestIndex); // Output: 2 (index of Desert) 103 | ``` 104 | 105 | ### 3. Greater Than or Equal / Less Than or Equal 106 | 107 | ```typescript 108 | import { binaryFindGte, binaryFindLte, binaryFindGteIndex, binaryFindLteIndex } from 'generic-binary-search'; 109 | 110 | const houses = [ 111 | { price: 150000, address: '123 Oak St' }, 112 | { price: 250000, address: '456 Elm St' }, 113 | { price: 350000, address: '789 Maple St' }, 114 | { price: 450000, address: '101 Pine St' }, 115 | ]; 116 | 117 | // Find the first house with price greater than or equal to 300000 118 | const gteHouse = binaryFindGte(houses, 300000, (house) => house.price); 119 | console.log(gteHouse); // Output: { price: 350000, address: '789 Maple St' } 120 | 121 | // Find the most expensive house with price less than or equal to 400000 122 | const lteHouse = binaryFindLte(houses, 400000, (house) => house.price); 123 | console.log(lteHouse); // Output: { price: 350000, address: '789 Maple St' } 124 | 125 | // Find the index of the first house with price greater than or equal to 250000 126 | const gteIndex = binaryFindGteIndex(houses, 250000, (house) => house.price); 127 | console.log(gteIndex); // Output: 1 128 | 129 | // Find the index of the most expensive house with price less than or equal to 300000 130 | const lteIndex = binaryFindLteIndex(houses, 300000, (house) => house.price); 131 | console.log(lteIndex); // Output: 1 (index of 456 Elm St) 132 | ``` 133 | 134 | ### 4. Greater Than / Less Than 135 | 136 | ```typescript 137 | import { binaryFindGtIndex, binaryFindLtIndex } from 'generic-binary-search'; 138 | 139 | const scores = [ 140 | { points: 10, player: 'Alice' }, 141 | { points: 20, player: 'Bob' }, 142 | { points: 30, player: 'Charlie' }, 143 | { points: 40, player: 'David' }, 144 | ]; 145 | 146 | // Find the index of the first player with score greater than 25 147 | const gtIndex = binaryFindGtIndex(scores, 25, (score) => score.points); 148 | console.log(gtIndex); // Output: 2 (index of Charlie) 149 | 150 | // Find the index of the last player with score less than 35 151 | const ltIndex = binaryFindLtIndex(scores, 35, (score) => score.points); 152 | console.log(ltIndex); // Output: 2 (index of Charlie) 153 | ``` 154 | 155 | ### 5. Find Items Within a Range 156 | 157 | ```typescript 158 | import { binaryFindBetween, binaryFindIndicesBetween } from 'generic-binary-search'; 159 | 160 | const products = [ 161 | { price: 10.99, name: 'Basic Widget' }, 162 | { price: 24.99, name: 'Standard Widget' }, 163 | { price: 49.99, name: 'Premium Widget' }, 164 | { price: 99.99, name: 'Deluxe Widget' }, 165 | ]; 166 | 167 | // Find products with prices between 20 and 60 168 | const rangeProducts = binaryFindBetween(products, 20, 60, (product) => product.price); 169 | console.log(rangeProducts); // Output: [{ price: 24.99, name: 'Standard Widget' }, { price: 49.99, name: 'Premium Widget' }] 170 | 171 | // Find indices of products with prices between 20 and 60 172 | const rangeIndices = binaryFindIndicesBetween(products, 20, 60, (product) => product.price); 173 | console.log(rangeIndices); // Output: [1, 2] 174 | ``` 175 | 176 | ### 6. Find All Occurrences 177 | 178 | ```typescript 179 | import { binaryFindAll, binaryFindAllIndices } from 'generic-binary-search'; 180 | 181 | const books = [ 182 | { pageCount: 100, title: 'Short Stories' }, 183 | { pageCount: 250, title: 'Novel A' }, 184 | { pageCount: 250, title: 'Novel B' }, 185 | { pageCount: 400, title: 'Epic Tale' }, 186 | ]; 187 | 188 | // Find all books with 250 pages 189 | const allBooks = binaryFindAll(books, 250, (book) => book.pageCount); 190 | console.log(allBooks); // Output: [{ pageCount: 250, title: 'Novel A' }, { pageCount: 250, title: 'Novel B' }] 191 | 192 | // Find all indices of books with 250 pages 193 | const allIndices = binaryFindAllIndices(books, 250, (book) => book.pageCount); 194 | console.log(allIndices); // Output: [1, 2] 195 | ``` 196 | 197 | ### 7. Find Item Containing a Value (Range-based data) 198 | 199 | ```typescript 200 | import { binaryFindRangeItem } from 'generic-binary-search'; 201 | 202 | const meetings = [ 203 | { startTime: 9, endTime: 10, title: 'Morning Standup' }, 204 | { startTime: 10, endTime: 11, title: 'Team Planning' }, 205 | { startTime: 13, endTime: 14, title: 'Client Call' }, 206 | { startTime: 15, endTime: 16, title: 'Project Review' }, 207 | ]; 208 | 209 | // Find the meeting happening at 13:30 210 | const currentMeeting = binaryFindRangeItem( 211 | meetings, 212 | 13.5, 213 | (meeting) => meeting.startTime, 214 | (meeting) => meeting.endTime, 215 | ); 216 | console.log(currentMeeting); // Output: { startTime: 13, endTime: 14, title: 'Client Call' } 217 | ``` 218 | 219 | ## Contributing 220 | 221 | Contributions are welcome! Please feel free to submit a Pull Request. 222 | 223 | ## License 224 | 225 | This project is licensed under the MIT License. 226 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generic-binary-search", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "author": { 6 | "name": "Adam Pietrasiak", 7 | "email": "adam@screen.studio" 8 | }, 9 | "main": "./dist/index.umd.cjs", 10 | "module": "./dist/index.js", 11 | "types": "./dist/index.d.ts", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/index.js", 15 | "require": "./dist/index.umd.cjs" 16 | } 17 | }, 18 | "scripts": { 19 | "dev": "vite", 20 | "build": "tsc && vite build", 21 | "preview": "vite preview", 22 | "test": "vitest", 23 | "coverage": "vitest run --coverage", 24 | "lint": "eslint . --ext .ts", 25 | "lint-and-fix": "eslint . --ext .ts --fix", 26 | "pretty": "prettier --write \"src/**/*.ts\"", 27 | "clean-up": "npm run lint-and-fix & npm run pretty" 28 | }, 29 | "devDependencies": { 30 | "@vitest/coverage-c8": "^0.33.0", 31 | "eslint": "^8.49.0", 32 | "prettier": "^3.0.3", 33 | "semantic-release": "^21.1.1", 34 | "typescript": "^5.2.2", 35 | "vite": "^4.4.9", 36 | "vite-plugin-eslint": "^1.8.1", 37 | "vitest": "^0.34.4" 38 | }, 39 | "files": [ 40 | "dist" 41 | ], 42 | "dependencies": { 43 | "@typescript-eslint/eslint-plugin": "^6.7.0", 44 | "@typescript-eslint/parser": "^6.7.0", 45 | "eslint-config-prettier": "^9.0.0", 46 | "eslint-plugin-import": "^2.28.1", 47 | "vite-plugin-dts": "^3.5.3" 48 | }, 49 | "packageManager": "yarn@4.4.1" 50 | } 51 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | binaryFind, 3 | binaryFindAllIndices, 4 | binaryFindBetween, 5 | binaryFindGtIndex, 6 | binaryFindGte, 7 | binaryFindGteIndex, 8 | binaryFindLtIndex, 9 | binaryFindLte, 10 | binaryFindLteIndex, 11 | } from './index'; 12 | import { describe, expect, it, vi } from 'vitest'; 13 | 14 | function identity(item: T): T { 15 | return item; 16 | } 17 | 18 | describe('binaryFind', () => { 19 | it('binaryFindGte', () => { 20 | expect(binaryFindGte([1, 2, 3, 4, 5], 6, identity)).toBe(null); 21 | expect(binaryFindGte([1, 2, 3, 4, 5], 0, identity)).toBe(1); 22 | expect(binaryFindGte([1, 2, 3, 4, 5], 1, identity)).toBe(1); 23 | expect(binaryFindGte([1, 2, 3, 4, 5], 2.5, identity)).toBe(3); 24 | expect(binaryFindGte([1, 2, 3, 4, 5], 5, identity)).toBe(5); 25 | expect(binaryFindGte([1, 2, 3, 4, 5], -5, identity)).toBe(1); 26 | expect(binaryFindGte([1, 2, 3, 4, 5], 100, identity)).toBe(null); 27 | }); 28 | 29 | it('binaryFindLte', () => { 30 | expect(binaryFindLte([1, 2, 3, 4, 5], 6, identity)).toBe(5); 31 | expect(binaryFindLte([1, 2, 3, 4, 5], 0, identity)).toBe(null); 32 | expect(binaryFindLte([1, 2, 3, 4, 5], 1, identity)).toBe(1); 33 | expect(binaryFindLte([1, 2, 3, 4, 5], 2.5, identity)).toBe(2); 34 | expect(binaryFindLte([1, 2, 3, 4, 5], 5, identity)).toBe(5); 35 | expect(binaryFindLte([1, 2, 3, 4, 5], -5, identity)).toBe(null); 36 | expect(binaryFindLte([1, 2, 3, 4, 5], 100, identity)).toBe(5); 37 | }); 38 | 39 | it('binaryFindGtIndex', () => { 40 | expect(binaryFindGtIndex([1, 2, 3, 4, 5], 6, identity)).toBe(null); 41 | expect(binaryFindGtIndex([1, 2, 3, 4, 5], 0, identity)).toBe(0); 42 | expect(binaryFindGtIndex([1, 2, 3, 4, 5], 1, identity)).toBe(1); 43 | expect(binaryFindGtIndex([1, 2, 3, 4, 5], 2.5, identity)).toBe(2); 44 | expect(binaryFindGtIndex([1, 2, 3, 4, 5], 4, identity)).toBe(4); 45 | expect(binaryFindGtIndex([1, 2, 3, 4, 5], 5, identity)).toBe(null); 46 | expect(binaryFindGtIndex([1, 2, 3, 4, 5], -5, identity)).toBe(0); 47 | expect(binaryFindGtIndex([1, 2, 3, 4, 5], 100, identity)).toBe(null); 48 | expect(binaryFindGtIndex([1, 2, 2, 2, 3, 4, 5], 1, identity)).toBe(1); 49 | }); 50 | 51 | it('binaryFindGteIndex', () => { 52 | expect(binaryFindGteIndex([1, 2, 3, 4, 5], 6, identity)).toBe(null); 53 | expect(binaryFindGteIndex([1, 2, 3, 4, 5], 0, identity)).toBe(0); 54 | expect(binaryFindGteIndex([1, 2, 3, 4, 5], 1, identity)).toBe(0); 55 | expect(binaryFindGteIndex([1, 2, 3, 4, 5], 2.5, identity)).toBe(2); 56 | expect(binaryFindGteIndex([1, 2, 3, 4, 5], 4, identity)).toBe(3); 57 | expect(binaryFindGteIndex([1, 2, 3, 4, 5], 5, identity)).toBe(4); 58 | expect(binaryFindGteIndex([1, 2, 3, 4, 5], -5, identity)).toBe(0); 59 | expect(binaryFindGteIndex([1, 2, 3, 4, 5], 100, identity)).toBe(null); 60 | expect(binaryFindGteIndex([1, 2, 2, 3, 3, 3, 3, 5], 3, identity)).toBe(3); 61 | expect(binaryFindGteIndex([1, 3, 3, 3, 3, 4, 5], 3, identity)).toBe(1); 62 | }); 63 | 64 | it('binaryFindLtIndex', () => { 65 | expect(binaryFindLtIndex([1, 2, 3, 4, 5], 6, identity)).toBe(4); 66 | expect(binaryFindLtIndex([1, 2, 3, 4, 5], 0, identity)).toBe(null); 67 | expect(binaryFindLtIndex([1, 2, 3, 4, 5], 1, identity)).toBe(null); 68 | expect(binaryFindLtIndex([1, 2, 3, 4, 5], 2.5, identity)).toBe(1); 69 | expect(binaryFindLtIndex([1, 2, 3, 4, 5], 5, identity)).toBe(3); 70 | expect(binaryFindLtIndex([1, 2, 3, 4, 5], -5, identity)).toBe(null); 71 | expect(binaryFindLtIndex([1, 2, 3, 4, 5], 100, identity)).toBe(4); 72 | expect(binaryFindLtIndex([1, 2, 2, 2, 3, 4, 5], 3, identity)).toBe(3); 73 | expect(binaryFindLtIndex([1, 2, 2, 3, 4, 5], 3, identity)).toBe(2); 74 | }); 75 | 76 | it('binaryFindLteIndex', () => { 77 | expect(binaryFindLteIndex([1, 2, 3, 4, 5], 6, identity)).toBe(4); 78 | expect(binaryFindLteIndex([1, 2, 3, 4, 5], 0, identity)).toBe(null); 79 | expect(binaryFindLteIndex([1, 2, 3, 4, 5], 1, identity)).toBe(0); 80 | expect(binaryFindLteIndex([1, 2, 3, 4, 5], 2.5, identity)).toBe(1); 81 | expect(binaryFindLteIndex([1, 2, 3, 4, 5], 5, identity)).toBe(4); 82 | expect(binaryFindLteIndex([1, 2, 3, 4, 5], -5, identity)).toBe(null); 83 | expect(binaryFindLteIndex([1, 2, 3, 4, 5], 100, identity)).toBe(4); 84 | expect(binaryFindLteIndex([1, 3, 3, 3, 3, 4, 5], 3, identity)).toBe(1); 85 | expect(binaryFindLteIndex([1, 2, 2, 3, 4, 5], 3, identity)).toBe(3); 86 | }); 87 | 88 | it('binaryFindRange', () => { 89 | expect(binaryFindBetween([1, 2, 3, 4, 5], 2, 4, identity)).toEqual([2, 3, 4]); 90 | expect(binaryFindBetween([1, 2, 3, 4, 5], 1.5, 4, identity)).toEqual([2, 3, 4]); 91 | expect(binaryFindBetween([1, 2, 3, 4, 5], 1.5, 4.5, identity)).toEqual([2, 3, 4]); 92 | expect(binaryFindBetween([1, 2, 3, 4, 5], 1, 4.5, identity)).toEqual([1, 2, 3, 4]); 93 | expect(binaryFindBetween([1, 2, 3, 4, 5], -5, 4.5, identity)).toEqual([1, 2, 3, 4]); 94 | expect(binaryFindBetween([1, 2, 3, 4, 5], -10, 10, identity)).toEqual([1, 2, 3, 4, 5]); 95 | expect(binaryFindBetween([3, 4, 5], 0, 2, identity)).toEqual([]); 96 | expect(binaryFindBetween([3, 4, 5], 6, 8, identity)).toEqual([]); 97 | expect(binaryFindBetween([3, 4, 5], 4, 8, identity)).toEqual([4, 5]); 98 | expect(binaryFindBetween([3, 4, 5], 2, 4, identity)).toEqual([3, 4]); 99 | }); 100 | 101 | it('binaryFind', () => { 102 | expect(binaryFind([1, 2, 3, 4, 5], 2, identity)).toBe(2); 103 | expect(binaryFind([1, 2, 3, 4, 5], 2.5, identity)).toBe(null); 104 | expect(binaryFind([1, 2, 3, 4, 5], -2, identity)).toBe(null); 105 | expect(binaryFind([1, 2, 3, 4, 5], 10, identity)).toBe(null); 106 | expect(binaryFind([1, 2, 3, 4, 5], 1, identity)).toBe(1); 107 | expect(binaryFind([1, 2, 3, 4, 5], 5, identity)).toBe(5); 108 | }); 109 | 110 | it('binaryFind', () => { 111 | expect(binaryFind([1, 2, 3, 4, 5], 2, identity)).toBe(2); 112 | expect(binaryFind([1, 2, 3, 4, 5], 2.5, identity)).toBe(null); 113 | expect(binaryFind([1, 2, 3, 4, 5], -2, identity)).toBe(null); 114 | expect(binaryFind([1, 2, 3, 4, 5], 10, identity)).toBe(null); 115 | expect(binaryFind([1, 2, 3, 4, 5], 1, identity)).toBe(1); 116 | expect(binaryFind([1, 2, 3, 4, 5], 5, identity)).toBe(5); 117 | }); 118 | 119 | it('binaryFindAllIndices', () => { 120 | expect(binaryFindAllIndices([1, 2, 3, 4, 5], 2, identity)).toEqual([1, 1]); 121 | expect(binaryFindAllIndices([1, 2, 3, 4, 5], 2.5, identity)).toEqual(null); 122 | expect(binaryFindAllIndices([1, 2, 3, 3, 3, 4, 5], 3, identity)).toEqual([2, 4]); 123 | expect(binaryFindAllIndices([1, 2, 3, 3, 3], 3, identity)).toEqual([2, 4]); 124 | expect(binaryFindAllIndices([3, 3, 3, 4, 5], 3, identity)).toEqual([0, 2]); 125 | expect(binaryFindAllIndices([3, 3, 3, 4, 5], 2, identity)).toEqual(null); 126 | expect(binaryFindAllIndices([3, 3, 3, 4, 5], 6, identity)).toEqual(null); 127 | expect(binaryFindAllIndices([3, 3, 3, 4, 5], -6, identity)).toEqual(null); 128 | expect(binaryFindAllIndices([1], 1, identity)).toEqual([0, 0]); 129 | expect(binaryFindAllIndices([1, 1, 1], 1, identity)).toEqual([0, 2]); 130 | expect(binaryFindAllIndices([], 1, identity)).toEqual(null); 131 | }); 132 | 133 | it('O(n)', () => { 134 | const getAndMeasure = vi.fn((i: number) => { 135 | return i; 136 | }); 137 | const arr = Array.from({ length: 1000 }, (_, i) => i); 138 | 139 | expect(binaryFind(arr, 0, getAndMeasure)).toBe(0); 140 | 141 | expect(getAndMeasure).toHaveBeenCalledTimes(9); 142 | 143 | getAndMeasure.mockClear(); 144 | 145 | expect(binaryFind(arr, 499, getAndMeasure)).toBe(499); 146 | 147 | expect(getAndMeasure).toHaveBeenCalledTimes(1); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export function binaryFindIndex( 2 | input: T[], 3 | targetValue: number, 4 | valueGetter: (item: T, index: number) => number, 5 | ): number | null { 6 | let left = 0; 7 | let right = input.length - 1; 8 | 9 | let mid: number; 10 | let midValue: number; 11 | 12 | while (left <= right) { 13 | mid = left + ((right - left) >> 1); 14 | midValue = valueGetter(input[mid], mid); 15 | 16 | if (midValue === targetValue) { 17 | return mid; // Exact match found 18 | } else if (midValue < targetValue) { 19 | left = mid + 1; 20 | } else { 21 | right = mid - 1; 22 | } 23 | } 24 | 25 | return null; // No exact match found 26 | } 27 | 28 | /** 29 | * Will find item in array by some condition using binary search algorithm. 30 | * 31 | * Note: it expects array to be sorted by field used in condition. 32 | */ 33 | export function binaryFind( 34 | input: T[], 35 | targetValue: number, 36 | valueGetter: (item: T, index: number) => number, 37 | ): T | null { 38 | const index = binaryFindIndex(input, targetValue, valueGetter); 39 | return index !== null ? input[index] : null; 40 | } 41 | 42 | export function binaryFindClosestIndex( 43 | input: T[], 44 | targetValue: number, 45 | valueGetter: (item: T, index: number) => number, 46 | ): number | null { 47 | if (input.length === 0) { 48 | return null; 49 | } 50 | 51 | let left = 0; 52 | let right = input.length - 1; 53 | 54 | let mid: number; 55 | let midValue: number; 56 | 57 | let nextMid: number; 58 | let nextMidValue: number; 59 | 60 | while (left < right) { 61 | mid = left + ((right - left) >> 1); 62 | midValue = valueGetter(input[mid], mid); 63 | 64 | if (midValue === targetValue) { 65 | return mid; // Exact match found 66 | } 67 | 68 | nextMid = mid + 1; 69 | nextMidValue = valueGetter(input[nextMid], nextMid); 70 | 71 | if (midValue < targetValue && targetValue < nextMidValue) { 72 | // Target is between mid and nextMid 73 | return Math.abs(targetValue - midValue) <= Math.abs(targetValue - nextMidValue) ? mid : nextMid; 74 | } 75 | 76 | if (targetValue < midValue) { 77 | right = mid; 78 | } else { 79 | left = nextMid; 80 | } 81 | } 82 | 83 | return left; // At this point, left == right 84 | } 85 | 86 | export function binaryFindClosest( 87 | items: T[], 88 | targetValue: number, 89 | getValue: (item: T, index: number) => number, 90 | ): T | null { 91 | const index = binaryFindClosestIndex(items, targetValue, getValue); 92 | 93 | if (index === null) return null; 94 | 95 | return items[index]; 96 | } 97 | 98 | export function binaryFindGtIndex( 99 | input: T[], 100 | targetValue: number, 101 | valueGetter: (item: T, index: number) => number, 102 | ): number | null { 103 | let left = 0; 104 | let right = input.length - 1; 105 | let result: number | null = null; 106 | 107 | let mid: number; 108 | let midValue: number; 109 | 110 | while (left <= right) { 111 | mid = left + ((right - left) >> 1); 112 | midValue = valueGetter(input[mid], mid); 113 | 114 | if (midValue > targetValue) { 115 | result = mid; 116 | right = mid - 1; // Continue searching to the left 117 | } else { 118 | left = mid + 1; 119 | } 120 | } 121 | 122 | return result; 123 | } 124 | 125 | export function binaryFindLtIndex( 126 | input: T[], 127 | targetValue: number, 128 | valueGetter: (item: T, index: number) => number, 129 | ): number | null { 130 | let left = 0; 131 | let right = input.length - 1; 132 | let result: number | null = null; 133 | 134 | let mid: number; 135 | let midValue: number; 136 | 137 | while (left <= right) { 138 | mid = left + ((right - left) >> 1); 139 | midValue = valueGetter(input[mid], mid); 140 | 141 | if (midValue < targetValue) { 142 | result = mid; 143 | left = mid + 1; // Continue searching to the right 144 | } else { 145 | right = mid - 1; 146 | } 147 | } 148 | 149 | return result; 150 | } 151 | 152 | export function binaryFindAllIndices( 153 | input: T[], 154 | targetValue: number, 155 | valueGetter: (item: T, index: number) => number, 156 | ): [number, number] | null { 157 | return binaryFindIndicesBetween(input, targetValue, targetValue, valueGetter); 158 | } 159 | 160 | export function binaryFindAll(items: T[], targetValue: number, getValue: (item: T, index: number) => number): T[] { 161 | const indices = binaryFindAllIndices(items, targetValue, getValue); 162 | 163 | if (indices === null) return []; 164 | 165 | const [low, high] = indices; 166 | 167 | return items.slice(low, high + 1); 168 | } 169 | 170 | export function binaryFindGteIndex( 171 | input: T[], 172 | targetValue: number, 173 | valueGetter: (item: T, index: number) => number, 174 | ): number | null { 175 | let left = 0; 176 | let right = input.length - 1; 177 | let result = null; 178 | 179 | let mid: number; 180 | let midValue: number; 181 | 182 | while (left <= right) { 183 | mid = left + ((right - left) >> 1); 184 | midValue = valueGetter(input[mid], mid); 185 | 186 | if (midValue >= targetValue) { 187 | result = mid; 188 | right = mid - 1; 189 | } else { 190 | left = mid + 1; 191 | } 192 | } 193 | 194 | return result; 195 | } 196 | 197 | export function binaryFindGte( 198 | items: T[], 199 | targetValue: number, 200 | getValue: (item: T, index: number) => number, 201 | ): T | null { 202 | const index = binaryFindGteIndex(items, targetValue, getValue); 203 | 204 | if (index === null) return null; 205 | 206 | return items[index]; 207 | } 208 | 209 | export function binaryFindLteIndex( 210 | input: T[], 211 | targetValue: number, 212 | valueGetter: (item: T, index: number) => number, 213 | ): number | null { 214 | if (input.length === 0) { 215 | return null; 216 | } 217 | 218 | let left = 0; 219 | let right = input.length - 1; 220 | 221 | // Check if target is less than the first element 222 | if (valueGetter(input[left], left) > targetValue) { 223 | return null; 224 | } 225 | 226 | let mid: number; 227 | let midValue: number; 228 | 229 | while (left <= right) { 230 | mid = left + ((right - left) >> 1); 231 | midValue = valueGetter(input[mid], mid); 232 | 233 | if (midValue > targetValue) { 234 | right = mid - 1; 235 | } else if (midValue < targetValue) { 236 | left = mid + 1; 237 | } else { 238 | // We found a matching value, now let's find the first occurrence 239 | if (mid === 0 || valueGetter(input[mid - 1], mid - 1) < midValue) { 240 | return mid; 241 | } 242 | right = mid - 1; // Continue searching to the left 243 | } 244 | } 245 | 246 | // At this point, right is the index of the last element <= targetValue 247 | return right; 248 | } 249 | 250 | export function binaryFindLte(items: T[], targetValue: number, getValue: (item: T) => number): T | null { 251 | const index = binaryFindLteIndex(items, targetValue, getValue); 252 | 253 | if (index === null) return null; 254 | 255 | return items[index]; 256 | } 257 | 258 | export function binaryFindBetween(items: T[], min: number, max: number, getValue: (item: T) => number): T[] { 259 | if (!items.length) return []; 260 | 261 | const rangeIndices = binaryFindIndicesBetween(items, min, max, getValue); 262 | 263 | if (!rangeIndices) { 264 | return []; 265 | } 266 | 267 | return items.slice(rangeIndices[0], rangeIndices[1] + 1); 268 | } 269 | 270 | export function binaryFindIndicesBetween( 271 | items: T[], 272 | startValue: number, 273 | endValue: number, 274 | getValue: (item: T, index: number) => number, 275 | ): [number, number] | null { 276 | if (items.length === 0 || startValue > endValue) { 277 | return null; 278 | } 279 | 280 | // Find the index of the first item >= startValue 281 | let left = 0; 282 | let right = items.length - 1; 283 | let startIndex = -1; 284 | 285 | let mid: number; 286 | let midValue: number; 287 | 288 | while (left <= right) { 289 | mid = left + ((right - left) >> 1); 290 | midValue = getValue(items[mid], mid); 291 | 292 | if (midValue >= startValue) { 293 | startIndex = mid; 294 | right = mid - 1; 295 | } else { 296 | left = mid + 1; 297 | } 298 | } 299 | 300 | // If no item is >= startValue, return null 301 | if (startIndex === -1) { 302 | return null; 303 | } 304 | 305 | // Find the index of the last item <= endValue 306 | left = startIndex; 307 | right = items.length - 1; 308 | let endIndex = -1; 309 | 310 | while (left <= right) { 311 | mid = left + ((right - left) >> 1); 312 | midValue = getValue(items[mid], mid); 313 | 314 | if (midValue <= endValue) { 315 | endIndex = mid; 316 | left = mid + 1; 317 | } else { 318 | right = mid - 1; 319 | } 320 | } 321 | 322 | // If no item is <= endValue, return null 323 | // This should not happen if the input is valid, but we check for safety 324 | if (endIndex === -1) { 325 | return null; 326 | } 327 | 328 | return [startIndex, endIndex]; 329 | } 330 | 331 | export function binaryFindRangeItem( 332 | items: T[], 333 | value: number, 334 | getStart: (item: T) => number, 335 | getEnd: (item: T) => number, 336 | ): T | null { 337 | let left = 0; 338 | let right = items.length - 1; 339 | 340 | let mid: number; 341 | let start: number; 342 | let end: number; 343 | let item: T; 344 | 345 | while (left <= right) { 346 | mid = left + ((right - left) >> 1); 347 | item = items[mid]; 348 | start = getStart(item); 349 | end = getEnd(item); 350 | 351 | if (start <= value && value <= end) { 352 | return item; // Found an item that contains the value 353 | } else if (value < start) { 354 | right = mid - 1; // Value is in the left half 355 | } else { 356 | left = mid + 1; // Value is in the right half 357 | } 358 | } 359 | 360 | return null; // No item found that contains the value 361 | } 362 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "allowJs": false, 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import dts from 'vite-plugin-dts'; 3 | import fs from 'fs'; 4 | import { resolve } from 'path'; 5 | 6 | function getPackageName() { 7 | const packageJSON = JSON.parse(fs.readFileSync(resolve(__dirname, 'package.json'), 'utf-8')); 8 | return packageJSON.name; 9 | } 10 | 11 | // https://vitejs.dev/guide/build.html#library-mode 12 | export default defineConfig({ 13 | build: { 14 | minify: false, 15 | lib: { 16 | entry: resolve(__dirname, 'src/index.ts'), 17 | name: getPackageName(), 18 | fileName: 'index', 19 | }, 20 | }, 21 | test: {}, 22 | plugins: [ 23 | dts({ 24 | insertTypesEntry: true, 25 | }) as any, 26 | ], 27 | }); 28 | --------------------------------------------------------------------------------