Skip to main content

dpp_rules/common/
numeric.rs

1//! Shared numeric helpers: percentage clamping, sum checks, thresholds.
2
3/// Whether `pct` is a valid percentage value: finite and in `[0.0, 100.0]`.
4#[must_use]
5pub fn percentage_in_range(pct: f64) -> bool {
6    pct.is_finite() && (0.0..=100.0).contains(&pct)
7}
8
9/// Whether the values yielded by `values` sum to `target` within `±
10/// tolerance` (percentage points). Returns the computed sum alongside the
11/// verdict, since callers reporting a failure need it in their message. A
12/// non-finite sum (e.g. one input was NaN) is always `false`.
13#[must_use]
14pub fn sums_to(values: impl IntoIterator<Item = f64>, target: f64, tolerance: f64) -> (bool, f64) {
15    let total: f64 = values.into_iter().sum();
16    let within_tolerance = total.is_finite() && (total - target).abs() <= tolerance;
17    (within_tolerance, total)
18}
19
20#[cfg(test)]
21mod tests {
22    use super::*;
23
24    #[test]
25    fn percentage_in_range_accepts_boundaries() {
26        assert!(percentage_in_range(0.0));
27        assert!(percentage_in_range(100.0));
28        assert!(percentage_in_range(50.0));
29    }
30
31    #[test]
32    fn percentage_in_range_rejects_out_of_bounds_and_non_finite() {
33        assert!(!percentage_in_range(-0.001));
34        assert!(!percentage_in_range(100.001));
35        assert!(!percentage_in_range(f64::NAN));
36        assert!(!percentage_in_range(f64::INFINITY));
37    }
38
39    #[test]
40    fn sums_to_within_tolerance() {
41        let (ok, total) = sums_to([60.0, 40.0], 100.0, 2.0);
42        assert!(ok);
43        assert!((total - 100.0).abs() < f64::EPSILON);
44
45        let (ok, total) = sums_to([98.5, 1.0], 100.0, 2.0);
46        assert!(ok, "99.5 is within ±2 of 100");
47        assert!((total - 99.5).abs() < f64::EPSILON);
48    }
49
50    #[test]
51    fn sums_to_outside_tolerance() {
52        let (ok, total) = sums_to([60.0, 30.0], 100.0, 2.0);
53        assert!(!ok);
54        assert!((total - 90.0).abs() < f64::EPSILON);
55    }
56
57    #[test]
58    fn sums_to_non_finite_input_is_never_ok() {
59        let (ok, total) = sums_to([f64::NAN, 50.0], 100.0, 2.0);
60        assert!(!ok);
61        assert!(total.is_nan());
62    }
63
64    #[test]
65    fn sums_to_empty_is_zero() {
66        let (ok, total) = sums_to([], 100.0, 2.0);
67        assert!(!ok);
68        assert_eq!(total, 0.0);
69    }
70}