@aggris2/ssz
Version:
Simple Serialize
237 lines • 9.13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUint8ByteToBitBooleanArray = exports.BitArray = void 0;
/** Globally cache this information. @see getUint8ByteToBitBooleanArray */
const uint8ByteToBitBooleanArrays = new Array(256);
/**
* BitArray may be represented as an array of bits or compressed into an array of bytes.
*
* **Array of bits**:
* Require 8.87 bytes per bit, so for 512 bits = 4500 bytes.
* Are 'faster' to iterate with native tooling but are as fast as array of bytes with precomputed caches.
*
* **Array of bytes**:
* Require an average cost of Uint8Array in JS = 220 bytes for 32 bytes, so for 512 bits = 220 bytes.
* With precomputed boolean arrays per bytes value are as fast to iterate as an array of bits above.
*
* This BitArray implementation will represent data as a Uint8Array since it's very cheap to deserialize and can be as
* fast to iterate as a native array of booleans, precomputing boolean arrays (total memory cost of 16000 bytes).
*/
class BitArray {
constructor(
/** Underlying BitArray Uint8Array data */
uint8Array,
/** Immutable bitLen of this BitArray */
bitLen) {
this.uint8Array = uint8Array;
this.bitLen = bitLen;
if (uint8Array.length !== Math.ceil(bitLen / 8)) {
throw Error("BitArray uint8Array length does not match bitLen");
}
}
/** Returns a zero'ed BitArray of `bitLen` */
static fromBitLen(bitLen) {
return new BitArray(new Uint8Array(Math.ceil(bitLen / 8)), bitLen);
}
/** Returns a BitArray of `bitLen` with a single bit set to true at position `bitIndex` */
static fromSingleBit(bitLen, bitIndex) {
const bitArray = BitArray.fromBitLen(bitLen);
bitArray.set(bitIndex, true);
return bitArray;
}
/** Returns a BitArray from an array of booleans representation */
static fromBoolArray(bitBoolArr) {
const bitArray = BitArray.fromBitLen(bitBoolArr.length);
for (let i = 0; i < bitBoolArr.length; i++) {
if (bitBoolArr[i] === true) {
bitArray.set(i, true);
}
}
return bitArray;
}
clone() {
// TODO: Benchmark if Uint8Array.slice(0) is the fastest way to copy data here
// Buffer.prototype.slice does not copy memory, Enforce Uint8Array usage https://github.com/nodejs/node/issues/28087
return new BitArray(Uint8Array.prototype.slice.call(this.uint8Array, 0), this.bitLen);
}
/**
* Get bit value at index `bitIndex`
*/
get(bitIndex) {
const byteIdx = Math.floor(bitIndex / 8);
const bitInBit = bitIndex % 8;
const mask = 1 << bitInBit;
return (this.uint8Array[byteIdx] & mask) === mask;
}
/**
* Set bit value at index `bitIndex`
*/
set(bitIndex, bit) {
if (bitIndex >= this.bitLen) {
throw Error(`BitArray set bitIndex ${bitIndex} beyond bitLen ${this.bitLen}`);
}
const byteIdx = Math.floor(bitIndex / 8);
const bitInBit = bitIndex % 8;
const mask = 1 << bitInBit;
let byte = this.uint8Array[byteIdx];
if (bit) {
// For bit in byte, 1,0 OR 1 = 1
// byte 100110
// mask 010000
// res 110110
byte |= mask;
this.uint8Array[byteIdx] = byte;
}
else {
// For bit in byte, 1,0 OR 1 = 0
if ((byte & mask) === mask) {
// byte 110110
// mask 010000
// res 100110
byte ^= mask;
this.uint8Array[byteIdx] = byte;
}
else {
// Ok, bit is already 0
}
}
}
/** Merge two BitArray bitfields with OR. Must have the same bitLen */
mergeOrWith(bitArray2) {
if (bitArray2.bitLen !== this.bitLen) {
throw Error("Must merge BitArrays of same bitLen");
}
// Merge bitFields
for (let i = 0; i < this.uint8Array.length; i++) {
this.uint8Array[i] = this.uint8Array[i] | bitArray2.uint8Array[i];
}
}
/**
* Returns an array with the indexes which have a bit set to true
*/
intersectValues(values) {
const yes = [];
if (values.length !== this.bitLen) {
throw Error(`Must not intersect values of length ${values.length} != bitLen ${this.bitLen}`);
}
const fullByteLen = Math.floor(this.bitLen / 8);
const remainderBits = this.bitLen % 8;
// Iterate over each byte of bits
const bytes = this.uint8Array;
for (let iByte = 0; iByte < fullByteLen; iByte++) {
// Get the precomputed boolean array for this byte
const booleansInByte = getUint8ByteToBitBooleanArray(bytes[iByte]);
// For each bit in the byte check participation and add to indexesSelected array
for (let iBit = 0; iBit < 8; iBit++) {
if (booleansInByte[iBit]) {
yes.push(values[iByte * 8 + iBit]);
}
}
}
if (remainderBits > 0) {
// Get the precomputed boolean array for this byte
const booleansInByte = getUint8ByteToBitBooleanArray(bytes[fullByteLen]);
// For each bit in the byte check participation and add to indexesSelected array
for (let iBit = 0; iBit < remainderBits; iBit++) {
if (booleansInByte[iBit]) {
yes.push(values[fullByteLen * 8 + iBit]);
}
}
}
return yes;
}
/**
* Returns the positions of all bits that are set to true
*/
getTrueBitIndexes() {
const indexes = [];
// Iterate over each byte of bits
const bytes = this.uint8Array;
for (let iByte = 0, byteLen = bytes.length; iByte < byteLen; iByte++) {
// Get the precomputed boolean array for this byte
const booleansInByte = getUint8ByteToBitBooleanArray(bytes[iByte]);
// For each bit in the byte check participation and add to indexesSelected array
for (let iBit = 0; iBit < 8; iBit++) {
if (booleansInByte[iBit]) {
indexes.push(iByte * 8 + iBit);
}
}
}
return indexes;
}
/**
* Return the position of a single bit set. If no bit set or more than 1 bit set, throws.
* @returns
* - number: if there's a single bit set, the number it the single bit set position
* - null: if ERROR_MORE_THAN_ONE_BIT_SET or ERROR_NO_BIT_SET
* @throws
* - ERROR_MORE_THAN_ONE_BIT_SET
* - ERROR_NO_BIT_SET
*/
getSingleTrueBit() {
let index = null;
const bytes = this.uint8Array;
// Iterate over each byte of bits
for (let iByte = 0, byteLen = bytes.length; iByte < byteLen; iByte++) {
// If it's exactly zero, there won't be any indexes, continue early
if (bytes[iByte] === 0) {
continue;
}
// Get the precomputed boolean array for this byte
const booleansInByte = getUint8ByteToBitBooleanArray(bytes[iByte]);
// For each bit in the byte check participation and add to indexesSelected array
for (let iBit = 0; iBit < 8; iBit++) {
if (booleansInByte[iBit] === true) {
if (index !== null) {
// ERROR_MORE_THAN_ONE_BIT_SET
return null;
}
index = iByte * 8 + iBit;
}
}
}
if (index === null) {
// ERROR_NO_BIT_SET
return null;
}
else {
return index;
}
}
toBoolArray() {
const bitBoolArr = new Array(this.bitLen);
for (let i = 0; i < this.bitLen; i++) {
bitBoolArr[i] = this.get(i);
}
return bitBoolArr;
}
}
exports.BitArray = BitArray;
/**
* Given a byte (0 -> 255), return a Array of boolean with length = 8, big endian.
* Ex: 1 => [true false false false false false false false]
* 5 => [true false true false false fase false false]
*/
function getUint8ByteToBitBooleanArray(byte) {
if (!uint8ByteToBitBooleanArrays[byte]) {
uint8ByteToBitBooleanArrays[byte] = computeUint8ByteToBitBooleanArray(byte);
}
return uint8ByteToBitBooleanArrays[byte];
}
exports.getUint8ByteToBitBooleanArray = getUint8ByteToBitBooleanArray;
/** @see getUint8ByteToBitBooleanArray */
function computeUint8ByteToBitBooleanArray(byte) {
// this returns little endian
const binaryStr = byte.toString(2);
const binaryLength = binaryStr.length;
const bits = new Array(8);
for (let i = 0; i < 8; i++) {
bits[i] =
i < binaryLength
? //
binaryStr[binaryLength - i - 1] === "1"
: false;
}
return bits;
}
//# sourceMappingURL=bitArray.js.map