mlb_api/endpoints/attendance/
mod.rs1use crate::endpoints::game::Game;
2use crate::endpoints::league::LeagueId;
3use crate::endpoints::teams::team::TeamId;
4use crate::endpoints::{GameType, StatsAPIUrl};
5use crate::gen_params;
6use crate::types::{Copyright, HomeAwaySplits, MLB_API_DATE_FORMAT};
7use chrono::{Datelike, Local, NaiveDate, NaiveDateTime};
8use serde::Deserialize;
9use serde_with::DisplayFromStr;
10use serde_with::serde_as;
11use std::cmp::Ordering;
12use std::fmt::{Debug, Display, Formatter};
13use std::iter::Sum;
14use std::ops::Add;
15
16#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
22#[serde(from = "AttendanceResponseStruct")]
23pub struct AttendanceResponse {
24 pub copyright: Copyright,
25 #[serde(rename = "records")]
26 pub annual_records: Vec<AttendanceRecord>,
27}
28
29impl AttendanceResponse {
30 #[must_use]
31 pub fn into_aggregate(self) -> AttendanceRecord {
32 self.annual_records.into_iter().sum()
33 }
34}
35
36#[derive(Deserialize)]
37struct AttendanceResponseStruct {
38 copyright: Copyright,
39 records: Vec<AttendanceRecord>,
40}
41
42impl From<AttendanceResponseStruct> for AttendanceResponse {
43 fn from(value: AttendanceResponseStruct) -> Self {
44 let AttendanceResponseStruct { copyright, records } = value;
45 Self { copyright, annual_records: records }
46 }
47}
48
49#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
50#[serde(from = "AnnualRecordStruct")]
51pub struct AttendanceRecord {
52 pub total_openings: HomeAwaySplits<u32>,
53 pub total_openings_lost: u32,
54 pub total_games: HomeAwaySplits<u32>,
55 pub season: u16,
56 pub attendance_totals: HomeAwaySplits<u32>,
57 pub single_opening_high: DatedAttendance,
58 pub single_opening_low: DatedAttendance,
59 pub game_type: GameType,
60}
61
62impl Add for AttendanceRecord {
63 type Output = Self;
64
65 fn add(self, rhs: Self) -> Self::Output {
67 Self {
68 total_openings: HomeAwaySplits {
69 home: self.total_openings.home + rhs.total_openings.home,
70 away: self.total_openings.away + rhs.total_openings.away,
71 },
72 total_openings_lost: self.total_openings_lost + rhs.total_openings_lost,
73 total_games: HomeAwaySplits {
74 home: self.total_games.home + rhs.total_games.home,
75 away: self.total_games.away + rhs.total_games.away,
76 },
77 season: u16::max(self.season, rhs.season),
78 attendance_totals: HomeAwaySplits {
79 home: self.attendance_totals.home + rhs.attendance_totals.home,
80 away: self.attendance_totals.away + rhs.attendance_totals.away,
81 },
82 single_opening_high: self.single_opening_high.max(rhs.single_opening_high), single_opening_low: self.single_opening_low.min(rhs.single_opening_low), game_type: rhs.game_type,
85 }
86 }
87}
88
89impl Default for AttendanceRecord {
90 fn default() -> Self {
91 Self {
92 total_openings: HomeAwaySplits::new(0, 0),
93 total_openings_lost: 0,
94 total_games: HomeAwaySplits::new(0, 0),
95 season: Local::now().year() as _,
96 attendance_totals: HomeAwaySplits::new(0, 0),
97 single_opening_high: DatedAttendance::default(),
98 single_opening_low: DatedAttendance::default(),
99 game_type: GameType::default(),
100 }
101 }
102}
103
104impl Sum for AttendanceRecord {
105 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
106 iter.fold(Self::default(), |acc, x| acc + x)
107 }
108}
109
110impl AttendanceRecord {
111 #[must_use]
112 pub fn average_attendance(&self) -> HomeAwaySplits<u32> {
113 let HomeAwaySplits { home, away } = self.attendance_totals;
114 let HomeAwaySplits { home: num_at_home, away: num_at_away } = self.total_openings;
115 HomeAwaySplits::new((home + num_at_home / 2) / num_at_home, (away + num_at_away / 2) / num_at_away)
116 }
117}
118
119#[serde_as]
120#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
121#[serde(rename_all = "camelCase")]
122struct AnnualRecordStruct {
123 openings_total_away: u32,
125 openings_total_home: u32,
126 openings_total_lost: u32,
127 games_away_total: u32,
129 games_home_total: u32,
130 #[serde_as(as = "DisplayFromStr")]
131 year: u16,
132 attendance_high: u32,
136 attendance_high_date: NaiveDateTime,
137 attendance_high_game: Game,
138 attendance_low: Option<u32>,
139 attendance_low_date: Option<NaiveDateTime>,
140 attendance_low_game: Option<Game>,
141 attendance_total_away: u32,
144 attendance_total_home: u32,
145 game_type: GameType,
146 }
148
149impl From<AnnualRecordStruct> for AttendanceRecord {
150 fn from(value: AnnualRecordStruct) -> Self {
151 let AnnualRecordStruct {
152 openings_total_away,
154 openings_total_home,
155 openings_total_lost,
156 games_away_total,
158 games_home_total,
159 year,
160 attendance_high,
164 attendance_high_date,
165 attendance_high_game,
166 attendance_low,
167 attendance_low_date,
168 attendance_low_game,
169 attendance_total_away,
172 attendance_total_home,
173 game_type,
174 } = value;
176 let high = DatedAttendance {
177 value: attendance_high,
178 date: attendance_high_date.date(),
179 game: attendance_high_game,
180 };
181 Self {
182 total_openings: HomeAwaySplits {
183 home: openings_total_home,
184 away: openings_total_away,
185 },
186 total_openings_lost: openings_total_lost,
187 total_games: HomeAwaySplits {
188 home: games_home_total,
189 away: games_away_total,
190 },
191 season: year,
192 attendance_totals: HomeAwaySplits {
193 home: attendance_total_home,
194 away: attendance_total_away,
195 },
196 single_opening_low: {
197 if let Some(attendance_low) = attendance_low
198 && let Some(attendance_low_date) = attendance_low_date
199 && let Some(attendance_low_game) = attendance_low_game
200 {
201 DatedAttendance {
202 value: attendance_low,
203 date: attendance_low_date.date(),
204 game: attendance_low_game,
205 }
206 } else {
207 high.clone()
208 }
209 },
210 single_opening_high: high,
211 game_type,
212 }
213 }
214}
215
216#[derive(Debug, PartialEq, Eq, Clone)]
217pub struct DatedAttendance {
218 pub value: u32,
219 pub date: NaiveDate,
220 pub game: Game,
221}
222
223impl Default for DatedAttendance {
224 fn default() -> Self {
225 Self {
226 value: 0,
227 date: NaiveDate::default(),
228 game: Game::default(),
229 }
230 }
231}
232
233impl PartialOrd<Self> for DatedAttendance {
234 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
235 Some(self.cmp(other))
236 }
237}
238
239impl Ord for DatedAttendance {
240 fn cmp(&self, other: &Self) -> Ordering {
241 self.value.cmp(&other.value)
242 }
243}
244
245pub struct AttendanceEndpointUrl {
246 pub id: Result<TeamId, LeagueId>,
247 pub season: Option<u16>,
248 pub date: Option<NaiveDate>,
249 pub game_type: GameType,
250}
251
252impl Display for AttendanceEndpointUrl {
253 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
254 write!(
255 f,
256 "http://statsapi.mlb.com/api/v1/attendance{}",
257 gen_params! { "teamId"?: self.id.clone().ok(), "leagueId"?: self.id.clone().err(), "date"?: self.date.as_ref().map(|date| date.format(MLB_API_DATE_FORMAT)), "gameType": format!("{:?}", self.game_type) }
258 )
259 }
260}
261
262impl StatsAPIUrl for AttendanceEndpointUrl { type Response = AttendanceResponse; }
263
264#[cfg(test)]
265mod tests {
266 use crate::endpoints::attendance::AttendanceEndpointUrl;
267 use crate::endpoints::sports::SportId;
268 use crate::endpoints::teams::TeamsEndpointUrl;
269 use crate::endpoints::{GameType, StatsAPIUrl};
270
271 #[tokio::test]
272 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
273 async fn parse_all_mlb_teams_2025() {
274 let mlb_teams = TeamsEndpointUrl {
275 sport_id: Some(SportId::MLB),
276 season: Some(2025),
277 }
278 .get()
279 .await
280 .unwrap()
281 .teams;
282 for team in mlb_teams {
283 let _response = AttendanceEndpointUrl {
284 id: Ok(team.id),
285 season: None,
286 date: None,
287 game_type: GameType::RegularSeason,
288 }
289 .get()
290 .await
291 .unwrap();
292 }
293 }
294}