Codex commited on
Commit
f72fb5e
·
1 Parent(s): 9d4cb90

Normalize matchup team aliases

Browse files
Files changed (2) hide show
  1. src/matchups.js +64 -2
  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) => !options.team || normalizeTeam(row.team) === normalizeTeam(options.team))
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) => !options.team || normalizeTeam(row.team) === normalizeTeam(options.team));
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' } },