Skip to content

Commit 46aabd3

Browse files
committed
optional year for all pitcher/batter commands
1 parent 266c345 commit 46aabd3

7 files changed

+96
-63
lines changed

commands/batter.js

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ module.exports = {
99
option.setName('player')
1010
.setDescription('An active player\'s name.')
1111
.setRequired(false))
12+
.addIntegerOption(option =>
13+
option.setName('year')
14+
.setDescription('Which season?')
15+
.setRequired(false)
16+
.setMinValue(new Date().getFullYear() - 10)
17+
.setMaxValue(new Date().getFullYear()))
1218
.addStringOption(option =>
1319
option.setName('stat_type')
1420
.setDescription('Regular Season (default), Postseason, or Spring Training?')

commands/batter_savant.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ module.exports = {
88
.addStringOption(option =>
99
option.setName('player')
1010
.setDescription('An active player\'s name.')
11-
.setRequired(false)),
11+
.setRequired(false))
12+
.addIntegerOption(option =>
13+
option.setName('year')
14+
.setDescription('Which season?')
15+
.setRequired(false)
16+
.setMinValue(new Date().getFullYear() - 10)
17+
.setMaxValue(new Date().getFullYear())),
1218
async execute (interaction) {
1319
try {
1420
await interactionHandlers.batterSavantHandler(interaction);

commands/pitcher.js

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ module.exports = {
99
option.setName('player')
1010
.setDescription('An active player\'s name.')
1111
.setRequired(false))
12+
.addIntegerOption(option =>
13+
option.setName('year')
14+
.setDescription('Which season?')
15+
.setRequired(false)
16+
.setMinValue(new Date().getFullYear() - 10)
17+
.setMaxValue(new Date().getFullYear()))
1218
.addStringOption(option =>
1319
option.setName('stat_type')
1420
.setDescription('Regular Season (default), Postseason, or Spring Training?')

commands/pitcher_savant.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ module.exports = {
88
.addStringOption(option =>
99
option.setName('player')
1010
.setDescription('An active player\'s name.')
11-
.setRequired(false)),
11+
.setRequired(false))
12+
.addIntegerOption(option =>
13+
option.setName('year')
14+
.setDescription('Which season?')
15+
.setRequired(false)
16+
.setMinValue(new Date().getFullYear() - 10)
17+
.setMaxValue(new Date().getFullYear())),
1218
async execute (interaction) {
1319
try {
1420
await interactionHandlers.pitcherSavantHandler(interaction);

modules/MLB-API-util.js

+15-15
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ const endpoints = {
1212
LOGGER.debug('https://statsapi.mlb.com/api/v1/schedule?hydrate=lineups&sportId=1&gamePk=' + gamePk + '&teamId=' + teamId);
1313
return 'https://statsapi.mlb.com/api/v1/schedule?hydrate=lineups&sportId=1&gamePk=' + gamePk + '&teamId=' + teamId;
1414
},
15-
hitter: (personId, statType) => {
16-
LOGGER.debug(`https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,statSplits,lastXGames],group=hitting,gameType=${statType},sitCodes=[vl,vr,risp],limit=7)`);
17-
return `https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,statSplits,lastXGames],group=hitting,gameType=${statType},sitCodes=[vl,vr,risp],limit=7)`;
15+
hitter: (personId, statType, season) => {
16+
LOGGER.debug(`https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,statSplits,lastXGames],group=hitting,gameType=${statType},sitCodes=[vl,vr,risp],limit=7,season=${season})`);
17+
return `https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,statSplits,lastXGames],group=hitting,gameType=${statType},sitCodes=[vl,vr,risp],limit=7,season=${season})`;
1818
},
19-
pitcher: (personId, lastXGamesLimit, statType) => {
20-
LOGGER.debug(`https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,lastXGames,sabermetrics,seasonAdvanced,expectedStatistics],groups=pitching,limit=${lastXGamesLimit},gameType=${statType})`);
21-
return `https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,lastXGames,sabermetrics,seasonAdvanced,expectedStatistics],groups=pitching,limit=${lastXGamesLimit},gameType=${statType})`;
19+
pitcher: (personId, lastXGamesLimit, statType, season) => {
20+
LOGGER.debug(`https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,lastXGames,sabermetrics,seasonAdvanced,expectedStatistics],groups=pitching,limit=${lastXGamesLimit},gameType=${statType},season=${season})`);
21+
return `https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,lastXGames,sabermetrics,seasonAdvanced,expectedStatistics],groups=pitching,limit=${lastXGamesLimit},gameType=${statType},season=${season})`;
2222
},
2323
liveFeed: (gamePk, fields = []) => {
2424
LOGGER.debug('https://statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live' + (fields.length > 0 ? '?fields=' + fields.join() : ''));
@@ -70,11 +70,11 @@ const endpoints = {
7070
LOGGER.debug('https://ws.statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live?fields=gameData,players,boxscoreName');
7171
return 'https://ws.statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live?fields=gameData,players,boxscoreName';
7272
},
73-
savantPitchData: (personId) => {
73+
savantPitchData: (personId, season) => {
7474
LOGGER.debug('https://baseballsavant.mlb.com/player-services/statcast-pitches-breakdown?playerId=' + personId +
75-
'&position=1&hand=&pitchBreakdown=pitches&timeFrame=yearly&pitchType=&count=&updatePitches=true&gameType=RP');
75+
`&position=1&hand=&pitchBreakdown=pitches&timeFrame=yearly&pitchType=&count=&updatePitches=true&gameType=RP&season=${season}`);
7676
return 'https://baseballsavant.mlb.com/player-services/statcast-pitches-breakdown?playerId=' + personId +
77-
'&position=1&hand=&pitchBreakdown=pitches&timeFrame=yearly&pitchType=&count=&updatePitches=true&gameType=RP';
77+
`&position=1&hand=&pitchBreakdown=pitches&timeFrame=yearly&pitchType=&count=&updatePitches=true&gameType=RP&season=${season}`;
7878
},
7979
savantPage: (personId, type) => {
8080
LOGGER.debug(`https://baseballsavant.mlb.com/savant-player/${personId}?stats=statcast-r-${type}-mlb`);
@@ -208,9 +208,9 @@ module.exports = {
208208
linescore: async (gamePk) => {
209209
return (await fetch(endpoints.linescore(gamePk))).json();
210210
},
211-
savantPitchData: async (personId) => {
211+
savantPitchData: async (personId, season) => {
212212
try {
213-
return (await fetch(endpoints.savantPitchData(personId),
213+
return (await fetch(endpoints.savantPitchData(personId, season),
214214
{
215215
signal: AbortSignal.timeout(5000)
216216
}
@@ -269,14 +269,14 @@ module.exports = {
269269
return {};
270270
}
271271
},
272-
hitter: async (personId, statType) => {
273-
return (await fetch(endpoints.hitter(personId, statType))).json();
272+
hitter: async (personId, statType, season) => {
273+
return (await fetch(endpoints.hitter(personId, statType, season))).json();
274274
},
275275
team: async (teamId) => {
276276
return (await fetch(endpoints.team(teamId))).json();
277277
},
278-
pitcher: async (personId, lastXGamesLimit, statType) => {
279-
return (await fetch(endpoints.pitcher(personId, lastXGamesLimit, statType))).json();
278+
pitcher: async (personId, lastXGamesLimit, statType, season) => {
279+
return (await fetch(endpoints.pitcher(personId, lastXGamesLimit, statType, season))).json();
280280
},
281281
players: async () => {
282282
return (await fetch(endpoints.players())).json();

modules/command-util.js

+35-30
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module.exports = {
4646
table.removeBorder();
4747
return await getScreenshotOfHTMLTables([table]);
4848
},
49-
hydrateProbable: async (probable, statType) => {
49+
hydrateProbable: async (probable, statType, season = (new Date().getFullYear())) => {
5050
const [spot, savant, people] = await Promise.all([
5151
new Promise((resolve, reject) => {
5252
if (probable) {
@@ -59,10 +59,10 @@ module.exports = {
5959
}
6060
reject(new Error('There was a problem getting the player spot.'));
6161
}),
62-
mlbAPIUtil.savantPitchData(probable),
62+
mlbAPIUtil.savantPitchData(probable, season),
6363
new Promise((resolve, reject) => {
6464
if (probable) {
65-
resolve(mlbAPIUtil.pitcher(probable, 3, statType));
65+
resolve(mlbAPIUtil.pitcher(probable, 3, statType, season));
6666
} else {
6767
resolve(undefined);
6868
}
@@ -79,7 +79,7 @@ module.exports = {
7979
};
8080
},
8181

82-
hydrateHitter: async (hitter, statType) => {
82+
hydrateHitter: async (hitter, statType, season = new Date().getFullYear()) => {
8383
const [spot, stats] = await Promise.all([
8484
new Promise((resolve, reject) => {
8585
if (hitter) {
@@ -94,7 +94,7 @@ module.exports = {
9494
}),
9595
new Promise((resolve, reject) => {
9696
if (hitter) {
97-
resolve(mlbAPIUtil.hitter(hitter, statType));
97+
resolve(mlbAPIUtil.hitter(hitter, statType, season));
9898
} else {
9999
resolve(undefined);
100100
}
@@ -285,21 +285,23 @@ module.exports = {
285285
return (await getScreenshotOfHTMLTables([table]));
286286
},
287287

288-
getStatcastData: (savantText) => {
288+
getStatcastData: (savantText, season) => {
289289
const statcast = /statcast: \[(?<statcast>.+)],/.exec(savantText)?.groups.statcast;
290290
const metricSummaries = /metricSummaryStats: {(?<metricSummaries>.+)},/.exec(savantText)?.groups.metricSummaries;
291291
if (statcast) {
292292
try {
293293
const statcastJSON = JSON.parse('[' + statcast + ']');
294294
const metricSummaryJSON = JSON.parse('{' + metricSummaries + '}');
295-
const mostRecentStatcast = statcastJSON.findLast(set => set.year != null);
295+
const matchingStatcast = season ? statcastJSON.find(set => set.year === season) : statcastJSON.findLast(set => set.year != null);
296296
// object properties are not guaranteed to always be in the same order, so we need to find the most recent year of data
297-
const mostRecentMetricYear = Object.keys(metricSummaryJSON)
298-
.map(k => parseInt(k))
299-
.sort((a, b) => {
300-
return a < b ? 1 : -1;
301-
})[0];
302-
return { mostRecentStatcast, metricSummaryJSON, mostRecentMetricYear };
297+
const matchingMetricYear = season
298+
? Object.keys(metricSummaryJSON).find(k => k === season.toString())
299+
: Object.keys(metricSummaryJSON)
300+
.map(k => parseInt(k))
301+
.sort((a, b) => {
302+
return a < b ? 1 : -1;
303+
})[0];
304+
return { matchingStatcast, metricSummaryJSON, matchingMetricYear };
303305
} catch (e) {
304306
console.error(e);
305307
return {};
@@ -756,7 +758,7 @@ module.exports = {
756758
return matchingPlayers;
757759
},
758760

759-
getPitcherEmbed: (pitcher, pitcherInfo, isLiveGame, description, statType = 'R', savantMode = false) => {
761+
getPitcherEmbed: (pitcher, pitcherInfo, isLiveGame, description, statType = 'R', savantMode = false, season = undefined) => {
760762
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
761763
if (isLiveGame) {
762764
const abbreviations = {
@@ -774,7 +776,7 @@ module.exports = {
774776
.setDescription('### ' + (pitcherInfo.handedness
775777
? pitcherInfo.handedness + 'HP **'
776778
: '**') + (pitcher.fullName || 'TBD') +
777-
'** (' + abbreviation + `): ${pitcherInfo.pitchingStats.yearOfStats || 'Latest'} ${(() => {
779+
'** (' + abbreviation + `): ${season || pitcherInfo.pitchingStats.yearOfStats || 'Latest'} ${(() => {
778780
if (savantMode) {
779781
return 'Percentile Rankings';
780782
}
@@ -802,7 +804,7 @@ module.exports = {
802804
const embed = new EmbedBuilder()
803805
.setTitle((pitcherInfo.handedness
804806
? pitcherInfo.handedness + 'HP '
805-
: '') + pitcher.fullName + ` (${globals.TEAMS.find(t => t.id === pitcher.currentTeam.id).abbreviation}): ${pitcherInfo.pitchingStats.yearOfStats || 'Latest'} ${(() => {
807+
: '') + pitcher.fullName + ` (${globals.TEAMS.find(t => t.id === pitcher.currentTeam.id).abbreviation}): ${season || pitcherInfo.pitchingStats.yearOfStats || 'Latest'} ${(() => {
806808
if (savantMode) {
807809
return 'Percentile Rankings';
808810
}
@@ -831,7 +833,7 @@ module.exports = {
831833
}
832834
},
833835

834-
getBatterEmbed: (batter, batterInfo, isLiveGame, description, statType = 'R', savantMode = false) => {
836+
getBatterEmbed: (batter, batterInfo, isLiveGame, description, statType = 'R', savantMode = false, season = undefined) => {
835837
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
836838
let expandedBatter;
837839
if (isLiveGame) {
@@ -847,7 +849,7 @@ module.exports = {
847849
const inning = feed.inning();
848850
const embed = new EmbedBuilder()
849851
.setTitle(halfInning.toUpperCase() + ' ' + inning + ', ' +
850-
abbreviations.away + ' vs. ' + abbreviations.home + ': Current Batter' + (savantMode ? ': Latest Percentile Rankings' : ''))
852+
abbreviations.away + ' vs. ' + abbreviations.home + ': Current Batter' + (savantMode ? `: ${season || 'Latest'} Percentile Rankings` : ''))
851853
.setDescription(`### ${batter.fullName} (${abbreviation})\n ${expandedBatter.primaryPosition.abbreviation} | Bats ${expandedBatter.batSide.description} ${(description || '')}`)
852854
.setImage('attachment://savant.png')
853855
.setColor((halfInning === 'top'
@@ -862,7 +864,7 @@ module.exports = {
862864
return embed;
863865
} else {
864866
const embed = new EmbedBuilder()
865-
.setTitle(`${batter.fullName} (${globals.TEAMS.find(team => team.id === batter.currentTeam.id).abbreviation})` + (savantMode ? ': Latest Percentile Rankings' : ''))
867+
.setTitle(`${batter.fullName} (${globals.TEAMS.find(team => team.id === batter.currentTeam.id).abbreviation})` + (savantMode ? `: ${season || 'Latest'} Percentile Rankings` : ''))
866868
.setDescription(`${batter.primaryPosition.abbreviation} | Bats ${batterInfo.stats.batSide.description}`)
867869
.setImage('attachment://savant.png')
868870
.setColor(globals.TEAMS.find(team => team.id === batter.currentTeam.id).primaryColor);
@@ -1144,20 +1146,23 @@ async function getScreenshotOfSavantTable (savantHTML) {
11441146
function buildSavantSection (statCollection, metricSummaries, isPitcher = false) {
11451147
const scale = chroma.scale(['#325aa1', '#a8c1c3', '#c91f26']);
11461148
const sliderScale = chroma.scale(['#3661ad', '#b4cfd1', '#d8221f']);
1147-
statCollection.forEach(stat => {
1148-
if (!stat.percentile) {
1149-
stat.percentile = calculateRoundedPercentileFromNormalDistribution(
1150-
stat.metric,
1151-
stat.value,
1152-
metricSummaries[stat.metric].avg_metric,
1153-
metricSummaries[stat.metric].stddev_metric,
1154-
stat.shouldInvert
1149+
for (let i = 0; i < statCollection.length; i ++) {
1150+
if (!statCollection[i].value) { // some metrics have been added in later years, like Bat Speed. Earlier seasons will have no value.
1151+
continue;
1152+
}
1153+
if (!statCollection[i].percentile) {
1154+
statCollection[i].percentile = calculateRoundedPercentileFromNormalDistribution(
1155+
statCollection[i].metric,
1156+
statCollection[i].value,
1157+
metricSummaries[statCollection[i].metric]?.avg_metric,
1158+
metricSummaries[statCollection[i].metric]?.stddev_metric,
1159+
statCollection[i].shouldInvert
11551160
);
1156-
stat.isQualified = false;
1161+
statCollection[i].isQualified = false;
11571162
} else {
1158-
stat.isQualified = true;
1163+
statCollection[i].isQualified = true;
11591164
}
1160-
});
1165+
}
11611166
return statCollection.reduce((acc, value) => acc + (value.value !== null
11621167
? `
11631168
<div class='savant-stat'>

0 commit comments

Comments
 (0)