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, Eq, 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
374pub 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}