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
376pub 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}