Skip to main content

mlb_api/requests/stats/
units.rs

1use derive_more::{Add, Deref, DerefMut, From, Into};
2use serde::de::{Error, Visitor};
3use serde::{Deserialize, Deserializer};
4use std::fmt::{Debug, Display, Formatter};
5use std::ops::{Add, AddAssign};
6use std::str::FromStr;
7use thiserror::Error;
8
9#[derive(Deref, DerefMut, From, Add, Copy, Clone, Into)]
10pub struct ThreeDecimalPlaceRateStat(f64);
11
12impl ThreeDecimalPlaceRateStat {
13	pub const NIL: Self = Self(f64::NAN);
14
15	#[must_use]
16	pub const fn new(inner: f64) -> Self {
17		Self(inner)
18	}
19}
20
21impl FromStr for ThreeDecimalPlaceRateStat {
22	type Err = <f64 as FromStr>::Err;
23
24	fn from_str(s: &str) -> Result<Self, Self::Err> {
25		if s == ".---" {
26			Ok(Self(f64::NAN))
27		} else {
28			Ok(Self(f64::from_str(s)?))
29		}
30	}
31}
32
33impl<'de> Deserialize<'de> for ThreeDecimalPlaceRateStat {
34	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
35	where
36		D: Deserializer<'de>
37	{
38		struct StatVisitor;
39
40		impl Visitor<'_> for StatVisitor {
41			type Value = ThreeDecimalPlaceRateStat;
42
43			fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
44				formatter.write_str("a float or string or .---")
45			}
46
47			fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> {
48				Ok(ThreeDecimalPlaceRateStat::new(v))
49			}
50
51			fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
52			where
53				E: Error,
54			{
55				ThreeDecimalPlaceRateStat::from_str(v).map_err(E::custom)
56			}
57		}
58
59		deserializer.deserialize_any(StatVisitor)
60	}
61}
62
63impl PartialEq for ThreeDecimalPlaceRateStat {
64	fn eq(&self, other: &Self) -> bool {
65		self.0 == other.0 || self.is_nan() && other.is_nan()
66	}
67}
68
69impl Default for ThreeDecimalPlaceRateStat {
70	fn default() -> Self {
71		Self(f64::NAN)
72	}
73}
74
75impl Display for ThreeDecimalPlaceRateStat {
76	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
77		if self.0.is_nan() {
78			write!(f, ".---")
79		} else {
80			write!(f, "{}", format!("{:.3}", self.0).trim_start_matches('0'))
81		}
82	}
83}
84
85impl Debug for ThreeDecimalPlaceRateStat {
86	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
87		<Self as Display>::fmt(self, f)
88	}
89}
90
91#[derive(Deref, DerefMut, From, Add, PartialEq, Copy, Clone)]
92pub struct PercentageStat(f64);
93
94impl PercentageStat {
95	pub const NIL: Self = Self(f64::NAN);
96
97	#[must_use]
98	pub const fn new(inner: f64) -> Self {
99		Self(inner)
100	}
101}
102
103impl<'de> Deserialize<'de> for PercentageStat {
104	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105	where
106		D: Deserializer<'de>
107	{
108		struct PercentageStatVisitor;
109
110		impl Visitor<'_> for PercentageStatVisitor {
111			type Value = PercentageStat;
112
113			fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
114				formatter.write_str("Percentage")
115			}
116
117			fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
118			where
119				E: Error,
120			{
121				if !v.contains(|c: char| c.is_ascii_digit()) {
122					return Ok(PercentageStat::NIL)
123				}
124
125				Ok(PercentageStat::new(v.parse::<f64>().map_err(E::custom)? / 100.0))
126			}
127
128			fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
129				Ok(PercentageStat::new(v / 100.0))
130			}
131
132			#[allow(clippy::cast_lossless, reason = "needlessly pedantic")]
133			fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
134			where
135				E: Error,
136			{
137				Ok(PercentageStat::new(v as f64 / 100.0))
138			}
139		}
140
141		deserializer.deserialize_any(PercentageStatVisitor)
142	}
143}
144
145impl Display for PercentageStat {
146	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147		if self.is_nan() {
148			write!(f, "--.-%")
149		} else {
150			write!(f, "{:.2}%", self.0 * 100.0)
151		}
152	}
153}
154
155impl Debug for PercentageStat {
156	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157		if self.is_nan() {
158			write!(f, "--.-%")
159		} else {
160			write!(f, "{}%", self.0 * 100.0)
161		}
162	}
163}
164
165impl Default for PercentageStat {
166	fn default() -> Self {
167		Self::NIL
168	}
169}
170
171#[derive(Deref, DerefMut, From, Add, Copy, Clone)]
172pub struct TwoDecimalPlaceRateStat(f64);
173
174impl TwoDecimalPlaceRateStat {
175	pub const NIL: Self = Self(f64::NAN);
176
177	#[must_use]
178	pub const fn new(inner: f64) -> Self {
179		Self(inner)
180	}
181}
182
183impl<'de> Deserialize<'de> for TwoDecimalPlaceRateStat {
184	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185	where
186		D: Deserializer<'de>
187	{
188		struct StatVisitor;
189
190		impl Visitor<'_> for StatVisitor {
191			type Value = TwoDecimalPlaceRateStat;
192
193			fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
194				formatter.write_str("a float or string or -.--")
195			}
196
197			fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> {
198				Ok(TwoDecimalPlaceRateStat::new(v))
199			}
200
201			fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
202			where
203				E: Error,
204			{
205				TwoDecimalPlaceRateStat::from_str(v).map_err(E::custom)
206			}
207		}
208
209		deserializer.deserialize_any(StatVisitor)
210	}
211}
212
213impl FromStr for TwoDecimalPlaceRateStat {
214	type Err = <f64 as FromStr>::Err;
215
216	fn from_str(s: &str) -> Result<Self, Self::Err> {
217		if s == "-.--" {
218			Ok(Self(f64::NAN))
219		} else {
220			Ok(Self(f64::from_str(s)?))
221		}
222	}
223}
224
225impl PartialEq for TwoDecimalPlaceRateStat {
226	fn eq(&self, other: &Self) -> bool {
227		self.0 == other.0 || self.is_nan() && other.is_nan()
228	}
229}
230
231impl Display for TwoDecimalPlaceRateStat {
232	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
233		if self.is_nan() {
234			write!(f, "-.--")
235		} else {
236			write!(f, "{:.2}", self.0)
237		}
238	}
239}
240
241impl Debug for TwoDecimalPlaceRateStat {
242	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
243		<Self as Display>::fmt(self, f)
244	}
245}
246
247impl Default for TwoDecimalPlaceRateStat {
248	fn default() -> Self {
249		Self::NIL
250	}
251}
252
253#[derive(Debug, Copy, Clone, PartialEq, Default)]
254pub struct InningsPitched {
255	major: u32,
256	minor: u8,
257}
258
259impl<'de> Deserialize<'de> for InningsPitched {
260	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
261	where
262		D: Deserializer<'de>
263	{
264		String::deserialize(deserializer)?.parse::<Self>().map_err(Error::custom)
265	}
266}
267
268impl Add for InningsPitched {
269	type Output = Self;
270
271	fn add(self, rhs: Self) -> Self::Output {
272		Self::from_outs(self.as_outs() + rhs.as_outs())
273	}
274}
275
276impl AddAssign for InningsPitched {
277	fn add_assign(&mut self, rhs: Self) {
278		*self = *self + rhs;
279	}
280}
281
282impl InningsPitched {
283	#[must_use]
284	pub const fn from_outs(outs: u32) -> Self {
285		Self {
286			major: outs / 3,
287			minor: (outs % 3) as u8,
288		}
289	}
290
291	#[must_use]
292	pub const fn new(whole_innings: u32, outs: u8) -> Self {
293		Self { major: whole_innings, minor: outs }
294	}
295
296	#[must_use]
297	pub fn as_fraction(self) -> f64 {
298		self.into()
299	}
300
301	#[must_use]
302	pub const fn as_outs(self) -> u32 {
303		self.major * 3 + self.minor as u32
304	}
305}
306
307impl From<InningsPitched> for f64 {
308
309	#[allow(clippy::cast_lossless, reason = "needlessly pedantic")]
310	fn from(value: InningsPitched) -> Self {
311		value.major as Self + value.minor as Self / 3.0
312	}
313}
314
315impl From<f64> for InningsPitched {
316	#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "needlessly pedantic")]
317	fn from(value: f64) -> Self {
318		let value = value.max(0.0);
319		let integer = value.trunc();
320		let fractional = value - integer;
321		let major = integer as u32;
322		let minor = fractional as u8;
323		Self { major, minor }
324	}
325}
326
327impl Display for InningsPitched {
328	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
329		write!(f, "{}.{}", self.major, self.minor)
330	}
331}
332
333#[derive(Debug, Error)]
334pub enum InningsPitchedFromStrError {
335	#[error("No . separator was present")]
336	NoSeparator,
337	#[error("Invalid whole inning quantity: {0}")]
338	InvalidWholeInningsQuantity(String),
339	#[error("Invalid inning out quantity: {0}")]
340	InvalidOutsQuantity(String),
341}
342
343impl FromStr for InningsPitched {
344	type Err = InningsPitchedFromStrError;
345
346	fn from_str(s: &str) -> Result<Self, Self::Err> {
347		let (major, minor) = s.split_once('.').ok_or(InningsPitchedFromStrError::NoSeparator)?;
348		let whole_innings = major.parse::<u32>().map_err(|_| InningsPitchedFromStrError::InvalidWholeInningsQuantity(major.to_owned()))?;
349		let Ok(outs @ 0..3) = minor.parse::<u8>() else { return Err(InningsPitchedFromStrError::InvalidOutsQuantity(minor.to_owned())) };
350		Ok(Self::new(whole_innings, outs))
351	}
352}
353
354#[derive(Deref, DerefMut, From, Add, Copy, Clone)]
355pub struct PlusStat(f64);
356
357impl PlusStat {
358	#[must_use]
359	pub const fn new(x: f64) -> Self {
360		Self(x)
361	}
362}
363
364impl Display for PlusStat {
365	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
366		if self.is_nan() {
367			write!(f, "-")
368		} else {
369			write!(f, "{:.0}", self.0.round())
370		}
371	}
372}
373
374/// Ex: Hits
375pub type CountingStat = u32;
376
377#[derive(Deserialize, PartialEq, Copy, Clone, Deref, DerefMut)]
378pub struct FloatCountingStat<const N: usize>(f64);
379
380impl<const N: usize> Add for FloatCountingStat<N> {
381	type Output = Self;
382
383	fn add(self, rhs: Self) -> Self::Output {
384		Self(self.0 + rhs.0)
385	}
386}
387
388impl<const N: usize> AddAssign for FloatCountingStat<N> {
389	fn add_assign(&mut self, rhs: Self) {
390		self.0 += rhs.0;
391	}
392}
393
394impl<const N: usize> FloatCountingStat<N> {
395	#[must_use]
396	pub const fn new(x: f64) -> Self {
397		Self(x)
398	}
399}
400
401impl<const N: usize> Display for FloatCountingStat<N> {
402	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
403		if self.0.is_nan() {
404			write!(f, "{:.N$}", "")
405		} else {
406			write!(f, "{:->N$}", self.0)
407		}
408	}
409}
410
411impl<const N: usize> Debug for FloatCountingStat<N> {
412	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
413		<Self as Display>::fmt(self, f)
414	}
415}