<!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>