Codex commited on
Commit ·
f72fb5e
1
Parent(s): 9d4cb90
Normalize matchup team aliases
Browse files- src/matchups.js +64 -2
- test/matchups.test.js +56 -0
src/matchups.js
CHANGED
|
@@ -8,6 +8,38 @@ const DEFAULT_WEIGHTED_MODE = 'weighted';
|
|
| 8 |
const DEFAULT_FALLBACK_DAYS = 7;
|
| 9 |
const COCKROACH_RETRY_CODES = new Set(['40001']);
|
| 10 |
const PROFILE_CANDIDATE_LIMIT = 5;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
const HITTER_COLUMNS = [
|
| 13 |
'game_pk',
|
|
@@ -234,6 +266,36 @@ function normalizeTeam(value) {
|
|
| 234 |
return String(value ?? '').trim().toUpperCase();
|
| 235 |
}
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
function numberOrNull(value) {
|
| 238 |
const numeric = Number(value);
|
| 239 |
return Number.isFinite(numeric) ? numeric : null;
|
|
@@ -493,7 +555,7 @@ export class HostedArtifactSource {
|
|
| 493 |
const filteredRows = hitterRows
|
| 494 |
.filter(keepRowForDefaults)
|
| 495 |
.map((row) => withDefaults(row, slateLookup))
|
| 496 |
-
.filter((row) =>
|
| 497 |
.filter((row) => {
|
| 498 |
const playerId = row.batter ?? row.player_id;
|
| 499 |
return !exclusionSet.has(String(playerId ?? ''));
|
|
@@ -522,7 +584,7 @@ export class HostedArtifactSource {
|
|
| 522 |
const filteredRows = pitcherRows
|
| 523 |
.filter(keepRowForDefaults)
|
| 524 |
.map((row) => withDefaults(row, slateLookup))
|
| 525 |
-
.filter((row) =>
|
| 526 |
|
| 527 |
return {
|
| 528 |
source: 'hosted',
|
|
|
|
| 8 |
const DEFAULT_FALLBACK_DAYS = 7;
|
| 9 |
const COCKROACH_RETRY_CODES = new Set(['40001']);
|
| 10 |
const PROFILE_CANDIDATE_LIMIT = 5;
|
| 11 |
+
const MLB_TEAM_ALIASES = new Map([
|
| 12 |
+
['ARI', ['ari', 'arizona', 'diamondbacks', 'arizona diamondbacks']],
|
| 13 |
+
['ATL', ['atl', 'atlanta', 'braves', 'atlanta braves']],
|
| 14 |
+
['BAL', ['bal', 'baltimore', 'orioles', 'baltimore orioles']],
|
| 15 |
+
['BOS', ['bos', 'boston', 'red sox', 'boston red sox']],
|
| 16 |
+
['CHC', ['chc', 'chicago cubs', 'cubs']],
|
| 17 |
+
['CWS', ['cws', 'chw', 'chicago white sox', 'white sox']],
|
| 18 |
+
['CIN', ['cin', 'cincinnati', 'reds', 'cincinnati reds']],
|
| 19 |
+
['CLE', ['cle', 'cleveland', 'guardians', 'cleveland guardians']],
|
| 20 |
+
['COL', ['col', 'colorado', 'rockies', 'colorado rockies']],
|
| 21 |
+
['DET', ['det', 'detroit', 'tigers', 'detroit tigers']],
|
| 22 |
+
['HOU', ['hou', 'houston', 'astros', 'houston astros']],
|
| 23 |
+
['KC', ['kc', 'kcr', 'kansas city', 'royals', 'kansas city royals']],
|
| 24 |
+
['LAA', ['laa', 'los angeles angels', 'angels', 'anaheim angels']],
|
| 25 |
+
['LAD', ['lad', 'los angeles dodgers', 'dodgers']],
|
| 26 |
+
['MIA', ['mia', 'miami', 'marlins', 'miami marlins', 'florida marlins']],
|
| 27 |
+
['MIL', ['mil', 'milwaukee', 'brewers', 'milwaukee brewers']],
|
| 28 |
+
['MIN', ['min', 'minnesota', 'twins', 'minnesota twins']],
|
| 29 |
+
['NYM', ['nym', 'mets', 'new york mets']],
|
| 30 |
+
['NYY', ['nyy', 'yankees', 'new york yankees']],
|
| 31 |
+
['ATH', ['ath', 'athletics', 'a\'s', 'as', 'oakland', 'oakland athletics']],
|
| 32 |
+
['PHI', ['phi', 'philadelphia', 'phillies', 'philadelphia phillies']],
|
| 33 |
+
['PIT', ['pit', 'pittsburgh', 'pirates', 'pittsburgh pirates']],
|
| 34 |
+
['SD', ['sd', 'sdp', 'san diego', 'padres', 'san diego padres']],
|
| 35 |
+
['SEA', ['sea', 'seattle', 'mariners', 'seattle mariners']],
|
| 36 |
+
['SF', ['sf', 'sfg', 'san francisco', 'giants', 'san francisco giants']],
|
| 37 |
+
['STL', ['stl', 'cardinals', 'st louis', 'st. louis', 'st louis cardinals', 'st. louis cardinals']],
|
| 38 |
+
['TB', ['tb', 'tbr', 'tampa bay', 'rays', 'tampa bay rays']],
|
| 39 |
+
['TEX', ['tex', 'texas', 'rangers', 'texas rangers']],
|
| 40 |
+
['TOR', ['tor', 'toronto', 'blue jays', 'toronto blue jays']],
|
| 41 |
+
['WSH', ['wsh', 'was', 'washington', 'nationals', 'washington nationals']],
|
| 42 |
+
]);
|
| 43 |
|
| 44 |
const HITTER_COLUMNS = [
|
| 45 |
'game_pk',
|
|
|
|
| 266 |
return String(value ?? '').trim().toUpperCase();
|
| 267 |
}
|
| 268 |
|
| 269 |
+
function resolveTeamAliases(value) {
|
| 270 |
+
const normalized = normalizeText(value);
|
| 271 |
+
if (!normalized) {
|
| 272 |
+
return [];
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
const matches = [];
|
| 276 |
+
for (const [canonical, aliases] of MLB_TEAM_ALIASES.entries()) {
|
| 277 |
+
if (aliases.includes(normalized) || canonical.toLowerCase() === normalized) {
|
| 278 |
+
matches.push(canonical);
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
if (matches.length > 0) {
|
| 283 |
+
return matches;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
return [normalizeTeam(value)];
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
function rowMatchesTeamFilter(rowTeam, teamFilter) {
|
| 290 |
+
if (!teamFilter) {
|
| 291 |
+
return true;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
const candidates = resolveTeamAliases(teamFilter);
|
| 295 |
+
const normalizedRowTeam = normalizeTeam(rowTeam);
|
| 296 |
+
return candidates.includes(normalizedRowTeam);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
function numberOrNull(value) {
|
| 300 |
const numeric = Number(value);
|
| 301 |
return Number.isFinite(numeric) ? numeric : null;
|
|
|
|
| 555 |
const filteredRows = hitterRows
|
| 556 |
.filter(keepRowForDefaults)
|
| 557 |
.map((row) => withDefaults(row, slateLookup))
|
| 558 |
+
.filter((row) => rowMatchesTeamFilter(row.team, options.team))
|
| 559 |
.filter((row) => {
|
| 560 |
const playerId = row.batter ?? row.player_id;
|
| 561 |
return !exclusionSet.has(String(playerId ?? ''));
|
|
|
|
| 584 |
const filteredRows = pitcherRows
|
| 585 |
.filter(keepRowForDefaults)
|
| 586 |
.map((row) => withDefaults(row, slateLookup))
|
| 587 |
+
.filter((row) => rowMatchesTeamFilter(row.team, options.team));
|
| 588 |
|
| 589 |
return {
|
| 590 |
source: 'hosted',
|
test/matchups.test.js
CHANGED
|
@@ -65,6 +65,62 @@ test('hosted artifact source resolves latest available daily slate and caches pa
|
|
| 65 |
assert.equal(calls.filter((url) => url.includes('daily_hitter_metrics.parquet')).length, 1);
|
| 66 |
});
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
test('matchup service falls back to Cockroach when hosted source fails', async () => {
|
| 69 |
const service = new MatchupService(
|
| 70 |
{ databaseUrl: 'postgresql://test', hosted: { baseUrl: 'https://example.test' } },
|
|
|
|
| 65 |
assert.equal(calls.filter((url) => url.includes('daily_hitter_metrics.parquet')).length, 1);
|
| 66 |
});
|
| 67 |
|
| 68 |
+
test('hosted artifact source matches common team aliases against team abbreviations', async () => {
|
| 69 |
+
const responses = new Map([
|
| 70 |
+
['https://example.test/daily/2026-04-07/slate.parquet', [
|
| 71 |
+
{
|
| 72 |
+
game_pk: 20,
|
| 73 |
+
away_team: 'ATH',
|
| 74 |
+
home_team: 'NYY',
|
| 75 |
+
away_probable_pitcher: 'JP Sears',
|
| 76 |
+
home_probable_pitcher: 'Carlos Rodon',
|
| 77 |
+
away_probable_hand: 'L',
|
| 78 |
+
home_probable_hand: 'L',
|
| 79 |
+
},
|
| 80 |
+
]],
|
| 81 |
+
['https://example.test/daily/2026-04-07/daily_hitter_metrics.parquet', [
|
| 82 |
+
{
|
| 83 |
+
team: 'NYY',
|
| 84 |
+
hitter_name: 'Aaron Judge',
|
| 85 |
+
batter: 99,
|
| 86 |
+
split_key: 'overall',
|
| 87 |
+
recent_window: 'season',
|
| 88 |
+
weighted_mode: 'weighted',
|
| 89 |
+
matchup_score: 88.6,
|
| 90 |
+
ceiling_score: 91.1,
|
| 91 |
+
zone_fit_score: 74.2,
|
| 92 |
+
likely_starter_score: 99.9,
|
| 93 |
+
xwoba: 0.455,
|
| 94 |
+
hard_hit_pct: 61.0,
|
| 95 |
+
},
|
| 96 |
+
]],
|
| 97 |
+
['https://example.test/daily/2026-04-07/hitter_pitcher_exclusions.parquet', []],
|
| 98 |
+
]);
|
| 99 |
+
|
| 100 |
+
const source = new HostedArtifactSource(
|
| 101 |
+
{
|
| 102 |
+
baseUrl: 'https://example.test',
|
| 103 |
+
cacheTtlMs: 60_000,
|
| 104 |
+
fallbackDays: 0,
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
readParquetImpl: async (url) => {
|
| 108 |
+
if (!responses.has(url)) {
|
| 109 |
+
throw new Error(`Missing ${url}`);
|
| 110 |
+
}
|
| 111 |
+
return responses.get(url);
|
| 112 |
+
},
|
| 113 |
+
logger: { debug() {}, warn() {} },
|
| 114 |
+
}
|
| 115 |
+
);
|
| 116 |
+
|
| 117 |
+
const result = await source.getBestMatchups({ date: '2026-04-07', team: 'Yankees' });
|
| 118 |
+
|
| 119 |
+
assert.equal(result.rows.length, 1);
|
| 120 |
+
assert.equal(result.rows[0].team, 'NYY');
|
| 121 |
+
assert.equal(result.rows[0].hitter_name, 'Aaron Judge');
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
test('matchup service falls back to Cockroach when hosted source fails', async () => {
|
| 125 |
const service = new MatchupService(
|
| 126 |
{ databaseUrl: 'postgresql://test', hosted: { baseUrl: 'https://example.test' } },
|