Files
JoshHeaps.Net/tests/holdem/money.test.js
T
jheapsandClaude Opus 5 db208a1a86 Make big numbers readable, and make the export add up
Seeded a table that looks like a real game — "Mike from work", "Jean-Philippe",
stacks from $12 to $3,200,000 — and measured what the layout does with it.

".00" was costing a third of a seat puck

Every amount rendered with two forced decimal places. Measured, the trailing
".00" is ~22px, against the 64px a seat puck has to show a stack in. So
"$1,250,000.00" needed 102px of a 64px box — and because a number has no spaces
to break on, it neither wrapped nor ellipsised. It drew straight out over the
seat next to it.

Money now shows cents only when there are cents. A chip game deals in whole
numbers nearly all the time, and "$12" says exactly what "$12.00" said in half
the width. A quarter-blind game is untouched: $0.25, $12.50 and $0.75 pots all
still read as before, checked against a 25c/50c table.

That alone took "$1,250,000.00" down to "$1,250,000" — 102px to 80px, still
wider than the 64px puck. The remaining overflow is handled by stepping the font
down on long amounts, which keeps every digit. Abbreviating to "$1.25M" would
have fit more comfortably and was the obvious move, but a dealer reading a
rounded stack off the felt is a worse failure than a small one, and the exact
figure is the entire point of the tool. Measured after: every stack now paints
inside its own puck, nothing spills onto a neighbour.

Because Money.format is the single formatter all 47 call sites go through, this
lands on the seat pucks, action bar, pot breakdown, seat menu, standings and
history at once — the payoff from consolidating it earlier.

The CSV export couldn't be added up

The file exists so a house manager or a disputed chip count has something that
outlives the tab, and every money column in it was written as text:
"$1,250,000.00". The commas were quoted correctly so the file always parsed —
it just imported as strings, so the one thing anyone opens it to do, total a
column, needed every cell cleaned first.

Money columns now go in as bare numbers (1250000.00) with no symbol and no
separators. The columns meant for humans stay human: names are still names,
payouts still read "Alexandra: $1,250,000", and a name with a comma in it is
still quoted.

12 new tests (88 total, all passing), including the case that would be ruined by
dropping cents unconditionally, and a proper quote-aware CSV parser to check
every money cell parses as a number.

Re-ran the occlusion sweep from the previous round at 9 seats, 10 seats and
tournament stacks: still clean, no regression from the font changes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 03:54:03 -06:00

127 lines
5.3 KiB
JavaScript

const { test, describe } = require("node:test");
const assert = require("node:assert/strict");
const { Money, HandHistoryExport } = require("./load");
describe("Money.format", () => {
test("whole amounts drop the pointless cents", () => {
assert.equal(Money.format(12), "$12");
assert.equal(Money.format(450), "$450");
assert.equal(Money.format(1250000), "$1,250,000");
assert.equal(Money.format(0), "$0");
});
test("amounts that carry cents still show them", () => {
assert.equal(Money.format(12.5), "$12.50");
assert.equal(Money.format(0.25), "$0.25");
assert.equal(Money.format(3.01), "$3.01");
});
test("a quarter-blind game reads exactly as it did", () => {
// The case that would be ruined by dropping cents unconditionally.
[0.25, 0.5, 1.75, 12.05].forEach(v => {
assert.match(Money.format(v), /\.\d\d$/, `${v} lost its cents`);
});
});
test("floating-point dust doesn't invent cents", () => {
assert.equal(Money.format(0.1 + 0.2), "$0.30");
assert.equal(Money.format(1.0000000001), "$1");
});
test("signed amounts put the sign out front, both scales", () => {
assert.equal(Money.formatSigned(87500), "+$87,500");
assert.equal(Money.formatSigned(-87500), "-$87,500");
assert.equal(Money.formatSigned(-12.5), "-$12.50");
assert.equal(Money.formatSigned(0), "$0");
});
test("rubbish in doesn't produce rubbish out", () => {
assert.equal(Money.format(null), "$0");
assert.equal(Money.format(undefined), "$0");
assert.equal(Money.format("nonsense"), "$0");
});
});
describe("Money.forSheet", () => {
test("a bare number a spreadsheet will add up", () => {
assert.equal(Money.forSheet(1250000), "1250000.00");
assert.equal(Money.forSheet(12.5), "12.50");
assert.equal(Money.forSheet(-87500), "-87500.00");
});
test("no currency symbol and no thousands separators", () => {
const written = Money.forSheet(1234567.89);
assert.ok(!written.includes("$"), "a $ makes the cell text");
assert.ok(!written.includes(","), "a comma makes the cell text");
assert.equal(Number(written), 1234567.89, "and it parses straight back to a number");
});
});
describe("the exported CSV", () => {
const history = [
{ handNumber: 2, timestamp: 0, pot: 1250000, payouts: [{ id: "a", label: "Alexandra", amount: 1250000 }] },
{ handNumber: 1, timestamp: 0, pot: 24.5, payouts: [{ id: "b", label: "Bob", amount: 24.5 }] },
];
const standings = [
{ id: "a", name: "Alexandra", in: 1250000, won: 1250000, net: 0, boughtIn: 3200000, chipsNow: 3200000, cashPosition: 0 },
{ id: "b", name: "Bob", in: 24.5, won: 0, net: -24.5, boughtIn: 100, chipsNow: 75.5, cashPosition: -24.5 },
];
// Splitting on commas isn't good enough: the timestamp column legitimately holds
// one ("1/1/1970, 5:00:00 PM") and is quoted for exactly that reason.
const splitRow = line => {
const fields = [];
let field = "";
let quoted = false;
for (let i = 0; i < line.length; i++) {
const c = line[i];
if (quoted) {
if (c === '"' && line[i + 1] === '"') { field += '"'; i++; }
else if (c === '"') quoted = false;
else field += c;
} else if (c === '"') quoted = true;
else if (c === ",") { fields.push(field); field = ""; }
else field += c;
}
fields.push(field);
return fields;
};
test("every money cell parses as a number", () => {
const csv = HandHistoryExport.toCsv(history, standings);
const lines = csv.split("\r\n");
lines.slice(1, 3).forEach(line => {
const cell = splitRow(line)[2]; // Pot
assert.ok(cell !== "" && !Number.isNaN(Number(cell)), `pot cell "${cell}" isn't a number`);
});
// Standings rows: every column after the name has to be numeric.
const standingsStart = lines.findIndex(l => l.startsWith("Player,"));
lines.slice(standingsStart + 1).filter(Boolean).forEach(line => {
splitRow(line).slice(1).forEach(cell => {
assert.ok(cell !== "" && !Number.isNaN(Number(cell)), `standings cell "${cell}" isn't a number`);
});
});
});
test("a million-dollar pot doesn't need quoting any more", () => {
const csv = HandHistoryExport.toCsv(history, []);
assert.ok(csv.includes("1250000.00"), "written plainly");
assert.ok(!csv.includes('"$1,250,000'), "not as a quoted, unsummable string");
});
test("names and payout descriptions stay human", () => {
const csv = HandHistoryExport.toCsv(history, standings);
assert.ok(csv.includes("Alexandra"), "the name column is still a name");
assert.ok(csv.includes("Alexandra: $1,250,000"), "the payout column still reads like a sentence");
});
test("a name with a comma is still quoted properly", () => {
const csv = HandHistoryExport.toCsv([], [
{ id: "x", name: "Smith, John", in: 0, won: 0, net: 0, boughtIn: 0, chipsNow: 0, cashPosition: 0 },
]);
assert.ok(csv.includes('"Smith, John"'), "the CSV escaping still applies where it's needed");
});
});