├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── dependabot-automerge.yml │ └── nodejs-test.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── package-lock.json ├── package.json ├── readme.md ├── src ├── index.spec.ts └── index.ts ├── tsconfig.eslint.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:n/recommended", "prettier"], 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "eqeqeq": [2, "smart"], 9 | "no-caller": 2, 10 | "dot-notation": 2, 11 | "no-var": 2, 12 | "prefer-const": 2, 13 | "prefer-arrow-callback": [2, { "allowNamedFunctions": true }], 14 | "arrow-body-style": [2, "as-needed"], 15 | "object-shorthand": 2, 16 | "prefer-template": 2, 17 | "one-var": [2, "never"], 18 | "prefer-destructuring": [2, { "object": true }], 19 | "capitalized-comments": 2, 20 | "multiline-comment-style": [2, "starred-block"], 21 | "spaced-comment": 2, 22 | "yoda": [2, "never"], 23 | "curly": [2, "multi-line"], 24 | "no-else-return": 2 25 | }, 26 | "overrides": [ 27 | { 28 | "files": "*.spec.*", 29 | "env": { "jest": true } 30 | }, 31 | { 32 | "files": "*.ts", 33 | "extends": [ 34 | "plugin:@typescript-eslint/eslint-recommended", 35 | "plugin:@typescript-eslint/recommended", 36 | "prettier" 37 | ], 38 | "parserOptions": { 39 | "sourceType": "module", 40 | "project": "./tsconfig.eslint.json" 41 | }, 42 | "settings": { 43 | "node": { 44 | "tryExtensions": [".js", ".json", ".node", ".ts"] 45 | } 46 | }, 47 | "rules": { 48 | "@typescript-eslint/prefer-for-of": 0, 49 | "@typescript-eslint/member-ordering": 0, 50 | "@typescript-eslint/explicit-function-return-type": 0, 51 | "@typescript-eslint/no-unused-vars": 0, 52 | "@typescript-eslint/no-use-before-define": [ 53 | 2, 54 | { "functions": false } 55 | ], 56 | "@typescript-eslint/consistent-type-definitions": [ 57 | 2, 58 | "interface" 59 | ], 60 | "@typescript-eslint/prefer-function-type": 2, 61 | "@typescript-eslint/no-unnecessary-type-arguments": 2, 62 | "@typescript-eslint/prefer-string-starts-ends-with": 2, 63 | "@typescript-eslint/prefer-readonly": 2, 64 | "@typescript-eslint/prefer-includes": 2, 65 | "@typescript-eslint/no-unnecessary-condition": 2, 66 | "@typescript-eslint/switch-exhaustiveness-check": 2, 67 | "@typescript-eslint/prefer-nullish-coalescing": 2, 68 | 69 | "n/no-unsupported-features/es-syntax": 0 70 | } 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v3 27 | with: 28 | languages: "javascript" 29 | 30 | - name: Perform CodeQL Analysis 31 | uses: github/codeql-action/analyze@v3 32 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | # Based on https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | name: Dependabot auto-merge 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v2.4.0 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | - name: Enable auto-merge for Dependabot PRs 20 | # Automatically merge semver-patch and semver-minor PRs 21 | if: "${{ steps.metadata.outputs.update-type == 22 | 'version-update:semver-minor' || 23 | steps.metadata.outputs.update-type == 24 | 'version-update:semver-patch' }}" 25 | run: gh pr merge --auto --delete-branch --squash "$PR_URL" 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | pull_request: 8 | 9 | env: 10 | CI: true 11 | FORCE_COLOR: 2 12 | NODE_COV: lts/* # The Node.js version to run coveralls on 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: lts/* 22 | cache: npm 23 | - run: npm ci 24 | - run: npm run lint 25 | 26 | test: 27 | name: Node ${{ matrix.node }} 28 | runs-on: ubuntu-latest 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | node: 34 | - 14 35 | - 16 36 | - 18 37 | - lts/* 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Use Node.js ${{ matrix.node }} 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: ${{ matrix.node }} 45 | cache: npm 46 | - run: npm ci 47 | - run: npm run build --if-present 48 | 49 | - name: Run Jest 50 | run: npm run test:jest 51 | if: matrix.node != env.NODE_COV 52 | 53 | - name: Run Jest with coverage 54 | run: npm run test:jest -- --coverage 55 | if: matrix.node == env.NODE_COV 56 | 57 | - name: Run Coveralls 58 | uses: coverallsapp/github-action@v2.3.6 59 | if: matrix.node == env.NODE_COV 60 | continue-on-error: true 61 | with: 62 | github-token: "${{ secrets.GITHUB_TOKEN }}" 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | lib/ 4 | src/maps/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2020 Felix Böhm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitfield", 3 | "description": "a simple bitfield, compliant with the BitTorrent spec", 4 | "version": "4.2.0", 5 | "author": "Felix Boehm ", 6 | "funding": { 7 | "url": "https://github.com/sponsors/fb55" 8 | }, 9 | "sideEffects": false, 10 | "main": "lib/index.js", 11 | "types": "lib/index.d.ts", 12 | "module": "lib/esm/index.js", 13 | "exports": { 14 | "require": "./lib/index.js", 15 | "import": "./lib/esm/index.js" 16 | }, 17 | "directories": { 18 | "lib": "lib/" 19 | }, 20 | "files": [ 21 | "lib/**/*" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/fb55/bitfield/issues" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^29.5.14", 28 | "@types/node": "^22.15.29", 29 | "@typescript-eslint/eslint-plugin": "^8.33.0", 30 | "@typescript-eslint/parser": "^8.33.1", 31 | "eslint": "^8.57.1", 32 | "eslint-config-prettier": "^10.1.5", 33 | "eslint-plugin-n": "^17.19.0", 34 | "jest": "^29.7.0", 35 | "prettier": "^3.5.3", 36 | "ts-jest": "^29.2.6", 37 | "typescript": "^5.8.3" 38 | }, 39 | "engines": { 40 | "node": ">=8" 41 | }, 42 | "keywords": [ 43 | "bitfield", 44 | "buffer", 45 | "bittorrent" 46 | ], 47 | "license": "MIT", 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/fb55/bitfield" 51 | }, 52 | "scripts": { 53 | "test": "npm run test:jest && npm run lint", 54 | "test:jest": "jest", 55 | "lint": "npm run lint:es && npm run lint:prettier", 56 | "lint:es": "eslint .", 57 | "lint:prettier": "npm run format:prettier:raw -- --check", 58 | "format": "npm run format:es && npm run format:prettier", 59 | "format:es": "npm run lint:es -- --fix", 60 | "format:prettier": "npm run format:prettier:raw -- --write", 61 | "format:prettier:raw": "prettier '**/*.{{m,c,}js,ts,md,json,yml}'", 62 | "build": "npm run build:cjs && npm run build:esm", 63 | "build:cjs": "tsc --sourceRoot https://raw.githubusercontent.com/fb55/bitfield/$(git rev-parse HEAD)/src/", 64 | "build:esm": "npm run build:cjs -- --module esnext --target es2019 --outDir lib/esm && echo '{\"type\":\"module\"}' > lib/esm/package.json", 65 | "prepare": "npm run build" 66 | }, 67 | "jest": { 68 | "preset": "ts-jest", 69 | "testEnvironment": "node", 70 | "moduleNameMapper": { 71 | "^(.*)\\.js$": "$1" 72 | } 73 | }, 74 | "prettier": { 75 | "tabWidth": 4 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # bitfield 2 | 3 | A simple bitfield, compliant with the BitTorrent spec. 4 | 5 | npm install bitfield 6 | 7 | #### Example 8 | 9 | ```js 10 | import Bitfield from "bitfield"; 11 | 12 | const field = new Bitfield(256); // Create a bitfield with 256 bits. 13 | 14 | field.set(128); // Set the 128th bit. 15 | field.set(128, true); // Same as above. 16 | 17 | field.get(128); // `true` 18 | field.get(200); // `false` (all values are initialised to `false`) 19 | field.get(1e3); // `false` (out-of-bounds is also false) 20 | 21 | field.set(128, false); // Set the 128th bit to 0 again. 22 | 23 | field.buffer; // The buffer used by the bitfield. 24 | ``` 25 | 26 | ## Class: BitField 27 | 28 | ### Constructors 29 | 30 | - [constructor](#constructor) 31 | 32 | ### Properties 33 | 34 | - [buffer](#buffer) 35 | 36 | ### Methods 37 | 38 | - [forEach](#foreach) 39 | - [get](#get) 40 | - [set](#set) 41 | 42 | ## Constructors 43 | 44 | ### constructor 45 | 46 | \+ **new BitField**(`data?`: number \| Uint8Array, `opts?`: BitFieldOptions): `BitField` 47 | 48 | #### Parameters: 49 | 50 | | Name | Type | Default value | Description | 51 | | ------- | -------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 52 | | `data` | number \| Uint8Array | 0 | Either a number representing the maximum number of supported bytes, or a Uint8Array. | 53 | | `opts?` | { grow: number } | { grow: 0 } |

**grow:**

If you `set` an index that is out-of-bounds, the bitfield will automatically grow so that the bitfield is big enough to contain the given index, up to the given size (in bit).

If you want the Bitfield to grow indefinitely, pass `Infinity`. | 54 | 55 | **Returns:** `BitField` 56 | 57 | ## Properties 58 | 59 | ### buffer 60 | 61 | • **buffer**: Uint8Array 62 | 63 | The internal storage of the bitfield. 64 | 65 | ## Methods 66 | 67 | ### forEach 68 | 69 | ▸ **forEach**(`fn`: (bit: boolean, index: number) => void, `start?`: number, `end?`: number): void 70 | 71 | Loop through the bits in the bitfield. 72 | 73 | #### Parameters: 74 | 75 | | Name | Type | Default value | Description | 76 | | ------- | ------------------------------------- | ----------------------- | ----------------------------------------------------------- | 77 | | `fn` | (bit: boolean, index: number) => void | - | Function to be called with the bit value and index. | 78 | | `start` | number | 0 | Index of the first bit to look at. | 79 | | `end` | number | this.buffer.length \* 8 | Index of the first bit that should no longer be considered. | 80 | 81 | **Returns:** void 82 | 83 | --- 84 | 85 | ### get 86 | 87 | ▸ **get**(`i`: number): boolean 88 | 89 | Get a particular bit. 90 | 91 | #### Parameters: 92 | 93 | | Name | Type | Description | 94 | | ---- | ------ | ---------------------- | 95 | | `i` | number | Bit index to retrieve. | 96 | 97 | **Returns:** boolean 98 | 99 | A boolean indicating whether the `i`th bit is set. 100 | 101 | --- 102 | 103 | ### set 104 | 105 | ▸ **set**(`i`: number, `value?`: boolean): void 106 | 107 | Set a particular bit. 108 | 109 | Will grow the underlying array if the bit is out of bounds and the `grow` option is set. 110 | 111 | #### Parameters: 112 | 113 | | Name | Type | Default value | Description | 114 | | ------- | ------- | ------------- | -------------------------------------------- | 115 | | `i` | number | - | Bit index to set. | 116 | | `value` | boolean | true | Value to set the bit to. Defaults to `true`. | 117 | 118 | **Returns:** void 119 | 120 | --- 121 | 122 | ### setAll 123 | 124 | ▸ **setAll**(`array`: `ArrayLike`, `offset?`: number): void 125 | 126 | Set the bits in the bitfield to the values in the given array. 127 | 128 | #### Parameters: 129 | 130 | | Name | Type | Default value | Description | 131 | | -------- | -------------------- | ------------- | ------------------------------------- | 132 | | `array` | `ArrayLike` | - | Array of booleans to set the bits to. | 133 | | `offset` | number | 0 | Index of the first bit to set. | 134 | 135 | **Returns:** void 136 | 137 | ## License 138 | 139 | MIT 140 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import BitField from "./index.js"; 2 | 3 | const data = "011011100110111".split("").map(Number).map(Boolean); 4 | 5 | describe("Bitfield", () => { 6 | describe("constructor", () => { 7 | it("should be empty when initialized", () => { 8 | const field = new BitField(data.length); 9 | 10 | for (let index = 0; index < data.length; index++) { 11 | expect(field.get(index)).toBe(false); 12 | } 13 | }); 14 | 15 | it("should assume size 0 if no data or size passed in", () => { 16 | const field = new BitField(); 17 | expect(field.buffer).not.toBeNull(); 18 | expect(field).toHaveLength(0); 19 | }); 20 | 21 | it("should accept a typed array as input", () => { 22 | const original = new BitField(0, { grow: 100 }); 23 | original.set(15); 24 | const copy = new BitField(original.buffer); 25 | expect(copy.get(15)).toBe(true); 26 | }); 27 | 28 | it("correct size bitfield", () => { 29 | expect(new BitField(1).buffer).toHaveLength(1); 30 | expect(new BitField(1)).toHaveLength(8); 31 | expect(new BitField(2).buffer).toHaveLength(1); 32 | expect(new BitField(3).buffer).toHaveLength(1); 33 | expect(new BitField(4).buffer).toHaveLength(1); 34 | expect(new BitField(5).buffer).toHaveLength(1); 35 | expect(new BitField(6).buffer).toHaveLength(1); 36 | expect(new BitField(7).buffer).toHaveLength(1); 37 | expect(new BitField(8).buffer).toHaveLength(1); 38 | expect(new BitField(9).buffer).toHaveLength(2); 39 | expect(new BitField(10).buffer).toHaveLength(2); 40 | expect(new BitField(11).buffer).toHaveLength(2); 41 | expect(new BitField(12).buffer).toHaveLength(2); 42 | expect(new BitField(13).buffer).toHaveLength(2); 43 | expect(new BitField(14).buffer).toHaveLength(2); 44 | expect(new BitField(15).buffer).toHaveLength(2); 45 | expect(new BitField(16).buffer).toHaveLength(2); 46 | expect(new BitField(17).buffer).toHaveLength(3); 47 | expect(new BitField(17)).toHaveLength(24); 48 | }); 49 | }); 50 | 51 | describe("`set`", () => { 52 | it("should reproduce written data", () => { 53 | const field = new BitField(data.length); 54 | 55 | for (let index = 0; index < data.length; index++) { 56 | field.set(index, data[index]); 57 | } 58 | 59 | for (let index = 0; index < data.length; index++) { 60 | expect(field.get(index)).toBe(data[index]); 61 | } 62 | }); 63 | 64 | it("out-of-bounds reads should be `false`", () => { 65 | const field = new BitField(data.length); 66 | 67 | for (let index = data.length; index < 1e3; index++) { 68 | expect(field.get(index)).toBe(false); 69 | } 70 | }); 71 | 72 | it("should support disabling a field", () => { 73 | const field = new BitField(0, { grow: 100 }); 74 | field.set(3, true); 75 | expect(field.get(3)).toBe(true); 76 | field.set(3, false); 77 | 78 | // Check the first 10 indices, to ensure we only mutated a single field 79 | for (let index = 0; index < 10; index++) { 80 | expect(field.get(index)).toBe(false); 81 | } 82 | 83 | // Set the first 10 fields, then disable one 84 | for (let index = 0; index < 10; index++) { 85 | field.set(index); 86 | } 87 | 88 | field.set(5, false); 89 | for (let index = 0; index < 10; index++) { 90 | expect(field.get(index)).toBe(index !== 5); 91 | } 92 | }); 93 | 94 | it("should ignore disables out of bounds", () => { 95 | const field = new BitField(0, { grow: 100 }); 96 | field.set(3, false); 97 | expect(field.buffer).toHaveLength(0); 98 | }); 99 | 100 | describe("`grow` option", () => { 101 | it("should not grow by default", () => { 102 | const field = new BitField(data.length); 103 | 104 | for (let index = 25; index < 125; index++) { 105 | index += 8 + Math.floor(32 * Math.random()); 106 | 107 | const oldLength = field.buffer.length; 108 | expect(field.get(index)).toBe(false); 109 | 110 | // Should not have grown for get() 111 | expect(field.buffer).toHaveLength(oldLength); 112 | 113 | field.set(index, true); 114 | 115 | // Should not have grown for set() 116 | expect(field.buffer).toHaveLength(oldLength); 117 | expect(field.get(index)).toBe(false); 118 | } 119 | }); 120 | 121 | it("should be able to grow to infinity", () => { 122 | const growField = new BitField(data.length, { 123 | grow: Number.POSITIVE_INFINITY, 124 | }); 125 | 126 | for (let index = 25; index < 125; index++) { 127 | index += 8 + Math.floor(32 * Math.random()); 128 | 129 | const oldLength = growField.buffer.length; 130 | expect(growField.get(index)).toBe(false); 131 | // Should not have grown for get() 132 | expect(growField.buffer).toHaveLength(oldLength); 133 | growField.set(index, true); 134 | // Should have grown for set() 135 | expect(growField.buffer.length).toBeGreaterThanOrEqual( 136 | Math.ceil((index + 1) / 8), 137 | ); 138 | expect(growField.get(index)).toBe(true); 139 | } 140 | }); 141 | 142 | it("should restrict growth to growth option", () => { 143 | const smallGrowField = new BitField(0, { grow: 50 }); 144 | 145 | for (let index = 0; index < 100; index++) { 146 | const oldLength = smallGrowField.buffer.length; 147 | smallGrowField.set(index, true); 148 | if (index <= 55) { 149 | // Should have grown for set() 150 | expect( 151 | smallGrowField.buffer.length, 152 | ).toBeGreaterThanOrEqual((index >> 3) + 1); 153 | expect(smallGrowField.get(index)).toBe(true); 154 | } else { 155 | // Should not have grown for set() 156 | expect(smallGrowField.buffer).toHaveLength(oldLength); 157 | expect(smallGrowField.get(index)).toBe(false); 158 | } 159 | } 160 | }); 161 | }); 162 | }); 163 | 164 | describe("`setAll`", () => { 165 | it("should reproduce written data", () => { 166 | const field = new BitField(data.length); 167 | 168 | field.setAll(data); 169 | 170 | for (let index = 0; index < data.length; index++) { 171 | expect(field.get(index)).toBe(data[index]); 172 | } 173 | }); 174 | 175 | it("should support offset", () => { 176 | const field = new BitField(data.length); 177 | 178 | field.setAll(data, 3); 179 | 180 | for (let index = 0; index < data.length; index++) { 181 | expect(field.get(index)).toBe( 182 | index < 3 ? false : data[index - 3], 183 | ); 184 | } 185 | 186 | for (let index = data.length + 3; index < 1e3; index++) { 187 | expect(field.get(index)).toBe(false); 188 | } 189 | }); 190 | 191 | it("should grow if needed", () => { 192 | const field = new BitField(data.length, { grow: 100 }); 193 | 194 | field.setAll(data, 3); 195 | 196 | for (let index = 0; index < data.length + 3; index++) { 197 | expect(field.get(index)).toBe( 198 | index < 3 ? false : data[index - 3], 199 | ); 200 | } 201 | 202 | for (let index = data.length + 3; index < 1e3; index++) { 203 | expect(field.get(index)).toBe(false); 204 | } 205 | }); 206 | }); 207 | 208 | describe("`forEach`", () => { 209 | it("should loop through all values", () => { 210 | const field = new BitField(data.length); 211 | field.setAll(data); 212 | 213 | const values: boolean[] = []; 214 | 215 | field.forEach((bit, index) => { 216 | expect(index).toBe(values.length); 217 | expect(field.get(index)).toBe(bit); 218 | values.push(bit); 219 | }); 220 | 221 | expect(values).toStrictEqual( 222 | // Data has 15 entries, append a `false` to make it match. 223 | [...data, false], 224 | ); 225 | }); 226 | 227 | it("should loop through some of the values", () => { 228 | const field = new BitField(data.length); 229 | field.setAll(data); 230 | 231 | const values: boolean[] = []; 232 | 233 | field.forEach( 234 | (bit, index) => { 235 | expect(field.get(index)).toBe(bit); 236 | values.push(bit); 237 | }, 238 | 3, 239 | 11, 240 | ); 241 | 242 | expect(values).toStrictEqual(data.slice(3, 11)); 243 | }); 244 | }); 245 | 246 | describe("`isEmpty`", () => { 247 | it("should return true for an empty BitField", () => { 248 | const field = new BitField(10); // Assuming this creates a BitField with 10 bits, all unset 249 | expect(field.isEmpty()).toBe(true); 250 | }); 251 | 252 | it("should return false for a BitField with at least one bit set", () => { 253 | const field = new BitField(10); 254 | field.set(5); // Set the 6th bit 255 | expect(field.isEmpty()).toBe(false); 256 | }); 257 | 258 | it("should return true for a BitField with all bits unset after some were set", () => { 259 | const field = new BitField(10); 260 | field.set(3); 261 | field.set(3, false); // Unset the 4th bit 262 | expect(field.isEmpty()).toBe(true); 263 | }); 264 | }); 265 | }); 266 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a number of bits to a number of bytes. 3 | * 4 | * @param numberOfBits The number of bits to convert. 5 | * @returns The number of bytes that are needed to store the given number of bits. 6 | */ 7 | function bitsToBytes(numberOfBits: number): number { 8 | return (numberOfBits >> 3) + Number(numberOfBits % 8 !== 0); 9 | } 10 | 11 | interface BitFieldOptions { 12 | /** 13 | * If you `set` an index that is out-of-bounds, the bitfield 14 | * will automatically grow so that the bitfield is big enough 15 | * to contain the given index, up to the given size (in bit). 16 | * 17 | * If you want the Bitfield to grow indefinitely, pass `Infinity`. 18 | * 19 | * @default 0. 20 | */ 21 | grow?: number; 22 | } 23 | 24 | export default class BitField { 25 | /** 26 | * Grow the bitfield up to this number of entries. 27 | * @default 0. 28 | */ 29 | private readonly grow: number; 30 | 31 | /** The internal storage of the bitfield. */ 32 | public buffer: Uint8Array; 33 | 34 | /** The number of bits in the bitfield. */ 35 | get length(): number { 36 | return this.buffer.length << 3; 37 | } 38 | 39 | /** 40 | * Constructs a BitField. 41 | * 42 | * @param data Either a number representing the maximum number of supported bits, or a Uint8Array. 43 | * @param opts Options for the bitfield. 44 | */ 45 | constructor(data: number | Uint8Array = 0, options?: BitFieldOptions) { 46 | const grow = options?.grow; 47 | this.grow = grow 48 | ? Number.isFinite(grow) 49 | ? bitsToBytes(grow) 50 | : grow 51 | : 0; 52 | this.buffer = 53 | typeof data === "number" ? new Uint8Array(bitsToBytes(data)) : data; 54 | } 55 | 56 | /** 57 | * Get a particular bit. 58 | * 59 | * @param bitIndex Bit index to retrieve. 60 | * @returns A boolean indicating whether the `i`th bit is set. 61 | */ 62 | get(bitIndex: number): boolean { 63 | const byteIndex = bitIndex >> 3; 64 | return ( 65 | byteIndex < this.buffer.length && 66 | !!(this.buffer[byteIndex] & (0b1000_0000 >> bitIndex % 8)) 67 | ); 68 | } 69 | 70 | /** 71 | * Set a particular bit. 72 | * 73 | * Will grow the underlying array if the bit is out of bounds and the `grow` option is set. 74 | * 75 | * @param bitIndex Bit index to set. 76 | * @param value Value to set the bit to. Defaults to `true`. 77 | */ 78 | set(bitIndex: number, value = true): void { 79 | const byteIndex = bitIndex >> 3; 80 | 81 | if (value) { 82 | if (byteIndex >= this.buffer.length) { 83 | const newLength = Math.max( 84 | byteIndex + 1, 85 | Math.min(2 * this.buffer.length, this.grow), 86 | ); 87 | if (newLength <= this.grow) { 88 | const newBuffer = new Uint8Array(newLength); 89 | newBuffer.set(this.buffer); 90 | this.buffer = newBuffer; 91 | } 92 | } 93 | this.buffer[byteIndex] |= 0b1000_0000 >> bitIndex % 8; 94 | } else if (byteIndex < this.buffer.length) { 95 | this.buffer[byteIndex] &= ~(0b1000_0000 >> bitIndex % 8); 96 | } 97 | } 98 | 99 | /** 100 | * Sets a value or an array of values. 101 | * 102 | * @param array An array of booleans to set. 103 | * @param offset The bit offset at which the values are to be written. 104 | */ 105 | setAll(array: ArrayLike, offset = 0): void { 106 | const targetLength = Math.min( 107 | bitsToBytes(offset + array.length), 108 | this.grow, 109 | ); 110 | 111 | if (this.buffer.length < targetLength) { 112 | const newBuffer = new Uint8Array(targetLength); 113 | newBuffer.set(this.buffer); 114 | this.buffer = newBuffer; 115 | } 116 | 117 | let byteIndex = offset >> 3; 118 | let bitMask = 0b1000_0000 >> offset % 8; 119 | for (let index = 0; index < array.length; index++) { 120 | if (array[index]) { 121 | this.buffer[byteIndex] |= bitMask; 122 | } else { 123 | this.buffer[byteIndex] &= ~bitMask; 124 | } 125 | 126 | if (bitMask === 1) { 127 | byteIndex += 1; 128 | 129 | if (byteIndex >= this.buffer.length) { 130 | break; 131 | } 132 | 133 | bitMask = 0b1000_0000; 134 | } else { 135 | bitMask >>= 1; 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Loop through the bits in the bitfield. 142 | * 143 | * @param callbackfn Function to be called with the bit value and index. 144 | * @param start Index of the first bit to look at. 145 | * @param end Index of the first bit that should no longer be considered. 146 | */ 147 | forEach( 148 | callbackfn: (bit: boolean, index: number) => void, 149 | start = 0, 150 | end: number = this.buffer.length * 8, 151 | ): void { 152 | let byteIndex = start >> 3; 153 | let bitMask = 0b1000_0000 >> start % 8; 154 | 155 | for (let bitIndex = start; bitIndex < end; bitIndex++) { 156 | callbackfn(!!(this.buffer[byteIndex] & bitMask), bitIndex); 157 | 158 | if (bitMask === 1) { 159 | byteIndex += 1; 160 | bitMask = 0b1000_0000; 161 | } else { 162 | bitMask >>= 1; 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * Check if all bits in the Bitfield are unset. 169 | * 170 | * @returns A boolean indicating whether all bits are unset. 171 | */ 172 | isEmpty(): boolean { 173 | for (let i = 0; i < this.buffer.length; i++) { 174 | if (this.buffer[i] !== 0) { 175 | return false; 176 | } 177 | } 178 | return true; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "scripts"], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", 5 | "module": "commonjs", 6 | "lib": ["ES2015.Core"], 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "outDir": "lib", 11 | 12 | /* Strict Type-Checking Options */ 13 | "strict": true, 14 | 15 | /* Additional Checks */ 16 | "exactOptionalPropertyTypes": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "isolatedModules": true, 19 | "isolatedDeclarations": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noImplicitOverride": true, 22 | "noImplicitReturns": true, 23 | "noPropertyAccessFromIndexSignature": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | 27 | /* Module Resolution Options */ 28 | "esModuleInterop": true, 29 | "moduleResolution": "node", 30 | "resolveJsonModule": true 31 | }, 32 | "include": ["src"], 33 | "exclude": [ 34 | "**/*.spec.ts", 35 | "**/__fixtures__/*", 36 | "**/__tests__/*", 37 | "**/__snapshots__/*" 38 | ] 39 | } 40 | --------------------------------------------------------------------------------