use crate::types::LineLengthItem;
use crate::types::{FileEntry, MappedContent};
use pad::{Alignment, PadStr};
use serde_json;
use std::collections::HashMap;
use std::error::Error;
use std::io::Write;
use unicode_width::UnicodeWidthStr;
const MAX_LINE_LENGTH_HISTOGRAM_BAR: usize = 60;
fn calculate_line_length_histogram(
files: &[FileEntry],
) -> HashMap<usize, usize> {
let mut histogram: HashMap<usize, usize> = HashMap::new();
for file in files {
let lines: Vec<&str> = match &file.content {
MappedContent::Mapped(mmap) => {
if let Ok(content) = std::str::from_utf8(mmap) {
content.lines().collect()
} else {
Vec::new() }
}
MappedContent::String(content) => content.lines().collect(),
};
for line in lines {
let length = UnicodeWidthStr::width(line);
*histogram.entry(length).or_insert(0) += 1;
}
}
histogram
}
fn format_line_length_histogram(histogram: HashMap<usize, usize>) -> String {
if histogram.is_empty() {
return "No lines found to analyze.".to_string();
}
let mut sorted_lengths: Vec<_> = histogram.keys().collect();
sorted_lengths.sort();
let max_length = **sorted_lengths.last().unwrap_or(&&0);
let max_count = *histogram.values().max().unwrap_or(&0);
let max_length_width = max_length.to_string().len();
let max_count_width = max_count.to_string().len();
let mut result = String::new();
result.push_str(&format!(
"{:>width$} {:>count_width$} Histogram\n",
"Length",
"Count",
width = max_length_width,
count_width = max_count_width
));
result.push_str(&format!(
"{} {} {}\n",
"-".repeat(max_length_width),
"-".repeat(max_count_width),
"-".repeat(9) ));
for length in sorted_lengths {
let count = histogram[length];
let bar_width = if max_count > 0 {
(MAX_LINE_LENGTH_HISTOGRAM_BAR as f64 * (count as f64 / max_count as f64))
.round() as usize
} else {
0
};
result += &format!(
"{} {} {}\n",
length
.to_string()
.pad_to_width_with_alignment(max_length_width, Alignment::Right),
count
.to_string()
.pad_to_width_with_alignment(max_count_width, Alignment::Right),
"▆".repeat(bar_width),
);
}
result
}
pub fn process_and_output_line_length<A: Write>(
files: Vec<FileEntry>,
mut output_stream: A,
json_output: bool,
) -> Result<(), Box<dyn Error>> {
let histogram = calculate_line_length_histogram(&files);
if json_output {
let mut histogram_vec: Vec<LineLengthItem> = histogram
.into_iter()
.map(|(length, count)| LineLengthItem { length, count })
.collect();
histogram_vec.sort_by_key(|item| item.length);
let json_string = serde_json::to_string_pretty(&histogram_vec)?;
writeln!(output_stream, "{json_string}")?;
} else {
let formatted_histogram = format_line_length_histogram(histogram);
writeln!(output_stream, "{formatted_histogram}")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::MappedContent;
#[test]
fn test_calculate_line_length_histogram_empty() {
let files = vec![];
let histogram = calculate_line_length_histogram(&files);
assert!(histogram.is_empty());
}
#[test]
fn test_calculate_line_length_histogram_basic() {
let file1 = FileEntry {
name: "file1.txt".to_string(),
content: MappedContent::String(
"line1\nline22\n".to_string(), ),
};
let file2 = FileEntry {
name: "file2.txt".to_string(),
content: MappedContent::String(
"line1\nline333\n".to_string(), ),
};
let files = vec![file1, file2];
let histogram = calculate_line_length_histogram(&files);
let expected: HashMap<usize, usize> =
[(5, 2), (6, 1), (7, 1)].iter().cloned().collect();
assert_eq!(histogram, expected);
}
#[test]
fn test_calculate_line_length_histogram_unicode() {
let file1 = FileEntry {
name: "file_unicode.txt".to_string(),
content: MappedContent::String("你好\n🚀\n".to_string()), };
let files = vec![file1];
let histogram = calculate_line_length_histogram(&files);
let expected: HashMap<usize, usize> =
[(4, 1), (2, 1)].iter().cloned().collect();
assert_eq!(histogram, expected);
}
#[test]
fn test_format_line_length_histogram_empty() {
let histogram = HashMap::new();
let formatted = format_line_length_histogram(histogram);
assert_eq!(formatted, "No lines found to analyze.");
}
#[test]
fn test_format_line_length_histogram_basic() {
let histogram: HashMap<usize, usize> =
[(5, 2), (10, 1), (15, 3)].iter().cloned().collect();
let formatted = format_line_length_histogram(histogram);
assert!(formatted.contains("Length Count Histogram"));
assert!(formatted.contains("-- - ---------"));
assert!(formatted.contains(" 5 2"));
assert!(formatted.contains("10 1"));
assert!(formatted.contains("15 3"));
assert!(formatted.contains("▆")); }
}