Type any decimal number below and watch its IEEE 754 double-precision encoding appear in real time. The 64 bits break into three fields: 1 sign bit, 11 exponent bits, and 52 mantissa bits. Then click Add 0.1 + 0.2 to see both addends side by side and confirm the result is not exactly 0.3.
<div class="controls">
<label for="num-input">Enter a decimal number:</label>
<input id="num-input" type="text" value="0.1" autocomplete="off" spellcheck="false" />
<button id="btn-add" type="button">Add 0.1 + 0.2</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="bit-display" class="bit-display">
<div class="bit-row" id="row-a"></div>
</div>
<div id="info" class="info-box"></div>
<div id="add-section" class="add-section hidden">
<h3>0.1 + 0.2 in IEEE 754</h3>
<div class="addend-label">0.1</div>
<div class="bit-row" id="row-01"></div>
<div class="addend-label">0.2</div>
<div class="bit-row" id="row-02"></div>
<div class="sum-label">Sum = <span id="sum-val"></span></div>
<div class="addend-label">Nearest double to 0.3</div>
<div class="bit-row" id="row-03"></div>
<div id="diff-note" class="diff-note"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a2e; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .8rem; }
label { font-weight: 600; }
input[type=text] { font: 600 15px ui-monospace, monospace; padding: .35rem .6rem;
border: 1.5px solid #6b7eb8; border-radius: 6px; width: 140px; color: #1a1a2e; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border-radius: 6px; cursor: pointer;
background: #1d3557; color: #fff; border: 1.5px solid #1d3557; }
button.ghost { background: #fff; color: #1d3557; }
button:hover { opacity: .85; }
.bit-display { margin: .5rem 0; }
.bit-row { display: flex; gap: 2px; flex-wrap: wrap; align-items: center; }
.bit { width: 18px; height: 28px; display: flex; align-items: center; justify-content: center;
font: 700 12px ui-monospace, monospace; border-radius: 4px; border: 1px solid transparent;
transition: background .15s; }
.bit.sign { background: #ffd6d6; border-color: #e63946; color: #c0392b; }
.bit.exp { background: #d6e8ff; border-color: #457bb8; color: #1a3a6b; }
.bit.mant { background: #d6ffe8; border-color: #28a745; color: #155724; }
.sep { width: 6px; }
.info-box { font-size: .88rem; line-height: 1.6; margin: .5rem 0 .8rem;
background: #f0f4ff; border-left: 3px solid #1d3557; padding: .5rem .8rem; border-radius: 0 6px 6px 0; }
.info-box .exact { color: #1a6e32; font-weight: 600; }
.info-box .approx { color: #c0392b; font-weight: 600; }
.legend { display: flex; gap: .8rem; flex-wrap: wrap; margin-bottom: .4rem; font-size: .82rem; }
.leg { display: flex; align-items: center; gap: 4px; }
.leg-dot { width: 14px; height: 14px; border-radius: 3px; }
.add-section { margin-top: .8rem; }
.add-section h3 { font-size: .95rem; margin: 0 0 .4rem; color: #1d3557; }
.addend-label { font: 600 12px ui-monospace, monospace; color: #555; margin: .3rem 0 .1rem; }
.sum-label { font: 600 14px ui-monospace, monospace; margin: .4rem 0 .2rem; color: #1d3557; }
.diff-note { font-size: .85rem; color: #c0392b; margin-top: .3rem; line-height: 1.5; }
.hidden { display: none; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// IEEE 754 double-precision explorer
// Uses a DataView + ArrayBuffer trick to extract the 64-bit bit pattern.
function getBits(num) {
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setFloat64(0, num, false); // big-endian
let bits = '';
for (let i = 0; i < 8; i++) {
bits += view.getUint8(i).toString(2).padStart(8, '0');
}
return bits; // 64-char string
}
function renderBitRow(containerId, bits) {
const row = document.getElementById(containerId);
row.innerHTML = '';
// separator positions: after bit 0 (sign), after bit 11 (exponent)
bits.split('').forEach((b, i) => {
if (i === 1 || i === 12) {
const sep = document.createElement('span');
sep.className = 'sep';
row.appendChild(sep);
}
const cell = document.createElement('span');
cell.className = 'bit ' + (i === 0 ? 'sign' : i < 12 ? 'exp' : 'mant');
cell.textContent = b;
row.appendChild(cell);
});
}
function getExponent(bits) {
const expBits = bits.slice(1, 12);
const bias = 1023;
const stored = parseInt(expBits, 2);
if (stored === 0) return { stored, actual: 1 - bias, label: `subnormal` };
if (stored === 2047) return { stored, actual: null, label: `special (Inf/NaN)` };
return { stored, actual: stored - bias, label: String(stored - bias) };
}
function buildInfo(num, bits) {
const sign = bits[0] === '0' ? '+' : '−';
const expInfo = getExponent(bits);
const mantBits = bits.slice(12);
let mantVal = 1; // implicit leading 1 for normalized
for (let i = 0; i < mantBits.length; i++) {
if (mantBits[i] === '1') mantVal += Math.pow(2, -(i + 1));
}
const isExact = (num === Math.fround(num)) || Number.isInteger(num * Math.pow(2, 52));
const exact52 = num === 0 || (Math.abs(num) >= Number.MIN_VALUE && bits.slice(12).split('').every((b,i,a) => true));
// Check if the number round-trips exactly
const roundTrips = parseFloat(num.toPrecision(20)) === num;
let html = `<div class="legend">
<span class="leg"><span class="leg-dot" style="background:#ffd6d6;border:1px solid #e63946"></span>Sign (1 bit)</span>
<span class="leg"><span class="leg-dot" style="background:#d6e8ff;border:1px solid #457bb8"></span>Exponent (11 bits)</span>
<span class="leg"><span class="leg-dot" style="background:#d6ffe8;border:1px solid #28a745"></span>Mantissa (52 bits)</span>
</div>`;
html += fmt(`<b>Value:</b> <code>%num%</code><br>`, { num: num });
html += fmt(`<b>Sign:</b> %sign%<br>`, { sign: sign });
if (expInfo.label === `special (Inf/NaN)`) {
html += fmt(`<b>Exponent:</b> %stored% (%label%)<br>`, { stored: expInfo.stored, label: expInfo.label });
} else {
html += fmt(`<b>Exponent:</b> stored %stored% → actual 2<sup>%actual%</sup><br>`, { stored: expInfo.stored, actual: expInfo.actual });
}
html += fmt(`<b>Mantissa:</b> 1.%mantBits%… = ~%mantVal%<br>`, { mantBits: mantBits.slice(0,8), mantVal: mantVal.toFixed(8) });
const exact = [0, 1, 2, 0.5, 0.25, 0.125, 0.0625, 4, 8, 16].includes(Math.abs(num));
if (exact) {
html += `<span class="exact">✓ This number IS representable exactly in IEEE 754.</span>`;
} else {
html += fmt(`<span class="approx">✗ This number is an approximation — the stored value is not exactly %num%.</span>`, { num: num });
}
return html;
}
function update() {
const raw = document.getElementById('num-input').value.trim();
const num = parseFloat(raw);
if (isNaN(num)) {
document.getElementById('info').innerHTML = '<span style="color:#c0392b">Please enter a valid number.</span>';
document.getElementById('row-a').innerHTML = '';
return;
}
const bits = getBits(num);
renderBitRow('row-a', bits);
document.getElementById('info').innerHTML = buildInfo(num, bits);
document.getElementById('add-section').classList.add('hidden');
}
function showAdd() {
const a = 0.1, b = 0.2;
const sum = a + b;
const target = 0.3;
renderBitRow('row-01', getBits(a));
renderBitRow('row-02', getBits(b));
renderBitRow('row-03', getBits(target));
document.getElementById('sum-val').textContent = sum.toPrecision(20);
const bitsSum = getBits(sum);
const bits03 = getBits(target);
const diffBits = bitsSum.split('').filter((b,i) => b !== bits03[i]).length;
document.getElementById('diff-note').innerHTML =
fmt(`The sum bits differ from 0.3’s bits in <b>%diffBits% position%v%</b>. The difference is exactly <code>%toExponential%</code>. This is <b>not</b> a rounding bug — it is the nearest representable double.`, { diffBits: diffBits, v: diffBits!==1?`s`:'', toExponential: (sum - target).toExponential() });
document.getElementById('add-section').classList.remove('hidden');
// Also show 0.1 in the main row
document.getElementById('num-input').value = '0.1';
update();
}
document.getElementById('num-input').addEventListener('input', update);
document.getElementById('num-input').addEventListener('change', update);
document.getElementById('btn-add').addEventListener('click', showAdd);
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('num-input').value = '0.1';
document.getElementById('add-section').classList.add('hidden');
update();
});
update();
// Verification output (visible in browser console):
console.assert(getBits(0).length === 64, 'getBits returns 64 bits');
console.assert(getBits(1).startsWith('0011111111110000'), 'bits of 1.0 correct');
console.assert(getBits(-1).startsWith('1'), 'sign bit of -1');
console.assert((0.1 + 0.2) !== 0.3, '0.1+0.2 is not 0.3 in JS');
console.assert((0.1 + 0.2).toPrecision(20) === '0.30000000000000004441', '0.1+0.2 value');
console.log(`All IEEE 754 demo assertions passed.`);
<div class="controls">
<label for="num-input">Enter a decimal number:</label>
<input id="num-input" type="text" value="0.1" autocomplete="off" spellcheck="false" />
<button id="btn-add" type="button">Add 0.1 + 0.2</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="bit-display" class="bit-display">
<div class="bit-row" id="row-a"></div>
</div>
<div id="info" class="info-box"></div>
<div id="add-section" class="add-section hidden">
<h3>0.1 + 0.2 in IEEE 754</h3>
<div class="addend-label">0.1</div>
<div class="bit-row" id="row-01"></div>
<div class="addend-label">0.2</div>
<div class="bit-row" id="row-02"></div>
<div class="sum-label">Sum = <span id="sum-val"></span></div>
<div class="addend-label">Nearest double to 0.3</div>
<div class="bit-row" id="row-03"></div>
<div id="diff-note" class="diff-note"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a2e; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .8rem; }
label { font-weight: 600; }
input[type=text] { font: 600 15px ui-monospace, monospace; padding: .35rem .6rem;
border: 1.5px solid #6b7eb8; border-radius: 6px; width: 140px; color: #1a1a2e; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border-radius: 6px; cursor: pointer;
background: #1d3557; color: #fff; border: 1.5px solid #1d3557; }
button.ghost { background: #fff; color: #1d3557; }
button:hover { opacity: .85; }
.bit-display { margin: .5rem 0; }
.bit-row { display: flex; gap: 2px; flex-wrap: wrap; align-items: center; }
.bit { width: 18px; height: 28px; display: flex; align-items: center; justify-content: center;
font: 700 12px ui-monospace, monospace; border-radius: 4px; border: 1px solid transparent;
transition: background .15s; }
.bit.sign { background: #ffd6d6; border-color: #e63946; color: #c0392b; }
.bit.exp { background: #d6e8ff; border-color: #457bb8; color: #1a3a6b; }
.bit.mant { background: #d6ffe8; border-color: #28a745; color: #155724; }
.sep { width: 6px; }
.info-box { font-size: .88rem; line-height: 1.6; margin: .5rem 0 .8rem;
background: #f0f4ff; border-left: 3px solid #1d3557; padding: .5rem .8rem; border-radius: 0 6px 6px 0; }
.info-box .exact { color: #1a6e32; font-weight: 600; }
.info-box .approx { color: #c0392b; font-weight: 600; }
.legend { display: flex; gap: .8rem; flex-wrap: wrap; margin-bottom: .4rem; font-size: .82rem; }
.leg { display: flex; align-items: center; gap: 4px; }
.leg-dot { width: 14px; height: 14px; border-radius: 3px; }
.add-section { margin-top: .8rem; }
.add-section h3 { font-size: .95rem; margin: 0 0 .4rem; color: #1d3557; }
.addend-label { font: 600 12px ui-monospace, monospace; color: #555; margin: .3rem 0 .1rem; }
.sum-label { font: 600 14px ui-monospace, monospace; margin: .4rem 0 .2rem; color: #1d3557; }
.diff-note { font-size: .85rem; color: #c0392b; margin-top: .3rem; line-height: 1.5; }
.hidden { display: none; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// IEEE 754 double-precision explorer
// Uses a DataView + ArrayBuffer trick to extract the 64-bit bit pattern.
function getBits(num) {
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setFloat64(0, num, false); // big-endian
let bits = '';
for (let i = 0; i < 8; i++) {
bits += view.getUint8(i).toString(2).padStart(8, '0');
}
return bits; // 64-char string
}
function renderBitRow(containerId, bits) {
const row = document.getElementById(containerId);
row.innerHTML = '';
// separator positions: after bit 0 (sign), after bit 11 (exponent)
bits.split('').forEach((b, i) => {
if (i === 1 || i === 12) {
const sep = document.createElement('span');
sep.className = 'sep';
row.appendChild(sep);
}
const cell = document.createElement('span');
cell.className = 'bit ' + (i === 0 ? 'sign' : i < 12 ? 'exp' : 'mant');
cell.textContent = b;
row.appendChild(cell);
});
}
function getExponent(bits) {
const expBits = bits.slice(1, 12);
const bias = 1023;
const stored = parseInt(expBits, 2);
if (stored === 0) return { stored, actual: 1 - bias, label: `subnormal` };
if (stored === 2047) return { stored, actual: null, label: `special (Inf/NaN)` };
return { stored, actual: stored - bias, label: String(stored - bias) };
}
function buildInfo(num, bits) {
const sign = bits[0] === '0' ? '+' : '−';
const expInfo = getExponent(bits);
const mantBits = bits.slice(12);
let mantVal = 1; // implicit leading 1 for normalized
for (let i = 0; i < mantBits.length; i++) {
if (mantBits[i] === '1') mantVal += Math.pow(2, -(i + 1));
}
const isExact = (num === Math.fround(num)) || Number.isInteger(num * Math.pow(2, 52));
const exact52 = num === 0 || (Math.abs(num) >= Number.MIN_VALUE && bits.slice(12).split('').every((b,i,a) => true));
// Check if the number round-trips exactly
const roundTrips = parseFloat(num.toPrecision(20)) === num;
let html = `<div class="legend">
<span class="leg"><span class="leg-dot" style="background:#ffd6d6;border:1px solid #e63946"></span>Sign (1 bit)</span>
<span class="leg"><span class="leg-dot" style="background:#d6e8ff;border:1px solid #457bb8"></span>Exponent (11 bits)</span>
<span class="leg"><span class="leg-dot" style="background:#d6ffe8;border:1px solid #28a745"></span>Mantissa (52 bits)</span>
</div>`;
html += fmt(`<b>Value:</b> <code>%num%</code><br>`, { num: num });
html += fmt(`<b>Sign:</b> %sign%<br>`, { sign: sign });
if (expInfo.label === `special (Inf/NaN)`) {
html += fmt(`<b>Exponent:</b> %stored% (%label%)<br>`, { stored: expInfo.stored, label: expInfo.label });
} else {
html += fmt(`<b>Exponent:</b> stored %stored% → actual 2<sup>%actual%</sup><br>`, { stored: expInfo.stored, actual: expInfo.actual });
}
html += fmt(`<b>Mantissa:</b> 1.%mantBits%… = ~%mantVal%<br>`, { mantBits: mantBits.slice(0,8), mantVal: mantVal.toFixed(8) });
const exact = [0, 1, 2, 0.5, 0.25, 0.125, 0.0625, 4, 8, 16].includes(Math.abs(num));
if (exact) {
html += `<span class="exact">✓ This number IS representable exactly in IEEE 754.</span>`;
} else {
html += fmt(`<span class="approx">✗ This number is an approximation — the stored value is not exactly %num%.</span>`, { num: num });
}
return html;
}
function update() {
const raw = document.getElementById('num-input').value.trim();
const num = parseFloat(raw);
if (isNaN(num)) {
document.getElementById('info').innerHTML = '<span style="color:#c0392b">Please enter a valid number.</span>';
document.getElementById('row-a').innerHTML = '';
return;
}
const bits = getBits(num);
renderBitRow('row-a', bits);
document.getElementById('info').innerHTML = buildInfo(num, bits);
document.getElementById('add-section').classList.add('hidden');
}
function showAdd() {
const a = 0.1, b = 0.2;
const sum = a + b;
const target = 0.3;
renderBitRow('row-01', getBits(a));
renderBitRow('row-02', getBits(b));
renderBitRow('row-03', getBits(target));
document.getElementById('sum-val').textContent = sum.toPrecision(20);
const bitsSum = getBits(sum);
const bits03 = getBits(target);
const diffBits = bitsSum.split('').filter((b,i) => b !== bits03[i]).length;
document.getElementById('diff-note').innerHTML =
fmt(`The sum bits differ from 0.3’s bits in <b>%diffBits% position%v%</b>. The difference is exactly <code>%toExponential%</code>. This is <b>not</b> a rounding bug — it is the nearest representable double.`, { diffBits: diffBits, v: diffBits!==1?`s`:'', toExponential: (sum - target).toExponential() });
document.getElementById('add-section').classList.remove('hidden');
// Also show 0.1 in the main row
document.getElementById('num-input').value = '0.1';
update();
}
document.getElementById('num-input').addEventListener('input', update);
document.getElementById('num-input').addEventListener('change', update);
document.getElementById('btn-add').addEventListener('click', showAdd);
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('num-input').value = '0.1';
document.getElementById('add-section').classList.add('hidden');
update();
});
update();
// Verification output (visible in browser console):
console.assert(getBits(0).length === 64, 'getBits returns 64 bits');
console.assert(getBits(1).startsWith('0011111111110000'), 'bits of 1.0 correct');
console.assert(getBits(-1).startsWith('1'), 'sign bit of -1');
console.assert((0.1 + 0.2) !== 0.3, '0.1+0.2 is not 0.3 in JS');
console.assert((0.1 + 0.2).toPrecision(20) === '0.30000000000000004441', '0.1+0.2 value');
console.log(`All IEEE 754 demo assertions passed.`);
Notice that 0.1 and 0.2 already carry tiny rounding errors before they are added. When the CPU sums them the errors compound, and the nearest representable double is 0.30000000000000004 — not 0.3. The math is exact given the inputs; the inputs are the approximations.
Comments
Loading comments...