1use 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
54pub 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
60pub 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
70pub 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
199pub(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
213pub(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 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}