1use std::{fmt, str::FromStr};
2
3#[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}