pub fn compute_bins(data: &[f64], bins: usize) -> (Vec<f64>, Vec<f64>) {
if data.is_empty() {
return (vec![], vec![]);
}
let bins = bins.max(1);
let min = data.iter().copied().fold(f64::INFINITY, f64::min);
let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if (max - min).abs() < 1e-15 {
return (vec![min], vec![data.len() as f64]);
}
let bin_width = (max - min) / bins as f64;
let mut counts = vec![0.0; bins];
for &v in data {
let mut idx = ((v - min) / bin_width) as usize;
if idx >= bins {
idx = bins - 1;
}
counts[idx] += 1.0;
}
let centers: Vec<f64> = (0..bins)
.map(|i| min + (i as f64 + 0.5) * bin_width)
.collect();
(centers, counts)
}
pub fn sturges_bins(n: usize) -> usize {
if n <= 1 {
return 1;
}
((n as f64).log2().ceil() as usize + 1).max(1)
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn bins_known_data() {
let data: Vec<f64> = (1..=9).map(f64::from).collect();
let (centers, counts) = compute_bins(&data, 3);
assert_eq!(centers.len(), 3);
assert_eq!(counts.len(), 3);
assert_eq!(counts[0], 3.0); assert_eq!(counts[1], 3.0); assert_eq!(counts[2], 3.0); }
#[test]
fn sturges_default_n100() {
let bins = sturges_bins(100);
assert_eq!(bins, 8); }
#[test]
fn empty_data() {
let (centers, counts) = compute_bins(&[], 5);
assert!(centers.is_empty());
assert!(counts.is_empty());
}
#[test]
fn single_value() {
let (centers, counts) = compute_bins(&[42.0, 42.0, 42.0], 5);
assert_eq!(centers.len(), 1);
assert_eq!(counts.len(), 1);
assert_eq!(centers[0], 42.0);
assert_eq!(counts[0], 3.0);
}
}