matchmaker/utils/
percentage.rs

1use std::fmt;
2
3use cli_boilerplate_automation::impl_restricted_wrapper;
4use serde::{Deserialize, Deserializer};
5
6impl_restricted_wrapper!(Percentage, u16, 100);
7impl Percentage {
8    pub fn new(value: u16) -> Self {
9        if value <= 100 { Self(value) } else { Self(100) }
10    }
11
12    pub fn compute_with_max(&self, total: u16, max: u16) -> u16 {
13        let pct_height = (total * self.inner()).div_ceil(100);
14        let max_height = if max == 0 { total } else { max };
15        pct_height.min(max_height)
16    }
17}
18
19impl fmt::Display for Percentage {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        write!(f, "{}%", self.0)
22    }
23}
24
25impl TryFrom<u16> for Percentage {
26    type Error = String;
27
28    fn try_from(value: u16) -> Result<Self, Self::Error> {
29        if value > 100 {
30            Err(format!("Percentage out of range: {}", value))
31        } else {
32            Ok(Self::new(value))
33        }
34    }
35}
36impl<'de> Deserialize<'de> for Percentage {
37    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38    where
39        D: Deserializer<'de>,
40    {
41        let v = u16::deserialize(deserializer)?;
42        v.try_into().map_err(serde::de::Error::custom)
43    }
44}
45impl std::str::FromStr for Percentage {
46    type Err = String;
47
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        let s = s.trim_end_matches('%'); // allow optional trailing '%'
50        let v: u16 = s
51            .parse()
52            .map_err(|e: std::num::ParseIntError| format!("Invalid number: {}", e))?;
53        v.try_into()
54    }
55}