Skip to main content

tokmd_analysis_types/complexity/
histogram.rs

1//! Cyclomatic complexity histogram DTO and display helper.
2//!
3//! This module owns the serde-stable histogram shape used by complexity
4//! receipts. The type remains re-exported from the crate root.
5
6use serde::{Deserialize, Serialize};
7
8/// Histogram of cyclomatic complexity distribution across files.
9///
10/// Used to visualize the distribution of complexity values in a codebase.
11/// Default bucket boundaries are 0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30+.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ComplexityHistogram {
14    /// Bucket boundaries (e.g., [0, 5, 10, 15, 20, 25, 30]).
15    pub buckets: Vec<u32>,
16    /// Count of files in each bucket.
17    pub counts: Vec<u32>,
18    /// Total files analyzed.
19    pub total: u32,
20}
21
22impl ComplexityHistogram {
23    /// Generate an ASCII bar chart visualization of the histogram.
24    ///
25    /// # Arguments
26    /// * `width` - Maximum width of the bars in characters
27    ///
28    /// # Returns
29    /// A multi-line string with labeled bars showing distribution
30    pub fn to_ascii(&self, width: usize) -> String {
31        use std::fmt::Write;
32        let max_count = self.counts.iter().max().copied().unwrap_or(1).max(1);
33        let mut output = String::with_capacity(self.counts.len() * (width + 20));
34        for (i, count) in self.counts.iter().enumerate() {
35            match (self.buckets.get(i), self.buckets.get(i + 1)) {
36                (Some(start), Some(next)) => {
37                    let end = next.saturating_sub(1).max(*start);
38                    let _ = write!(output, "{:>2}-{:<2} |", start, end);
39                }
40                (Some(start), None) => {
41                    let _ = write!(output, "{:>2}+  |", start);
42                }
43                (None, _) => {
44                    let _ = write!(output, "{:>2}+  |", 0);
45                }
46            }
47
48            let bar_len = (*count as f64 / max_count as f64 * width as f64) as usize;
49            for _ in 0..bar_len {
50                output.push('\u{2588}');
51            }
52            let _ = writeln!(output, " {}", count);
53        }
54        output
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::ComplexityHistogram;
61
62    #[test]
63    fn complexity_histogram_to_ascii_basic() {
64        let h = ComplexityHistogram {
65            buckets: vec![0, 5, 10],
66            counts: vec![10, 5, 2],
67            total: 17,
68        };
69        let ascii = h.to_ascii(20);
70        assert!(!ascii.is_empty());
71        assert_eq!(ascii.lines().count(), 3);
72    }
73
74    #[test]
75    fn complexity_histogram_to_ascii_empty_counts() {
76        let h = ComplexityHistogram {
77            buckets: vec![0, 5],
78            counts: vec![0, 0],
79            total: 0,
80        };
81        let ascii = h.to_ascii(20);
82        assert!(!ascii.is_empty());
83    }
84
85    #[test]
86    fn complexity_histogram_to_ascii_handles_counts_without_buckets() {
87        let h = ComplexityHistogram {
88            buckets: Vec::new(),
89            counts: vec![3, 1],
90            total: 4,
91        };
92        let ascii = h.to_ascii(10);
93        assert_eq!(ascii.lines().count(), 2);
94        assert!(ascii.lines().all(|line| line.starts_with(" 0+")));
95    }
96
97    #[test]
98    fn complexity_histogram_to_ascii_handles_non_increasing_buckets() {
99        let h = ComplexityHistogram {
100            buckets: vec![5, 0],
101            counts: vec![1, 2],
102            total: 3,
103        };
104        let ascii = h.to_ascii(10);
105        assert!(ascii.lines().next().unwrap().starts_with(" 5-5"));
106    }
107}