The CSV export carries the hands, the standings and the settle-up. It carried neither of the two places a dealer types context. Each seat can hold custom fields -- "IOU: $20 to Ann", "Seat rent: paid to 2am", "Venmo: @ann" -- and the table has a notes box for the rest: side bets, a high-hand promo, who owes the box from last week. Both are typed precisely because they matter to settling up, and both were visible only inside the tool. The one place the context was written down was the one place it could not leave. Now the standings rows end with a Notes column carrying that seat's fields, and the table's notes go in a section of their own at the end. Player,Paid In,Won,Net,Bought In,Chips Now,Up/Down,Notes Ann,20.00,60.00,40.00,200.00,150.00,-50.00,Seat rent: paid to 2am; Venmo: @ann Ben,20.00,0.00,-20.00,200.00,200.00,0.00,IOU: $20 to Ann Cal,20.00,0.00,-20.00,200.00,250.00,50.00, Table Notes "High-hand promo until midnight. Mo owes the box $15, from last week." Notes go last so the money columns keep the shape a spreadsheet expects, and the multi-line note is quoted as one cell rather than becoming rows that read as more players. An existing test caught this, correctly "every money cell parses as a number" asserted that every column after the name was numeric, and a Notes column is text by definition. The invariant it protects is real -- the file exists so somebody can total a column -- so it now names the six money columns and checks those, rather than "everything after the first". That is the assertion it always meant. 9 tests (278 total): a seat's fields ride along, the money columns keep their order, a seat with nothing written gets an empty cell rather than the word undefined, a blank field does not leave a stray colon, a multi-line note survives as one cell, no notes means no section rather than an empty heading, and asking for the standings does not change the seats. Driven through the real Export CSV button, not just the function: the downloaded file carries both seats' notes and the table's. Audit and contrast clean, no console errors. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
142 lines
6.1 KiB
JavaScript
142 lines
6.1 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: the money columns have to be numeric. Named rather than
|
|
// "everything after the first", which is what this used to say — the row now
|
|
// ends with a Notes column carrying whatever the dealer typed against the seat,
|
|
// and that is text by definition. The point of this test is that a column
|
|
// somebody totals adds up, so it checks the columns somebody totals.
|
|
const standingsStart = lines.findIndex(l => l.startsWith("Player,"));
|
|
const header = splitRow(lines[standingsStart]);
|
|
const moneyColumns = ["Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down"]
|
|
.map(name => {
|
|
const index = header.indexOf(name);
|
|
assert.ok(index > 0, `the standings header should still have a ${name} column`);
|
|
return index;
|
|
});
|
|
|
|
lines.slice(standingsStart + 1).filter(Boolean).forEach(line => {
|
|
const cells = splitRow(line);
|
|
moneyColumns.forEach(index => {
|
|
const cell = cells[index];
|
|
assert.ok(cell !== "" && !Number.isNaN(Number(cell)),
|
|
`standings cell "${cell}" in column ${header[index]} 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");
|
|
});
|
|
});
|