Skip to main content

mlb_api/
types.rs

1//! Shared types across multiple requests
2
3use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Timelike};
4use compact_str::CompactString;
5use derive_more::{Display, FromStr};
6use serde::de::Error;
7use serde::{Deserialize, Deserializer};
8use std::fmt::{Debug, Display, Formatter};
9use std::num::{ParseFloatError, ParseIntError};
10use std::ops::{Add, RangeInclusive};
11use std::str::FromStr;
12use thiserror::Error;
13
14#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
15#[serde(from = "__CopyrightStruct")]
16pub enum Copyright {
17	Typical {
18		year: u32,
19	},
20	UnknownSpec(CompactString),
21}
22
23#[derive(Deserialize)]
24#[doc(hidden)]
25struct __CopyrightStruct(String);
26
27impl From<__CopyrightStruct> for Copyright {
28	fn from(value: __CopyrightStruct) -> Self {
29		let __CopyrightStruct(value) = value;
30		if let Some(value) = value.strip_prefix("Copyright ") && let Some(value) = value.strip_suffix(" MLB Advanced Media, L.P.  Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt") && let Ok(year) = value.parse::<u32>() {
31			Self::Typical { year }
32		} else {
33			Self::UnknownSpec(CompactString::from(value))
34		}
35	}
36}
37
38impl Display for Copyright {
39	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
40		match self {
41			Self::Typical { year } => write!(f, "Copyright {year} MLB Advanced Media, L.P.  Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt"),
42			Self::UnknownSpec(copyright) => write!(f, "{copyright}"),
43		}
44	}
45}
46
47impl Default for Copyright {
48	#[allow(clippy::cast_sign_loss, reason = "jesus is not alive")]
49	fn default() -> Self {
50		Self::Typical { year: Local::now().year() as _ }
51	}
52}
53
54/// # Errors
55/// If a string cannot be parsed from the deserializer.
56pub fn try_from_str<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Option<T>, D::Error> {
57	Ok(String::deserialize(deserializer)?.parse::<T>().ok())
58}
59
60/// # Errors
61/// 1. If a string cannot be parsed from the deserializer.
62/// 2. If the type cannot be parsed from the string.
63pub fn from_str<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<T, D::Error>
64where
65	<T as FromStr>::Err: Debug,
66{
67	String::deserialize(deserializer)?.parse::<T>().map_err(|e| Error::custom(format!("{e:?}")))
68}
69
70/// # Errors
71/// If the type cannot be parsed into a Y or N string
72pub fn from_yes_no<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
73	#[derive(Deserialize)]
74	#[repr(u8)]
75	enum Boolean {
76		#[serde(rename = "Y")]
77		Yes = 1,
78		#[serde(rename = "N")]
79		No = 0,
80	}
81
82	Ok(match Boolean::deserialize(deserializer)? {
83		Boolean::Yes => true,
84		Boolean::No => false,
85	})
86}
87
88#[derive(Debug, PartialEq, Eq, Copy, Clone)]
89pub enum HeightMeasurement {
90	FeetAndInches { feet: u8, inches: u8 },
91	Centimeters { cm: u16 },
92}
93
94impl FromStr for HeightMeasurement {
95	type Err = HeightMeasurementParseError;
96
97	fn from_str(s: &str) -> Result<Self, Self::Err> {
98		if let Some((feet, Some((inches, "")))) = s.split_once("' ").map(|(feet, rest)| (feet, rest.split_once('"'))) {
99			let feet = feet.parse::<u8>()?;
100			let inches = inches.parse::<u8>()?;
101			Ok(Self::FeetAndInches { feet, inches })
102		} else if let Some((cm, "")) = s.split_once("cm") {
103			let cm = cm.parse::<u16>()?;
104			Ok(Self::Centimeters { cm })
105		} else {
106			Err(HeightMeasurementParseError::UnknownSpec(s.to_owned()))
107		}
108	}
109}
110
111impl<'de> Deserialize<'de> for HeightMeasurement {
112	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113	where
114		D: Deserializer<'de>
115	{
116		String::deserialize(deserializer)?.parse().map_err(D::Error::custom)
117	}
118}
119
120#[derive(Debug, Error)]
121pub enum HeightMeasurementParseError {
122	#[error(transparent)]
123	ParseIntError(#[from] ParseIntError),
124	#[error("Unknown height '{0}'")]
125	UnknownSpec(String),
126}
127
128#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, Default)]
129pub enum PlayerPool {
130	#[default]
131	#[display("ALL")]
132	All,
133	#[display("QUALIFIED")]
134	Qualified,
135	#[display("ROOKIES")]
136	Rookies,
137	#[display("QUALIFIED_ROOKIES")]
138	QualifiedAndRookies,
139	#[display("ORGANIZATION")]
140	Organization,
141	#[display("ORGANIZATION_NO_MLB")]
142	OrganizationNotMlb,
143	#[display("CURRENT")]
144	Current,
145	#[display("ALL_CURRENT")]
146	AllCurrent,
147	#[display("QUALIFIED_CURRENT")]
148	QualifiedAndCurrent,
149}
150
151#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Default)]
152pub enum Gender {
153	#[serde(rename = "M")]
154	Male,
155	#[serde(rename = "F")]
156	Female,
157	#[default]
158	#[serde(other)]
159	Other,
160}
161
162#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
163#[serde(try_from = "__HandednessStruct")]
164pub enum Handedness {
165	Left,
166	Right,
167	Switch,
168}
169
170#[derive(Deserialize)]
171#[doc(hidden)]
172struct __HandednessStruct {
173	code: String,
174}
175
176#[derive(Debug, Error)]
177pub enum HandednessParseError {
178	#[error("Invalid handedness '{0}'")]
179	InvalidHandedness(String),
180}
181
182impl TryFrom<__HandednessStruct> for Handedness {
183	type Error = HandednessParseError;
184
185	fn try_from(value: __HandednessStruct) -> Result<Self, Self::Error> {
186		Ok(match &*value.code {
187			"L" => Self::Left,
188			"R" => Self::Right,
189			"S" => Self::Switch,
190			_ => return Err(HandednessParseError::InvalidHandedness(value.code)),
191		})
192	}
193}
194
195pub type NaiveDateRange = RangeInclusive<NaiveDate>;
196
197pub(crate) const MLB_API_DATE_FORMAT: &str = "%m/%d/%Y";
198
199/// # Errors
200/// 1. If a string cannot be deserialized
201/// 2. If the data does not appear in the format `%Y-%m-%dT%H:%M:%SZ(%#z)?`. Why the MLB removes the +00:00 or -00:00 sometimes? I have no clue.
202pub(crate) fn deserialize_datetime<'de, D: Deserializer<'de>>(deserializer: D) -> Result<NaiveDateTime, D::Error> {
203	let string = String::deserialize(deserializer)?;
204	let fmt = match (string.ends_with('Z'), string.contains('.')) {
205		(false, false) => "%FT%TZ%#z",
206		(false, true) => "%FT%TZ%.3f%#z",
207		(true, false) => "%FT%TZ",
208		(true, true) => "%FT%T%.3fZ",
209	};
210	NaiveDateTime::parse_from_str(&string, fmt).map_err(D::Error::custom)
211}
212
213/// # Errors
214/// 1. If a string cannot be deserialized
215/// 2. If the data does not appear in the format of `/(?:<t parser here>,)*<t parser here>?/g`
216pub(crate) fn deserialize_comma_separated_vec<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Vec<T>, D::Error>
217where
218	<T as FromStr>::Err: Debug,
219{
220	String::deserialize(deserializer)?
221		.split(", ")
222		.map(|entry| T::from_str(entry))
223		.collect::<Result<Vec<T>, <T as FromStr>::Err>>()
224		.map_err(|e| Error::custom(format!("{e:?}")))
225}
226
227#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
228pub struct HomeAwaySplits<T> {
229	pub home: T,
230	pub away: T,
231}
232
233impl<T> HomeAwaySplits<T> {
234	#[must_use]
235	pub const fn new(home: T, away: T) -> Self {
236		Self { home, away }
237	}
238}
239
240impl<T: Add> HomeAwaySplits<T> {
241	#[must_use]
242	pub fn combined(self) -> <T as Add>::Output {
243		self.home + self.away
244	}
245}
246
247#[derive(Debug, Deserialize, PartialEq, Clone)]
248#[serde(rename_all = "camelCase")]
249pub struct Location {
250	pub address_line_1: Option<String>,
251	pub address_line_2: Option<String>,
252	pub address_line_3: Option<String>,
253	pub address_line_4: Option<String>,
254	pub attention: Option<String>,
255	pub phone_number: Option<String>,
256	pub city: Option<String>,
257	pub state: Option<String>,
258	pub country: Option<String>,
259	#[serde(rename = "stateAbbrev")] pub state_abbreviation: Option<String>,
260	pub postal_code: Option<String>,
261	pub latitude: Option<f64>,
262	pub longitude: Option<f64>,
263	pub azimuth_angle: Option<f64>,
264	pub elevation: Option<u32>,
265}
266
267impl Eq for Location {}
268
269#[derive(Debug, Copy, Clone)]
270pub enum IntegerOrFloatStat {
271	Integer(i64),
272	Float(f64),
273}
274
275impl PartialEq for IntegerOrFloatStat {
276	fn eq(&self, other: &Self) -> bool {
277		match (*self, *other) {
278			(Self::Integer(lhs), Self::Integer(rhs)) => lhs == rhs,
279			(Self::Float(lhs), Self::Float(rhs)) => lhs == rhs,
280
281			#[allow(clippy::cast_precision_loss, reason = "we checked if it's perfectly representable")]
282			#[allow(clippy::cast_possible_truncation, reason = "we checked if it's perfectly representable")]
283			(Self::Integer(int), Self::Float(float)) | (Self::Float(float), Self::Integer(int)) => {
284				// fast way to check if the float is representable perfectly as an integer and if it's within range of `i64`
285				// we inline the f64 casts of i64::MIN and i64::MAX, and change the upper bound to be < as i64::MAX is not perfectly representable.
286				if float.is_normal() && float.floor() == float && (i64::MIN as f64..-(i64::MIN as f64)).contains(&float) {
287					float as i64 == int
288				} else {
289					false
290				}
291			},
292		}
293	}
294}
295
296impl Eq for IntegerOrFloatStat {}
297
298impl<'de> Deserialize<'de> for IntegerOrFloatStat {
299	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
300	where
301		D: Deserializer<'de>
302	{
303		struct Visitor;
304
305		impl serde::de::Visitor<'_> for Visitor {
306			type Value = IntegerOrFloatStat;
307
308			fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
309				formatter.write_str("integer, float, or string that can be parsed to either")
310			}
311
312			fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
313			where
314				E: Error,
315			{
316				Ok(IntegerOrFloatStat::Integer(v))
317			}
318
319			fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
320			where
321				E: Error,
322			{
323				Ok(IntegerOrFloatStat::Float(v))
324			}
325
326			fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
327			where
328				E: Error,
329			{
330				if v == "-.--" || v == ".---" {
331					Ok(IntegerOrFloatStat::Float(0.0))
332				} else if let Ok(i) = v.parse::<i64>() {
333					Ok(IntegerOrFloatStat::Integer(i))
334				} else if let Ok(f) = v.parse::<f64>() {
335					Ok(IntegerOrFloatStat::Float(f))
336				} else {
337					Err(E::invalid_value(serde::de::Unexpected::Str(v), &self))
338				}
339			}
340		}
341
342		deserializer.deserialize_any(Visitor)
343	}
344}
345
346#[derive(Debug, Deserialize, Display)]
347#[display("An error occurred parsing the statsapi http request: {message}")]
348pub struct MLBError {
349	message: String,
350}
351
352impl std::error::Error for MLBError {}
353
354#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Default)]
355#[serde(try_from = "&str")]
356pub struct RGBAColor {
357	pub red: u8,
358	pub green: u8,
359	pub blue: u8,
360	pub alpha: u8,
361}
362
363impl Display for RGBAColor {
364	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
365		write!(f, "0x{:02x}{:02x}{:02x}{:02x}", self.alpha, self.red, self.green, self.blue)
366	}
367}
368
369#[derive(Debug, Error)]
370pub enum RGBAColorFromStrError {
371	#[error("Invalid spec")]
372	InvalidFormat,
373	#[error(transparent)]
374	InvalidInt(#[from] ParseIntError),
375	#[error(transparent)]
376	InvalidFloat(#[from] ParseFloatError),
377}
378
379impl<'a> TryFrom<&'a str> for RGBAColor {
380	type Error = <Self as FromStr>::Err;
381
382	fn try_from(value: &'a str) -> Result<Self, Self::Error> {
383		<Self as FromStr>::from_str(value)
384	}
385}
386
387impl FromStr for RGBAColor {
388	type Err = RGBAColorFromStrError;
389
390	#[allow(clippy::single_char_pattern, reason = "other patterns are strings, the choice to make that one a char does not denote any special case")]
391	#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "intended behaviour with alpha channel")]
392	fn from_str(mut s: &str) -> Result<Self, Self::Err> {
393		s = s.strip_suffix("rgba(").ok_or(Self::Err::InvalidFormat)?;
394		let (red, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
395		let red = red.parse::<u8>()?;
396		let (green, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
397		let green = green.parse::<u8>()?;
398		let (blue, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
399		let blue = blue.parse::<u8>()?;
400		let (alpha, s) = s.split_once(")").ok_or(Self::Err::InvalidFormat)?;
401		let alpha = (alpha.parse::<f32>()? * 255.0).round() as u8;
402		if !s.is_empty() { return Err(Self::Err::InvalidFormat); }
403		Ok(Self {
404			red,
405			green,
406			blue,
407			alpha
408		})
409	}
410}
411
412#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, FromStr)]
413#[serde(try_from = "&str")]
414pub enum SimpleTemperature {
415	Hot,
416	Warm,
417	Lukewarm,
418	Cool,
419	Cold,
420}
421
422impl<'a> TryFrom<&'a str> for SimpleTemperature {
423	type Error = <Self as FromStr>::Err;
424
425	fn try_from(value: &'a str) -> Result<Self, Self::Error> {
426		<Self as FromStr>::from_str(value)
427	}
428}
429
430#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, FromStr)]
431#[serde(try_from = "&str")]
432pub enum DayHalf {
433	AM,
434	PM,
435}
436
437impl DayHalf {
438	#[must_use]
439	pub fn into_24_hour_time(self, mut time: NaiveTime) -> NaiveTime {
440		if (self == Self::PM) ^ (time.hour() == 12) {
441			time += TimeDelta::hours(12);
442		}
443
444		time
445	}
446}
447
448impl<'a> TryFrom<&'a str> for DayHalf {
449	type Error = <Self as FromStr>::Err;
450
451	fn try_from(value: &'a str) -> Result<Self, Self::Error> {
452		<Self as FromStr>::from_str(value)
453	}
454}
455
456#[cfg(test)]
457mod tests {
458	use super::*;
459
460	#[test]
461	fn test_ampm() {
462		assert_eq!(NaiveTime::from_hms_opt(0, 0, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
463		assert_eq!(NaiveTime::from_hms_opt(12, 0, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
464		assert_eq!(NaiveTime::from_hms_opt(0, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
465		assert_eq!(NaiveTime::from_hms_opt(12, 1, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
466		assert_eq!(NaiveTime::from_hms_opt(0, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
467		assert_eq!(NaiveTime::from_hms_opt(23, 59, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(11, 59, 0).unwrap()));
468		assert_eq!(NaiveTime::from_hms_opt(1, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(1, 1, 0).unwrap()));
469	}
470}