Skip to main content

mlb_api/requests/schedule/
mod.rs

1#![allow(non_snake_case)]
2
3use crate::game::{DoubleHeaderKind, GameId};
4use crate::league::LeagueId;
5use crate::season::SeasonId;
6use crate::team::TeamId;
7use crate::types::{Copyright, HomeAwaySplits, NaiveDateRange, MLB_API_DATE_FORMAT};
8use crate::venue::{NamedVenue, VenueId};
9use crate::game_status::GameStatus;
10use crate::game_types::GameType;
11use crate::request::RequestURL;
12use crate::sky::Sky;
13use crate::sport::SportId;
14use bon::Builder;
15use chrono::{NaiveDate, NaiveDateTime, Utc};
16use either::Either;
17use itertools::Itertools;
18use serde::Deserialize;
19use serde_with::serde_as;
20use serde_with::DefaultOnError;
21use std::fmt::{Display, Formatter};
22use uuid::Uuid;
23use crate::team::NamedTeam;
24
25pub mod postseason;
26pub mod tied;
27
28#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
29#[serde(rename_all = "camelCase")]
30pub struct ScheduleResponse {
31	pub copyright: Copyright,
32	pub dates: Vec<ScheduleDate>,
33}
34
35#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
36#[serde(rename_all = "camelCase")]
37pub struct ScheduleDate {
38	pub date: NaiveDate,
39	pub games: Vec<ScheduleGame>,
40}
41
42#[allow(clippy::struct_excessive_bools, reason = "false positive")]
43#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
44#[serde(from = "__ScheduleGameStruct")]
45pub struct ScheduleGame {
46	pub game_id: GameId,
47	pub game_guid: Uuid,
48	pub game_type: GameType,
49	pub season: SeasonId,
50	pub game_date: NaiveDateTime,
51	/// Different from `game_date.date()` in cases such as a rescheduled/postponed game (ex: Toronto @ Boston June 26, 2024)
52	pub official_date: NaiveDate,
53	pub status: GameStatus,
54	pub teams: HomeAwaySplits<TeamWithStandings>,
55	pub venue: NamedVenue,
56	pub is_tie: bool,
57
58	/// Refers to the ordinal in the day? (maybe season?).
59	/// Starts at 1.
60	pub game_ordinal: u32,
61	pub is_public_facing: bool,
62	pub double_header: DoubleHeaderKind,
63	// #[serde(rename = "gamedayType")]
64	// pub gameday_game_type: GamedayGameType,
65	pub is_tiebreaker: bool,
66	// pub calender_event_id: CalenderEventId,
67	pub displayed_season: SeasonId,
68	pub day_night: Sky,
69	pub description: Option<String>,
70	pub scheduled_innings: u32,
71	pub reverse_home_away_status: bool,
72	pub inning_break_length: uom::si::i32::Time,
73	/// [`None`] if the current game is not of a series-format (ex: [Spring Training](`GameType::SpringTraining`))
74	pub series_data: Option<SeriesData>,
75}
76
77#[serde_as]
78#[derive(Deserialize)]
79#[serde(rename_all = "camelCase")]
80struct __ScheduleGameStruct {
81	#[serde(rename = "gamePk")]
82	game_id: GameId,
83	game_guid: Uuid,
84	game_type: GameType,
85	season: SeasonId,
86	#[serde(deserialize_with = "crate::types::deserialize_datetime")]
87	game_date: NaiveDateTime,
88	official_date: NaiveDate,
89	status: GameStatus,
90	teams: HomeAwaySplits<TeamWithStandings>,
91	#[serde_as(deserialize_as = "DefaultOnError")]
92	venue: Option<NamedVenue>,
93	is_tie: Option<bool>,
94	#[serde(rename = "gameNumber")]
95	game_ordinal: u32,
96	#[serde(rename = "publicFacing")]
97	is_public_facing: bool,
98	double_header: DoubleHeaderKind,
99	// #[serde(rename = "gamedayType")]
100	// gameday_game_type: GamedayGameType,
101	#[serde(rename = "tiebreaker", deserialize_with = "crate::types::from_yes_no")]
102	is_tiebreaker: bool,
103	// calender_event_id: CalenderEventId,
104	#[serde(rename = "seasonDisplay")]
105	displayed_season: SeasonId,
106	day_night: Sky,
107	description: Option<String>,
108	scheduled_innings: u32,
109	reverse_home_away_status: bool,
110	inning_break_length: Option<u32>,
111	#[serde(flatten)]
112	series_data: Option<SeriesData>,
113}
114
115impl From<__ScheduleGameStruct> for ScheduleGame {
116	#[allow(clippy::cast_possible_wrap, reason = "not gonna happen")]
117	fn from(
118		__ScheduleGameStruct {
119			game_id,
120			game_guid,
121			game_type,
122			season,
123			game_date,
124			official_date,
125			status,
126			teams,
127			venue,
128			is_tie,
129			game_ordinal,
130			is_public_facing,
131			double_header,
132			is_tiebreaker,
133			displayed_season,
134			day_night,
135			description,
136			scheduled_innings,
137			reverse_home_away_status,
138			inning_break_length,
139			series_data,
140		}: __ScheduleGameStruct,
141	) -> Self {
142		Self {
143			game_id,
144			game_guid,
145			game_type,
146			season,
147			game_date,
148			official_date,
149			status,
150			teams,
151			venue: venue.unwrap_or_else(NamedVenue::unknown_venue),
152			is_tie: is_tie.unwrap_or(false),
153			game_ordinal,
154			is_public_facing,
155			double_header,
156			is_tiebreaker,
157			displayed_season,
158			day_night,
159			description,
160			scheduled_innings,
161			reverse_home_away_status,
162			inning_break_length: uom::si::i32::Time::new::<uom::si::time::second>(inning_break_length.unwrap_or(120) as i32),
163			series_data,
164		}
165	}
166}
167
168#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
169#[serde(rename_all = "camelCase")]
170pub struct SeriesData {
171	pub games_in_series: u32,
172	#[serde(rename = "seriesGameNumber")]
173	pub game_in_series_ordinal: u32,
174}
175
176#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
177#[serde(rename_all = "camelCase")]
178pub struct TeamWithStandings {
179	pub team: NamedTeam,
180	#[serde(rename = "leagueRecord")]
181	pub standings: Standings,
182	#[serde(flatten)]
183	pub score: Option<TeamWithStandingsGameScore>,
184	#[serde(rename = "splitSquad")]
185	pub is_split_squad_game: bool,
186
187	/// Refers to the ordinal of series, not within the current series.
188	/// Starts at 1.
189	/// [`None`] if the current game is not of a series-format (ex: [Spring Training](`GameType::SpringTraining`))
190	#[serde(rename = "seriesNumber")]
191	pub series_ordinal: Option<u32>,
192}
193
194#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
195#[serde(rename_all = "camelCase")]
196pub struct TeamWithStandingsGameScore {
197	#[serde(rename = "score")]
198	pub runs_scored: u32,
199	pub is_winner: bool,
200}
201
202#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
203#[serde(rename_all = "camelCase")]
204pub struct Standings {
205	pub wins: u32,
206	pub losses: u32,
207}
208
209impl Standings {
210	#[must_use]
211	pub const fn games_played(self) -> u32 {
212		self.wins + self.losses
213	}
214
215	#[must_use]
216	pub fn pct(self) -> f64 {
217		f64::from(self.wins) / f64::from(self.games_played())
218	}
219}
220
221#[allow(dead_code, reason = "rust analyzer says that opponent_id and season are dead, while being used in Display")]
222#[derive(Builder)]
223#[builder(derive(Into))]
224pub struct ScheduleRequest {
225	#[builder(into)]
226	#[builder(default)]
227	sport_id: SportId,
228	#[builder(setters(vis = "", name = __game_ids_internal))]
229	game_ids: Option<Vec<GameId>>,
230	#[builder(into)]
231	team_id: Option<TeamId>,
232	#[builder(into)]
233	league_id: Option<LeagueId>,
234	#[builder(setters(vis = "", name = __venue_ids_internal))]
235	venue_ids: Option<Vec<VenueId>>,
236	#[builder(default = Either::Left(Utc::now().date_naive()))]
237	#[builder(setters(vis = "", name = __date_internal))]
238	date: Either<NaiveDate, NaiveDateRange>,
239	#[builder(into)]
240	opponent_id: Option<TeamId>,
241	#[builder(into)]
242	season: Option<SeasonId>,
243}
244
245
246impl<S: schedule_request_builder::State + schedule_request_builder::IsComplete> crate::request::RequestURLBuilderExt for ScheduleRequestBuilder<S> {
247    type Built = ScheduleRequest;
248}
249
250impl<S: schedule_request_builder::State> ScheduleRequestBuilder<S> {
251	pub fn game_ids(self, game_ids: Vec<impl Into<GameId>>) -> ScheduleRequestBuilder<schedule_request_builder::SetGameIds<S>> where S::GameIds: schedule_request_builder::IsUnset {
252		self.__game_ids_internal(game_ids.into_iter().map(Into::into).collect())
253	}
254
255	pub fn venue_ids(self, venue_ids: Vec<impl Into<VenueId>>) -> ScheduleRequestBuilder<schedule_request_builder::SetVenueIds<S>> where S::VenueIds: schedule_request_builder::IsUnset {
256		self.__venue_ids_internal(venue_ids.into_iter().map(Into::into).collect())
257	}
258
259	pub fn date(self, date: NaiveDate) -> ScheduleRequestBuilder<schedule_request_builder::SetDate<S>> where S::Date: schedule_request_builder::IsUnset {
260		self.__date_internal(Either::Left(date))
261	}
262
263	pub fn date_range(self, range: NaiveDateRange) -> ScheduleRequestBuilder<schedule_request_builder::SetDate<S>> where S::Date: schedule_request_builder::IsUnset {
264		self.__date_internal(Either::Right(range))
265	}
266}
267
268impl Display for ScheduleRequest {
269	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
270		write!(
271			f,
272			"http://statsapi.mlb.com/api/v1/schedule{params}",
273			params = gen_params! {
274				"sportId": self.sport_id,
275				"gamePks"?: self.game_ids.as_ref().map(|ids| ids.iter().map(ToString::to_string).join(",")),
276				"teamId"?: self.team_id,
277				"leagueId"?: self.league_id,
278				"venueIds"?: self.venue_ids.as_ref().map(|ids| ids.iter().map(ToString::to_string).join(",")),
279				"date"?: self.date.as_ref().left().map(|x| x.format(MLB_API_DATE_FORMAT)),
280				"startDate"?: self.date.as_ref().right().map(|range| range.start().format(MLB_API_DATE_FORMAT)),
281				"endDate"?: self.date.as_ref().right().map(|range| range.end().format(MLB_API_DATE_FORMAT)),
282				"opponentId"?; self.opponent_id,
283				"season"?: self.season,
284			}
285		)
286	}
287}
288
289impl RequestURL for ScheduleRequest {
290	type Response = ScheduleResponse;
291}
292
293#[cfg(test)]
294mod tests {
295	use crate::schedule::ScheduleRequest;
296	use crate::TEST_YEAR;
297	use chrono::NaiveDate;
298	use crate::request::RequestURLBuilderExt;
299
300	#[tokio::test]
301	async fn test_one_date() {
302		let date = NaiveDate::from_ymd_opt(2020, 8, 2).expect("Valid date");
303		let _ = ScheduleRequest::builder().date(date).build_and_get().await.unwrap();
304	}
305
306	#[tokio::test]
307	async fn test_all_dates_current_year() {
308		let _ = ScheduleRequest::builder().date_range(NaiveDate::from_ymd_opt(TEST_YEAR.try_into().unwrap(), 1, 1).expect("Valid date")..=NaiveDate::from_ymd_opt(TEST_YEAR.try_into().unwrap(), 12, 31).expect("Valid date")).build_and_get().await.unwrap();
309	}
310
311	#[tokio::test]
312	#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
313	async fn test_all_dates_all_years() {
314		for year in 1876..=TEST_YEAR {
315			let _ = ScheduleRequest::builder().date_range(NaiveDate::from_ymd_opt(year.try_into().unwrap(), 1, 1).unwrap()..=NaiveDate::from_ymd_opt(year.try_into().unwrap(), 12, 31).unwrap())
316			.build_and_get()
317			.await
318			.unwrap();
319		}
320	}
321}