'use strict'; /** * Player/market grouping (P0-3) — the phone audit found the same player listed * as separate flat entries everywhere (Bohm's strikeout ladder = 3 ledger cards, * a leaderboard flooded with 9 identical stolen_bases rows). Props must present * as ONE row per player per market family, with alt lines nested as a ladder, * and no single market may flood a ranked list. * * CommonJS so it's unit-testable by the plain-JS Jest suite AND importable by * the .tsx surfaces (allowJs) — same doctrine as vyndrTokens / playerName. */ const { nameKey } = require('./playerName'); /** A stat's MARKET FAMILY — alt lines of the same market collapse together. * (Side/line are stripped; only the stat type identifies the family.) */ function marketFamily(stat) { return String(stat || '').trim().toLowerCase().replace(/\s+/g, '_'); } function playerMarketKey(player, stat) { return `${nameKey(player)}|${marketFamily(stat)}`; } /** * Dedupe ranked leaderboard rows: ONE row per (player, market family) — keeping * the FIRST (best-ranked) occurrence — then a per-market cap so one prop type * can't flood the board. Input order is preserved (callers pass pre-ranked). * * @param {Array<{player?:string, stat?:string}>} rows * @param {number} [perMarketCap=4] max rows of one market family on the board * @returns {Array} deduped, capped, order-preserving */ function dedupeLeaders(rows, perMarketCap = 4) { const seen = new Set(); const marketCount = new Map(); const out = []; for (const r of Array.isArray(rows) ? rows : []) { const who = r.player || r.player_name; if (!r || !who) continue; const key = playerMarketKey(who, r.stat); if (seen.has(key)) continue; // one row per player+market const fam = marketFamily(r.stat); const n = marketCount.get(fam) || 0; if (perMarketCap > 0 && n >= perMarketCap) continue; // don't let one market flood seen.add(key); marketCount.set(fam, n + 1); out.push(r); } return out; } /** * Group flat prop rows into ONE entry per (player, market family) with the alt * lines nested as a ladder — the Alt Line Ladder shape (the /pricing Desk demo). * `betterOf(a, b)` picks the primary/base line (default: keep the first seen). * Order of first appearance is preserved. * * @param {Array} rows each { player, stat, line, ... } * @param {(a,b)=>any} [betterOf] choose the representative row of a ladder * @returns {Array<{ player, stat, market, primary, ladder }>} */ function groupIntoLadders(rows, betterOf) { const byKey = new Map(); const order = []; for (const r of Array.isArray(rows) ? rows : []) { const who = r.player || r.player_name; if (!r || !who) continue; const key = playerMarketKey(who, r.stat); if (!byKey.has(key)) { byKey.set(key, { player: who, stat: r.stat, market: marketFamily(r.stat), primary: r, ladder: [r] }); order.push(key); } else { const g = byKey.get(key); g.ladder.push(r); if (typeof betterOf === 'function') g.primary = betterOf(g.primary, r); } } // Sort each ladder by line ascending for a stable rungs display. for (const key of order) { const g = byKey.get(key); g.ladder.sort((a, b) => (Number(a.line) || 0) - (Number(b.line) || 0)); } return order.map((k) => byKey.get(k)); } module.exports = { marketFamily, playerMarketKey, dedupeLeaders, groupIntoLadders };