Skip to content

Commit

Permalink
Merge pull request #634 from jimmywarting/feature/modernize-bitset
Browse files Browse the repository at this point in the history
Feature/modernize bitset
  • Loading branch information
squaremo authored Jul 6, 2021
2 parents b183cd8 + 8f4fec6 commit fe53e18
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 93 deletions.
171 changes: 99 additions & 72 deletions lib/bitset.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,102 +2,129 @@
//
//

// A bitset implementation, after that in java.util. Yes there
// already exist such things, but none implement next{Clear|Set}Bit or
// equivalent, and none involved me tooling about for an evening.

'use strict';

function BitSet(size) {
if (size) {
var numWords = Math.ceil(size / 32);
this.words = new Array(numWords);
/**
* A bitset implementation, after that in java.util. Yes there
* already exist such things, but none implement next{Clear|Set}Bit or
* equivalent, and none involved me tooling about for an evening.
*/
class BitSet {
/**
* @param {number} [size]
*/
constructor(size) {
if (size) {
const numWords = Math.ceil(size / 32);
this.words = new Array(numWords);
}
else {
this.words = [];
}
this.wordsInUse = 0; // = number, not index
}
else {
this.words = [];

/**
* @param {number} numWords
*/
ensureSize(numWords) {
const wordsPresent = this.words.length;
if (wordsPresent < numWords) {
this.words = this.words.concat(new Array(numWords - wordsPresent));
}
}
this.wordsInUse = 0; // = number, not index
}

var P = BitSet.prototype;
/**
* @param {number} bitIndex
*/
set(bitIndex) {
const w = wordIndex(bitIndex);
if (w >= this.wordsInUse) {
this.ensureSize(w + 1);
this.wordsInUse = w + 1;
}
const bit = 1 << bitIndex;
this.words[w] |= bit;
}

function wordIndex(bitIndex) {
return Math.floor(bitIndex / 32);
}
/**
* @param {number} bitIndex
*/
clear(bitIndex) {
const w = wordIndex(bitIndex);
if (w >= this.wordsInUse) return;
const mask = ~(1 << bitIndex);
this.words[w] &= mask;
}

// Make sure we have at least numWords
P.ensureSize = function(numWords) {
var wordsPresent = this.words.length;
if (wordsPresent < numWords) {
this.words = this.words.concat(new Array(numWords - wordsPresent));
/**
* @param {number} bitIndex
*/
get(bitIndex) {
const w = wordIndex(bitIndex);
if (w >= this.wordsInUse) return false; // >= since index vs size
const bit = 1 << bitIndex;
return !!(this.words[w] & bit);
}
}

P.set = function(bitIndex) {
var w = wordIndex(bitIndex);
if (w >= this.wordsInUse) {
this.ensureSize(w + 1);
this.wordsInUse = w + 1;
/**
* Give the next bit that is set on or after fromIndex, or -1 if no such bit
*
* @param {number} fromIndex
*/
nextSetBit(fromIndex) {
let w = wordIndex(fromIndex);
if (w >= this.wordsInUse) return -1;

// the right-hand side is shifted to only test the bits of the first
// word that are > fromIndex
let word = this.words[w] & (0xffffffff << fromIndex);
while (true) {
if (word) return (w * 32) + trailingZeros(word);
w++;
if (w === this.wordsInUse) return -1;
word = this.words[w];
}
}
var bit = 1 << bitIndex;
this.words[w] |= bit;
};

P.clear = function(bitIndex) {
var w = wordIndex(bitIndex);
if (w >= this.wordsInUse) return;
var mask = ~(1 << bitIndex);
this.words[w] &= mask;
};
/**
* @param {number} fromIndex
*/
nextClearBit(fromIndex) {
let w = wordIndex(fromIndex);
if (w >= this.wordsInUse) return fromIndex;

P.get = function(bitIndex) {
var w = wordIndex(bitIndex);
if (w >= this.wordsInUse) return false; // >= since index vs size
var bit = 1 << bitIndex;
return !!(this.words[w] & bit);
let word = ~(this.words[w]) & (0xffffffff << fromIndex);
while (true) {
if (word) return (w * 32) + trailingZeros(word);
w++;
if (w == this.wordsInUse) return w * 32;
word = ~(this.words[w]);
}
}
}

/**
* @param {number} bitIndex
*/
function wordIndex(bitIndex) {
return Math.floor(bitIndex / 32);
}

/**
* @param {number} i
*/
function trailingZeros(i) {
// From Hacker's Delight, via JDK. Probably far less effective here,
// since bit ops are not necessarily the quick way to do things in
// JS.
if (i === 0) return 32;
var y, n = 31;
let y, n = 31;
y = i << 16; if (y != 0) { n = n -16; i = y; }
y = i << 8; if (y != 0) { n = n - 8; i = y; }
y = i << 4; if (y != 0) { n = n - 4; i = y; }
y = i << 2; if (y != 0) { n = n - 2; i = y; }
return n - ((i << 1) >>> 31);
}

// Give the next bit that's set on or after fromIndex, or -1 if no such
// bit
P.nextSetBit = function(fromIndex) {
var w = wordIndex(fromIndex);
if (w >= this.wordsInUse) return -1;

// the right-hand side is shifted to only test the bits of the first
// word that are > fromIndex
var word = this.words[w] & (0xffffffff << fromIndex);
while (true) {
if (word) return (w * 32) + trailingZeros(word);
w++;
if (w === this.wordsInUse) return -1;
word = this.words[w];
}
};

P.nextClearBit = function(fromIndex) {
var w = wordIndex(fromIndex);
if (w >= this.wordsInUse) return fromIndex;

var word = ~(this.words[w]) & (0xffffffff << fromIndex);
while (true) {
if (word) return (w * 32) + trailingZeros(word);
w++;
if (w == this.wordsInUse) return w * 32;
word = ~(this.words[w]);
}
};

module.exports.BitSet = BitSet;
43 changes: 22 additions & 21 deletions test/bitset.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,69 @@
'use strict';

var claire = require('claire');
const claire = require('claire');
const {BitSet} = require('../lib/bitset');

var forAll = claire.forAll,
arb = claire.data,
label = claire.label,
choice = claire.choice,
transform = claire.transform;
const {
forAll,
data: arb,
label,
choice,
transform
} = claire;

var BitSet = require('../lib/bitset').BitSet;
var PosInt = transform(Math.floor, arb.Positive);
const PosInt = transform(Math.floor, arb.Positive);

var EmptyBitSet = label('bitset', transform(
function(size) {
const EmptyBitSet = label('bitset', transform(
size => {
return new BitSet(size);
},
choice(arb.Nothing, PosInt)));

suite('BitSet', function() {
suite('BitSet', () => {

test('get bit', forAll(EmptyBitSet, PosInt)
.satisfy(function(b, bit) {
.satisfy((b, bit) => {
b.set(bit);
return b.get(bit);
}).asTest());

test('clear bit', forAll(EmptyBitSet, PosInt)
.satisfy(function(b, bit) {
.satisfy((b, bit) => {
b.set(bit);
b.clear(bit);
return !b.get(bit);
}).asTest());

test('next set of empty', forAll(EmptyBitSet)
.satisfy(function(b) {
.satisfy(b => {
return b.nextSetBit(0) === -1;
}).asTest());

test('next set of one bit', forAll(EmptyBitSet, PosInt)
.satisfy(function(b, bit) {
.satisfy((b, bit) => {
b.set(bit);
return b.nextSetBit(0) === bit;
}).asTest());

test('next set same bit', forAll(EmptyBitSet, PosInt)
.satisfy(function(b, bit) {
.satisfy((b, bit) => {
b.set(bit);
return b.nextSetBit(bit) === bit;
}).asTest());

test('next set following bit', forAll(EmptyBitSet, PosInt)
.satisfy(function(b, bit) {
.satisfy((b, bit) => {
b.set(bit);
return b.nextSetBit(bit+1) === -1;
}).asTest());

test('next clear of empty', forAll(EmptyBitSet, PosInt)
.satisfy(function(b, bit) { return b.nextClearBit(bit) === bit; })
.satisfy((b, bit) => { return b.nextClearBit(bit) === bit; })
.asTest());

test('next clear of one set', forAll(EmptyBitSet, PosInt)
.satisfy(function(b, bit) {
.satisfy((b, bit) => {
b.set(bit);
return b.nextClearBit(bit) === bit + 1;
}).asTest());

});

0 comments on commit fe53e18

Please sign in to comment.