A longer example is hard to fit into a comment :)
Up front, please note that very often, network data is compressed already - e.g. with gzip (especially when there is concern about data volume and the network libraries are setup properly). However, this is not always the case and would still not be as compact as doing it manually.
You need to keep track of two things, the current array index and the current slot inside the 8-Bit that is being read or written. For writing, | is useful, for reading &. Shifts (<< or >>) are used to select the position.
const randomTwoBitData = () => Math.floor(Math.random() * 4);
//Array of random 2-Bit data
const sampleData = Array(256).fill().map(e => randomTwoBitData());
//four entries per 8-Bit
let buffer = new Uint8Array(sampleData.length / 4);
//Writing data, i made my life easy
//because the data is divisible by four and fits perfectly.
for (let i = 0; i < sampleData.length; i += 4) {
buffer[i / 4] =
sampleData[i] |
(sampleData[i + 1] << 2) |
(sampleData[i + 2] << 4) |
(sampleData[i + 3] << 6);
}
//padding for console logging
const pad = (s, n) => "0".repeat(Math.max(0, n - s.length)) + s;
//Some output to see results at the middle
console.log(`buffer: ${pad(buffer[31].toString(2), 8)}, ` +
`original data: ${pad(sampleData[127].toString(2), 2)}, ` +
`${pad(sampleData[126].toString(2), 2)}, ` +
`${pad(sampleData[125].toString(2), 2)}, ` +
`${pad(sampleData[124].toString(2), 2)}`);
console.log("(order of original data inverted for readability)");
console.log("");
//Reading back:
let readData = [];
buffer.forEach(byte => {
readData.push(byte & 3); // 3 is 00000011 binary
readData.push((byte & 12) >> 2); // 12 is 00001100 binary
readData.push((byte & 48) >> 4); // 48 is 00110000 binary
readData.push((byte & 192) >> 6); // 192 is 11000000 binary
});
//Check if data read from compacted buffer is the same
//as the original
console.log(`original data and re-read data are identical: ` +
readData.every((e, i) => e === sampleData[i]));
&and>>((Number.parseInt("10100110", 2) & Number.parseInt("11110000", 2)) >> 4).toString(2);("10100110" is our data, "11110000" is our bitmask)|, e.g. consider two 2-Bit data points have been written already (e.g. 11 and 10), and the data currently looks like this: 00001011 (binary), just uselet x = Number.parseInt("00001011", 2); x |= (Number.parseInt("10", 2) << 4);(where 10 is the example data we want to add). As there are only 4 positions per 8-Bit, i'd not write a loop but just write it all explicitly.