← back to Rentv 2026
harden(deals): Cody gate — parseAmount handles bare $NM/$NB shorthand (was $150M→150) and skips more per-sf spellings (psf/sqft/per sq ft/per foot); latent gaps (live data uses spelled Mil/Bil) but real, guarded 'per <non-unit>' stays a total
424c6244f84ebf62d64165ae2904507a308d7ba4 · 2026-08-06 08:43:43 -0700 · Steve
Files touched
M scripts/lib/deal-parse.mjsM test/deals/parse.test.mjs
Diff
commit 424c6244f84ebf62d64165ae2904507a308d7ba4
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 08:43:43 2026 -0700
harden(deals): Cody gate — parseAmount handles bare $NM/$NB shorthand (was $150M→150) and skips more per-sf spellings (psf/sqft/per sq ft/per foot); latent gaps (live data uses spelled Mil/Bil) but real, guarded 'per <non-unit>' stays a total
---
scripts/lib/deal-parse.mjs | 9 ++++++---
test/deals/parse.test.mjs | 11 +++++++++++
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/scripts/lib/deal-parse.mjs b/scripts/lib/deal-parse.mjs
index 3e3df113..434077c6 100644
--- a/scripts/lib/deal-parse.mjs
+++ b/scripts/lib/deal-parse.mjs
@@ -11,14 +11,17 @@
// "$928k per unit"), which is a derived metric, not the deal price (grabbing it showed an office campus
// as "$153"). If a title states only a per-X price, the total is unknown → null (honest-empty).
export function parseAmount(text) {
- const re = /\$\s?([\d,]+(?:\.\d+)?)\s?(bil|billion|mil|million|k)?/gi;
+ // Spelled words (mil/billion) tried before the bare single-letter M/B so "$52 Mil" → mil, "$150M" → m.
+ const re = /\$\s?([\d,]+(?:\.\d+)?)\s?(bil|billion|mil|million|[mb]|k)?/gi;
let m;
while ((m = re.exec(text)) !== null) {
const after = text.slice(m.index + m[0].length);
- if (/^\s*(?:\/|per\s+)(?:sf|s\.f\.|square\s*(?:foot|feet)|unit|door|key|bed|pad|acre)\b/i.test(after)) continue;
+ // per-unit / per-sf(/-foot/-door/-key) figures are derived metrics, not the transaction total — skip.
+ // Covers the slash form ("/sf"), the "per X" form ("per unit"/"per sq ft"), and the bare psf/sqft abbrevs.
+ if (/^\s*(?:psf\b|sqft\b|(?:\/|per\s+)(?:sf|s\.f\.|sq\.?\s?ft|square\s*(?:foot|feet)|foot|ft|unit|door|key|bed|pad|acre))/i.test(after)) continue;
let n = parseFloat(m[1].replace(/,/g, ''));
const unit = (m[2] || '').toLowerCase();
- if (/bil/.test(unit)) n *= 1e9; else if (/mil/.test(unit)) n *= 1e6; else if (unit === 'k') n *= 1e3;
+ if (unit === 'b' || /bil/.test(unit)) n *= 1e9; else if (unit === 'm' || /mil/.test(unit)) n *= 1e6; else if (unit === 'k') n *= 1e3;
const label = n >= 1e9 ? `$${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `$${(n / 1e6).toFixed(1)}M` : `$${n.toLocaleString()}`;
return { amount: Math.round(n), amount_label: label };
}
diff --git a/test/deals/parse.test.mjs b/test/deals/parse.test.mjs
index dd3a2cf3..8fcf6b4c 100644
--- a/test/deals/parse.test.mjs
+++ b/test/deals/parse.test.mjs
@@ -87,6 +87,17 @@ test('parseAmount: a per-unit / per-sf figure is NOT the deal total', () => {
// when BOTH a total and a per-X are present, the TOTAL wins
assert.equal(parseAmount('Sold for $50 Mil ($928k per unit)').amount, 50000000);
});
+test('parseAmount: bare $NM / $NB shorthand', () => {
+ assert.equal(parseAmount('$150M deal').amount, 150000000);
+ assert.equal(parseAmount('$2.5B portfolio').amount, 2500000000);
+ assert.equal(parseAmount('$850M tower').amount, 850000000);
+ // "per <non-unit word>" must NOT be skipped as a per-X metric
+ assert.equal(parseAmount('$90M per SEC filing').amount, 90000000);
+});
+test('parseAmount: broadened per-sf spellings (psf / sqft / per sq ft / per foot) are skipped', () => {
+ for (const t of ['$153 psf', '$153 per sq ft', '$153 sqft', '$153 per foot'])
+ assert.equal(parseAmount(t).amount, null, t);
+});
// ── parseSize ─────────────────────────────────────────────────────────────────
test('parseSize: units / sf / acres', () => {
← f1043d95 PR intel: multi-tenant + RBAC auth foundation (template for
·
back to Rentv 2026
·
Grow geo-cache to 442 geocoded cities for /map (94%+ corpus 75001ae6 →