e m p t y

CORE-IS-ALSO-THE-FINAL-WINNER

SAVE

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