import type { Game, Team } from './apiResponseTypes' import type { Player } from './playersService' import { MODERN_STAT_ERA_START, SITE_URL, avg, obp, ops, slg, woba } from './utilities' // TODO make a stats object that has properties for current regular season, current post season, // last 4 games, historical seasons, career totals // could split into batting/pitching/fielding so only necessary ones are called to save time export interface PitchingStat { player: Player team: Team game: Game | 'TOT' // stats below tbf: number outs: number games: number gs: number win: number loss: number hold: number save: number bsave: number ir: number ir_sc: number ab: number run: number e_run: number hits: number double: number triple: number hr: number bb: number so: number hbp: number sac: number ibb: number gidp: number sb: number cs: number bphr: number bpfo: number bp1b: number bplo: number wp: number balk: number wpa: number era: number whip: number avg: number obp: number slg: number ops: number woba: number kPer9: number bbPer9: number kPerBB: number } interface PitchingStatRaw { player: Player team: Team game: Game | 'TOT' // stats below tbf: number outs: number games: number gs: number win: number loss: number hold: number save: number bsave: number ir: number ir_sc: number ab: number run: number e_run: number hits: number double: number triple: number hr: number bb: number so: number hbp: number sac: number ibb: number gidp: number sb: number cs: number bphr: number bpfo: number bp1b: number bplo: number wp: number balk: number wpa: number era: number whip: number avg: number obp: number slg: number ops: number woba: number "k/9": number "bb/9": number "k/bb": number } interface LegacyPitchingStat { player: Player team: Team ip: number, hit: number, run: number, erun: number, so: number, bb: number, hbp: number, wp: number, balk: number, hr: number, ir: number, irs: number, gs: number, games: number, win: number, loss: number, hold: number, sv: number, bsv: number } export async function fetchPitchingStatsBySeasonAndPlayerId(seasonNumber: number, playerId: number, isRegularSeason: boolean): Promise { // different endpoint for pre-modern stats (/plays) era if (seasonNumber < MODERN_STAT_ERA_START) { return await fetchLegacyPitchingStatsBySeasonAndPlayerId(seasonNumber, playerId, isRegularSeason) } const response = await fetch(`${SITE_URL}/api/v3/plays/pitching?season=${seasonNumber}&player_id=${playerId}&s_type=${isRegularSeason ? 'regular' : 'post'}`) const pitchingStatsResponse: { count: number stats: PitchingStatRaw[] } = await response.json() if (pitchingStatsResponse.count === 0) return undefined if (pitchingStatsResponse.count > 1) { throw new Error('pitchingStatsService.fetchPitchingStatsBySeasonAndPlayerId - Expected one stat line for season/player, return contained many') } return normalizePitchingStat(pitchingStatsResponse.stats[0]) } async function fetchLegacyPitchingStatsBySeasonAndPlayerId(seasonNumber: number, playerId: number, isRegularSeason: boolean): Promise { const response = await fetch(`${SITE_URL}/api/v3/pitchingstats/totals?season=${seasonNumber}&player_id=${playerId}&s_type=${isRegularSeason ? 'regular' : 'post'}`) const legacyPitchingStatsResponse: { count: number stats: LegacyPitchingStat[] } = await response.json() if (legacyPitchingStatsResponse.count === 0) return undefined if (legacyPitchingStatsResponse.count > 1) { throw new Error('pitchingStatsService.fetchLegacyPitchingStatsBySeasonAndPlayerId - Expected one stat line for season/player, return contained many') } return makeModernPitchingStatFromLegacy(legacyPitchingStatsResponse.stats[0]) } export async function fetchPitchingStatsForLastFourGamesBySeasonAndPlayerId(seasonNumber: number, playerId: number): Promise { const response = await fetch(`${SITE_URL}/api/v3/plays/pitching?season=${seasonNumber}&player_id=${playerId}&group_by=playergame&limit=4&s_type=regular&sort=newest`) const pitchingStatsResponse: { count: number stats: PitchingStatRaw[] } = await response.json() if (pitchingStatsResponse.count > 4) { throw new Error(`pitchingStatsService.fetchPitchingStatsForLastFourBySeasonAndPlayerId - Expected at most 4 games, return contained ${pitchingStatsResponse.count}`) } return pitchingStatsResponse.stats.map(normalizePitchingStat) } export function aggregatePitchingStats(pitchingStats: PitchingStat[]): PitchingStat { let totalStat: PitchingStat = { player: pitchingStats[0].player, team: pitchingStats[0].team, game: 'TOT', // stats below tbf: 0, outs: 0, games: 0, gs: 0, win: 0, loss: 0, hold: 0, save: 0, bsave: 0, ir: 0, ir_sc: 0, ab: 0, run: 0, e_run: 0, hits: 0, double: 0, triple: 0, hr: 0, bb: 0, so: 0, hbp: 0, sac: 0, ibb: 0, gidp: 0, sb: 0, cs: 0, bphr: 0, bpfo: 0, bp1b: 0, bplo: 0, wp: 0, balk: 0, wpa: 0, era: 0, whip: 0, avg: 0, obp: 0, slg: 0, ops: 0, woba: 0, kPer9: 0, bbPer9: 0, kPerBB: 0 } pitchingStats.forEach(stat => { totalStat.tbf += stat.tbf, totalStat.outs += stat.outs, totalStat.games += stat.games, totalStat.gs += stat.gs, totalStat.win += stat.win, totalStat.loss += stat.loss, totalStat.hold += stat.hold, totalStat.save += stat.save, totalStat.bsave += stat.bsave, totalStat.ir += stat.ir, totalStat.ir_sc += stat.ir_sc, totalStat.ab += stat.ab, totalStat.run += stat.run, totalStat.e_run += stat.e_run, totalStat.hits += stat.hits, totalStat.double += stat.double, totalStat.triple += stat.triple, totalStat.hr += stat.hr, totalStat.bb += stat.bb, totalStat.so += stat.so, totalStat.hbp += stat.hbp, totalStat.sac += stat.sac, totalStat.ibb += stat.ibb, totalStat.gidp += stat.gidp, totalStat.sb += stat.sb, totalStat.cs += stat.cs, totalStat.bphr += stat.bphr, totalStat.bpfo += stat.bpfo, totalStat.bp1b += stat.bp1b, totalStat.bplo += stat.bplo, totalStat.wp += stat.wp, totalStat.balk += stat.balk, totalStat.wpa += stat.wpa }) // a shim since batting stat has "hit" and pitching stat has "hits" and I want to share functions // TODO will need to figure out if there are pas for pitching or if there is an easy way to back into it const totalStatWithHit = { ...totalStat, hit: totalStat.hits } return { ...totalStat, era: eraFromOuts(totalStat), whip: whipFromOuts(totalStat), kPer9: kPer9FromOuts(totalStat), bbPer9: bbPer9FromOuts(totalStat), kPerBB: kPerBB(totalStat), avg: avg(totalStatWithHit), obp: 0, //obp({pa: totalStat.}), slg: slg(totalStatWithHit), ops: 0, //ops(totalStat), woba: woba(totalStatWithHit) } } function makeModernPitchingStatFromLegacy(legacyStat: LegacyPitchingStat): PitchingStat { return { player: legacyStat.player, team: legacyStat.team, game: 'TOT', // stats below outs: Math.round(legacyStat.ip * 3), games: legacyStat.games, gs: legacyStat.gs, win: legacyStat.win, loss: legacyStat.loss, hold: legacyStat.hold, save: legacyStat.sv, bsave: legacyStat.bsv, ir: legacyStat.ir, ir_sc: legacyStat.irs, run: legacyStat.run, e_run: legacyStat.erun, hits: legacyStat.hit, hr: legacyStat.hr, bb: legacyStat.bb, so: legacyStat.so, hbp: legacyStat.hbp, wp: legacyStat.wp, balk: legacyStat.balk, // calculated stats era: era({ ip: legacyStat.ip, e_run: legacyStat.erun }), whip: whip({ ip: legacyStat.ip, bb: legacyStat.bb, hits: legacyStat.hit }), kPer9: kPer9(legacyStat), bbPer9: bbPer9(legacyStat), kPerBB: kPerBB(legacyStat), // no easy way to get below values from legacy stats afaik tbf: 0, ab: 0, double: 0, triple: 0, sac: 0, ibb: 0, gidp: 0, sb: 0, cs: 0, bphr: 0, bpfo: 0, bp1b: 0, bplo: 0, wpa: 0, avg: 0, obp: 0, slg: 0, ops: 0, woba: 0, } } function normalizePitchingStat(rawStat: PitchingStatRaw): PitchingStat { return { player: rawStat.player, team: rawStat.team, game: rawStat.game, // stats below tbf: rawStat.tbf, outs: rawStat.outs, games: rawStat.games, gs: rawStat.gs, win: rawStat.win, loss: rawStat.loss, hold: rawStat.hold, save: rawStat.save, bsave: rawStat.bsave, ir: rawStat.ir, ir_sc: rawStat.ir_sc, ab: rawStat.ab, run: rawStat.run, e_run: rawStat.e_run, hits: rawStat.hits, double: rawStat.double, triple: rawStat.triple, hr: rawStat.hr, bb: rawStat.bb, so: rawStat.so, hbp: rawStat.hbp, sac: rawStat.sac, ibb: rawStat.ibb, gidp: rawStat.gidp, sb: rawStat.sb, cs: rawStat.cs, bphr: rawStat.bphr, bpfo: rawStat.bpfo, bp1b: rawStat.bp1b, bplo: rawStat.bplo, wp: rawStat.wp, balk: rawStat.balk, wpa: rawStat.wpa, era: rawStat.era, whip: rawStat.whip, avg: rawStat.avg, obp: rawStat.obp, slg: rawStat.slg, ops: rawStat.ops, woba: rawStat.woba, kPer9: rawStat["k/9"], bbPer9: rawStat["bb/9"], kPerBB: rawStat["k/bb"] } } function era(stat: { e_run: number, ip: number }): number { if (stat.ip === 0) return stat.e_run > 0 ? NaN : 0 return stat.e_run * 9 / stat.ip } function eraFromOuts(stat: { e_run: number, outs: number }): number { if (stat.outs === 0) return stat.e_run > 0 ? NaN : 0 return stat.e_run * 27 / stat.outs } function whip(stat: { bb: number, hits: number, ip: number }): number { if (stat.ip === 0) return stat.bb + stat.hits > 0 ? NaN : 0 return (stat.bb + stat.hits) / stat.ip } function whipFromOuts(stat: { bb: number, hits: number, outs: number }): number { if (stat.outs === 0) return stat.bb + stat.hits > 0 ? NaN : 0 return (stat.bb + stat.hits) * 3 / stat.outs } function kPer9(stat: { so: number, ip: number }): number { if (stat.ip === 0) return 0 return stat.so * 9 / stat.ip } function kPer9FromOuts(stat: { so: number, outs: number }): number { if (stat.outs === 0) return 0 return stat.so * 27 / stat.outs } function bbPer9(stat: { bb: number, ip: number }): number { if (stat.ip === 0) return stat.bb > 0 ? NaN : 0 return stat.bb * 9 / stat.ip } function bbPer9FromOuts(stat: { bb: number, outs: number }): number { if (stat.outs === 0) return stat.bb > 0 ? NaN : 0 return stat.bb * 27 / stat.outs } function kPerBB(stat: { so: number, bb: number }): number { if (stat.bb === 0) return 0 return stat.so / stat.bb }