Skip to content

Commit

Permalink
feat: add encoding/decoding of bit array to string (#1)
Browse files Browse the repository at this point in the history
* Add encoding/decoding from character set

* Adding base64 helpers

Co-authored-by: Ryan Rolnicki <[email protected]>
  • Loading branch information
rollie42 and Ryan Rolnicki authored May 11, 2022
1 parent 3dadcb6 commit 47ccefd
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/*
dist/*
79 changes: 79 additions & 0 deletions src/bitarray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,85 @@ export default class BitArray extends BitTypedArray {
return ret;
}

/**
*
* @param charArray a set of n characters to use to encode the BitArray; charArray.length must be a power of 2 (2, 4, 8, etc)
* The more characters in the set, the more compact the resulting output will be
* @returns a string encoded using the provided character set (e.g., base64 encoding can be achieved with this)
*/
encodeWithCharacterSet( charArray: string): string {
const log2 = Math.log2(charArray.length);

if (log2 < 1 || log2 % 1 !== 0) {
throw new RangeError('Provided charArray\'s length must non-0 positive power of 2');
}

const ret = [];

let val = 0;
let valLen = 0;
for (const b of this) {
valLen++;
val <<= 1;
val += b;

if (valLen === log2) {
ret.push(charArray[val]);
valLen = val = 0;
}
}

if (valLen !== 0) {
val <<= (log2 - valLen);
ret.push(charArray[val]);
}

return ret.join('');
}

/**
*
* @param charArray a set of n characters to use to encode the BitArray; charArray.length must be a power of 2 (2, 4, 8, etc),
* and should generally match the set used in the original encoding
* @param encodedString an encoded string built with encodeWithCharacterSet
* @returns a BitArray of the encodedString decoded using charArray
*/
static decodeWithCharacterSet( charArray: string, encodedString: string ): BitArray {
const log2 = Math.log2(charArray.length);

if (log2 < 1 || log2 % 1 !== 0) {
throw new RangeError('Provided charArray\'s length must non-0 positive power of 2');
}

const pad = (s: string) => '0'.repeat(log2 - s.length) + s

const charMap = {} // maps each character to its integral value
for (var i = 0; i < charArray.length; i++) {
charMap[charArray[i]] = pad(i.toString(2))
}

const deserialized = Array.from(encodedString).map(c => {
if (!(c in charMap)) {
throw new RangeError('Invalid character found in encoded string');
}
return charMap[c];
}).join('')
const ret = BitArray.from(deserialized);
return ret;
}

// Convenience specializations for encoding base64MIME and base64Url
static base64MIMEChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
encodeBase64MIME() { return this.encodeWithCharacterSet(BitArray.base64MIMEChars) }
static decodeBase64MIME(encodedString: string) {
return BitArray.decodeWithCharacterSet(BitArray.base64MIMEChars, encodedString);
}

static base64UrlChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
encodeBase64Url() { return this.encodeWithCharacterSet(BitArray.base64UrlChars) }
static decodeBase64Url(encodedString: string) {
return BitArray.decodeWithCharacterSet(BitArray.base64UrlChars, encodedString);
}
}

// create aliases
Expand Down
37 changes: 36 additions & 1 deletion test/suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ const arr2 = new Array( len + 10 ).fill(false).map( x => Math.random() > 0.5 )

const sample1 = BitArray.from( arr1 );
const sample2 = BitArray.of( ...arr2 );
const sample3 = BitArray.from( '0110');

// Returns true if the block throws
function expectThrow( fn: () => void) {
try {
fn();
} catch(e) {
return true;
}

return false;
}

// matches the format of BitArray.toSting()
function toString( arr ) {
Expand Down Expand Up @@ -55,9 +67,32 @@ const binary_operations = (()=>{

})();

/** suite 4 */
const character_encoding_from_set = {
".encodeWithCharacterSet_1bit": sample3.encodeWithCharacterSet('ab') === 'abba',
".encodeWithCharacterSet_3bit": sample3.encodeWithCharacterSet('abcdefgh') === 'da',
".encodeWithCharacterSet_": expectThrow(() => sample3.encodeWithCharacterSet('')),
".encodeWithCharacterSet_a": expectThrow(() => sample3.encodeWithCharacterSet('a')),
".encodeWithCharacterSet_abc": expectThrow(() => sample3.encodeWithCharacterSet('abc'))
};

/** suite 5 */
const character_encode_decode = {
".decodeWithCharacterSet_1bit": BitArray.decodeWithCharacterSet('ab', 'abba').toString() === sample3.toString(),
// Note: the substring is needed because when deserializing, we have some number of padding 0s that we can't know were
// in the original string or not
".decodeWithCharacterSet_3bit": BitArray.decodeWithCharacterSet('abcdefgh', 'da').toString().substring(0, 4) === sample3.toString(),
".decodeWithCharacterSet_empty": BitArray.decodeWithCharacterSet('ab', '').toString() === '',
".decodeWithCharacterSet_invalid": expectThrow(() => BitArray.decodeWithCharacterSet('ab', 'abc')),
".decodeWithCharacterSet_": expectThrow(() => BitArray.decodeWithCharacterSet('', 'abba'))
};


export default {
instantiating,
properties,
binary_operations
binary_operations,
character_encoding_from_set,
character_encode_decode
};

0 comments on commit 47ccefd

Please sign in to comment.