use derive_more::{Add, Deref, DerefMut, From, Into};
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer};
use std::fmt::{Debug, Display, Formatter};
use std::ops::{Add, AddAssign};
use std::str::FromStr;
use thiserror::Error;
#[derive(Deref, DerefMut, From, Add, Copy, Clone, Into)]
pub struct ThreeDecimalPlaceRateStat(f64);
impl ThreeDecimalPlaceRateStat {
pub const NIL: Self = Self(f64::NAN);
#[must_use]
pub const fn new(inner: f64) -> Self {
Self(inner)
}
}
impl FromStr for ThreeDecimalPlaceRateStat {
type Err = <f64 as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == ".---" {
Ok(Self(f64::NAN))
} else {
Ok(Self(f64::from_str(s)?))
}
}
}
impl<'de> Deserialize<'de> for ThreeDecimalPlaceRateStat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
struct StatVisitor;
impl Visitor<'_> for StatVisitor {
type Value = ThreeDecimalPlaceRateStat;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a float or string or .---")
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> {
Ok(ThreeDecimalPlaceRateStat::new(v))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
ThreeDecimalPlaceRateStat::from_str(v).map_err(E::custom)
}
}
deserializer.deserialize_any(StatVisitor)
}
}
impl PartialEq for ThreeDecimalPlaceRateStat {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0 || self.is_nan() && other.is_nan()
}
}
impl Default for ThreeDecimalPlaceRateStat {
fn default() -> Self {
Self(f64::NAN)
}
}
impl Display for ThreeDecimalPlaceRateStat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.0.is_nan() {
write!(f, ".---")
} else {
write!(f, "{}", format!("{:.3}", self.0).trim_start_matches('0'))
}
}
}
impl Debug for ThreeDecimalPlaceRateStat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(self, f)
}
}
#[derive(Deref, DerefMut, From, Add, PartialEq, Copy, Clone)]
pub struct PercentageStat(f64);
impl PercentageStat {
pub const NIL: Self = Self(f64::NAN);
#[must_use]
pub const fn new(inner: f64) -> Self {
Self(inner)
}
}
impl<'de> Deserialize<'de> for PercentageStat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
struct PercentageStatVisitor;
impl Visitor<'_> for PercentageStatVisitor {
type Value = PercentageStat;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("Percentage")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
if !v.contains(|c: char| c.is_ascii_digit()) {
return Ok(PercentageStat::NIL)
}
Ok(PercentageStat::new(v.parse::<f64>().map_err(E::custom)? / 100.0))
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
Ok(PercentageStat::new(v / 100.0))
}
#[allow(clippy::cast_lossless, reason = "needlessly pedantic")]
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
where
E: Error,
{
Ok(PercentageStat::new(v as f64 / 100.0))
}
}
deserializer.deserialize_any(PercentageStatVisitor)
}
}
impl Display for PercentageStat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_nan() {
write!(f, "--.-%")
} else {
write!(f, "{:.2}%", self.0 * 100.0)
}
}
}
impl Debug for PercentageStat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_nan() {
write!(f, "--.-%")
} else {
write!(f, "{}%", self.0 * 100.0)
}
}
}
impl Default for PercentageStat {
fn default() -> Self {
Self::NIL
}
}
#[derive(Deref, DerefMut, From, Add, Copy, Clone)]
pub struct TwoDecimalPlaceRateStat(f64);
impl TwoDecimalPlaceRateStat {
pub const NIL: Self = Self(f64::NAN);
#[must_use]
pub const fn new(inner: f64) -> Self {
Self(inner)
}
}
impl<'de> Deserialize<'de> for TwoDecimalPlaceRateStat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
struct StatVisitor;
impl Visitor<'_> for StatVisitor {
type Value = TwoDecimalPlaceRateStat;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a float or string or -.--")
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> {
Ok(TwoDecimalPlaceRateStat::new(v))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
TwoDecimalPlaceRateStat::from_str(v).map_err(E::custom)
}
}
deserializer.deserialize_any(StatVisitor)
}
}
impl FromStr for TwoDecimalPlaceRateStat {
type Err = <f64 as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-.--" {
Ok(Self(f64::NAN))
} else {
Ok(Self(f64::from_str(s)?))
}
}
}
impl PartialEq for TwoDecimalPlaceRateStat {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0 || self.is_nan() && other.is_nan()
}
}
impl Display for TwoDecimalPlaceRateStat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_nan() {
write!(f, "-.--")
} else {
write!(f, "{:.2}", self.0)
}
}
}
impl Debug for TwoDecimalPlaceRateStat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(self, f)
}
}
impl Default for TwoDecimalPlaceRateStat {
fn default() -> Self {
Self::NIL
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub struct InningsPitched {
major: u32,
minor: u8,
}
impl<'de> Deserialize<'de> for InningsPitched {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
String::deserialize(deserializer)?.parse::<Self>().map_err(Error::custom)
}
}
impl Add for InningsPitched {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::from_outs(self.as_outs() + rhs.as_outs())
}
}
impl AddAssign for InningsPitched {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
impl InningsPitched {
#[must_use]
pub const fn from_outs(outs: u32) -> Self {
Self {
major: outs / 3,
minor: (outs % 3) as u8,
}
}
#[must_use]
pub const fn new(whole_innings: u32, outs: u8) -> Self {
Self { major: whole_innings, minor: outs }
}
#[must_use]
pub fn as_fraction(self) -> f64 {
self.into()
}
#[must_use]
pub const fn as_outs(self) -> u32 {
self.major * 3 + self.minor as u32
}
}
impl From<InningsPitched> for f64 {
#[allow(clippy::cast_lossless, reason = "needlessly pedantic")]
fn from(value: InningsPitched) -> Self {
value.major as Self + value.minor as Self / 3.0
}
}
impl From<f64> for InningsPitched {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "needlessly pedantic")]
fn from(value: f64) -> Self {
let value = value.max(0.0);
let integer = value.trunc();
let fractional = value - integer;
let major = integer as u32;
let minor = fractional as u8;
Self { major, minor }
}
}
impl Display for InningsPitched {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
#[derive(Debug, Error)]
pub enum InningsPitchedFromStrError {
#[error("No . separator was present")]
NoSeparator,
#[error("Invalid whole inning quantity: {0}")]
InvalidWholeInningsQuantity(String),
#[error("Invalid inning out quantity: {0}")]
InvalidOutsQuantity(String),
}
impl FromStr for InningsPitched {
type Err = InningsPitchedFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (major, minor) = s.split_once('.').ok_or(InningsPitchedFromStrError::NoSeparator)?;
let whole_innings = major.parse::<u32>().map_err(|_| InningsPitchedFromStrError::InvalidWholeInningsQuantity(major.to_owned()))?;
let Ok(outs @ 0..3) = minor.parse::<u8>() else { return Err(InningsPitchedFromStrError::InvalidOutsQuantity(minor.to_owned())) };
Ok(Self::new(whole_innings, outs))
}
}
#[derive(Deref, DerefMut, From, Add, Copy, Clone)]
pub struct PlusStat(f64);
impl PlusStat {
#[must_use]
pub const fn new(x: f64) -> Self {
Self(x)
}
}
impl Display for PlusStat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_nan() {
write!(f, "-")
} else {
write!(f, "{:.0}", self.0.round())
}
}
}
pub type CountingStat = u32;
#[derive(Deserialize, PartialEq, Copy, Clone, Deref, DerefMut, Default)]
pub struct FloatCountingStat<const N: usize>(f64);
impl<const N: usize> Add for FloatCountingStat<N> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl<const N: usize> AddAssign for FloatCountingStat<N> {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl<const N: usize> FloatCountingStat<N> {
#[must_use]
pub const fn new(x: f64) -> Self {
Self(x)
}
}
impl<const N: usize> Display for FloatCountingStat<N> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.0.is_nan() {
write!(f, "{:.N$}", "")
} else {
write!(f, "{:->N$}", self.0)
}
}
}
impl<const N: usize> Debug for FloatCountingStat<N> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(self, f)
}
}