Skip to main content

matchmaker/utils/
percentage.rs

1use std::fmt;
2
3use cli_boilerplate_automation::define_restricted_wrapper;
4use serde::{Deserialize, Deserializer};
5
6define_restricted_wrapper!(
7    #[derive(Clone, Copy, serde::Serialize, PartialOrd, Eq, Ord)]
8    #[serde(transparent)]
9    Percentage: u16 = 100
10);
11impl Percentage {
12    pub fn new(value: u16) -> Self {
13        if value <= 100 { Self(value) } else { Self(100) }
14    }
15
16    /// Rounds up
17    pub fn compute_clamped(&self, total: u16, min: u16, max: u16) -> u16 {
18        let pct_height = (total * self.inner()).div_ceil(100);
19        pct_height.clamp(min, if max == 0 { total } else { max })
20    }
21
22    pub fn complement(&self) -> Self {
23        Self(100 - self.0)
24    }
25
26    pub fn saturating_sub(&self, other: u16) -> Self {
27        Self(self.0.saturating_sub(other))
28    }
29}
30
31impl fmt::Display for Percentage {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(f, "{}%", self.0)
34    }
35}
36
37impl TryFrom<u16> for Percentage {
38    type Error = String;
39
40    fn try_from(value: u16) -> Result<Self, Self::Error> {
41        if value > 100 {
42            Err(format!("Percentage out of range: {}", value))
43        } else {
44            Ok(Self::new(value))
45        }
46    }
47}
48impl<'de> Deserialize<'de> for Percentage {
49    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50    where
51        D: Deserializer<'de>,
52    {
53        let v = u16::deserialize(deserializer)?;
54        v.try_into().map_err(serde::de::Error::custom)
55    }
56}
57impl std::str::FromStr for Percentage {
58    type Err = String;
59
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        let s = s.trim_end_matches('%');
62        let v: u16 = s
63            .parse()
64            .map_err(|e: std::num::ParseIntError| format!("Invalid number: {}", e))?;
65        v.try_into()
66    }
67}