Skip to main content

monitrs_core/units/
rate.rs

1//! Per-second rates derived from cumulative counters.
2
3use core::fmt;
4
5/// A non-negative, finite "units per second" value.
6///
7/// The unit itself (bytes, packets, operations) is implied by the field the rate
8/// is stored in. Rates are calculated, so floating point is permitted (§10.4).
9///
10/// A rate can only be constructed from a *validated* delta: [`Rate::new`]
11/// rejects negatives, so a counter reset cannot silently become a huge or
12/// negative rate (§8.2).
13#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(transparent))]
16pub struct Rate(f64);
17
18impl Rate {
19    /// Zero units per second.
20    pub const ZERO: Self = Self(0.0);
21
22    /// Builds a rate, rejecting NaN, infinities, and negative values.
23    #[must_use]
24    pub fn new(per_second: f64) -> Option<Self> {
25        if per_second.is_finite() && per_second >= 0.0 {
26            Some(Self(per_second))
27        } else {
28            None
29        }
30    }
31
32    /// Builds a rate from a validated non-negative delta and the *actual*
33    /// elapsed time.
34    ///
35    /// Never assume a one-second interval: suspend/resume, load, and scheduler
36    /// delay all make the real interval variable (§8.1). Returns `None` when
37    /// `elapsed` is zero or the result is not finite.
38    #[must_use]
39    pub fn from_delta(delta: u64, elapsed: core::time::Duration) -> Option<Self> {
40        let seconds = elapsed.as_secs_f64();
41        if seconds <= 0.0 {
42            return None;
43        }
44        Self::new(delta as f64 / seconds)
45    }
46
47    /// The underlying units-per-second value.
48    #[must_use]
49    pub const fn per_second(self) -> f64 {
50        self.0
51    }
52
53    /// Signed difference against another rate, for comparison columns (§2.5).
54    #[must_use]
55    pub fn delta_from(self, other: Self) -> f64 {
56        self.0 - other.0
57    }
58}
59
60impl fmt::Display for Rate {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "{:.0}/s", self.0)
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use core::time::Duration;
70
71    #[test]
72    fn rejects_non_finite_and_negative() {
73        assert!(Rate::new(-1.0).is_none());
74        assert!(Rate::new(f64::NAN).is_none());
75        assert!(Rate::new(f64::INFINITY).is_none());
76    }
77
78    #[test]
79    fn zero_elapsed_yields_no_rate_instead_of_infinity() {
80        assert!(Rate::from_delta(1024, Duration::ZERO).is_none());
81    }
82
83    #[test]
84    fn uses_actual_elapsed_time_not_an_assumed_second() {
85        let half = Rate::from_delta(1000, Duration::from_millis(500)).expect("valid");
86        assert!((half.per_second() - 2000.0).abs() < f64::EPSILON);
87        let double = Rate::from_delta(1000, Duration::from_secs(2)).expect("valid");
88        assert!((double.per_second() - 500.0).abs() < f64::EPSILON);
89    }
90
91    #[test]
92    fn a_zero_delta_is_a_real_zero_rate() {
93        let r = Rate::from_delta(0, Duration::from_secs(1)).expect("valid");
94        assert_eq!(r, Rate::ZERO);
95    }
96}