diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3659f1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/* +dist/* diff --git a/src/bitarray.ts b/src/bitarray.ts index 4b3155d..231d3b1 100644 --- a/src/bitarray.ts +++ b/src/bitarray.ts @@ -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 diff --git a/test/suite.ts b/test/suite.ts index 1155f1b..945311d 100644 --- a/test/suite.ts +++ b/test/suite.ts @@ -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 ) { @@ -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 };