Skip to main content

oxihuman_core/
bucket_histogram.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Fixed-bucket histogram.
6
7#[derive(Debug, Clone)]
8pub struct BucketHistogram {
9    pub min: f64,
10    pub max: f64,
11    buckets: Vec<u64>,
12    pub underflow: u64,
13    pub overflow: u64,
14    total: u64,
15}
16
17impl BucketHistogram {
18    pub fn new(min: f64, max: f64, num_buckets: usize) -> Self {
19        assert!(num_buckets > 0, "need at least one bucket");
20        assert!(max > min, "max must be greater than min");
21        BucketHistogram {
22            min,
23            max,
24            buckets: vec![0u64; num_buckets],
25            underflow: 0,
26            overflow: 0,
27            total: 0,
28        }
29    }
30
31    pub fn num_buckets(&self) -> usize {
32        self.buckets.len()
33    }
34
35    pub fn bucket_width(&self) -> f64 {
36        (self.max - self.min) / self.buckets.len() as f64
37    }
38
39    pub fn add(&mut self, value: f64) {
40        self.total += 1;
41        if value < self.min {
42            self.underflow += 1;
43            return;
44        }
45        if value >= self.max {
46            self.overflow += 1;
47            return;
48        }
49        let idx = ((value - self.min) / self.bucket_width()) as usize;
50        let idx = idx.min(self.buckets.len() - 1);
51        self.buckets[idx] += 1;
52    }
53
54    pub fn count(&self, bucket: usize) -> u64 {
55        self.buckets.get(bucket).copied().unwrap_or(0)
56    }
57
58    pub fn total(&self) -> u64 {
59        self.total
60    }
61
62    pub fn mode_bucket(&self) -> usize {
63        self.buckets
64            .iter()
65            .enumerate()
66            .max_by_key(|(_, &c)| c)
67            .map(|(i, _)| i)
68            .unwrap_or(0)
69    }
70
71    pub fn bucket_lower(&self, idx: usize) -> f64 {
72        self.min + idx as f64 * self.bucket_width()
73    }
74
75    pub fn bucket_upper(&self, idx: usize) -> f64 {
76        self.bucket_lower(idx) + self.bucket_width()
77    }
78
79    pub fn clear(&mut self) {
80        self.buckets.iter_mut().for_each(|c| *c = 0);
81        self.underflow = 0;
82        self.overflow = 0;
83        self.total = 0;
84    }
85}
86
87pub fn histogram_mean(hist: &BucketHistogram) -> Option<f64> {
88    let in_range = hist.total.saturating_sub(hist.underflow + hist.overflow);
89    if in_range == 0 {
90        return None;
91    }
92    let sum: f64 = (0..hist.num_buckets())
93        .map(|i| {
94            let mid = (hist.bucket_lower(i) + hist.bucket_upper(i)) / 2.0;
95            mid * hist.count(i) as f64
96        })
97        .sum();
98    Some(sum / in_range as f64)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_basic_add() {
107        let mut h = BucketHistogram::new(0.0, 10.0, 10);
108        h.add(5.0);
109        assert_eq!(h.total(), 1);
110    }
111
112    #[test]
113    fn test_underflow() {
114        let mut h = BucketHistogram::new(0.0, 10.0, 10);
115        h.add(-1.0);
116        assert_eq!(h.underflow, 1);
117    }
118
119    #[test]
120    fn test_overflow() {
121        let mut h = BucketHistogram::new(0.0, 10.0, 10);
122        h.add(10.0);
123        assert_eq!(h.overflow, 1);
124    }
125
126    #[test]
127    fn test_correct_bucket() {
128        let mut h = BucketHistogram::new(0.0, 10.0, 10);
129        h.add(2.5);
130        assert_eq!(h.count(2), 1 /* 2.5 falls in bucket 2 (2.0-3.0) */,);
131    }
132
133    #[test]
134    fn test_mode_bucket() {
135        let mut h = BucketHistogram::new(0.0, 10.0, 10);
136        h.add(5.0);
137        h.add(5.1);
138        h.add(5.2);
139        h.add(1.0);
140        assert_eq!(h.mode_bucket(), 5 /* most values in bucket 5 */,);
141    }
142
143    #[test]
144    fn test_clear() {
145        let mut h = BucketHistogram::new(0.0, 10.0, 5);
146        h.add(1.0);
147        h.add(2.0);
148        h.clear();
149        assert_eq!(h.total(), 0);
150        assert_eq!(h.count(0), 0);
151    }
152
153    #[test]
154    fn test_bucket_width() {
155        let h = BucketHistogram::new(0.0, 10.0, 5);
156        assert!((h.bucket_width() - 2.0).abs() < 1e-10 /* 10/5 = 2 */,);
157    }
158
159    #[test]
160    fn test_histogram_mean() {
161        let mut h = BucketHistogram::new(0.0, 10.0, 10);
162        for _ in 0..10 {
163            h.add(5.0);
164        }
165        let m = histogram_mean(&h).expect("should succeed");
166        assert!((m - 5.5).abs() < 1.0 /* midpoint of bucket 5 = 5.5 */,);
167    }
168
169    #[test]
170    fn test_bucket_bounds() {
171        let h = BucketHistogram::new(0.0, 10.0, 10);
172        assert!((h.bucket_lower(0) - 0.0).abs() < 1e-10);
173        assert!((h.bucket_upper(0) - 1.0).abs() < 1e-10);
174    }
175}