diff --git a/web/src/gofixed.js b/web/src/gofixed.js new file mode 100644 index 0000000..40f752c --- /dev/null +++ b/web/src/gofixed.js @@ -0,0 +1,16 @@ +// 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). +// +// It lives here so the page chrome and the run route can both print with it. +// page.js must not import run.js: run.js imports the page. +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); + } + return v.toFixed(digits); +} diff --git a/web/src/page.js b/web/src/page.js index 987891a..7841e07 100644 --- a/web/src/page.js +++ b/web/src/page.js @@ -1,3 +1,5 @@ +import { goFixed } from "./gofixed.js"; + // The chrome every page shares. // // One stylesheet, inline, because the pages are small and a published run @@ -222,5 +224,7 @@ export function esc(s) { // `?`; never print a default you did not observe). export function fmt(n) { if (!n) return "?"; - return n >= 100 ? n.toFixed(0) : n.toFixed(1); + // The card's formatRate. goFixed is the tie rule; toFixed would print the + // other way on an exact half. + return n >= 100 ? goFixed(n, 0) : goFixed(n, 1); } diff --git a/web/src/run.js b/web/src/run.js index 6f8708e..c9e5e6d 100644 --- a/web/src/run.js +++ b/web/src/run.js @@ -10,6 +10,7 @@ // a door — the id is the secret (§9.3). import { avatarPath } from "./author.js"; +import { goFixed } from "./gofixed.js"; import { fail, json } from "./http.js"; import { esc, fmt, head, layout } from "./page.js"; @@ -659,20 +660,6 @@ function formatProbeMs(v) { return `${goFixed(v, 0)} ms`; } -// 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); - } - return v.toFixed(digits); -} - // memoryRow is the card's MEMORY lines that the index carries, in one cell: // "GPU0 10.4/12.0 GiB · host RSS 11.6 GiB (file 11.1 / anon 0.2)". Each // gpu_mem entry is "GPUn used/total GiB", or "GPUn used GiB" when the diff --git a/web/test/gofixed.test.mjs b/web/test/gofixed.test.mjs index 1b8d8a5..6f8bbc2 100644 --- a/web/test/gofixed.test.mjs +++ b/web/test/gofixed.test.mjs @@ -6,7 +6,8 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { goFixed } from "../src/run.js"; +import { fmt } from "../src/page.js"; +import { goFixed } from "../src/gofixed.js"; test("an exact tie rounds to even, as Go does", () => { assert.equal(goFixed(0.25, 1), "0.2"); @@ -16,6 +17,13 @@ test("an exact tie rounds to even, as Go does", () => { assert.equal(goFixed(-0.25, 1), "-0.2"); }); +test("fmt uses that tie rule for the figures the page shares with the card", () => { + assert.equal(fmt(0.25), "0.2"); + assert.equal(fmt(0.75), "0.8"); + assert.equal(fmt(150.5), "150"); + assert.equal(fmt(0), "?"); +}); + 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");