bit-sequence 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: 'github-actions'
4
+ directory: '/'
5
+ schedule:
6
+ interval: 'daily'
7
+ commit-message:
8
+ prefix: 'chore'
9
+ include: 'scope'
10
+ cooldown:
11
+ default-days: 5
12
+ - package-ecosystem: 'npm'
13
+ directory: '/'
14
+ schedule:
15
+ interval: 'daily'
16
+ commit-message:
17
+ prefix: 'chore'
18
+ include: 'scope'
19
+ cooldown:
20
+ default-days: 5
21
+ - package-ecosystem: 'gomod'
22
+ directory: '/go'
23
+ schedule:
24
+ interval: 'daily'
25
+ commit-message:
26
+ prefix: 'chore'
27
+ include: 'scope'
28
+ cooldown:
29
+ default-days: 5
@@ -0,0 +1,47 @@
1
+ name: Go CI
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ test:
6
+ strategy:
7
+ fail-fast: false
8
+ matrix:
9
+ os: [macos-latest, ubuntu-latest, windows-latest]
10
+ runs-on: ${{ matrix.os }}
11
+ defaults:
12
+ run:
13
+ working-directory: go
14
+ steps:
15
+ - name: Checkout Repository
16
+ uses: actions/checkout@v6
17
+ - name: Setup Go
18
+ uses: actions/setup-go@v6
19
+ with:
20
+ go-version: '1.25'
21
+ cache-dependency-path: go/go.mod
22
+ - name: Run tests
23
+ run: go test -v ./...
24
+ - name: Vet
25
+ run: go vet ./...
26
+ - name: Check formatting
27
+ if: runner.os != 'Windows'
28
+ run: |
29
+ test -z "$(gofmt -l .)" || (gofmt -d . && exit 1)
30
+
31
+ staticcheck:
32
+ runs-on: ubuntu-latest
33
+ defaults:
34
+ run:
35
+ working-directory: go
36
+ steps:
37
+ - name: Checkout Repository
38
+ uses: actions/checkout@v6
39
+ - name: Setup Go
40
+ uses: actions/setup-go@v6
41
+ with:
42
+ go-version: '1.25'
43
+ cache-dependency-path: go/go.mod
44
+ - name: Install staticcheck
45
+ run: go install honnef.co/go/tools/cmd/staticcheck@latest
46
+ - name: Run staticcheck
47
+ run: staticcheck ./...
@@ -0,0 +1,56 @@
1
+ name: Test & Maybe Release
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ test:
6
+ strategy:
7
+ fail-fast: false
8
+ matrix:
9
+ node: [lts/*, current]
10
+ os: [macos-latest, ubuntu-latest, windows-latest]
11
+ runs-on: ${{ matrix.os }}
12
+ steps:
13
+ - name: Checkout Repository
14
+ uses: actions/checkout@v6
15
+ - name: Use Node.js ${{ matrix.node }}
16
+ uses: actions/setup-node@v6
17
+ with:
18
+ node-version: ${{ matrix.node }}
19
+ - name: Install Dependencies
20
+ run: npm install --no-progress
21
+ - name: Check build is up to date
22
+ run: |
23
+ npm run build
24
+ git diff --exit-code || (echo "::error::Build artifacts not committed. Run 'npm run build' and commit the changes." && exit 1)
25
+ - name: Run tests
26
+ run: npm test
27
+
28
+ release:
29
+ name: Release
30
+ needs: [test]
31
+ runs-on: ubuntu-latest
32
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
33
+ permissions:
34
+ contents: write
35
+ issues: write
36
+ pull-requests: write
37
+ id-token: write
38
+ steps:
39
+ - name: Checkout
40
+ uses: actions/checkout@v6
41
+ with:
42
+ fetch-depth: 0
43
+ - name: Setup Node.js
44
+ uses: actions/setup-node@v6
45
+ with:
46
+ node-version: lts/*
47
+ registry-url: 'https://registry.npmjs.org'
48
+ - name: Install dependencies
49
+ run: npm install --no-progress --no-package-lock --no-save
50
+ - name: Build
51
+ run: npm run build
52
+ - name: Release
53
+ env:
54
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55
+ NPM_CONFIG_PROVENANCE: true
56
+ run: npx semantic-release
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## [1.2.0](https://github.com/rvagg/bit-sequence/compare/v1.1.0...v1.2.0) (2026-02-16)
2
+
3
+ ### Features
4
+
5
+ * dep & CI updates & minor modernisation ([d37d229](https://github.com/rvagg/bit-sequence/commit/d37d229fea152b35e0329af0304bf9abb502abd7))
6
+
7
+ ### Trivial Changes
8
+
9
+ * **deps:** bump actions/setup-go from 5 to 6 ([#3](https://github.com/rvagg/bit-sequence/issues/3)) ([8786d6d](https://github.com/rvagg/bit-sequence/commit/8786d6d92408d6ffe069eae6b5cd8bb00c4d2919))
package/README.md CHANGED
@@ -1,25 +1,37 @@
1
1
  # bit-sequence
2
2
 
3
- **Turn an arbitrary sequence of bits from a byte array and turn it into an integer**
3
+ [![NPM](https://nodei.co/npm/bit-sequence.svg?style=flat&data=n,v&color=blue)](https://nodei.co/npm/bit-sequence/)
4
4
 
5
- [![NPM](https://nodei.co/npm/bit-sequence.svg)](https://nodei.co/npm/bit-sequence/)
5
+ **Turn an arbitrary sequence of bits from a byte array into an integer**
6
+
7
+ * [JavaScript](#javascript)
8
+ * [Example](#example)
9
+ * [API](#api)
10
+ * [Go](#go)
11
+ * [License and Copyright](#license-and-copyright)
12
+
13
+ ## Requirements
14
+
15
+ Node.js >= 20
16
+
17
+ ## JavaScript
6
18
 
7
19
  Given an `Array`-like containing bytes (unsigned 8-bit integers), extract an arbitrary sequence of the underlying bits and convert them into an unsigned integer value.
8
20
 
9
21
  Useful for cases where a sub-sequence of bits within a longer byte sequence is used to form an index, such as in a [hash array mapped trie](https://en.wikipedia.org/wiki/Hash_array_mapped_trie) where an index at each level of the tree structure is formed by incremental chunks of a key's hash.
10
22
 
11
- ## Example
23
+ ### Example
12
24
 
13
25
  ```js
14
- const bitSequence = require('bit-sequence')
15
- const assert = require('assert')
26
+ import bitSequence from 'bit-sequence'
27
+ import assert from 'node:assert'
16
28
  const bytes = new Uint8Array([ 0b00010101, 0b10101000, 0b00000000, 0b00000000 ])
17
29
  // extract bits from here ^ to here ^
18
30
  const int = bitSequence(bytes, 7, 11)
19
31
  assert.strictEqual(int, 0b11010100000) // or `1696`
20
32
  ```
21
33
 
22
- ## API
34
+ ### API
23
35
 
24
36
  **`bitSequence(bytes, start, length)`**
25
37
 
@@ -33,6 +45,21 @@ As per the example above, the assumption is that we are extracting bytes where t
33
45
 
34
46
  JavaScript's crazy numbers allows us to extract potentially very large bit sequences, but the usual caveats apply at 32-bits and beyond [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
35
47
 
48
+ ## Go
49
+
50
+ `github.com/rvagg/bit-sequence/go`
51
+
52
+ ### API
53
+
54
+ Exports `BitSequence(bytes []byte, bitStart uint32, bitLength uint32) (uint32, error)` where:
55
+
56
+ * `bytes` is a simple byte array of arbitrary length
57
+ * `bitStart` is a _bit_ index to start extraction (not a _byte_ index).
58
+ * `bitLength` is the number of bits to extract from the `bytes` array. This value can only be a maximum of `32`, higher values will be adjusted down.
59
+
60
+ Returns an unsigned integer version of the bit sequence. The most significant bit is not interpreted for a two's compliment representation.
61
+ Returns an error if `bitLength` exceeds 32 or the requested sequence overflows the bytes boundary.
62
+
36
63
  ## License and Copyright
37
64
 
38
65
  Copyright 2019 Rod Vagg
@@ -0,0 +1,58 @@
1
+ package bitsequence
2
+
3
+ import "fmt"
4
+
5
+ // BitSequence turns an arbitrary sequence of bits from a byte array into an integer.
6
+ //
7
+ // - `bytes` is a simple byte array of arbitrary length
8
+ //
9
+ // - `bitStart` is a _bit_ index to start extraction (not a _byte_ index).
10
+ //
11
+ // - `bitLength` is the number of bits to extract from the `bytes` array. This value can only be a maximum of `32`, higher values will be adjusted down.
12
+ //
13
+ // Returns an unsigned integer version of the bit sequence. The most significant bit is not interpreted for a two's compliment representation.
14
+ // Returns an error if `bitLength` exceeds 32 or the requested sequence overflows the bytes boundary.
15
+ func BitSequence(bytes []byte, bitStart uint32, bitLength uint32) (uint32, error) {
16
+ if bitLength > 32 {
17
+ return 0, fmt.Errorf("maximum bits that can be read is 32")
18
+ }
19
+ startOffset := bitStart % 8
20
+ byteCount := (7 + startOffset + bitLength) / 8
21
+ byteStart := bitStart / 8
22
+ endOffset := byteCount*8 - bitLength - startOffset
23
+
24
+ if int(byteStart+byteCount) > len(bytes) {
25
+ return 0, fmt.Errorf("cannot read past end of bytes array")
26
+ }
27
+
28
+ var result uint32
29
+
30
+ var i uint32
31
+ for i = 0; i < byteCount; i++ {
32
+ local := bytes[byteStart+i]
33
+ var shift uint32
34
+ var localBitLength uint32 = 8 // how many bits of this byte we are extracting
35
+
36
+ if i == 0 {
37
+ localBitLength -= startOffset
38
+ }
39
+
40
+ if i == byteCount-1 {
41
+ localBitLength -= endOffset
42
+ shift = endOffset
43
+ local = local >> shift // take off the trailing bits
44
+ }
45
+
46
+ if localBitLength < 8 {
47
+ mask := uint8((1 << localBitLength) - 1)
48
+ local = local & mask // mask off the leading bits
49
+ }
50
+
51
+ if shift < 8 {
52
+ result = result << (8 - shift)
53
+ }
54
+ result = result | uint32(local)
55
+ }
56
+
57
+ return result, nil
58
+ }
@@ -0,0 +1,111 @@
1
+ package bitsequence
2
+
3
+ import (
4
+ "bufio"
5
+ "encoding/hex"
6
+ "os"
7
+ "strconv"
8
+ "strings"
9
+ "testing"
10
+ )
11
+
12
+ const FixturesFile = "../test-fixture.csv"
13
+
14
+ type TestCase struct {
15
+ bytes []byte
16
+ start, length, expected uint32
17
+ }
18
+
19
+ func TestFixtures(t *testing.T) {
20
+ testCases, err := readTestCases(FixturesFile)
21
+ if err != nil {
22
+ panic(err)
23
+ }
24
+
25
+ for _, testCase := range testCases {
26
+ actual, err := BitSequence(testCase.bytes, testCase.start, testCase.length)
27
+ if err != nil {
28
+ t.Errorf("Bytes [%s] start=%d length=%d returned an error reading",
29
+ hex.EncodeToString(testCase.bytes),
30
+ testCase.start,
31
+ testCase.length,
32
+ )
33
+ }
34
+ if actual != testCase.expected {
35
+ t.Errorf("Bytes [%s] start=%d length=%d expected %d but got %d",
36
+ hex.EncodeToString(testCase.bytes),
37
+ testCase.start,
38
+ testCase.length,
39
+ testCase.expected,
40
+ actual)
41
+ } /* else {
42
+ fmt.Printf("Bytes [%s] start=%d length=%d expected %d and got %d\n",
43
+ hex.EncodeToString(testCase.bytes),
44
+ testCase.start,
45
+ testCase.length,
46
+ testCase.expected,
47
+ actual)
48
+ } */
49
+ }
50
+ }
51
+
52
+ func TestReadPastEndErrors(t *testing.T) {
53
+ bytes, err := hex.DecodeString("ff")
54
+ if err != nil {
55
+ panic(err)
56
+ }
57
+ _, err = BitSequence(bytes, 16, 1)
58
+ if err == nil {
59
+ t.Errorf("Should not have allowed reading past end of byte array but did")
60
+ }
61
+ _, err = BitSequence(bytes, 14, 6)
62
+ if err == nil {
63
+ t.Errorf("Should not have allowed reading past end of byte array but did")
64
+ }
65
+ }
66
+
67
+ func readTestCases(path string) ([]TestCase, error) {
68
+ file, err := os.Open(path)
69
+ if err != nil {
70
+ return nil, err
71
+ }
72
+
73
+ defer file.Close()
74
+
75
+ scanner := bufio.NewScanner(file)
76
+ scanner.Split(bufio.ScanLines)
77
+
78
+ var testCases []TestCase
79
+ for scanner.Scan() {
80
+ line := scanner.Text()
81
+ s := strings.Split(line, ",")
82
+
83
+ testCase := TestCase{}
84
+ var err error
85
+ var ii int
86
+
87
+ testCase.bytes, err = hex.DecodeString(s[0])
88
+ if err != nil {
89
+ return nil, err
90
+ }
91
+ ii, err = strconv.Atoi(s[1])
92
+ if err != nil {
93
+ return nil, err
94
+ }
95
+ testCase.start = uint32(ii)
96
+ ii, err = strconv.Atoi(s[2])
97
+ if err != nil {
98
+ return nil, err
99
+ }
100
+ testCase.length = uint32(ii)
101
+ ii, err = strconv.Atoi(s[3])
102
+ if err != nil {
103
+ return nil, err
104
+ }
105
+ testCase.expected = uint32(ii)
106
+
107
+ testCases = append(testCases, testCase)
108
+ }
109
+
110
+ return testCases, nil
111
+ }
package/go/go.mod ADDED
@@ -0,0 +1,3 @@
1
+ module github.com/rvagg/bit-sequence/go
2
+
3
+ go 1.25
@@ -1,4 +1,10 @@
1
- function bitSequence (bytes, bitStart, bitLength) {
1
+ /**
2
+ * @param {Uint8Array} bytes
3
+ * @param {number} bitStart
4
+ * @param {number} bitLength
5
+ * @returns {number}
6
+ */
7
+ export default function bitSequence (bytes, bitStart, bitLength) {
2
8
  // making an assumption that bytes is an Array-like that will give us one
3
9
  // byte per element, so either an Array full of 8-bit integers or a
4
10
  // Uint8Array or a Node.js Buffer, or something like that
@@ -48,5 +54,3 @@ function bitSequence (bytes, bitStart, bitLength) {
48
54
 
49
55
  return result
50
56
  }
51
-
52
- module.exports = bitSequence
package/js/test.js CHANGED
@@ -1,5 +1,6 @@
1
- const assert = require('assert')
2
- const bitSequence = require('./bit-sequence.js')
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import bitSequence from './bit-sequence.js'
3
4
 
4
5
  function binaryStringToBytes (s) {
5
6
  const byteLength = Math.ceil(s.length / 8)
@@ -11,82 +12,92 @@ function binaryStringToBytes (s) {
11
12
  return bytes
12
13
  }
13
14
 
14
- // sanity check binaryStringToBytes
15
- ;[
16
- '00000001:01',
17
- '10000000:80',
18
- '11111111:ff',
19
- '11000000:c0',
20
- '11110000:f0',
21
- '1111111111111111:ffff',
22
- '0000000000000001:0001',
23
- '000000000000000000000001:000001',
24
- '111111111111111111111111:ffffff',
25
- '100000001000000010000000:808080',
26
- '10000000100000001000000010000000:80808080',
27
- '10000000111111111000000011111111:80ff80ff',
28
- '001:01',
29
- '111:07',
30
- '1111:0f',
31
- '01111:0f',
32
- '001111:0f',
33
- '0000001111:000f',
34
- '10000000000001111:01000f'
35
- ].forEach((s) => {
36
- const [ binary, hex ] = s.split(':')
37
- const bytesHex = Array.prototype.map.call(
38
- binaryStringToBytes(binary),
39
- (b) => b.toString(16).padStart(2, '0')
40
- ).join('')
41
- // console.log(binary, '=>', binaryStringToBytes(binary).toString('hex'), '<>', hex, '?', parseInt(binary, 2).toString(16))
42
- assert.strictEqual(bytesHex, hex)
43
- // sanity check the sanity check by reading the binary
44
- assert.strictEqual(bytesHex.replace(/^0+/, ''), parseInt(binary, 2).toString(16))
15
+ test('binaryStringToBytes sanity check', () => {
16
+ ;[
17
+ '00000001:01',
18
+ '10000000:80',
19
+ '11111111:ff',
20
+ '11000000:c0',
21
+ '11110000:f0',
22
+ '1111111111111111:ffff',
23
+ '0000000000000001:0001',
24
+ '000000000000000000000001:000001',
25
+ '111111111111111111111111:ffffff',
26
+ '100000001000000010000000:808080',
27
+ '10000000100000001000000010000000:80808080',
28
+ '10000000111111111000000011111111:80ff80ff',
29
+ '001:01',
30
+ '111:07',
31
+ '1111:0f',
32
+ '01111:0f',
33
+ '001111:0f',
34
+ '0000001111:000f',
35
+ '10000000000001111:01000f'
36
+ ].forEach((s) => {
37
+ const [binary, hex] = s.split(':')
38
+ const bytesHex = Array.prototype.map.call(
39
+ binaryStringToBytes(binary),
40
+ (b) => b.toString(16).padStart(2, '0')
41
+ ).join('')
42
+ assert.strictEqual(bytesHex, hex)
43
+ assert.strictEqual(bytesHex.replace(/^0+/, ''), parseInt(binary, 2).toString(16))
44
+ })
45
45
  })
46
46
 
47
- let asserts = 0
48
- ;[
49
- '00000001',
50
- '11111111',
51
- '01010101',
52
- '10001000',
53
- '0000000000000001',
54
- '0000000100000001',
55
- '1111111111111111',
56
- '1010101010101010',
57
- '0101010101010101',
58
- '1001001001001001',
59
- '0100100100100100',
60
- '1000100010001000',
61
- '0100010001000100',
62
- '1111111100000000',
63
- '0000000011111111',
64
- '0000111111110000',
65
- '000000000000000000000001',
66
- '111111111111111111111111',
67
- '001001100100110010011111',
68
- '00000000000000000000000000000001',
69
- '11111111111111111111111111111111',
70
- '10000000000000000000000000000001',
71
- '10001100010110011110001010111010',
72
- '1111111111111111111111111111111111111111111111111111111111111111',
73
- '0000000000000000000000000000000000000000000000000000000000000001',
74
- '1000000000000000000000000000000000000000000000000000000000000001',
75
- '1010110100111110101110101001010001000000001101011101110010011111',
76
- '11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'
77
- ].forEach((s) => {
78
- const bytes = binaryStringToBytes(s)
47
+ test('bitSequence extracts correct bit sequences', () => {
48
+ const testCases = [
49
+ '00000001',
50
+ '11111111',
51
+ '01010101',
52
+ '10001000',
53
+ '0000000000000001',
54
+ '0000000100000001',
55
+ '1111111111111111',
56
+ '1010101010101010',
57
+ '0101010101010101',
58
+ '1001001001001001',
59
+ '0100100100100100',
60
+ '1000100010001000',
61
+ '0100010001000100',
62
+ '1111111100000000',
63
+ '0000000011111111',
64
+ '0000111111110000',
65
+ '000000000000000000000001',
66
+ '111111111111111111111111',
67
+ '001001100100110010011111',
68
+ '00000000000000000000000000000001',
69
+ '11111111111111111111111111111111',
70
+ '10000000000000000000000000000001',
71
+ '10001100010110011110001010111010',
72
+ '1111111111111111111111111111111111111111111111111111111111111111',
73
+ '0000000000000000000000000000000000000000000000000000000000000001',
74
+ '1000000000000000000000000000000000000000000000000000000000000001',
75
+ '1010110100111110101110101001010001000000001101011101110010011111',
76
+ '11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111',
77
+ '1111101000010110000011111100000101110000010110111001100000000000',
78
+ '10010101000110001000101011111111101000010110000011111100000101110000010110111001111000011001000000001000111101010010101001110000'
79
+ ]
79
80
 
80
- for (let start = 0; start < bytes.length * 8; start++) {
81
- for (let length = 1; length <= bytes.length * 8 - start; length++) {
82
- const expected = parseInt(s.substring(start, start + length), 2)
83
- const actual = bitSequence(bytes, start, length)
84
- // console.log(`[${s}] start=${start} length=${length} ${actual} <> ${expected}`)
85
- asserts++
86
- assert.strictEqual(actual, expected, `[${s}] start=${start} length=${length} ${actual} <> ${expected}`)
87
- assert.ok(actual >= 0, 'sanity check that we\'re only dealing with unsigned integers')
81
+ let asserts = 0
82
+ let tooBig = 0
83
+ testCases.forEach((s) => {
84
+ const bytes = binaryStringToBytes(s)
85
+
86
+ for (let start = 0; start < bytes.length * 8; start++) {
87
+ for (let length = 1; length <= bytes.length * 8 - start; length++) {
88
+ const expected = parseInt(s.substring(start, start + length), 2)
89
+ const actual = bitSequence(bytes, start, length)
90
+ if (actual !== expected && actual > Number.MAX_SAFE_INTEGER) {
91
+ tooBig++
92
+ continue
93
+ }
94
+ asserts++
95
+ assert.strictEqual(actual, expected, `[${s}] start=${start} length=${length} ${actual} <> ${expected}`)
96
+ assert.ok(actual >= 0, 'sanity check that we\'re only dealing with unsigned integers')
97
+ }
88
98
  }
89
- }
90
- })
99
+ })
91
100
 
92
- assert.ok(asserts > 10000, 'did a lot of asserts')
101
+ assert.ok(asserts > 10000, 'did a lot of asserts')
102
+ assert.ok(tooBig < 200, 'not too many non-matches beyond MAX_SAFE_INTEGER')
103
+ })