0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
The most famous surprise in programming, taken apart switch by switch. Type any number and you see the 64 bits your computer actually stores, the full untruncated decimal those bits really mean (0.1 is secretly 55 digits long), the nearest representable number on either side, and the whole band of decimals that get filed under the same one. Every bit is a clickable switch, and the two step buttons walk you to the next number the format can hold — there is nothing in between.
Start by reading the amber tail on "actually stored" — those are the digits nobody asked for. Then step through the six-panel 0.1 + 0.2 story, where the exact sum needs 54 bits and only 53 are available, and try the 2⁵³ + 1 preset: type an odd number, get an even one back.
Source — the code you see is the code that just ran 1178 lines 53.0 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Why 0.1 + 0.2 ≠ 0.3 — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
Identity: instrument-panel. Machine facts in mono; human words in sans.
Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ---- derived surfaces (theme-paired, same rules as the tokens above) ---- */
:root { --off-face: #FBFBF9; --glow: rgba(192,86,0,.15); --wash: #F1F0EC; --dim: .45; --on-ink: #FFFFFF; --danger-soft: #FBEBEA; --man-on: #1A1D21; --man-ink: #FFFFFF; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --off-face: #181B21; --glow: rgba(255,180,84,.14); --wash: #15181D; --dim: .40; --on-ink: #14161A; --danger-soft: #2C1A19; --man-on: #B4BAC2; --man-ink: #14161A; }
}
:root[data-theme="dark"] { --off-face: #181B21; --glow: rgba(255,180,84,.14); --wash: #15181D; --dim: .40; --on-ink: #14161A; --danger-soft: #2C1A19; --man-on: #B4BAC2; --man-ink: #14161A; }
/* ---- shell ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; overflow-x: hidden; -webkit-text-size-adjust: 100%; }
.wrap { max-width: 1040px; margin: 0 auto; padding: 26px 16px 34px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
h2 { font-size: .95rem; margin: 0; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 70ch; }
.lede b { color: var(--ink); font-weight: 600; }
.mono, code { font-family: var(--font-mono); }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
footer { margin-top: 22px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }
.k-lab {
font-family: var(--font-mono); font-size: .64rem; letter-spacing: .11em;
text-transform: uppercase; color: var(--muted);
}
.panel {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 14px; margin-top: 16px;
}
.panelhead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-bottom: 12px; }
.panelhead .k-lab.right { margin-left: auto; }
.btn {
font-family: var(--font-mono); font-size: .73rem; letter-spacing: .02em;
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
border-radius: 999px; padding: 6px 11px; min-height: 32px; cursor: pointer;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn.ghost { background: transparent; color: var(--muted); }
.btn.ghost:hover { color: var(--accent); }
.btn:disabled { opacity: var(--dim); cursor: not-allowed; }
.btn:disabled:hover { border-color: var(--line); color: var(--ink); }
/* ---- input row ---- */
.inrow { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; }
.infield { flex: 1 1 210px; min-width: 0; }
.infield label { display: block; margin-bottom: 4px; }
.infield input {
width: 100%; font-family: var(--font-mono); font-size: 1rem; color: var(--ink);
background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
padding: 10px 11px; min-height: 44px;
}
.infield input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--glow); }
.infield input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.infield.bad input { border-color: var(--danger); }
.steppers { display: flex; gap: 6px; }
.hint { margin: 8px 0 0; font-family: var(--font-mono); font-size: .68rem; color: var(--muted); }
.hint.bad { color: var(--danger); }
.presets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 11px; }
/* ---- chips + readouts ---- */
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.chip {
font-family: var(--font-mono); font-size: .7rem; border-radius: 999px;
padding: 3px 10px; border: 1px solid var(--line); color: var(--muted); background: var(--bg);
}
.chip.amber { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.chip.good { background: var(--run-soft); border-color: var(--run); color: var(--run); }
.chip.bad { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }
.chip[hidden] { display: none; }
.rows { margin-top: 13px; border-top: 1px dashed var(--line); padding-top: 12px; display: grid; gap: 11px; }
.rw { display: grid; grid-template-columns: 132px minmax(0,1fr); gap: 4px 12px; align-items: baseline; }
.rw[hidden] { display: none; }
.rw > .v { font-family: var(--font-mono); font-size: .86rem; overflow-wrap: anywhere; min-width: 0; }
.rw .sub { display: block; font-family: var(--font-sans); font-size: .78rem; color: var(--muted); margin-top: 2px; }
.exact { display: block; max-height: 8.6em; overflow-y: auto; overflow-x: hidden; }
.exact:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.exact .tail { color: var(--accent); }
.exact .head { color: var(--ink); }
@media (max-width: 620px) { .rw { grid-template-columns: 1fr; } }
/* ---- the 64 switches ---- */
.bank { display: flex; flex-wrap: wrap; gap: 14px 20px; }
.band { min-width: 0; }
.band.sign { flex: 0 0 auto; }
.band.exp { flex: 0 1 340px; }
.band.grow { flex: 1 1 100%; }
.bandhead { display: flex; align-items: baseline; gap: 8px; margin-bottom: 5px; flex-wrap: wrap; }
.bandhead .nm { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; }
.bandhead .rng { font-family: var(--font-mono); font-size: .6rem; color: var(--muted); }
.band.sign .nm { color: var(--danger); }
.band.exp .nm { color: var(--accent); }
.band.man .nm { color: var(--ink); }
.grid { display: grid; gap: 3px; }
.band.sign .grid { grid-template-columns: 38px; }
.band.exp .grid { grid-template-columns: repeat(11, minmax(0,1fr)); width: 100%; }
.band.man .grid { grid-template-columns: repeat(26, minmax(0,1fr)); }
.bit {
height: 28px; padding: 0; cursor: pointer; min-width: 0;
border: 1px solid var(--line); background: var(--off-face); color: var(--muted);
border-radius: 5px; font-family: var(--font-mono); font-size: .72rem; font-weight: 600;
display: grid; place-items: center; line-height: 1;
}
.bit:hover { border-color: var(--accent); }
.bit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; z-index: 2; }
.band.sign .bit[aria-checked="true"] { background: var(--danger); border-color: var(--danger); color: var(--on-ink); }
.band.exp .bit[aria-checked="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-ink); }
.band.man .bit[aria-checked="true"] { background: var(--man-on); border-color: var(--man-on); color: var(--man-ink); }
.inspect {
margin: 12px 0 0; padding: 9px 11px; background: var(--bg); border: 1px solid var(--line);
border-radius: 8px; font-family: var(--font-mono); font-size: .72rem; color: var(--muted);
min-height: 38px; overflow-wrap: anywhere;
}
.inspect b { color: var(--ink); font-weight: 600; }
.inspect .am { color: var(--accent); }
.formula {
margin: 12px 0 0; padding: 11px 12px; background: var(--wash); border: 1px solid var(--line);
border-radius: 8px; font-family: var(--font-mono); font-size: .78rem; color: var(--muted);
overflow-wrap: anywhere; line-height: 1.7;
}
.formula b { color: var(--ink); font-weight: 600; }
.formula .am { color: var(--accent); }
.formula .dg { color: var(--danger); }
.bankfoot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
.pattern {
font-family: var(--font-mono); font-size: .72rem; color: var(--ink);
background: var(--bg); border: 1px solid var(--line); border-radius: 999px; padding: 5px 11px;
}
.pattern span { color: var(--muted); }
/* ---- neighbours rail ---- */
.railwrap { display: grid; grid-template-columns: minmax(0,1fr); gap: 12px 26px; align-items: center; }
@media (min-width: 780px) { .railwrap { grid-template-columns: minmax(0,360px) minmax(0,1fr); } }
.rail { position: relative; height: 208px; margin: 4px 0 2px; }
.rail .stem {
position: absolute; left: 9px; top: 12px; bottom: 12px; width: 2px;
background: var(--line); border-radius: 2px;
}
.rail .zone {
position: absolute; left: 3px; width: 14px; border-radius: 7px;
background: var(--accent-soft); border: 1px dashed var(--accent);
}
.rail .node { position: absolute; left: 0; transform: translateY(-50%); display: flex; align-items: center; gap: 9px; max-width: 100%; }
.rail .dot { width: 22px; height: 10px; border-radius: 3px; background: var(--muted); opacity: .55; flex: none; }
.rail .node.self .dot { background: var(--accent); height: 14px; opacity: 1; }
.rail .lab { font-family: var(--font-mono); font-size: .7rem; color: var(--muted); overflow-wrap: anywhere; min-width: 0; }
.rail .node.self .lab { color: var(--ink); font-size: .78rem; }
.rail .lab em { font-style: normal; display: block; font-family: var(--font-sans); font-size: .66rem; color: var(--muted); }
.rail .mark { position: absolute; left: 2px; right: 0; transform: translateY(-50%); display: flex; align-items: center; gap: 8px; }
.rail .mark.pushright { justify-content: space-between; }
.rail .mark .ln { width: 26px; height: 0; border-top: 2px dashed var(--danger); flex: none; }
.rail .mark .tx { font-family: var(--font-mono); font-size: .66rem; color: var(--danger); }
.nnote { margin: 4px 0 0; font-size: .86rem; color: var(--muted); max-width: 66ch; }
.nnote b { color: var(--ink); font-weight: 600; }
.facts {
list-style: none; margin: 12px 0 0; padding: 12px 0 0; border-top: 1px dashed var(--line);
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px 14px;
}
.facts li { min-width: 0; }
.facts .v { display: block; font-family: var(--font-mono); font-size: .84rem; color: var(--ink); overflow-wrap: anywhere; }
/* ---- story ---- */
.dots { display: flex; gap: 5px; margin-left: auto; }
.dot-b {
width: 26px; height: 26px; padding: 0; border-radius: 999px; cursor: pointer;
border: 1px solid var(--line); background: var(--bg); color: var(--muted);
font-family: var(--font-mono); font-size: .66rem;
}
.dot-b[aria-current="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.dot-b:hover { border-color: var(--accent); }
.dot-b:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.step h3 { margin: 0 0 6px; font-size: 1.02rem; letter-spacing: -.01em; }
.step .say { margin: 0 0 11px; color: var(--muted); font-size: .92rem; max-width: 68ch; }
.step .say b { color: var(--ink); font-weight: 600; }
.slist { display: grid; gap: 9px; margin: 0; padding: 0; }
.sitem {
border-left: 2px solid var(--line); padding: 2px 0 2px 11px;
}
.sitem.hi { border-left-color: var(--accent); }
.sitem.good { border-left-color: var(--run); }
.sitem.bad { border-left-color: var(--danger); }
.sitem .t.code { text-transform: none; letter-spacing: 0; font-size: .72rem; }
.sitem .t { display: block; font-family: var(--font-mono); font-size: .62rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.sitem .m { display: block; font-family: var(--font-mono); font-size: .8rem; color: var(--ink); overflow-wrap: anywhere; }
.sitem.hi .m { color: var(--accent); }
.sitem.good .m { color: var(--run); }
.sitem.bad .m { color: var(--danger); }
.stepfoot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 13px; }
.stepfoot .sp { margin-left: auto; font-family: var(--font-mono); font-size: .66rem; color: var(--muted); }
.lesson {
margin: 16px 0 0; padding: 13px 15px; background: var(--surface);
border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: var(--radius);
font-size: .96rem; line-height: 1.55;
}
.lesson b { font-weight: 600; }
/* ---- motion (opt-in only) ---- */
@media (prefers-reduced-motion: no-preference) {
.btn, .chip, .bit, .dot-b, .infield input, .rail .node, .rail .zone, .rail .mark {
transition: background-color .14s ease, border-color .14s ease, color .14s ease,
box-shadow .14s ease, top .22s cubic-bezier(.3,.8,.35,1), height .14s ease;
}
.bit.flash { animation: ring .5s ease-out; }
@keyframes ring {
0% { box-shadow: 0 0 0 0 var(--glow); } 45% { box-shadow: 0 0 0 8px var(--glow); }
100% { box-shadow: 0 0 0 0 var(--glow); }
}
.step.swap { animation: fade .22s ease-out both; }
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
}
/* ---- narrow ---- */
@media (max-width: 700px) {
.band.man .grid { grid-template-columns: repeat(13, minmax(0,1fr)); }
}
@media (max-width: 560px) {
.wrap { padding: 20px 12px 30px; }
.panel { padding: 12px; }
.bit { height: 26px; font-size: .66rem; }
.band.sign .grid { grid-template-columns: 34px; }
.rail { height: 232px; }
.rail .lab { font-size: .64rem; }
.rail .node.self .lab { font-size: .7rem; }
}
</style>
</head>
<body>
<main class="wrap">
<header>
<h1>Why 0.1 + 0.2 ≠ 0.3</h1>
<p class="lede">Your computer stores every number in <b>64 switches</b>. Most decimals do not fit, so it keeps the nearest one it has. Type a number below — or flip the switches yourself — and watch the tidy decimal you asked for turn into the long one that is actually stored.</p>
</header>
<section class="panel" aria-labelledby="h-num">
<div class="panelhead">
<h2 id="h-num">The number</h2>
<span class="k-lab right" id="klass">normal</span>
</div>
<div class="inrow">
<div class="infield" id="fWrap">
<label class="k-lab" for="numIn">type any number</label>
<input id="numIn" type="text" autocomplete="off" spellcheck="false" autocapitalize="off"
inputmode="decimal" aria-describedby="inHint">
</div>
<div class="steppers" role="group" aria-label="Move to a neighbouring number">
<button class="btn" type="button" id="stepDn" title="the next number down that this format can hold">▼ one step down</button>
<button class="btn" type="button" id="stepUp" title="the next number up that this format can hold">▲ one step up</button>
</div>
</div>
<p class="hint" id="inHint">plain (0.1) · scientific (5e-324) · or a raw pattern (0x3FB999999999999A)</p>
<div class="presets" id="presets" role="group" aria-label="Example numbers"></div>
<div class="chips" id="chips"></div>
<div class="rows">
<div class="rw"><span class="k-lab">prints as</span><span class="v" id="rPrint"></span></div>
<div class="rw">
<span class="k-lab">actually stored</span>
<span class="v mono" id="rExact"></span>
</div>
<div class="rw" id="rwDiff"><span class="k-lab">the gap</span><span class="v" id="rDiff"></span></div>
</div>
<p class="sr-only" id="live" aria-live="polite"></p>
</section>
<section class="panel" aria-labelledby="h-bits">
<div class="panelhead">
<h2 id="h-bits">The 64 switches</h2>
<span class="k-lab right">click any switch to flip it</span>
</div>
<div class="bank" id="bank">
<div class="band sign">
<div class="bandhead"><span class="nm">sign</span><span class="rng">bit 63</span></div>
<div class="grid" id="gSign" role="group" aria-label="Sign bit"></div>
</div>
<div class="band exp">
<div class="bandhead"><span class="nm">exponent</span><span class="rng">bits 62–52 · how big</span></div>
<div class="grid" id="gExp" role="group" aria-label="Eleven exponent bits, from bit 62 down to bit 52"></div>
</div>
<div class="band man grow">
<div class="bandhead"><span class="nm">fraction</span><span class="rng">bits 51–0 · the digits</span></div>
<div class="grid" id="gMan" role="group" aria-label="Fifty-two fraction bits, from bit 51 down to bit 0"></div>
</div>
</div>
<p class="inspect" id="inspect"></p>
<p class="formula" id="formula"></p>
<div class="bankfoot">
<span class="pattern" id="pattern"></span>
<button class="btn ghost" type="button" id="copyBtn">copy pattern</button>
<span class="chip" id="copyNote" hidden></span>
</div>
</section>
<section class="panel" aria-labelledby="h-nb">
<div class="panelhead">
<h2 id="h-nb">Its nearest neighbours</h2>
<span class="k-lab right">nothing exists in between</span>
</div>
<div class="railwrap">
<div class="rail" id="rail"></div>
<p class="nnote" id="railNote"></p>
</div>
<ul class="facts" id="facts"></ul>
</section>
<section class="panel" aria-labelledby="h-story">
<div class="panelhead">
<h2 id="h-story">The 0.1 + 0.2 story</h2>
<div class="dots" id="dots" role="group" aria-label="Jump to a step"></div>
</div>
<div class="step" id="step"></div>
<div class="stepfoot">
<button class="btn" type="button" id="prevBtn">← back</button>
<button class="btn" type="button" id="nextBtn">next →</button>
<button class="btn ghost" type="button" id="loadBtn">load this number into the switches</button>
<span class="sp" id="stepCount"></span>
</div>
<p class="sr-only" id="stepLive" aria-live="polite"></p>
</section>
<p class="lesson"><b>The one-line lesson:</b> binary cannot write 0.1 exactly, the same way decimal cannot write one third exactly — so the machine stores the closest number it owns and quietly carries the difference forward.</p>
<footer>demo · sloprun.dev</footer>
</main>
<script>
(function () {
"use strict";
var $ = function (id) { return document.getElementById(id); };
var dv = new DataView(new ArrayBuffer(8));
var M64 = (1n << 64n) - 1n, SIGN = 1n << 63n, MANT = (1n << 52n) - 1n, HIDDEN = 1n << 52n;
var INFBITS = 0x7FF0000000000000n;
/* ---------- raw bits <-> numbers ---------- */
function bitsOf(x) { dv.setFloat64(0, x); return dv.getBigUint64(0); }
function numOf(u) { dv.setBigUint64(0, u & M64); return dv.getFloat64(0); }
function expField(u) { return Number((u >> 52n) & 0x7FFn); }
function mantOf(u) { return u & MANT; }
function kindOf(u) {
var eb = expField(u), m = mantOf(u);
if (eb === 2047) return m === 0n ? "inf" : "nan";
if (eb === 0) return m === 0n ? "zero" : "sub";
return "norm";
}
function isFiniteBits(u) { var k = kindOf(u); return k !== "inf" && k !== "nan"; }
/* exact value of a finite double as M * 2^e, M signed */
function sigOf(u) {
var neg = (u & SIGN) !== 0n, eb = expField(u), m = mantOf(u), M;
if (eb === 0) { M = m; } else { M = m + HIDDEN; }
return { M: neg ? -M : M, e: eb === 0 ? -1074 : eb - 1075 };
}
/* ---------- exact dyadic arithmetic (no precision lost) ---------- */
function dyAdd(a, b) {
var e = Math.min(a.e, b.e);
return { M: (a.M << BigInt(a.e - e)) + (b.M << BigInt(b.e - e)), e: e };
}
function dySub(a, b) { return dyAdd(a, { M: -b.M, e: b.e }); }
function dyHalf(a) { return { M: a.M, e: a.e - 1 }; }
var p5 = {}, p10 = {};
function pow5(k) { if (!p5[k]) p5[k] = 5n ** BigInt(k); return p5[k]; }
function pow10(k) { if (!p10[k]) p10[k] = 10n ** BigInt(k); return p10[k]; }
/* num / 10^k -> full decimal string, nothing rounded */
function decFromScaled(num, k) {
var neg = num < 0n; if (neg) num = -num;
var t = num.toString();
while (t.length <= k) t = "0" + t;
var ip = t.slice(0, t.length - k);
var fp = k ? t.slice(t.length - k).replace(/0+$/, "") : "";
return (neg ? "-" : "") + ip + (fp ? "." + fp : "");
}
function exactDec(dy) {
if (dy.e >= 0) return decFromScaled(dy.M << BigInt(dy.e), 0);
var k = -dy.e;
return decFromScaled(dy.M * pow5(k), k);
}
function sigDigits(s) {
var t = s.replace(/^-/, "").replace(".", "").replace(/^0+/, "");
return t.length || 1;
}
function clip(s, n) { return s.length > n ? s.slice(0, n) + "…" : s; }
function decExpOf(s) {
if (s.charAt(0) === "-") s = s.slice(1);
var p = s.split("."), ip = p[0], all = ip + (p[1] || ""), i;
for (i = 0; i < all.length; i++) if (all.charAt(i) !== "0") return ip.length - 1 - i;
return null;
}
/* scientific notation straight from the exact digits — never underflows to 0 */
function approx(s, d) {
var dp = d === undefined ? 4 : d;
var neg = s.charAt(0) === "-"; if (neg) s = s.slice(1);
var p = s.split("."), ip = p[0], all = ip + (p[1] || ""), first = -1, i;
for (i = 0; i < all.length; i++) if (all.charAt(i) !== "0") { first = i; break; }
if (first < 0) return "0";
var exp = ip.length - 1 - first;
var digits = all.slice(first).replace(/0+$/, "") || "0";
var keep = dp + 1;
if (digits.length > keep) {
var head = digits.slice(0, keep).split(""), j = keep - 1;
if (digits.charCodeAt(keep) - 48 >= 5) {
while (j >= 0) {
if (head[j] === "9") { head[j] = "0"; j--; }
else { head[j] = String(Number(head[j]) + 1); break; }
}
if (j < 0) { head.unshift("1"); head.pop(); exp++; }
}
digits = head.join("").replace(/0+$/, "") || "0";
}
var mant = digits.charAt(0) + (digits.length > 1 ? "." + digits.slice(1) : "");
return (neg ? "-" : "") + mant + "e" + (exp >= 0 ? "+" : "-") + Math.abs(exp);
}
/* readable at human scales, scientific once the zeros take over */
function fmtAt(s, d) {
var e = decExpOf(s);
if (e === null) return "0";
return (e < -7 || e > 21) ? approx(s, d) : clip(s, d + 20);
}
/* two band edges must never print the same — widen until they differ */
function friendlyPair(a, b) {
for (var d = 6; d <= 26; d += 2) {
var fa = fmtAt(a, d), fb = fmtAt(b, d);
if (fa !== fb) return [fa, fb];
}
return [fmtAt(a, 26), fmtAt(b, 26)];
}
/* a long value inline: keep the real digits while they mean something,
switch to scientific once a clip would show nothing but leading zeros */
function fmtValue(s, n) {
var e = decExpOf(s);
if (e !== null && (e < -7 || e > 21)) return approx(s, 17);
return clip(s, n);
}
/* plain digits while they stay readable, scientific once they do not */
function niceNum(s) {
var e = decExpOf(s);
if (e === null) return "0";
return (e >= -4 && e <= 9) ? clip(s, 24) : approx(s, 4);
}
function commas(n) { return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ","); }
/* ---------- neighbours ---------- */
function stepBits(u, dir) {
if (kindOf(u) === "nan") return u;
var mag = u & ~SIGN, neg = (u & SIGN) !== 0n;
if (mag === 0n) return dir > 0 ? 1n : (SIGN | 1n);
if (!neg) {
if (dir > 0) { return mag === INFBITS ? u : u + 1n; }
return u - 1n;
}
if (dir > 0) return u - 1n;
return mag === INFBITS ? u : u + 1n;
}
/* ---------- decimal literals, kept exact ---------- */
function parseLit(str) {
var m = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(str.trim());
if (!m) return null;
var ip = m[2] || "", fp = m[3] || "";
if (!ip && !fp) return null;
var ex = m[4] ? parseInt(m[4], 10) : 0;
if (!isFinite(ex) || Math.abs(ex) > 20000 || ip.length + fp.length > 20000) return null;
var n = BigInt((ip + fp).replace(/^0+(?=\d)/, "") || "0");
var k = fp.length - ex;
if (k < 0) { n = n * pow10(-k); k = 0; }
if (m[1] === "-") n = -n;
return { n: n, k: k, raw: str.trim() };
}
/* literal minus dyadic, exact, as num / 10^k */
function litMinusDy(L, D) {
var K = Math.max(0, -D.e);
var lhs = L.n << BigInt(K);
var rhs = (D.M << BigInt(D.e + K)) * pow10(L.k);
return { num: (lhs - rhs) * pow5(K), k: L.k + K };
}
/* ---------- state ---------- */
var state = { u: bitsOf(0.1), lit: parseLit("0.1"), focus: 52, step: 0 };
var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var PRESETS = [
{ label: "0.1", lit: "0.1" },
{ label: "0.2", lit: "0.2" },
{ label: "0.3", lit: "0.3" },
{ label: "0.1 + 0.2", val: 0.1 + 0.2 },
{ label: "1/3", val: 1 / 3 },
{ label: "0.5", lit: "0.5" },
{ label: "1", lit: "1" },
{ label: "2⁵³ + 1", lit: "9007199254740993" },
{ label: "1e21", lit: "1e21" },
{ label: "tiniest", val: 5e-324 },
{ label: "biggest", val: 1.7976931348623157e308 },
{ label: "− 0", val: -0 },
{ label: "Infinity", val: Infinity },
{ label: "NaN", val: NaN }
];
/* ---------- build the switch bank ---------- */
var byPos = [];
(function buildBits() {
function make(host, pos, band) {
var b = document.createElement("button");
b.type = "button";
b.className = "bit";
b.setAttribute("role", "switch");
b.setAttribute("aria-checked", "false");
b.setAttribute("tabindex", "-1");
b.dataset.pos = String(pos);
b.dataset.band = band;
b.textContent = "0";
host.appendChild(b);
byPos[pos] = b;
}
make($("gSign"), 63, "sign");
var i;
for (i = 62; i >= 52; i--) make($("gExp"), i, "exp");
for (i = 51; i >= 0; i--) make($("gMan"), i, "man");
})();
function bandName(pos) { return pos === 63 ? "sign" : pos >= 52 ? "exponent" : "fraction"; }
function bitLabel(pos) {
if (pos === 63) return "bit 63, sign";
if (pos >= 52) return "bit " + pos + ", exponent, worth " + Math.pow(2, pos - 52) + " in the exponent field";
return "bit " + pos + ", fraction, worth 2 to the power " + (pos - 52);
}
function setFocusPos(pos) {
state.focus = pos;
for (var i = 0; i < 64; i++) byPos[i].setAttribute("tabindex", i === pos ? "0" : "-1");
}
setFocusPos(52);
function toggle(pos) {
var nu = state.u ^ (1n << BigInt(pos));
var x = numOf(nu);
apply(nu, isFiniteBits(nu) ? parseLit(Object.is(x, -0) ? "-0" : String(x)) : null, false);
if (!reduce) {
var el = byPos[pos];
el.classList.remove("flash");
void el.offsetWidth;
el.classList.add("flash");
}
say("Bit " + pos + " is now " + ((nu >> BigInt(pos)) & 1n) + ". The number is " + describe(x) + ".");
}
$("bank").addEventListener("click", function (e) {
var t = e.target.closest ? e.target.closest(".bit") : null;
if (!t) return;
var pos = Number(t.dataset.pos);
setFocusPos(pos);
toggle(pos);
});
$("bank").addEventListener("keydown", function (e) {
var t = e.target;
if (!t || !t.classList || !t.classList.contains("bit")) return;
var pos = Number(t.dataset.pos), d = 63 - pos, nd = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") nd = d + 1;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") nd = d - 1;
else if (e.key === "Home") nd = 0;
else if (e.key === "End") nd = 63;
if (nd === null) return;
e.preventDefault();
nd = Math.max(0, Math.min(63, nd));
setFocusPos(63 - nd);
byPos[63 - nd].focus();
});
$("bank").addEventListener("focusin", function (e) {
var t = e.target;
if (!t.classList || !t.classList.contains("bit")) return;
setFocusPos(Number(t.dataset.pos));
inspect(Number(t.dataset.pos));
});
$("bank").addEventListener("focusout", function () { inspect(null); });
$("bank").addEventListener("mouseover", function (e) {
var t = e.target.closest ? e.target.closest(".bit") : null;
if (t) inspect(Number(t.dataset.pos));
});
$("bank").addEventListener("mouseleave", function () {
var a = document.activeElement;
if (a && a.classList && a.classList.contains("bit")) inspect(Number(a.dataset.pos));
else inspect(null);
});
function describe(x) {
if (Number.isNaN(x)) return "not a number";
if (x === Infinity) return "infinity";
if (x === -Infinity) return "minus infinity";
if (Object.is(x, -0)) return "negative zero";
return String(x);
}
function inspect(pos) {
var host = $("inspect");
if (pos === null || pos === undefined) {
host.innerHTML = "Point at a switch to see what it is worth. " +
"<b>Sign</b> picks the direction, the <b>exponent</b> picks how big, the <b>fraction</b> holds the digits.";
return;
}
var on = ((state.u >> BigInt(pos)) & 1n) === 1n;
var flipped = numOf(state.u ^ (1n << BigInt(pos)));
var what;
if (pos === 63) what = "sign · on means negative";
else if (pos >= 52) what = "exponent · adds " + commas(Math.pow(2, pos - 52)) + " to the exponent field";
else what = "fraction · worth 2^" + (pos - 52) + " of the leading 1";
host.innerHTML = "<b>bit " + pos + "</b> · " + what + " · currently <b>" + (on ? "1" : "0") +
"</b> · flip it and the number becomes <span class=\"am\">" + esc(describe(flipped)) + "</span>";
}
function esc(s) {
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
/* ---------- announcements ---------- */
var sayTimer = null;
function say(msg) {
if (sayTimer) window.clearTimeout(sayTimer);
sayTimer = window.setTimeout(function () { $("live").textContent = msg; }, 320);
}
/* ---------- the main render ---------- */
function apply(u, lit, fromInput) {
state.u = u & M64;
state.lit = lit || null;
render(fromInput);
}
function render(fromInput) {
var u = state.u, x = numOf(u), kind = kindOf(u), finite = isFiniteBits(u);
var negZero = Object.is(x, -0);
if (!fromInput) $("numIn").value = state.lit ? state.lit.raw : (finite ? (negZero ? "-0" : String(x)) : describeInput(x));
$("fWrap").classList.remove("bad");
/* classification */
var klass = { norm: "normal number", sub: "subnormal · below the normal range",
zero: negZero ? "negative zero" : "zero", inf: x > 0 ? "infinity" : "minus infinity",
nan: "not a number" }[kind];
$("klass").textContent = klass;
/* prints as / stored as */
$("rPrint").innerHTML = "<span>" + esc(negZero ? "0" : String(x)) + "</span>" +
(negZero ? "<span class=\"sub\">JavaScript prints negative zero as plain 0, but the sign switch is on.</span>"
: "<span class=\"sub\">the shortest text that reads back as exactly this number</span>");
var dyad = finite ? sigOf(u) : null;
var exactStr = finite ? (negZero ? "-0" : exactDec(dyad)) : (kind === "inf" ? (x > 0 ? "Infinity" : "-Infinity") : "NaN");
var printStr = negZero ? "-0" : String(x);
/* String(x) may be scientific (1e+21); expand it so the two strings line up digit for digit */
var printFull = printStr;
if (finite && !negZero) {
var PL = parseLit(printStr);
if (PL) printFull = decFromScaled(PL.n, PL.k);
}
var i = 0, lim = Math.min(printFull.length, exactStr.length);
while (i < lim && printFull[i] === exactStr[i]) i++;
var hasTail = finite && i < exactStr.length;
$("rExact").innerHTML = finite
? "<span class=\"exact\"><span class=\"head\">" + esc(exactStr.slice(0, i)) + "</span><span class=\"tail\">" + esc(exactStr.slice(i)) + "</span></span>" +
"<span class=\"sub\">" + commas(sigDigits(exactStr)) + (sigDigits(exactStr) === 1 ? " significant digit" : " significant digits") +
(hasTail ? " — the amber ones are the digits you never asked for" : ", and every one of them is exact") + "</span>"
: "<span class=\"head\">" + esc(exactStr) + "</span><span class=\"sub\">" +
(kind === "inf" ? "the exponent switches are all on and the fraction is empty" :
"the exponent switches are all on and the fraction is not empty") + "</span>";
/* chips */
var chips = [];
chips.push({ t: kind === "norm" ? "normal" : kind === "sub" ? "subnormal" : kind === "zero" ? "zero" : kind === "inf" ? "infinity" : "not a number",
c: kind === "norm" || kind === "zero" ? "" : "amber" });
var exact = null, dstr = null, ddir = 0;
if (finite && state.lit) {
var d = litMinusDy(state.lit, dyad);
exact = d.num === 0n;
dstr = decFromScaled(d.num < 0n ? -d.num : d.num, d.k);
ddir = d.num === 0n ? 0 : (d.num > 0n ? -1 : 1);
chips.push(exact ? { t: "stored exactly", c: "good" } : { t: "rounded to the nearest one it has", c: "amber" });
}
if (kind === "sub") {
var subBits = mantOf(u).toString(2).length;
chips.push({ t: "only " + subBits + (subBits === 1 ? " bit" : " bits") + " of digits left, not 53", c: "amber" });
} else if (finite) {
chips.push({ t: "53 bits of digits ≈ 15.95 decimal digits", c: "" });
}
$("chips").innerHTML = chips.map(function (c) {
return "<span class=\"chip " + c.c + "\">" + esc(c.t) + "</span>";
}).join("");
/* the gap between what you typed and what is kept */
if (finite && state.lit) {
$("rwDiff").hidden = false;
if (exact) {
$("rDiff").innerHTML = "<span style=\"color:var(--run)\">none — this decimal has an exact binary form</span>" +
"<span class=\"sub\">every digit you typed survived the trip</span>";
} else {
$("rDiff").innerHTML = "<span class=\"tail\" style=\"color:var(--accent)\">" + esc(niceNum(dstr)) + "</span>" +
"<span class=\"sub\">what is stored sits that far " + (ddir > 0 ? "above" : "below") +
" the decimal you typed</span>";
}
} else {
$("rwDiff").hidden = true;
}
/* a long stored value scrolls — so it has to be reachable from the keyboard,
and it has to say so, or the clipped last line just looks like a bug */
var exEl = $("rExact").querySelector(".exact");
if (exEl) {
if (exEl.scrollHeight > exEl.clientHeight + 2) {
exEl.setAttribute("tabindex", "0");
exEl.setAttribute("role", "region");
exEl.setAttribute("aria-label", "the full stored value, " + sigDigits(exactStr) + " digits, scrollable");
var subEl = $("rExact").querySelector(".sub");
if (subEl) subEl.textContent += " — scroll the box for the rest";
} else {
exEl.removeAttribute("tabindex"); exEl.removeAttribute("role"); exEl.removeAttribute("aria-label");
}
}
/* the two steppers must never be dead: at the ends of the format there is
genuinely nowhere to go, so say that on the button instead of doing nothing */
var upDead = stepBits(u, 1) === u, dnDead = stepBits(u, -1) === u;
var noWhy = kind === "nan" ? "not a number has no neighbours to step to"
: "there is nothing past infinity";
var wasFocused = document.activeElement;
$("stepUp").disabled = upDead;
$("stepDn").disabled = dnDead;
$("stepUp").title = upDead ? noWhy : "the next number up that this format can hold";
$("stepDn").title = dnDead ? noWhy : "the next number down that this format can hold";
/* keyboard users must not be dropped when the button under them switches off */
if (wasFocused === $("stepUp") && upDead && !dnDead) $("stepDn").focus();
else if (wasFocused === $("stepDn") && dnDead && !upDead) $("stepUp").focus();
renderBits(u);
renderFormula(u, x, kind, dyad, exactStr);
renderNeighbours(u, x, kind, finite, dyad);
$("pattern").innerHTML = "<span>raw pattern</span> 0x" + u.toString(16).toUpperCase().padStart(16, "0");
$("copyNote").hidden = true;
}
function describeInput(x) { return Number.isNaN(x) ? "NaN" : (x > 0 ? "Infinity" : "-Infinity"); }
function renderBits(u) {
for (var pos = 0; pos < 64; pos++) {
var on = ((u >> BigInt(pos)) & 1n) === 1n;
var b = byPos[pos];
if (b.textContent !== (on ? "1" : "0")) b.textContent = on ? "1" : "0";
b.setAttribute("aria-checked", on ? "true" : "false");
b.setAttribute("aria-label", bitLabel(pos));
}
}
function renderFormula(u, x, kind, dyad, exactStr) {
var eb = expField(u), m = mantOf(u), neg = (u & SIGN) !== 0n;
var out;
if (kind === "nan") {
out = "exponent = <span class=\"am\">all 11 switches on</span> and the fraction is not empty<br>" +
"<b>→ not a number</b> — what you get from 0/0 or the square root of a negative";
} else if (kind === "inf") {
out = "exponent = <span class=\"am\">all 11 switches on</span>, fraction = empty<br>" +
"<b>→ " + (neg ? "-" : "") + "infinity</b> — what a number becomes when it grows past the biggest one that fits";
} else if (kind === "zero") {
out = "exponent = <span class=\"am\">0</span>, fraction = <b>0</b><br><b>→ " + (neg ? "-0" : "0") +
"</b> — the sign switch still works, which is why negative zero exists";
} else if (kind === "sub") {
var f = exactDec({ M: m, e: -52 });
out = "(-1)<sup>" + (neg ? 1 : 0) + "</sup> × 2<sup class=\"am\">-1022</sup> × <b>0." +
esc(f.split(".")[1] || "0") + "</b><br>= <b>" + esc(fmtValue(exactStr, 90)) + "</b>" +
"<br><span class=\"am\">subnormal</span> — too small for the usual leading 1, so it trades digits for reach";
} else {
var lead = exactDec({ M: m + HIDDEN, e: -52 });
out = "(-1)<sup>" + (neg ? 1 : 0) + "</sup> × 2<sup class=\"am\">(" + eb + " - 1023)</sup> × <b>" + esc(lead) + "</b>" +
"<br>= (-1)<sup>" + (neg ? 1 : 0) + "</sup> × 2<sup class=\"am\">" + (eb - 1023) + "</sup> × <b>" + esc(lead) + "</b>" +
"<br>= <b>" + esc(fmtValue(exactStr, 120)) + "</b>";
}
$("formula").innerHTML = out;
}
/* ---------- neighbours ---------- */
function powLabel(dy) {
var M = dy.M < 0n ? -dy.M : dy.M;
if (M === 0n) return null;
if ((M & (M - 1n)) !== 0n) return null;
return "2^" + (dy.e + (M.toString(2).length - 1));
}
function renderNeighbours(u, x, kind, finite, dyad) {
var rail = $("rail"), note = $("railNote"), facts = $("facts");
var eb = expField(u), mh = mantOf(u).toString(16).toUpperCase().padStart(13, "0");
var rows = [];
if (!finite) {
rail.innerHTML = "";
rail.style.display = "none";
note.innerHTML = kind === "nan"
? "<b>Not a number</b> has no neighbours — and it is not even equal to itself. Try <code>NaN === NaN</code> and you get <code>false</code>."
: (x > 0
? "<b>Infinity</b> is the end of the line. One step down from here is the biggest number that fits; one step up does not exist."
: "<b>Minus infinity</b> is the end of the line. One step up from here is the most negative number that fits; one step down does not exist.");
rows.push(["exponent field", eb + " · all on"]);
rows.push(["fraction field", "0x" + mh]);
facts.innerHTML = rows.map(factRow).join("");
return;
}
var dnB = stepBits(u, -1), upB = stepBits(u, 1);
var dnFin = isFiniteBits(dnB), upFin = isFiniteBits(upB);
rows.push(["exponent field", kind === "norm" ? eb + " → 2^" + (eb - 1023) : "0 → fixed at 2^-1022"]);
rows.push(["fraction field", "0x" + mh]);
if (!dnFin || !upFin) {
rail.innerHTML = "";
rail.style.display = "none";
var neg = (u & SIGN) !== 0n;
note.innerHTML = neg
? "This is the <b>most negative number the format can hold</b>. One step further down and it becomes minus infinity, so there is no neighbour to draw on that side."
: "This is the <b>largest number the format can hold</b>. One step further up and it becomes infinity, so there is no neighbour to draw on that side.";
facts.innerHTML = rows.map(factRow).join("");
return;
}
rail.style.display = "";
var dDn = sigOf(dnB), dUp = sigOf(upB);
var gapUp = dySub(dUp, dyad), gapDn = dySub(dyad, dDn);
var gUpS = exactDec(gapUp), gDnS = exactDec(gapDn);
var pu = powLabel(gapUp), pd = powLabel(gapDn);
rows.push(["step up from here", niceNum(gUpS) + (pu ? " · " + pu : "")]);
rows.push(["step down from here", niceNum(gDnS) + (pd ? " · " + pd : "")]);
facts.innerHTML = rows.map(factRow).join("");
/* exact positions on the rail */
var K = Math.max(0, -Math.min(dDn.e, dyad.e, dUp.e));
var kL = state.lit ? state.lit.k : 0;
var scale = function (d) { return (d.M << BigInt(d.e + K)) * pow10(kL); };
var lo = scale(dDn), mid = scale(dyad), hi = scale(dUp);
var span = hi - lo;
var tOf = function (v) {
if (span === 0n) return 0.5;
return Number((v - lo) * 1000000n / span) / 1e6;
};
var tMid = tOf(mid);
var zLo = tMid / 2, zHi = (tMid + 1) / 2;
var top = function (t) { return (10 + (1 - t) * 80).toFixed(2) + "%"; };
var midLow = dyHalf(dyAdd(dDn, dyad)), midHigh = dyHalf(dyAdd(dyad, dUp));
var html = "<div class=\"stem\"></div>";
html += "<div class=\"zone\" style=\"top:" + top(zHi) + ";height:" + ((zHi - zLo) * 80).toFixed(2) + "%\"></div>";
html += node(top(1), clip(String(numOf(upB)), 26), "one step up", false);
html += node(top(tMid), clip(Object.is(x, -0) ? "-0" : String(x), 26), "this number", true);
html += node(top(0), clip(String(numOf(dnB)), 26), "one step down", false);
var exactHit = false, tL = null;
if (state.lit) {
var dd = litMinusDy(state.lit, dyad);
exactHit = dd.num === 0n;
tL = tOf(state.lit.n << BigInt(K));
}
/* only draw the "you typed" marker where it will not sit on top of a node label */
var markRoom = tL !== null && Math.abs(tL - tMid) > 0.055 && tL > 0.055 && tL < 0.945;
if (state.lit && !exactHit && markRoom) {
html += "<div class=\"mark\" title=\"your " + esc(clip(state.lit.raw, 18)) + " lands here\" style=\"top:" + top(tL) + "\"><div class=\"ln\"></div>" +
"<div class=\"tx\">your " + esc(clip(state.lit.raw, 18)) + " lands here</div></div>";
}
rail.innerHTML = html;
/* the tick line is always safe; its caption slides right if a node label is
sitting there, and only disappears if even that does not clear it */
var mkWrap = rail.querySelector(".mark"), mk = rail.querySelector(".mark .tx");
if (mk) {
var labs = rail.querySelectorAll(".node .lab");
var clashes = function () {
var mb = mk.getBoundingClientRect(), j;
for (j = 0; j < labs.length; j++) {
var lb = labs[j].getBoundingClientRect();
if (mb.left < lb.right && lb.left < mb.right && mb.top < lb.bottom && lb.top < mb.bottom) return true;
}
return false;
};
if (clashes()) {
mkWrap.classList.add("pushright");
if (clashes()) mk.style.display = "none";
}
}
var edges = friendlyPair(exactDec(midLow), exactDec(midHigh));
var band = "Every decimal from <b>" + esc(edges[0]) + "</b> up to <b>" +
esc(edges[1]) + "</b> is filed under this one number. ";
if (state.lit && exactHit) {
band += "Yours lands right on it — no rounding needed.";
} else if (state.lit && tL !== null && tL >= 0 && tL <= 1) {
band += "Your decimal sits inside that band, so it gets rounded to the tick beside it.";
} else {
band += "Nothing exists in the gaps: these three numbers are next-door neighbours.";
}
note.innerHTML = band;
}
function factRow(r) {
return "<li><span class=\"k-lab\">" + esc(r[0]) + "</span><span class=\"v\">" + esc(r[1]) + "</span></li>";
}
function node(topPct, val, cap, self) {
return "<div class=\"node" + (self ? " self" : "") + "\" style=\"top:" + topPct + "\">" +
"<div class=\"dot\"></div><div class=\"lab\">" + esc(val) + "<em>" + esc(cap) + "</em></div></div>";
}
/* ---------- the 0.1 + 0.2 story, computed live ---------- */
var STEPS = (function () {
var A = 0.1, B = 0.2, C = 0.3, S = A + B;
var dA = sigOf(bitsOf(A)), dB = sigOf(bitsOf(B)), dC = sigOf(bitsOf(C)), dS = sigOf(bitsOf(S));
var exactSum = dyAdd(dA, dB);
var trimmed = exactSum.M;
while (trimmed % 2n === 0n && trimmed !== 0n) trimmed /= 2n;
var width = trimmed.toString(2).length;
var adjacent = stepBits(bitsOf(C), 1) === bitsOf(S);
var tol = Math.abs(A + B - C) < 1e-9;
var scaled = (A * 10 + B * 10) / 10 === C;
var cents = (10 + 20) === 30;
return [
{ title: "Two innocent decimals go in", load: A,
say: "Neither 0.1 nor 0.2 has an exact form in binary — like one third in decimal, the digits never stop. Both get filed under the nearest number the format owns, and both land a hair <b>above</b> what you asked for.",
items: [
["you write", "0.1", ""], ["the machine keeps", exactDec(dA), "hi"],
["you write", "0.2", ""], ["the machine keeps", exactDec(dB), "hi"]
] },
{ title: "Add the two stored values, exactly", load: null,
say: "This next line is the true sum of the two numbers above — no rounding yet. The trouble is its digits need <b>" + width + " bits</b>, and a double only has room for 53.",
items: [
["exact sum of what was stored", exactDec(exactSum), "hi"],
["bits needed for those digits", width + " — one too many", "bad"]
] },
{ title: "Squeeze it back into 53 bits", load: S,
say: "The sum gets rounded to the nearest number that fits, which nudges it <b>up</b> one more time. Now print it and the extra digits finally surface.",
items: [
["nearest number that fits", exactDec(dS), "hi"],
["so it prints as", String(S), "hi"]
] },
{ title: "Meanwhile, plain 0.3", load: C,
say: "Typed on its own, 0.3 rounds <b>down</b> instead. Two different roads, two different numbers — and they land side by side with nothing in between.",
items: [
["0.3 is stored as", exactDec(dC), "hi"],
["distance between them", adjacent ? "one single step — the smallest gap that exists here" : "one step", ""]
] },
{ title: "So the equality check fails", load: S,
say: "Nothing is broken and no bug was introduced. The two values really are different numbers, by the smallest amount this format can express.",
items: [
["0.1 + 0.2 === 0.3", String(A + B === C), "bad", 1],
["0.1 + 0.2", String(S), "", 1],
["0.1 + 0.2 === " + String(S), String(A + B === S), "good", 1]
] },
{ title: "What to do instead", load: null,
say: "Compare with a tolerance, scale into whole units before you divide, or keep money in whole cents and only format it at the very end.",
items: [
["Math.abs(0.1 + 0.2 - 0.3) < 1e-9", String(tol), tol ? "good" : "bad", 1],
["(0.1*10 + 0.2*10) / 10 === 0.3", String(scaled), scaled ? "good" : "bad", 1],
["count in whole cents: 10 + 20 === 30", String(cents), cents ? "good" : "bad", 1]
] }
];
})();
function renderStep() {
var s = STEPS[state.step], host = $("step");
var html = "<h3>" + esc(s.title) + "</h3><p class=\"say\">" + s.say + "</p><div class=\"slist\">";
html += s.items.map(function (it) {
return "<div class=\"sitem " + it[2] + "\"><span class=\"t" + (it[3] ? " code" : "") + "\">" + esc(it[0]) + "</span>" +
"<span class=\"m\">" + esc(it[1]) + "</span></div>";
}).join("");
html += "</div>";
host.innerHTML = html;
if (!reduce) { host.classList.remove("swap"); void host.offsetWidth; host.classList.add("swap"); }
$("prevBtn").disabled = state.step === 0;
$("nextBtn").disabled = state.step === STEPS.length - 1;
$("stepCount").textContent = "step " + (state.step + 1) + " of " + STEPS.length;
var lb = $("loadBtn");
if (s.load === null) { lb.hidden = true; }
else { lb.hidden = false; lb.textContent = "load " + String(s.load) + " into the switches"; }
var ds = $("dots").children;
for (var i = 0; i < ds.length; i++) ds[i].setAttribute("aria-current", i === state.step ? "true" : "false");
$("stepLive").textContent = "Step " + (state.step + 1) + " of " + STEPS.length + ": " + s.title;
}
function goStep(n) {
state.step = Math.max(0, Math.min(STEPS.length - 1, n));
renderStep();
}
/* ---------- reading what the visitor types ---------- */
var HINT = "plain (0.1) · scientific (5e-324) · or a raw pattern (0x3FB999999999999A)";
function fromInputText(raw) {
raw = String(raw).trim();
if (!raw) return null;
if (/^0x[0-9a-fA-F]{1,16}$/.test(raw)) return { u: BigInt(raw) & M64, lit: null };
if (/^[+-]?infinity$/i.test(raw)) return { u: bitsOf(raw.charAt(0) === "-" ? -Infinity : Infinity), lit: null };
if (/^nan$/i.test(raw)) return { u: bitsOf(NaN), lit: null };
var L = parseLit(raw);
if (!L) return null;
var x = Number(raw);
if (!isFinite(x)) return { u: bitsOf(x), lit: null };
return { u: bitsOf(x), lit: L };
}
$("numIn").addEventListener("input", function () {
if (!this.value.trim()) {
$("fWrap").classList.remove("bad");
$("inHint").textContent = HINT;
$("inHint").classList.remove("bad");
return;
}
var r = fromInputText(this.value);
if (!r) {
$("fWrap").classList.add("bad");
$("inHint").textContent = "cannot read that one — try 0.1, 5e-324, or 0x3FB999999999999A";
$("inHint").classList.add("bad");
return;
}
$("inHint").textContent = HINT;
$("inHint").classList.remove("bad");
apply(r.u, r.lit, true);
say("Now showing " + describe(numOf(r.u)) + ".");
});
function setNumber(spec) {
var r = spec.lit !== undefined ? fromInputText(spec.lit) : null;
if (!r) {
var v = spec.val;
r = { u: bitsOf(v), lit: isFinite(v) ? parseLit(Object.is(v, -0) ? "-0" : String(v)) : null };
}
$("inHint").textContent = HINT;
$("inHint").classList.remove("bad");
apply(r.u, r.lit, false);
say(describe(numOf(r.u)) + " loaded.");
}
(function buildPresets() {
var host = $("presets");
PRESETS.forEach(function (p) {
var b = document.createElement("button");
b.type = "button";
b.className = "btn";
b.textContent = p.label;
b.addEventListener("click", function () { setNumber(p); });
host.appendChild(b);
});
})();
$("stepUp").addEventListener("click", function () { nudge(1); });
$("stepDn").addEventListener("click", function () { nudge(-1); });
function nudge(dir) {
var nu = stepBits(state.u, dir);
if (nu === state.u) { say("There is nothing beyond this one."); return; }
var x = numOf(nu);
apply(nu, isFiniteBits(nu) ? parseLit(Object.is(x, -0) ? "-0" : String(x)) : null, false);
say("One step " + (dir > 0 ? "up" : "down") + ": " + describe(x) + ".");
}
/* ---------- story controls ---------- */
(function buildDots() {
var host = $("dots");
STEPS.forEach(function (s, i) {
var b = document.createElement("button");
b.type = "button";
b.className = "dot-b";
b.textContent = String(i + 1);
b.setAttribute("aria-label", "step " + (i + 1) + ": " + s.title);
b.addEventListener("click", function () { goStep(i); });
host.appendChild(b);
});
})();
$("prevBtn").addEventListener("click", function () { goStep(state.step - 1); });
$("nextBtn").addEventListener("click", function () { goStep(state.step + 1); });
$("loadBtn").addEventListener("click", function () {
var v = STEPS[state.step].load;
if (v === null) return;
setNumber({ val: v });
var p = $("numIn");
if (p.scrollIntoView) p.scrollIntoView({ block: "center", behavior: reduce ? "auto" : "smooth" });
});
/* ---------- copy the raw pattern ---------- */
$("copyBtn").addEventListener("click", function () {
var txt = "0x" + state.u.toString(16).toUpperCase().padStart(16, "0");
var note = $("copyNote");
function ok() { note.hidden = false; note.className = "chip good"; note.textContent = "copied " + txt; }
function no() { note.hidden = false; note.className = "chip"; note.textContent = txt + " — select it to copy"; }
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(txt).then(ok, no);
return;
}
} catch (err) { /* fall through */ }
var ta = document.createElement("textarea");
ta.value = txt;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
var done = false;
try { done = document.execCommand("copy"); } catch (err2) { done = false; }
document.body.removeChild(ta);
if (done) ok(); else no();
});
/* ---------- platform theme ---------- */
window.addEventListener("message", function (e) {
var d = e && e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
}
});
/* ---------- boot ---------- */
$("numIn").value = "0.1";
inspect(null);
render(false);
renderStep();
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.