Skip to main content

mlb_api/requests/stats/
units.rs

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