<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>21-bit to Integer Converter — output is post-subtract binary (LSB disamb, leading 1-bit stripped)</title>
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; margin: 24px; color:#111; }
h1 { margin-bottom: 8px; }
h2 { margin-top: 24px; margin-bottom: 8px; font-size: 20px; }
.card { border:1px solid #e1e4e8; border-radius:8px; padding:16px; max-width:920px; background:#fff; box-shadow:0 1px 2px rgba(0,0,0,0.03); margin-bottom: 16px; }
label { display:block; margin-bottom:8px; font-weight:600; }
input[type="text"], input[type="number"] { width:100%; padding:10px 12px; font-size:16px; border:1px solid #cbd5e1; border-radius:6px; box-sizing:border-box; }
button { margin-top:12px; padding:8px 12px; font-size:15px; border-radius:6px; border:0; background:#0366d6; color:#fff; cursor:pointer; }
button.secondary { background:#6b7280; }
.error { color:#b91c1c; margin-top:8px; }
.success { color:#059669; margin-top:8px; font-weight:600; }
pre { background:#f6f8fa; padding:12px; border-radius:6px; overflow:auto; }
.steps { margin-top:12px; }
.result { margin-top:12px; font-weight:700; font-size:18px; }
.small { font-size:13px; color:#374151; }
.row { display:flex; gap:8px; align-items:center; margin-top:8px; }
.copy { background:#10b981; }
.inline { display:inline-block; width:auto; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Courier New", monospace; }
</style>
</head>
<body>
<div class="card">
<h1>21-bit to Integer Converter</h1>
<div class="small">
Enter exactly 21 bits. The calculator follows your rule set and <strong>subtracts 1,048,576</strong>.
<strong>Output</strong> is the binary representation of the value <em>after</em> that subtraction, with the leading 1-bit stripped (decompressor adds it back), and a single
deterministic disambiguation bit appended as the LSB. All UI elements and trial statistics use that same total length (core bits without leading 1 + 1 LSB).
</div>
<label for="bits">Bits</label>
<input id="bits" type="text" placeholder="e.g. 010101001110000111000" maxlength="21" />
<div class="row">
<button id="compute">Compute</button>
<button id="random" class="secondary">Random</button>
<button id="example" class="secondary">Fill Example</button>
<button id="copy" class="copy">Copy Result</button>
</div>
<div style="margin-top:12px;">
<label for="count">Run Random Tests (count)</label>
<div class="row">
<input id="count" type="number" min="1" max="100000" value="100" style="width:120px;" />
<button id="runTests">Run</button>
</div>
</div>
<div id="error" class="error" role="status" aria-live="polite"></div>
<div id="output" class="steps" aria-live="polite"></div>
</div>
<div class="card">
<h2>Decompressor (Test Reversibility)</h2>
<div class="small">
Enter a compressed payload (binary with leading 1-bit stripped + LSB disambiguation bit) to decompress it back to the original 21-bit input.
</div>
<label for="compressed">Compressed Payload</label>
<input id="compressed" type="text" placeholder="e.g. 01010100111000011100" />
<div class="row">
<button id="decompress">Decompress</button>
<button id="testRoundtrip" class="secondary">Test Roundtrip</button>
</div>
<div id="decompressError" class="error" role="status" aria-live="polite"></div>
<div id="decompressOutput" class="steps" aria-live="polite"></div>
</div>
<script>
const bitsInput = document.getElementById('bits');
const computeBtn = document.getElementById('compute');
const randomBtn = document.getElementById('random');
const exampleBtn = document.getElementById('example');
const copyBtn = document.getElementById('copy');
const runTestsBtn = document.getElementById('runTests');
const countInput = document.getElementById('count');
const errorEl = document.getElementById('error');
const outputEl = document.getElementById('output');
const compressedInput = document.getElementById('compressed');
const decompressBtn = document.getElementById('decompress');
const testRoundtripBtn = document.getElementById('testRoundtrip');
const decompressErrorEl = document.getElementById('decompressError');
const decompressOutputEl = document.getElementById('decompressOutput');
function validateBits(s) {
if (!s) return 'Input is empty';
if (s.length !== 21) return 'Input must be exactly 21 bits';
if (!/^[01]{21}$/.test(s)) return 'Input must contain only 0 and 1';
return '';
}
function validateCompressed(s) {
if (!s) return 'Input is empty';
if (!/^[01]+$/.test(s)) return 'Input must contain only 0 and 1';
if (s.length < 2) return 'Input must be at least 2 bits (core + LSB)';
return '';
}
const SUBTRACT = 1024n * 1024n; // 1,048,576n
/**
* computeFromBits:
* - runs the stepwise rule to produce valueBeforeSubtract
* - computes valueAfterSubtract = valueBeforeSubtract - SUBTRACT
* - produces binary representation of valueAfterSubtract (fullBinaryAfter)
* - strips the leading 1-bit from fullBinaryAfter to get coreBinaryAfter
* - appends disambiguation bit (LSB) which is the original first input bit
*
* Important: the UI and trial mode treat the "compressed-with-disamb" as:
* compressedWithDisambLSB = coreBinaryAfter + disambiguationBit
*
* The total length in bits reported to the user counts only the binary digits (without leading 1) plus the 1 disambiguation bit.
*/
function computeFromBits(bitString) {
const bits = bitString.split('').map(b => b === '1' ? 1 : 0);
// initial value
let value = bits[0] === 0 ? 2n : 3n;
const trace = [];
trace.push({ step: 0, bit: bits[0], op: `start`, value: value.toString() });
for (let i = 1; i < bits.length; i++) {
const b = bits[i];
if (b === 0) {
value = value * 2n;
trace.push({ step: i, bit: 0, op: `×2`, value: value.toString() });
} else {
value = value * 2n - 1n;
trace.push({ step: i, bit: 1, op: `×2 − 1`, value: value.toString() });
}
}
const valueBeforeSubtract = value;
const valueAfterSubtract = valueBeforeSubtract - SUBTRACT;
// full binary is the binary representation of the value AFTER subtraction
const fullBinaryAfter = valueAfterSubtract.toString(2);
// strip the leading 1-bit (decompressor will add it back)
const coreBinaryAfter = fullBinaryAfter.slice(1);
const coreBitsLength = coreBinaryAfter.length;
// deterministic disambiguation bit: original first input bit
const disambiguationBit = String(bits[0]);
// compressed-with-disambiguation (LSB placement) is coreBinaryAfter (without leading 1) + disambiguation bit
const compressedWithDisambLSB = coreBinaryAfter + disambiguationBit;
// total length in bits used for statistics: coreBitsLength + 1 (LSB disambiguation)
const totalLengthBits = coreBitsLength + 1;
return {
trace,
valueBeforeSubtract: valueBeforeSubtract.toString(),
valueAfterSubtract: valueAfterSubtract.toString(),
fullBinaryAfter,
coreBinaryAfter,
coreBitsLength,
disambiguationBit,
compressedWithDisambLSB,
totalLengthBits,
originalBits: bitString
};
}
/**
* decompressPayload:
* - takes compressed payload (core binary without leading 1 + LSB disambiguation)
* - extracts LSB disambiguation bit
* - prepends "1" to restore full binary
* - parses to get valueAfterSubtract
* - adds SUBTRACT to get valueBeforeSubtract
* - reverses the encoding algorithm to recover original 21 bits
*/
function decompressPayload(compressedPayload) {
// Extract LSB (last bit)
const disambiguationBit = compressedPayload[compressedPayload.length - 1];
// Extract core binary (all but last bit)
const coreBinary = compressedPayload.slice(0, -1);
// Restore full binary by prepending "1"
const fullBinary = '1' + coreBinary;
// Parse to get valueAfterSubtract
const valueAfterSubtract = BigInt('0b' + fullBinary);
// Add SUBTRACT to get valueBeforeSubtract
const valueBeforeSubtract = valueAfterSubtract + SUBTRACT;
// Now reverse the encoding algorithm to recover the original 21 bits
// We need to work backwards from valueBeforeSubtract
const recovered21Bits = reverseEncode(valueBeforeSubtract);
return {
disambiguationBit,
coreBinary,
fullBinary,
valueAfterSubtract: valueAfterSubtract.toString(),
valueBeforeSubtract: valueBeforeSubtract.toString(),
recovered21Bits
};
}
/**
* reverseEncode:
* Given a valueBeforeSubtract, reverse the encoding algorithm to recover the original 21 bits.
*
* The encoding algorithm is:
* - Start with value = 2 (if first bit is 0) or 3 (if first bit is 1)
* - For each subsequent bit:
* - If bit is 0: value = value * 2
* - If bit is 1: value = value * 2 - 1
*
* To reverse:
* - We work backwards from the final value
* - For each step, determine if the previous operation was *2 or *2-1
* - If current value is even: previous was *2, so bit is 0, prev_value = value / 2
* - If current value is odd: previous was *2-1, so bit is 1, prev_value = (value + 1) / 2
*/
function reverseEncode(finalValue) {
const bits = [];
let value = finalValue;
// Work backwards for 20 steps (bits 20 down to 1)
for (let i = 0; i < 20; i++) {
if (value % 2n === 0n) {
// Even: previous operation was *2, so bit is 0
bits.unshift(0);
value = value / 2n;
} else {
// Odd: previous operation was *2-1, so bit is 1
bits.unshift(1);
value = (value + 1n) / 2n;
}
}
// Now value should be either 2 or 3, which tells us the first bit
if (value === 2n) {
bits.unshift(0);
} else if (value === 3n) {
bits.unshift(1);
} else {
throw new Error('Decompression failed: invalid starting value');
}
return bits.join('');
}
function renderResult(resultObj) {
const {
trace,
valueBeforeSubtract,
valueAfterSubtract,
fullBinaryAfter,
coreBinaryAfter,
coreBitsLength,
disambiguationBit,
compressedWithDisambLSB,
totalLengthBits
} = resultObj;
let html = '';
html += '<div class="small"><strong>Trace (step-by-step building of value before subtract)</strong></div>';
html += '<pre class="mono">';
html += 'Step Bit Operation Value\n';
html += '---- --- --------- ----------------\n';
trace.forEach(t => {
const step = String(t.step).padEnd(4, ' ');
const bit = String(t.bit).padEnd(3, ' ');
const op = String(t.op).padEnd(9, ' ');
const val = t.value;
html += `${step} ${bit} ${op} ${val}\n`;
});
html += '</pre>';
html += `<div class="result"><strong>Value before subtracting 1,048,576</strong>: ${valueBeforeSubtract}</div>`;
html += `<div class="result"><strong>Value after subtracting 1,048,576</strong>: ${valueAfterSubtract}</div>`;
html += '<hr />';
html += '<div class="small"><strong>Full binary of value after subtraction</strong></div>';
html += `<pre class="mono">${fullBinaryAfter}</pre>`;
html += '<div class="small" style="margin-top:8px;"><strong>Compressed output (leading 1-bit stripped)</strong></div>';
html += `<pre class="mono">${coreBinaryAfter}</pre>`;
html += '<div class="small" style="margin-top:8px;"><strong>Disambiguation bit (LSB)</strong></div>';
html += `<div class="mono">disambiguationBit (LSB) = <strong>${disambiguationBit}</strong> (original first input bit)</div>`;
html += '<div style="margin-top:8px;" class="small"><strong>Final transmitted payload (core binary without leading 1 + LSB disambiguation)</strong></div>';
html += `<pre class="mono">${compressedWithDisambLSB}</pre>`;
html += `<div style="margin-top:8px;" class="small"><strong>Total length used for payload (for statistics)</strong></div>`;
html += `<div class="mono"><strong>${totalLengthBits}</strong> bits (core binary digits without leading 1: ${coreBitsLength} + 1 LSB disambiguation)</div>`;
html += '<div style="margin-top:8px;" class="small"><strong>Decompressor recipe (full and accurate)</strong></div>';
html += '<pre class="mono">';
html += '1) Read the last bit of the payload => disambiguation bit (LSB).\n';
html += '2) Take the remaining bits: the value-after-subtract binary with its leading 1 stripped.\n';
html += '3) Prepend "1" to restore the full binary of valueAfterSubtract.\n';
html += '4) Parse it to the integer valueAfterSubtract.\n';
html += '5) valueBeforeSubtract = valueAfterSubtract + 1,048,576.\n';
html += '6) Recover the 21 bits by undoing the build. Repeat 20 times:\n';
html += ' if the value is even, the bit was 0 and value = value / 2;\n';
html += ' if the value is odd, the bit was 1 and value = (value + 1) / 2.\n';
html += '7) After 20 undo-steps the value is 2 or 3; that is the first bit (2 => 0, 3 => 1).\n';
html += '\n';
html += 'Note: step 7 recovers the first bit from the value itself, so the LSB disambiguation bit\n';
html += 'read in step 1 is never consulted. It is currently a placeholder and carries no information;\n';
html += 'the value alone already determines all 21 original bits.\n';
html += '\n';
html += 'Example (decompressor):\n';
html += `received payload: ${compressedWithDisambLSB}\n`;
html += `disambiguation (LSB): ${disambiguationBit} (not used by the decoder)\n`;
html += `core binary (leading 1 stripped): ${coreBinaryAfter}\n`;
html += `restored full binary: ${fullBinaryAfter}\n`;
html += `parsed valueAfterSubtract: ${valueAfterSubtract}\n`;
html += `recovered valueBeforeSubtract: ${valueBeforeSubtract}\n`;
html += `recovered 21 bits (via steps 6-7): ${resultObj.originalBits}\n`;
html += '</pre>';
outputEl.innerHTML = html;
}
function renderDecompressResult(decompressObj, originalBits = null) {
const {
disambiguationBit,
coreBinary,
fullBinary,
valueAfterSubtract,
valueBeforeSubtract,
recovered21Bits
} = decompressObj;
let html = '';
html += '<div class="small"><strong>Decompression Steps</strong></div>';
html += '<pre class="mono">';
html += `1) LSB disambiguation bit: ${disambiguationBit}\n`;
html += `2) Core binary (leading 1 stripped): ${coreBinary}\n`;
html += `3) Restored full binary: ${fullBinary}\n`;
html += `4) Parsed valueAfterSubtract: ${valueAfterSubtract}\n`;
html += `5) Recovered valueBeforeSubtract: ${valueBeforeSubtract}\n`;
html += '</pre>';
html += `<div class="result"><strong>Recovered 21-bit input</strong>: ${recovered21Bits}</div>`;
if (originalBits) {
if (originalBits === recovered21Bits) {
html += '<div class="success">✓ Roundtrip successful! Original and recovered bits match.</div>';
} else {
html += '<div class="error">✗ Roundtrip failed! Bits do not match.</div>';
html += `<div class="small">Original: ${originalBits}</div>`;
html += `<div class="small">Recovered: ${recovered21Bits}</div>`;
}
}
decompressOutputEl.innerHTML = html;
}
computeBtn.addEventListener('click', () => {
errorEl.textContent = '';
outputEl.innerHTML = '';
const s = bitsInput.value.trim();
const v = validateBits(s);
if (v) { errorEl.textContent = v; return; }
try {
const result = computeFromBits(s);
renderResult(result);
// Auto-fill the compressed input for easy testing
compressedInput.value = result.compressedWithDisambLSB;
} catch (err) {
errorEl.textContent = 'Computation error';
console.error(err);
}
});
decompressBtn.addEventListener('click', () => {
decompressErrorEl.textContent = '';
decompressOutputEl.innerHTML = '';
const s = compressedInput.value.trim();
const v = validateCompressed(s);
if (v) { decompressErrorEl.textContent = v; return; }
try {
const result = decompressPayload(s);
renderDecompressResult(result);
} catch (err) {
decompressErrorEl.textContent = 'Decompression error: ' + err.message;
console.error(err);
}
});
testRoundtripBtn.addEventListener('click', () => {
errorEl.textContent = '';
outputEl.innerHTML = '';
decompressErrorEl.textContent = '';
decompressOutputEl.innerHTML = '';
// Use current bits input or generate random
let originalBits = bitsInput.value.trim();
if (!originalBits || originalBits.length !== 21) {
originalBits = randomBits21();
bitsInput.value = originalBits;
}
try {
// Compress
const compressResult = computeFromBits(originalBits);
renderResult(compressResult);
// Decompress
const decompressResult = decompressPayload(compressResult.compressedWithDisambLSB);
renderDecompressResult(decompressResult, originalBits);
compressedInput.value = compressResult.compressedWithDisambLSB;
} catch (err) {
errorEl.textContent = 'Roundtrip test error: ' + err.message;
console.error(err);
}
});
exampleBtn.addEventListener('click', () => {
bitsInput.value = '010101001110000111000';
errorEl.textContent = '';
outputEl.innerHTML = '';
});
randomBtn.addEventListener('click', () => {
bitsInput.value = randomBits21();
errorEl.textContent = '';
outputEl.innerHTML = '';
computeBtn.click();
});
copyBtn.addEventListener('click', async () => {
const out = outputEl.innerText || '';
if (!out) { errorEl.textContent = 'Nothing to copy'; return; }
try {
await navigator.clipboard.writeText(out);
errorEl.textContent = 'Result copied to clipboard';
setTimeout(() => { errorEl.textContent = ''; }, 2000);
} catch (e) {
errorEl.textContent = 'Copy failed';
}
});
function randomBits21() {
let s = '';
for (let i = 0; i < 21; i++) {
s += (Math.random() < 0.5 ? '0' : '1');
}
return s;
}
// Run many random tests and show summary
runTestsBtn.addEventListener('click', () => {
errorEl.textContent = '';
outputEl.innerHTML = '';
const count = Math.max(1, Math.min(100000, Number(countInput.value) || 100));
// Track bit-length distribution
const lengthCounts = new Map();
let min = null;
let max = null;
let sum = 0n;
for (let i = 0; i < count; i++) {
const bits = randomBits21();
const res = computeFromBits(bits);
const valAfter = BigInt(res.valueAfterSubtract);
if (min === null || valAfter < min) min = valAfter;
if (max === null || valAfter > max) max = valAfter;
sum += valAfter;
const len = res.totalLengthBits;
lengthCounts.set(len, (lengthCounts.get(len) || 0) + 1);
}
const avg = sum / BigInt(count);
// Sort lengths for display
const sortedLengths = Array.from(lengthCounts.keys()).sort((a, b) => a - b);
let html = '<div class="small"><strong>Random tests summary (using post-subtract binary with leading 1-bit stripped + 1-bit LSB disambiguation)</strong></div>';
html += '<pre>';
html += `Total samples: ${count}\n`;
html += `Min valueAfterSubtract: ${min.toString()}\n`;
html += `Max valueAfterSubtract: ${max.toString()}\n`;
html += `Average valueAfterSubtract: ${avg.toString()}\n`;
html += '\n';
html += '<strong>Payload Length Distribution (bits):</strong>\n';
html += 'Bits Count Percentage\n';
html += '---- --------- ----------\n';
sortedLengths.forEach(len => {
const cnt = lengthCounts.get(len);
const pct = ((cnt / count) * 100).toFixed(2);
const lenStr = String(len).padEnd(4, ' ');
const cntStr = String(cnt).padStart(9, ' ');
const pctStr = String(pct).padStart(6, ' ') + '%';
html += `${lenStr} ${cntStr} ${pctStr}\n`;
});
// Calculate statistics
const minLen = sortedLengths[0];
const maxLen = sortedLengths[sortedLengths.length - 1];
let weightedSum = 0;
sortedLengths.forEach(len => {
weightedSum += len * lengthCounts.get(len);
});
const avgLen = (weightedSum / count).toFixed(2);
html += '\n';
html += `Min payload length: ${minLen} bits\n`;
html += `Max payload length: ${maxLen} bits\n`;
html += `Average payload length: ${avgLen} bits\n`;
html += `Compression ratio: ${avgLen}/21 = ${(avgLen / 21).toFixed(4)} (${((avgLen / 21) * 100).toFixed(2)}%)\n`;
html += '</pre>';
outputEl.innerHTML = html;
});
// allow Enter to compute
bitsInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { computeBtn.click(); }
});
compressedInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { decompressBtn.click(); }
});
</script>
</body>
</html>