Files
vyndr/web/src/lib/oddsFormat.js
T
builtbykev 956a7455eb Session 38: Design system Phase G — living layer, i18n/odds, a11y, paywall, parlay math (1890 tests)
VYNDR 2.0 conversion, Phase G (the systems that make the design alive). All 5
wired. Frontend-only; zero backend changes.

- lib/parlayMath.js: correlation model (0.62/0.34/0.06/0) + parlayGrade penalty
  + grade->odds + combined odds (frontend; backend parlayService unchanged).
- lib/oddsFormat.js: fmtOdds across american/decimal/fractional/implied with the
  totals-pass-through rule (safer than the prototype's parseAm, which would
  mis-convert 228.5) + region presets.
- lib/prefs.js: applyPrefs sets <html data-*> (the S33 a11y CSS layer) + load/save.
- lib/liveTick.js: single tick engine (SSR/test-safe, no auto-start, fresh state).
- lib/checkout.js: checkoutUrl(plan).
- LiveLayer (useLive/LiveNumber/HeartbeatBar) under the Nav ticker; GlobalHosts in
  layout applies prefs + registers __prefs/__goPaywall/__checkout + hosts the
  Preferences and Paywall modals. Nav read-meter is now a paywall trigger.

Gotchas: useEffect can't return a Set.delete unsub directly (boolean != cleanup);
header grew to 124px so layout paddingTop + Slate sticky-top updated to match.

18 new tests. Backend 1872 -> 1890, 146 suites, zero regressions. Web build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:37:31 -04:00

99 lines
3.1 KiB
JavaScript

/* ============================================================
VYNDR 2.0 — odds formatting + region presets (§9).
Plain CommonJS so components import it (allowJs) AND Jest tests it.
CRITICAL (the #1 i18n footgun): fmtOdds only converts MONEYLINES —
bare signed-integer strings (+150 / -110) or integer numbers (the
grade→odds map). Totals/lines/spreads like 228.5, 229, -7.5 PASS
THROUGH UNCHANGED. Apply to moneylines/book odds/parlay legs; NEVER
to O/U totals or stat lines.
This deliberately DEVIATES from the prototype's parseAm, whose regex
accepted decimals (`228.5` would have been mis-converted to a fake
implied % on O/U cells). The prompt's spec is authoritative here.
============================================================ */
const REGIONS = {
US: { label: 'United States', odds: 'american', currency: 'USD' },
UK: { label: 'United Kingdom', odds: 'fractional', currency: 'GBP' },
EU: { label: 'Europe', odds: 'decimal', currency: 'EUR' },
BR: { label: 'Brazil', odds: 'decimal', currency: 'BRL' },
AU: { label: 'Australia', odds: 'decimal', currency: 'AUD' },
};
const CURRENCIES = { USD: '$', GBP: '£', EUR: '€', BRL: 'R$', AUD: 'A$' };
const ODDS_FORMATS = ['american', 'decimal', 'fractional', 'implied'];
function gcd(a, b) {
a = Math.abs(a);
b = Math.abs(b);
while (b) {
[a, b] = [b, a % b];
}
return a || 1;
}
/**
* Return the American-odds integer for a moneyline input, or null if the
* value is NOT a moneyline (and must pass through unchanged).
* - number → only an integer counts (the grade→odds map); 228.5 → null
* - string → only an explicitly SIGNED integer (+150 / -110); "228.5",
* "229", "-7.5" → null
*/
function parseMoneyline(value) {
if (typeof value === 'number') return Number.isInteger(value) ? value : null;
if (typeof value !== 'string') return null;
return /^[+-]\d+$/.test(value.trim()) ? parseInt(value, 10) : null;
}
function americanToDecimal(am) {
return am > 0 ? am / 100 + 1 : 100 / -am + 1;
}
/**
* Format an odds value into the requested style. Non-moneyline inputs are
* returned verbatim (the pass-through guarantee).
*/
function fmtOdds(value, format = 'american') {
const am = parseMoneyline(value);
if (am === null) return value;
if (format === 'american') return (am > 0 ? '+' : '') + Math.round(am);
const dec = americanToDecimal(am);
if (format === 'decimal') return dec.toFixed(2);
if (format === 'implied') return Math.round((1 / dec) * 100) + '%';
// fractional
let n;
let d;
if (am > 0) {
n = Math.round(am);
d = 100;
} else {
n = 100;
d = Math.round(-am);
}
const g = gcd(n, d);
return `${n / g}/${d / g}`;
}
/** Region → { odds, currency, currencySymbol } preset (§9). */
function regionPreset(region) {
const r = REGIONS[region] || REGIONS.US;
return { odds: r.odds, currency: r.currency, currencySymbol: CURRENCIES[r.currency] || '$' };
}
function currencySymbol(code) {
return CURRENCIES[code] || '$';
}
module.exports = {
REGIONS,
CURRENCIES,
ODDS_FORMATS,
parseMoneyline,
americanToDecimal,
fmtOdds,
regionPreset,
currencySymbol,
};