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"); }); });