diff --git a/js/utils.js b/js/utils.js index fc9cf19..ece2acf 100644 --- a/js/utils.js +++ b/js/utils.js @@ -74,8 +74,10 @@ function log(b, n) { // https://stackoverflow.com/a/27696695 // Modified for fixed precision +// Base64.fromInt(-2147483648); // gives "200000" +// Base64.toInt("200000"); // gives -2147483648 Base64 = (function () { - var digitsStr = + var digitsStr = // 0 8 16 24 32 40 48 56 63 // v v v v v v v v v "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-"; @@ -125,8 +127,246 @@ Base64 = (function () { }; })(); -// Base64.fromInt(-2147483648); // gives "200000" -// Base64.toInt("200000"); // gives -2147483648 + +/** A class used to represent an arbitrary length bit vector. Very useful for encoding and decoding. + * + */ + class BitVector { + + /** Constructs an arbitrary-length bit vector. + * @class + * @param {String | Number} data - The data to append. + * @param {Number} length - A set length for the data. Ignored if data is a string. + * + * The structure of the Uint32Array should be [[last, ..., first], ..., [last, ..., first], [empty space, last, ..., first]] + */ + constructor(data, length) { + let bit_vec = []; + + if (typeof data === "string") { + let int = 0; + let bv_idx = 0; + length = data.length * 6; + + for (let i = 0; i < data.length; i++) { + let char = Base64.toInt(data[i]); + let pre_pos = bv_idx % 32; + int |= (char << bv_idx); + bv_idx += 6; + let post_pos = bv_idx % 32; + if (post_pos < pre_pos) { //we have to have filled up the integer + bit_vec.push(int); + int = (char >>> (6 - post_pos)); + } + + if (i == data.length - 1 && post_pos != 0) { + bit_vec.push(int); + } + } + } else if (typeof data === "number") { + if (typeof length === "undefined") + if (length < 0) { + throw new RangeError("BitVector must have nonnegative length."); + } + + //convert to int just in case + data = Math.round(data); + + //range of numbers that won't fit in a uint32 + if (data > 2**32 - 1 || data < -(2 ** 32 - 1)) { + throw new RangeError("Numerical data has to fit within a 32-bit integer range to instantiate a BitVector."); + } + bit_vec.push(data); + } else { + throw new TypeError("BitVector must be instantiated with a Number or a B64 String"); + } + + this.length = length; + this.bits = new Uint32Array(bit_vec); + } + + /** Return value of bit at index idx. + * + * @param {Number} idx - The index to read + * + * @returns The bit value at position idx + */ + read_bit(idx) { + if (idx < 0 || idx >= this.length) { + throw new RangeError("Cannot read bit outside the range of the BitVector."); + } + return ((this.bits[Math.floor(idx / 32)] & (1 << (idx % 32))) == 0 ? 0 : 1); + } + + /** Returns an integer value (if possible) made from the range of bits [start, end). Undefined behavior if the range to read is too big. + * + * @param {Number} start - The index to start slicing from. Inclusive. + * @param {Number} end - The index to end slicing at. Exclusive. + * + * @returns An integer representation of the sliced bits. + */ + slice(start, end) { + //TO NOTE: JS shifting is ALWAYS in mod 32. a << b will do a << (b mod 32) implicitly. + + if (end < start) { + throw new RangeError("Cannot slice a range where the end is before the start."); + } else if (end == start) { + return 0; + } else if (end - start > 32) { + //requesting a slice of longer than 32 bits (safe integer "length") + throw new RangeError("Cannot slice a range of longer than 32 bits (unsafe to store in an integer)."); + } + + let res = 0; + if (Math.floor((end - 1) / 32) == Math.floor(start / 32)) { + //the range is within 1 uint32 section - do some relatively fast bit twiddling + res = (this.bits[Math.floor(start / 32)] & ~((((~0) << ((end - 1))) << 1) | ~((~0) << (start)))) >>> (start % 32); + } else { + //the number of bits in the uint32s + let start_pos = (start % 32); + let int_idx = Math.floor(start/32); + res = (this.bits[int_idx] & ((~0) << (start))) >>> (start_pos); + res |= (this.bits[int_idx + 1] & ~((~0) << (end))) << (32 - start_pos); + } + + return res; + + // General code - slow + // for (let i = start; i < end; i++) { + // res |= (get_bit(i) << (i - start)); + // } + } + + /** Assign bit at index idx to 1. + * + * @param {Number} idx - The index to set. + */ + set_bit(idx) { + if (idx < 0 || idx >= this.length) { + throw new RangeError("Cannot set bit outside the range of the BitVector."); + } + this.bits[Math.floor(idx / 32)] |= (1 << idx % 32); + } + + /** Assign bit at index idx to 0. + * + * @param {Number} idx - The index to clear. + */ + clear_bit(idx) { + if (idx < 0 || idx >= this.length) { + throw new RangeError("Cannot clear bit outside the range of the BitVector."); + } + this.bits[Math.floor(idx / 32)] &= ~(1 << idx % 32); + } + + /** Creates a string version of the bit vector in B64. Does not keep the order of elements a sensible human readable format. + * + * @returns A b64 string representation of the BitVector. + */ + toB64() { + if (this.length == 0) { + return ""; + } + let b64_str = ""; + let i = 0; + while (i < this.length) { + b64_str += Base64.fromIntV(this.slice(i, i + 6), 1); + i += 6; + } + + return b64_str; + } + + /** Returns a BitVector in bitstring format. Probably only useful for dev debugging. + * + * @returns A bit string representation of the BitVector. Goes from higher-indexed bits to lower-indexed bits. (n ... 0) + */ + toString() { + let ret_str = ""; + for (let i = 0; i < this.length; i++) { + ret_str = (this.read_bit(i) == 0 ? "0": "1") + ret_str; + } + return ret_str; + } + + /** Returns a BitVector in bitstring format. Probably only useful for dev debugging. + * + * @returns A bit string representation of the BitVector. Goes from lower-indexed bits to higher-indexed bits. (0 ... n) + */ + toStringR() { + let ret_str = ""; + for (let i = 0; i < this.length; i++) { + ret_str += (this.read_bit(i) == 0 ? "0": "1"); + } + return ret_str; + } + + /** Appends data to the BitVector. + * + * @param {Number | String} data - The data to append. + * @param {Number} length - The length, in bits, of the new data. This is ignored if data is a string. + */ + append(data, length) { + if (length < 0) { + throw new RangeError("BitVector length must increase by a nonnegative number."); + } + + let bit_vec = []; + for (const uint of this.bits) { + bit_vec.push(uint); + } + if (typeof data === "string") { + let int = bit_vec[bit_vec.length - 1]; + let bv_idx = this.length; + length = data.length * 6; + let updated_curr = false; + for (let i = 0; i < data.length; i++) { + let char = Base64.toInt(data[i]); + let pre_pos = bv_idx % 32; + int |= (char << bv_idx); + bv_idx += 6; + let post_pos = bv_idx % 32; + if (post_pos < pre_pos) { //we have to have filled up the integer + if (bit_vec.length == this.bits.length && !updated_curr) { + bit_vec[bit_vec.length - 1] = int; + updated_curr = true; + } else { + bit_vec.push(int); + } + int = (char >>> (6 - post_pos)); + } + + if (i == data.length - 1) { + if (bit_vec.length == this.bits.length && !updated_curr) { + bit_vec[bit_vec.length - 1] = int; + } else if (post_pos != 0) { + bit_vec.push(int); + } + } + } + } else if (typeof data === "number") { + //convert to int just in case + let int = Math.round(data); + + //range of numbers that "could" fit in a uint32 -> [0, 2^32) U [-2^31, 2^31) + if (data > 2**32 - 1 || data < -(2 ** 31)) { + throw new RangeError("Numerical data has to fit within a 32-bit integer range to instantiate a BitVector."); + } + //could be split between multiple new ints + //reminder that shifts implicitly mod 32 + bit_vec[bit_vec.length - 1] |= ((int & ~((~0) << length)) << (this.length)); + if (((this.length - 1) % 32 + 1) + length > 32) { + bit_vec.push(int >>> (32 - this.length)); + } + } else { + throw new TypeError("BitVector must be appended with a Number or a B64 String"); + } + + this.bits = new Uint32Array(bit_vec); + this.length += length; + } +}; + /* Turns a raw stat and a % stat into a final stat on the basis that - raw and >= 100% becomes 0 and + raw and <=-100% becomes negative. @@ -238,7 +478,7 @@ function randomColor() { /** * Generates a random color, but lightning must be relatively high (>0.5). - * + * * @returns a random color in RGB 6-bit form. */ function randomColorLight() { @@ -246,7 +486,7 @@ function randomColorLight() { } /** Generates a random color given HSL restrictions. - * + * * @returns a random color in RGB 6-bit form. */ function randomColorHSL(h,s,l) { @@ -298,8 +538,8 @@ function randomColorHSL(h,s,l) { return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } -/** Creates a tooltip. - * +/** Creates a tooltip. + * * @param {DOM Element} elem - the element to make a tooltip * @param {String} element_type - the HTML element type that the tooltiptext should be. * @param {String} tooltiptext - the text to display in the tooltip. @@ -330,7 +570,7 @@ function createTooltip(elem, element_type, tooltiptext, parent, classList) { } /** A generic function that toggles the on and off state of a button. - * + * * @param {String} button_id - the id name of the button. */ function toggleButton(button_id) { @@ -374,8 +614,8 @@ function addClasses(elem, classes) { return elem; } -/** A utility function that reloads the page forcefully. - * +/** A utility function that reloads the page forcefully. + * */ async function hardReload() { //https://gist.github.com/rmehner/b9a41d9f659c9b1c3340 @@ -413,13 +653,13 @@ const getScript = url => new Promise((resolve, reject) => { document.head.appendChild(script); }) -/* +/* GENERIC TEST FUNCTIONS */ /** The generic assert function. Fails on all "false-y" values. Useful for non-object equality checks, boolean value checks, and existence checks. - * + * * @param {*} arg - argument to assert. - * @param {String} msg - the error message to throw. + * @param {String} msg - the error message to throw. */ function assert(arg, msg) { if (!arg) { @@ -428,10 +668,10 @@ GENERIC TEST FUNCTIONS } /** Asserts object equality of the 2 parameters. For loose and strict asserts, use assert(). - * + * * @param {*} arg1 - first argument to compare. * @param {*} arg2 - second argument to compare. - * @param {String} msg - the error message to throw. + * @param {String} msg - the error message to throw. */ function assert_equals(arg1, arg2, msg) { if (!Object.is(arg1, arg2)) { @@ -440,10 +680,10 @@ function assert_equals(arg1, arg2, msg) { } /** Asserts object inequality of the 2 parameters. For loose and strict asserts, use assert(). - * + * * @param {*} arg1 - first argument to compare. * @param {*} arg2 - second argument to compare. - * @param {String} msg - the error message to throw. + * @param {String} msg - the error message to throw. */ function assert_not_equals(arg1, arg2, msg) { if (Object.is(arg1, arg2)) { @@ -452,11 +692,11 @@ function assert_equals(arg1, arg2, msg) { } /** Asserts proximity between 2 arguments. Should be used for any floating point datatype. - * + * * @param {*} arg1 - first argument to compare. * @param {*} arg2 - second argument to compare. * @param {Number} epsilon - the margin of error (<= del difference is ok). Defaults to -1E5. - * @param {String} msg - the error message to throw. + * @param {String} msg - the error message to throw. */ function assert_near(arg1, arg2, epsilon = 1E-5, msg) { if (Math.abs(arg1 - arg2) > epsilon) { @@ -465,7 +705,7 @@ function assert_near(arg1, arg2, epsilon = 1E-5, msg) { } /** Asserts that the input argument is null. - * + * * @param {*} arg - the argument to test for null. * @param {String} msg - the error message to throw. */ @@ -476,7 +716,7 @@ function assert_null(arg, msg) { } /** Asserts that the input argument is undefined. - * + * * @param {*} arg - the argument to test for undefined. * @param {String} msg - the error message to throw. */ @@ -487,7 +727,7 @@ function assert_null(arg, msg) { } /** Asserts that there is an error when a callback function is run. - * + * * @param {Function} func_binding - a function binding to run. Can be passed in with func.bind(null, arg1, ..., argn) * @param {String} msg - the error message to throw. */ @@ -496,7 +736,7 @@ function assert_error(func_binding, msg) { func_binding(); } catch (err) { return; - } + } throw new Error(msg ? msg : "Function didn't throw an error."); }