irithyll_core/histogram/
categorical.rs1use alloc::boxed::Box;
17use alloc::vec::Vec;
18
19use super::{BinEdges, BinningStrategy};
20
21pub const MAX_CATEGORIES: usize = 64;
25
26#[derive(Debug, Clone)]
31pub struct CategoricalBinning {
32 categories: Vec<i64>,
34}
35
36impl CategoricalBinning {
37 pub fn new() -> Self {
39 Self {
40 categories: Vec::new(),
41 }
42 }
43
44 #[inline]
46 pub fn n_categories(&self) -> usize {
47 self.categories.len()
48 }
49
50 pub fn categories(&self) -> &[i64] {
52 &self.categories
53 }
54
55 pub fn category_index(&self, value: f64) -> Option<usize> {
60 let v = value as i64;
61 self.categories.binary_search(&v).ok()
62 }
63}
64
65impl Default for CategoricalBinning {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl BinningStrategy for CategoricalBinning {
72 fn observe(&mut self, value: f64) {
73 let v = value as i64;
74 match self.categories.binary_search(&v) {
76 Ok(_) => {} Err(pos) => {
78 if self.categories.len() < MAX_CATEGORIES {
79 self.categories.insert(pos, v);
80 }
81 }
83 }
84 }
85
86 fn compute_edges(&self, _n_bins: usize) -> BinEdges {
87 if self.categories.len() <= 1 {
90 return BinEdges { edges: Vec::new() };
91 }
92
93 let edges: Vec<f64> = self
94 .categories
95 .windows(2)
96 .map(|w| (w[0] as f64 + w[1] as f64) / 2.0)
97 .collect();
98
99 BinEdges { edges }
100 }
101
102 fn reset(&mut self) {
103 self.categories.clear();
104 }
105
106 fn clone_fresh(&self) -> Box<dyn BinningStrategy> {
107 Box::new(CategoricalBinning::new())
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn empty_binner() {
117 let b = CategoricalBinning::new();
118 assert_eq!(b.n_categories(), 0);
119 let edges = b.compute_edges(10);
120 assert!(edges.edges.is_empty());
121 assert_eq!(edges.n_bins(), 1);
122 }
123
124 #[test]
125 fn single_category() {
126 let mut b = CategoricalBinning::new();
127 b.observe(3.0);
128 b.observe(3.0); assert_eq!(b.n_categories(), 1);
130 let edges = b.compute_edges(10);
131 assert!(edges.edges.is_empty());
132 assert_eq!(edges.n_bins(), 1);
133 }
134
135 #[test]
136 fn two_categories() {
137 let mut b = CategoricalBinning::new();
138 b.observe(1.0);
139 b.observe(5.0);
140 assert_eq!(b.n_categories(), 2);
141
142 let edges = b.compute_edges(10);
143 assert_eq!(edges.edges.len(), 1);
144 assert!((edges.edges[0] - 3.0).abs() < 1e-10);
146 assert_eq!(edges.n_bins(), 2);
147 }
148
149 #[test]
150 fn multiple_categories_sorted() {
151 let mut b = CategoricalBinning::new();
152 b.observe(5.0);
154 b.observe(1.0);
155 b.observe(3.0);
156 b.observe(7.0);
157
158 assert_eq!(b.categories(), &[1, 3, 5, 7]);
159
160 let edges = b.compute_edges(10);
161 assert_eq!(edges.edges.len(), 3);
162 assert!((edges.edges[0] - 2.0).abs() < 1e-10); assert!((edges.edges[1] - 4.0).abs() < 1e-10); assert!((edges.edges[2] - 6.0).abs() < 1e-10); }
166
167 #[test]
168 fn find_bin_routes_correctly() {
169 let mut b = CategoricalBinning::new();
170 for i in 0..5 {
171 b.observe(i as f64 * 2.0); }
173
174 let edges = b.compute_edges(10);
175 assert_eq!(edges.find_bin(0.0), 0); assert_eq!(edges.find_bin(2.0), 1); assert_eq!(edges.find_bin(4.0), 2); assert_eq!(edges.find_bin(6.0), 3); assert_eq!(edges.find_bin(8.0), 4); }
182
183 #[test]
184 fn category_index_lookup() {
185 let mut b = CategoricalBinning::new();
186 b.observe(10.0);
187 b.observe(20.0);
188 b.observe(30.0);
189
190 assert_eq!(b.category_index(10.0), Some(0));
191 assert_eq!(b.category_index(20.0), Some(1));
192 assert_eq!(b.category_index(30.0), Some(2));
193 assert_eq!(b.category_index(15.0), None);
194 }
195
196 #[test]
197 fn max_categories_enforced() {
198 let mut b = CategoricalBinning::new();
199 for i in 0..100 {
200 b.observe(i as f64);
201 }
202 assert_eq!(b.n_categories(), MAX_CATEGORIES);
203 }
204
205 #[test]
206 fn reset_clears() {
207 let mut b = CategoricalBinning::new();
208 b.observe(1.0);
209 b.observe(2.0);
210 b.observe(3.0);
211 b.reset();
212 assert_eq!(b.n_categories(), 0);
213 }
214
215 #[test]
216 fn n_bins_ignored() {
217 let mut b = CategoricalBinning::new();
218 b.observe(1.0);
219 b.observe(2.0);
220 b.observe(3.0);
221
222 let edges_2 = b.compute_edges(2);
224 let edges_100 = b.compute_edges(100);
225 assert_eq!(edges_2.edges, edges_100.edges);
226 }
227}