From 20b6b5605217763ce17280fae76a921065df86a1 Mon Sep 17 00:00:00 2001 From: clawedassistant26 <307253840+clawedassistant26@users.noreply.github.com> Date: Sat, 8 Aug 2026 02:25:43 +0000 Subject: [PATCH] fix(ticker): carry a rounded thousand to the next unit in compact() compact() picks its unit from the raw value but rounds the displayed number to 2 decimals afterward, so a value just under a boundary prints a thousand of the smaller unit instead of carrying: 999,999,999 rendered as "1000M" and 999,999,999,999 as "1000B". A market cap a hair under $1B read as "1000M". Round first, then carry: when the scaled value rounds to >= 1000 of the current unit, promote it to the next unit up. Values that legitimately sit below a boundary (999.5M) keep their more precise smaller-unit form. Adds a regression test that renders a report whose fundamentals land just under the million/billion/trillion boundaries. --- src/advisor.mjs | 14 ++++++++++++-- test/advisor.test.mjs | 13 +++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/advisor.mjs b/src/advisor.mjs index a5c1583..7e09f9b 100644 --- a/src/advisor.mjs +++ b/src/advisor.mjs @@ -279,8 +279,18 @@ function compact(v) { const n = Number(v); if (!Number.isFinite(n)) return null; const units = [[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]]; - for (const [size, suffix] of units) { - if (Math.abs(n) >= size) return `${(n / size).toFixed(2).replace(/\.?0+$/, "")}${suffix}`; + for (let i = 0; i < units.length; i++) { + const [size, suffix] = units[i]; + if (Math.abs(n) < size) continue; + // Rounding to 2 decimals can push a value just under the next boundary up to + // a full thousand of this unit (999,999,999 → "1000M"); carry it to the next + // unit up (→ "1B") so the number never reads as an un-carried thousand. + const scaled = (n / size).toFixed(2); + if (Math.abs(Number(scaled)) >= 1000 && i > 0) { + const [upSize, upSuffix] = units[i - 1]; + return `${(n / upSize).toFixed(2).replace(/\.?0+$/, "")}${upSuffix}`; + } + return `${scaled.replace(/\.?0+$/, "")}${suffix}`; } return String(n); } diff --git a/test/advisor.test.mjs b/test/advisor.test.mjs index a710817..413f206 100644 --- a/test/advisor.test.mjs +++ b/test/advisor.test.mjs @@ -185,6 +185,19 @@ test("a report missing its optional sections still renders", () => { assert.match(renderAdvisor("report", bare, { columns: 88 }), /AAA/); }); +test("fundamentals just under a magnitude boundary carry to the next unit", () => { + // 999,999,999 rounds to 1000.00 of a million; it must read "1B", not "1000M". + const near = { + ticker: "AAA", lastPrice: null, disclaimer: "d", + facts: { source: "sec", marketCap: 999999999, revenue: 999999, freeCashFlow: 999999999999 }, + }; + const out = renderAdvisor("report", near, { columns: 88 }); + assert.match(out, /cap 1B\b/); + assert.match(out, /rev 1M\b/); + assert.match(out, /fcf 1T\b/); + assert.doesNotMatch(out, /1000M|1000K|1000B/); +}); + test("empty result sets say so instead of rendering an empty table", () => { assert.match(renderAdvisor("signals", { ticker: "AAA", signals: [] }), /no signals indexed/); assert.match(renderAdvisor("search", { query: "zzz", results: [] }), /nothing indexed matches/);