#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HistogramConfig {
pub default_bins: usize,
pub bar_char: char,
pub show_edges: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HistogramBin {
pub low: f32,
pub high: f32,
pub count: u64,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Histogram {
pub bins: Vec<HistogramBin>,
pub min: f32,
pub max: f32,
pub total_samples: u64,
}
#[allow(dead_code)]
pub fn default_histogram_config() -> HistogramConfig {
HistogramConfig {
default_bins: 10,
bar_char: '#',
show_edges: false,
}
}
#[allow(dead_code)]
pub fn new_histogram(min: f32, max: f32, n_bins: usize) -> Histogram {
assert!(n_bins > 0, "n_bins must be > 0");
assert!(min < max, "min must be < max");
let width = (max - min) / n_bins as f32;
let bins = (0..n_bins)
.map(|i| HistogramBin {
low: min + i as f32 * width,
high: min + (i + 1) as f32 * width,
count: 0,
})
.collect();
Histogram {
bins,
min,
max,
total_samples: 0,
}
}
#[allow(dead_code)]
pub fn histogram_add_value(hist: &mut Histogram, value: f32) {
hist.total_samples += 1;
if hist.bins.is_empty() {
return;
}
let n = hist.bins.len();
let t = (value - hist.min) / (hist.max - hist.min);
let idx = (t * n as f32).floor() as isize;
let idx = idx.clamp(0, (n as isize) - 1) as usize;
hist.bins[idx].count += 1;
}
#[allow(dead_code)]
pub fn histogram_bin_count(hist: &Histogram) -> usize {
hist.bins.len()
}
#[allow(dead_code)]
pub fn histogram_total_samples(hist: &Histogram) -> u64 {
hist.total_samples
}
#[allow(dead_code)]
pub fn histogram_peak_bin(hist: &Histogram) -> usize {
hist.bins
.iter()
.enumerate()
.max_by_key(|(_, b)| b.count)
.map(|(i, _)| i)
.unwrap_or(0)
}
#[allow(dead_code)]
pub fn histogram_mean(hist: &Histogram) -> f32 {
if hist.total_samples == 0 {
return 0.0;
}
let sum: f64 = hist
.bins
.iter()
.map(|b| {
let mid = ((b.low + b.high) / 2.0) as f64;
mid * b.count as f64
})
.sum();
(sum / hist.total_samples as f64) as f32
}
#[allow(dead_code)]
pub fn histogram_to_ascii(hist: &Histogram, width: usize) -> String {
if hist.bins.is_empty() {
return String::new();
}
let max_count = hist.bins.iter().map(|b| b.count).max().unwrap_or(1).max(1);
let mut out = String::new();
for (i, bin) in hist.bins.iter().enumerate() {
let bar_len = if width == 0 {
0
} else {
(bin.count as f64 / max_count as f64 * width as f64).round() as usize
};
let bar = "#".repeat(bar_len);
out.push_str(&format!("[{i:>3}] |{bar:<width$}| {}\n", bin.count, width = width));
}
out
}
#[allow(dead_code)]
pub fn histogram_normalize(hist: &Histogram) -> Vec<f32> {
if hist.total_samples == 0 {
return Vec::new();
}
hist.bins
.iter()
.map(|b| b.count as f32 / hist.total_samples as f32)
.collect()
}
#[allow(dead_code)]
pub fn histogram_clear(hist: &mut Histogram) {
for bin in &mut hist.bins {
bin.count = 0;
}
hist.total_samples = 0;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = default_histogram_config();
assert_eq!(cfg.default_bins, 10);
assert_eq!(cfg.bar_char, '#');
}
#[test]
fn test_new_histogram_bin_count() {
let hist = new_histogram(0.0, 1.0, 5);
assert_eq!(histogram_bin_count(&hist), 5);
}
#[test]
fn test_add_value_increments_total() {
let mut hist = new_histogram(0.0, 10.0, 10);
histogram_add_value(&mut hist, 5.0);
histogram_add_value(&mut hist, 3.0);
assert_eq!(histogram_total_samples(&hist), 2);
}
#[test]
fn test_add_value_correct_bin() {
let mut hist = new_histogram(0.0, 10.0, 10);
histogram_add_value(&mut hist, 1.5); assert_eq!(hist.bins[1].count, 1);
}
#[test]
fn test_add_value_clamped_below() {
let mut hist = new_histogram(0.0, 10.0, 10);
histogram_add_value(&mut hist, -5.0);
assert_eq!(hist.bins[0].count, 1);
}
#[test]
fn test_add_value_clamped_above() {
let mut hist = new_histogram(0.0, 10.0, 10);
histogram_add_value(&mut hist, 100.0);
assert_eq!(hist.bins[9].count, 1);
}
#[test]
fn test_peak_bin() {
let mut hist = new_histogram(0.0, 10.0, 10);
for _ in 0..5 {
histogram_add_value(&mut hist, 7.5); }
histogram_add_value(&mut hist, 1.0); assert_eq!(histogram_peak_bin(&hist), 7);
}
#[test]
fn test_mean_single_bin() {
let mut hist = new_histogram(0.0, 10.0, 10);
histogram_add_value(&mut hist, 5.5); let mean = histogram_mean(&hist);
assert!((mean - 5.5).abs() < 0.5);
}
#[test]
fn test_mean_no_samples() {
let hist = new_histogram(0.0, 1.0, 4);
assert_eq!(histogram_mean(&hist), 0.0);
}
#[test]
fn test_to_ascii_nonempty() {
let mut hist = new_histogram(0.0, 5.0, 5);
histogram_add_value(&mut hist, 2.5);
let s = histogram_to_ascii(&hist, 20);
assert!(!s.is_empty());
assert!(s.contains('#'));
}
#[test]
fn test_normalize_sums_to_one() {
let mut hist = new_histogram(0.0, 10.0, 5);
for v in [1.0f32, 3.0, 5.0, 7.0, 9.0] {
histogram_add_value(&mut hist, v);
}
let norm = histogram_normalize(&hist);
let sum: f32 = norm.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
}
#[test]
fn test_clear_resets_counts() {
let mut hist = new_histogram(0.0, 10.0, 10);
histogram_add_value(&mut hist, 5.0);
histogram_clear(&mut hist);
assert_eq!(histogram_total_samples(&hist), 0);
assert!(hist.bins.iter().all(|b| b.count == 0));
}
}