diff --git a/web/src/run.js b/web/src/run.js index f1bdf2d..527035a 100644 --- a/web/src/run.js +++ b/web/src/run.js @@ -645,44 +645,18 @@ function formatProbeMs(v) { return `${goFixed(v, 0)} ms`; } -// goFixed is strconv.FormatFloat(v, 'f', digits, 64) for a finite v: the -// exact binary value rounded to `digits` decimals, ties to even, which -// toFixed (ties away) does not match. The sign is kept: formatProbeMs can -// name a negative intercept if a fit ever produces one. -function goFixed(v, digits) { - const buf = new ArrayBuffer(8); - const f64 = new Float64Array(buf); - const u64 = new BigUint64Array(buf); - f64[0] = v; - const bits = u64[0]; - const neg = (bits >> 63n) === 1n; - let exp = Number((bits >> 52n) & 0x7ffn); - let mant = bits & 0xfffffffffffffn; - if (exp === 0x7ff) return neg ? "-?" : "?"; - if (exp === 0) { - exp = 1 - 1023 - 52; - } else { - mant |= 1n << 52n; - exp = exp - 1023 - 52; +// goFixed is strconv.FormatFloat(v, 'f', digits, 64). toFixed already rounds +// the exact binary value; the one difference is an exact tie, which toFixed +// takes away from zero and Go takes to even. A tie is a multiple of +// 2^-(digits+1), and that test is exact (a power-of-two scale). +export function goFixed(v, digits) { + const m = 10 ** digits; + const x = v * m; + if (Number.isInteger(v * 2 ** (digits + 1)) && !Number.isInteger(x)) { + const f = Math.floor(x); + return ((f % 2 === 0 ? f : f + 1) / m).toFixed(digits); } - // scaled = round(mant * 2^exp * 10^digits), half to even. - const num = mant * 10n ** BigInt(digits); - let scaled; - if (exp >= 0) { - scaled = num << BigInt(exp); - } else { - const denom = 1n << BigInt(-exp); - const q = num / denom; - const r = num % denom; - const half = denom >> 1n; - scaled = r > half || (r === half && (q & 1n) === 1n) ? q + 1n : q; - } - let s = scaled.toString(); - if (digits > 0) { - s = s.padStart(digits + 1, "0"); - s = `${s.slice(0, -digits)}.${s.slice(-digits)}`; - } - return neg ? `-${s}` : s; + return v.toFixed(digits); } // memoryRow is the card's MEMORY lines that the index carries, in one cell: diff --git a/web/test/gofixed.test.mjs b/web/test/gofixed.test.mjs new file mode 100644 index 0000000..1b8d8a5 --- /dev/null +++ b/web/test/gofixed.test.mjs @@ -0,0 +1,25 @@ +// goFixed prints what Go's strconv.FormatFloat(v, 'f', d, 64) prints, so the +// page's figures read the same as the card's: an exact tie goes to even. +// +// Run: node --test test/*.test.mjs (from web/) + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { goFixed } from "../src/run.js"; + +test("an exact tie rounds to even, as Go does", () => { + assert.equal(goFixed(0.25, 1), "0.2"); + assert.equal(goFixed(0.75, 1), "0.8"); + assert.equal(goFixed(2.5, 0), "2"); + assert.equal(goFixed(3.5, 0), "4"); + assert.equal(goFixed(-0.25, 1), "-0.2"); +}); + +test("a value that only looks like a tie rounds by its exact value", () => { + // 0.15 and 1.45 are stored just under the tie (0.1499…, 1.4499…): Go prints 0.1 and 1.4. + assert.equal(goFixed(0.15, 1), "0.1"); + assert.equal(goFixed(1.45, 1), "1.4"); + assert.equal(goFixed(11164188672 / 2 ** 30, 1), "10.4"); + assert.equal(goFixed(141.918, 0), "142"); +});