Skip to main content

rill_ml/stats/
extrema.rs

1//! Online min and max trackers.
2//!
3//! Time complexity per update: `O(1)`. Space complexity: `O(1)`.
4
5use crate::error::{RillError, checked_increment, ensure_finite};
6use crate::traits::OnlineStatistic;
7
8/// Running minimum of observed values.
9#[derive(Debug, Clone, Default)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct Min {
12    current: Option<f64>,
13    count: u64,
14}
15
16impl Min {
17    /// Create a new empty minimum tracker.
18    pub const fn new() -> Self {
19        Self {
20            current: None,
21            count: 0,
22        }
23    }
24
25    /// Current minimum, or `None` if no observations have been seen.
26    pub const fn value(&self) -> Option<f64> {
27        self.current
28    }
29}
30
31impl OnlineStatistic for Min {
32    fn update(&mut self, value: f64) -> Result<(), RillError> {
33        ensure_finite("value", value)?;
34        let next_count = checked_increment(self.count, "minimum sample")?;
35        self.current = Some(match self.current {
36            None => value,
37            Some(c) => c.min(value),
38        });
39        self.count = next_count;
40        Ok(())
41    }
42
43    fn samples_seen(&self) -> u64 {
44        self.count
45    }
46
47    fn reset(&mut self) {
48        self.current = None;
49        self.count = 0;
50    }
51}
52
53/// Running maximum of observed values.
54#[derive(Debug, Clone, Default)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56pub struct Max {
57    current: Option<f64>,
58    count: u64,
59}
60
61impl Max {
62    /// Create a new empty maximum tracker.
63    pub const fn new() -> Self {
64        Self {
65            current: None,
66            count: 0,
67        }
68    }
69
70    /// Current maximum, or `None` if no observations have been seen.
71    pub const fn value(&self) -> Option<f64> {
72        self.current
73    }
74}
75
76impl OnlineStatistic for Max {
77    fn update(&mut self, value: f64) -> Result<(), RillError> {
78        ensure_finite("value", value)?;
79        let next_count = checked_increment(self.count, "maximum sample")?;
80        self.current = Some(match self.current {
81            None => value,
82            Some(c) => c.max(value),
83        });
84        self.count = next_count;
85        Ok(())
86    }
87
88    fn samples_seen(&self) -> u64 {
89        self.count
90    }
91
92    fn reset(&mut self) {
93        self.current = None;
94        self.count = 0;
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn min_tracks_minimum() {
104        let mut m = Min::new();
105        assert!(m.value().is_none());
106        for x in [3.0, 1.0, 4.0, 1.0, 5.0, -2.0] {
107            m.update(x).unwrap();
108        }
109        assert_eq!(m.value(), Some(-2.0));
110    }
111
112    #[test]
113    fn max_tracks_maximum() {
114        let mut m = Max::new();
115        for x in [3.0, 1.0, 4.0, 1.0, 5.0, -2.0] {
116            m.update(x).unwrap();
117        }
118        assert_eq!(m.value(), Some(5.0));
119    }
120}