<!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 (with compressed binary, LSB disambiguation)</title>
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; margin: 24px; color:#111; }
h1 { margin-bottom: 8px; }
.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); }
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; }
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 app follows your rule set and subtracts <strong>1024 × 1024</strong> at the end. A single <strong>disambiguation bit</strong> is always appended as the LSB; the UI shows the total length used for the compressed payload (binary of the value before subtract) and trial statistics will use that same total length.</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>
<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');
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 '';
}
// Subtract 1024 * 1024 using BigInt
const SUBTRACT = 1024n * 1024n; // 1048576n
// Keep the last computed total length (compressed payload + 1 disambiguation LSB).
// When set by a manual Compute, trial mode will use this same total length.
window.lastTotalLength = null;
/**
* Compute value from 21-bit string using your rule:
* start = 2 if first bit 0 else 3
* for each subsequent bit: if 0 => value *= 2; if 1 => value = value*2 - 1
*
* Compressed payload = binary of value BEFORE subtract.
* Disambiguation bit is always appended as LSB (value: original first input bit).
*
* Returns trace, numeric results, compressed binary and compressed-with-disamb (LSB).
*/
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 finalBeforeSubtract = value;
const finalAfterSubtract = value - SUBTRACT;
// compressed binary representation is the binary of finalBeforeSubtract
const compressedBinary = finalBeforeSubtract.toString(2);
// deterministic disambiguation bit: original first input bit
const disambiguationBit = String(bits[0]);
// LSB placement (append)
const compressedWithDisambLSB = compressedBinary + disambiguationBit; // disamb as last bit
return {
trace,
finalBeforeSubtract: finalBeforeSubtract.toString(),
finalAfterSubtract: finalAfterSubtract.toString(),
compressedBinary,
disambiguationBit,
compressedWithDisambLSB
};
}
function renderResult(resultObj) {
const {
trace,
finalBeforeSubtract,
finalAfterSubtract,
compressedBinary,
disambiguationBit,
compressedWithDisambLSB
} = resultObj;
// set the global lastTotalLength so trial mode uses the same total length
window.lastTotalLength = compressedWithDisambLSB.length;
let html = '';
html += '<div class="small"><strong>Trace</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>: ${finalBeforeSubtract}</div>`;
html += `<div class="result"><strong>Final output (after subtract)</strong>: ${finalAfterSubtract}</div>`;
html += '<hr />';
html += '<div class="small"><strong>Compressed binary (value before subtract)</strong></div>';
html += `<pre class="mono">${compressedBinary}</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>Compressed with disambiguation (LSB placement)</strong></div>';
html += `<pre class="mono">${compressedWithDisambLSB}</pre>`;
html += `<div style="margin-top:8px;" class="small"><strong>Total length used (compressed payload + 1 disambiguation LSB)</strong></div>`;
html += `<div class="mono"><strong>${window.lastTotalLength}</strong> bits</div>`;
html += '<div style="margin-top:8px;" class="small"><strong>Decompressor recipe (LSB placement)</strong></div>';
html += '<pre class="mono">';
html += '1) Read last bit => disambiguation bit (LSB).\n';
html += '2) Parse remaining bits (all but last) as binary to get valueBeforeSubtract.\n';
html += '3) Compute valueBeforeSubtract - 1,048,576 to get final output.\n';
html += '</pre>';
outputEl.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);
} catch (err) {
errorEl.textContent = 'Computation error';
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';
}
});
// Generate a random 21-bit string
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));
// Determine the total length to use for trial statistics:
// If the user has computed a sample (window.lastTotalLength set), use that.
// Otherwise, compute it from the first random sample and use that for all trials.
let totalLengthUsed = window.lastTotalLength; // may be null
let min = null;
let max = null;
let sum = 0n;
// Counters for how many samples match/shorter/longer than totalLengthUsed
let matches = 0;
let shorter = 0;
let longer = 0;
// If totalLengthUsed is not set, compute it from the first sample and then proceed.
let firstSampleComputed = false;
for (let i = 0; i < count; i++) {
const bits = randomBits21();
const res = computeFromBits(bits);
const val = BigInt(res.finalAfterSubtract);
if (min === null || val < min) min = val;
if (max === null || val > max) max = val;
sum += val;
const len = res.compressedWithDisambLSB.length;
if (!firstSampleComputed && totalLengthUsed === null) {
totalLengthUsed = len;
firstSampleComputed = true;
}
if (len === totalLengthUsed) matches++;
else if (len < totalLengthUsed) shorter++;
else longer++;
}
const avg = sum / BigInt(count);
let html = '<div class="small"><strong>Random tests summary</strong></div>';
html += '<pre>';
html += `Count: ${count}\n`;
html += `Total length used for compressed payload + LSB disambiguation: ${totalLengthUsed} bits\n`;
html += `Min final output: ${min.toString()}\n`;
html += `Max final output: ${max.toString()}\n`;
html += `Average final output: ${avg.toString()}\n`;
html += '\n';
html += `Samples with compressed-with-disamb length equal to ${totalLengthUsed}: ${matches}\n`;
html += `Samples shorter than ${totalLengthUsed}: ${shorter}\n`;
html += `Samples longer than ${totalLengthUsed}: ${longer}\n`;
html += '</pre>';
outputEl.innerHTML = html;
});
// allow Enter to compute
bitsInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { computeBtn.click(); }
});
</script>
</body>
</html>