Skip to main content

mlb_api/requests/
season.rs

1//! Data about important dates in a season for a specific [`SportId`].
2//!
3//! When spring training starts, ends. Regular season dates, Postseason dates, ASG, etc.
4
5use crate::{Copyright, NaiveDateRange};
6use chrono::{Datelike, NaiveDate, Utc};
7use derive_more::{Deref, Display, From};
8use serde::{Deserialize, Deserializer};
9use std::fmt::{Display, Formatter};
10use bon::Builder;
11use serde::de::Error;
12use crate::request::RequestURL;
13use crate::sport::SportId;
14
15#[derive(Debug, Default, Deref, Display, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, From)]
16#[repr(transparent)]
17pub struct SeasonId(u32);
18
19impl<'de> Deserialize<'de> for SeasonId {
20	fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
21		#[derive(::serde::Deserialize)]
22		#[serde(untagged)]
23		enum Repr {
24			Wrapped { id: u32 },
25			Inline(u32),
26			String(String),
27		}
28
29		let id = match Repr::deserialize(deserializer)? {
30			Repr::Wrapped { id } | Repr::Inline(id) => id,
31			Repr::String(id) => id.parse::<u32>().map_err(D::Error::custom)?,
32		};
33		Ok(Self(id))
34	}
35}
36
37impl SeasonId {
38	#[must_use]
39	pub const fn new(id: u32) -> Self {
40		Self(id)
41	}
42
43	#[allow(clippy::cast_sign_loss, reason = "jesus is not alive")]
44	#[must_use]
45	pub fn current_season() -> Self {
46		Self::new(Utc::now().year() as _)
47	}
48}
49
50#[derive(Deserialize)]
51struct SeasonRaw {
52	#[serde(alias = "season", alias = "seasonId")]
53	pub id: SeasonId,
54
55	#[serde(default)] // will be overwriten if not present because of bad league schedule schema
56	#[serde(rename = "hasWildcard")]
57	pub has_wildcard: bool,
58
59	#[serde(rename = "preSeasonStartDate")]
60	pub preseason_start: Option<NaiveDate>,
61	#[serde(rename = "preSeasonEndDate")]
62	pub preseason_end: Option<NaiveDate>,
63	#[serde(rename = "springStartDate")]
64	pub spring_start: Option<NaiveDate>,
65	#[serde(rename = "springEndDate")]
66	pub spring_end: Option<NaiveDate>,
67	#[serde(rename = "seasonStartDate")]
68	pub season_start: Option<NaiveDate>,
69	#[serde(rename = "regularSeasonStartDate")]
70	pub regular_season_start: Option<NaiveDate>,
71	#[serde(rename = "lastDate1stHalf")]
72	pub first_half_end: Option<NaiveDate>,
73	#[serde(rename = "allStarDate")]
74	pub all_star: Option<NaiveDate>,
75	#[serde(rename = "firstDate2ndHalf")]
76	pub second_half_start: Option<NaiveDate>,
77	#[serde(rename = "regularSeasonEndDate")]
78	pub regular_season_end: Option<NaiveDate>,
79	#[serde(rename = "postSeasonStartDate")]
80	pub postseason_start: Option<NaiveDate>,
81	#[serde(rename = "postSeasonEndDate")]
82	pub postseason_end: Option<NaiveDate>,
83	#[serde(rename = "seasonEndDate")]
84	pub season_end: Option<NaiveDate>,
85	#[serde(rename = "offseasonStartDate")]
86	pub offseason_start: Option<NaiveDate>,
87	#[serde(rename = "offSeasonEndDate")]
88	pub offseason_end: Option<NaiveDate>,
89	#[serde(flatten)]
90	pub qualification_multipliers: Option<QualificationMultipliers>,
91}
92
93impl From<SeasonRaw> for Season {
94	fn from(value: SeasonRaw) -> Self {
95		let SeasonRaw {
96			id,
97			has_wildcard,
98			preseason_start,
99			preseason_end,
100			spring_start,
101			spring_end,
102			season_start,
103			regular_season_start,
104			first_half_end,
105			all_star,
106			second_half_start,
107			regular_season_end,
108			postseason_start,
109			postseason_end,
110			season_end,
111			offseason_start,
112			offseason_end,
113			qualification_multipliers,
114		} = value;
115
116        // their API does the same thing.
117		let season_start = season_start.unwrap_or_else(|| NaiveDate::from_ymd_opt(*id as _, 1, 1).expect("Valid year"));
118		let offseason_end = offseason_end.unwrap_or_else(|| NaiveDate::from_ymd_opt(*id as _, 12, 31).expect("Valid year"));
119
120		Self {
121			id,
122			has_wildcard,
123			preseason: preseason_start.unwrap_or(season_start)..=preseason_end.unwrap_or(season_start),
124			spring: spring_start.zip(spring_end).map(|(start, end)| start..=end),
125			season: season_start..=season_end.unwrap_or(offseason_end),
126			regular_season: regular_season_start.unwrap_or(season_start)..=regular_season_end.or(season_end).unwrap_or(offseason_end),
127			first_half_end,
128			all_star,
129			second_half_start,
130			postseason: postseason_start.zip(postseason_end).map(|(start, end)| start..=end),
131			offseason: offseason_start.unwrap_or(offseason_end)..=offseason_end,
132			qualification_multipliers,
133		}
134	}
135}
136
137/// A season and it's info - dependent on [`SportId`].
138/// Stores multiple date ranges for different parts of the season (spring training, postseason, etc)
139///
140/// These fields are arranged in a chronological order but the specification makes no guarantees that this order remain consistent.
141#[derive(Debug, Deserialize, PartialEq, Clone)]
142#[serde(from = "SeasonRaw")]
143pub struct Season {
144	pub id: SeasonId,
145	/// If the season has a wildcard system
146	pub has_wildcard: bool,
147	/// Preseason date range
148	pub preseason: NaiveDateRange,
149	/// Spring Training date range
150	pub spring: Option<NaiveDateRange>,
151	/// Full Season date range
152	pub season: NaiveDateRange,
153	/// Regular Season date range
154	pub regular_season: NaiveDateRange,
155	/// End of the first half of the season (if the season halves are defined)
156	pub first_half_end: Option<NaiveDate>,
157	/// When the ASG is
158	pub all_star: Option<NaiveDate>,
159	/// Start of the second half of the season (if the season halves are defined)
160	pub second_half_start: Option<NaiveDate>,
161	/// When the postseason happens
162	pub postseason: Option<NaiveDateRange>,
163	/// When the offseason is active (different from preseason)
164	pub offseason: NaiveDateRange,
165	/// [`QualificationMultipliers`]
166	pub qualification_multipliers: Option<QualificationMultipliers>,
167	// opt<(season_level_gameday_type, game_level_gameday_type)>
168}
169
170// Coefficients for the qualified player cutoffs.
171#[derive(Debug, Deserialize, PartialEq, Clone)]
172#[serde(rename_all = "camelCase")]
173pub struct QualificationMultipliers {
174	/// Amount of plate appearances needed per game your (current?) team has played to be considered qualified
175	#[serde(rename = "qualifierPlateAppearances")]
176	pub plate_appearances_per_game: f64,
177	// Amount of outs pitched per game your (current?) team has played to be considered qualified
178	#[serde(rename = "qualifierOutsPitched")]
179	pub outs_pitched_per_game: f64,
180}
181
182/// Current state of the season
183#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
184pub enum SeasonState {
185	#[serde(rename = "spring training")]
186	SpringTraining,
187	#[serde(rename = "inseason")]
188	Inseason,
189	#[serde(rename = "offseason")]
190	Offseason,
191	#[serde(rename = "preseason")]
192	Preseason,
193}
194
195/// Returns a [`Vec`] of [`Season`]s.
196#[derive(Debug, Deserialize, PartialEq, Clone)]
197#[serde(rename_all = "camelCase")]
198pub struct SeasonsResponse {
199	pub copyright: Copyright,
200	pub seasons: Vec<Season>,
201}
202
203/// Returns a [`SeasonsResponse`]
204#[derive(Builder)]
205#[builder(derive(Into))]
206pub struct SeasonsRequest {
207	#[builder(into)]
208	#[builder(default)]
209	sport_id: SportId,
210	#[builder(into)]
211	season: Option<SeasonId>,
212}
213
214impl<S: seasons_request_builder::State + seasons_request_builder::IsComplete> crate::request::RequestURLBuilderExt for SeasonsRequestBuilder<S> {
215	type Built = SeasonsRequest;
216}
217
218impl Display for SeasonsRequest {
219	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
220		write!(f, "http://statsapi.mlb.com/api/v1/seasons{}", gen_params! { "sportId": self.sport_id, "season"?: self.season })
221	}
222}
223
224impl RequestURL for SeasonsRequest {
225	type Response = SeasonsResponse;
226}
227
228#[cfg(test)]
229mod tests {
230	use crate::season::SeasonsRequest;
231	use crate::sport::SportsRequest;
232	use crate::TEST_YEAR;
233	use crate::request::RequestURLBuilderExt;
234
235	#[tokio::test]
236	#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
237	async fn parses_all_seasons() {
238		let all_sport_ids = SportsRequest::<()>::builder().build_and_get().await.unwrap().sports.into_iter().map(|sport| sport.id).collect::<Vec<_>>();
239
240		for season in 1871..=TEST_YEAR {
241			for id in all_sport_ids.iter().copied() {
242				let _response = SeasonsRequest::builder().sport_id(id).season(season).build_and_get().await.unwrap();
243			}
244		}
245	}
246
247	#[tokio::test]
248	async fn parse_this_season_mlb() {
249		let _response = SeasonsRequest::builder().build_and_get().await.unwrap();
250	}
251}