Skip to main content

brk/
acceptance.rs

1use std::{fmt, str::FromStr};
2
3/// Exact required success fraction. Decisions never use floating-point rounding.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub struct Acceptance {
6    numerator: u64,
7    denominator: u64,
8}
9
10impl Acceptance {
11    pub fn new(
12        numerator: u64,
13        denominator: u64,
14    ) -> Result<Self, &'static str> {
15        if numerator == 0 || denominator == 0 || numerator > denominator {
16            return Err(
17                "acceptance must be greater than 0 and at most 100 percent",
18            );
19        }
20        Ok(Self { numerator, denominator })
21    }
22
23    pub fn accepts(
24        self,
25        successful: u64,
26        attempted: u64,
27    ) -> bool {
28        attempted > 0
29            && successful <= attempted
30            && u128::from(successful) * u128::from(self.denominator)
31                >= u128::from(attempted) * u128::from(self.numerator)
32    }
33}
34
35impl FromStr for Acceptance {
36    type Err = &'static str;
37
38    fn from_str(input: &str) -> Result<Self, Self::Err> {
39        let (whole, fractional) = input.split_once('.').unwrap_or((input, ""));
40        if whole.is_empty()
41            || !whole.bytes().all(|b| b.is_ascii_digit())
42            || !fractional.bytes().all(|b| b.is_ascii_digit())
43        {
44            return Err("acceptance must be a decimal percentage");
45        }
46        let precision = u32::try_from(fractional.len())
47            .map_err(|_| "acceptance precision is too large")?;
48        let scale = 10_u64
49            .checked_pow(precision)
50            .ok_or("acceptance precision is too large")?;
51        let whole = whole
52            .parse::<u64>()
53            .map_err(|_| "invalid acceptance percentage")?;
54        let fraction = if fractional.is_empty() {
55            0
56        } else {
57            fractional
58                .parse::<u64>()
59                .map_err(|_| "invalid acceptance percentage")?
60        };
61        let numerator = whole
62            .checked_mul(scale)
63            .and_then(|v| v.checked_add(fraction))
64            .ok_or("acceptance percentage overflows")?;
65        let denominator = scale
66            .checked_mul(100)
67            .ok_or("acceptance precision is too large")?;
68        Self::new(numerator, denominator)
69    }
70}
71
72impl TryFrom<f64> for Acceptance {
73    type Error = &'static str;
74
75    fn try_from(value: f64) -> Result<Self, Self::Error> {
76        if !value.is_finite() {
77            return Err("acceptance must be a finite percentage");
78        }
79        value.to_string().parse()
80    }
81}
82
83impl fmt::Display for Acceptance {
84    fn fmt(
85        &self,
86        f: &mut fmt::Formatter<'_>,
87    ) -> fmt::Result {
88        write!(
89            f,
90            "{:.4}%",
91            self.numerator as f64 * 100.0 / self.denominator as f64
92        )
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn acceptance_observes_exact_boundary_without_rounding() {
102        let acceptance: Acceptance = "99.99".parse().unwrap();
103        assert!(acceptance.accepts(9_999, 10_000));
104        assert!(!acceptance.accepts(9_998, 10_000));
105        assert!(!acceptance.accepts(4_999, 5_000));
106        assert!(!acceptance.accepts(0, 0));
107        assert!(acceptance.accepts(u64::MAX, u64::MAX));
108    }
109
110    #[test]
111    fn percentages_are_validated_before_traffic() {
112        assert!("100".parse::<Acceptance>().unwrap().accepts(1, 1));
113        for invalid in ["0", "100.01", "-1", "NaN", "99.9.9", ".99"] {
114            assert!(invalid.parse::<Acceptance>().is_err(), "{invalid}");
115        }
116    }
117}