use crate::types::{AnalyzeResult, FileCounts};
use chrono::Local;
pub fn format(a: &AnalyzeResult) -> String {
let mut output = String::new();
if let Some(ref path) = a.analyzed_path {
let dir_name = std::path::Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path);
let now = Local::now();
let friendly_date = now.format("%B %d, %Y at %I:%M %p").to_string();
output.push_str(
"═══════════════════════════════════════════════════════════════════════════════════\n",
);
output.push_str(&format!(
" REPORT FOR: {}\n",
dir_name.to_uppercase()
));
output.push_str(&format!(
" Generated: {}\n",
friendly_date
));
output.push_str("═══════════════════════════════════════════════════════════════════════════════════\n\n");
}
if let Some(ref stats) = a.stats {
output.push_str("File Statistics:\n");
output.push_str("─────────────────────────────────────\n");
output.push_str(&format!(
" Text Files : {:>10}\n",
format_num(stats.total_files)
));
output.push_str(&format!(
" Unique Files : {:>10}\n",
format_num(stats.unique_files)
));
output.push_str(&format!(
" Ignored Files : {:>10}\n",
format_num(stats.ignored_files)
));
if stats.empty_files > 0 {
output.push_str(&format!(
" Empty Files : {:>10}\n",
format_num(stats.empty_files)
));
}
output.push_str("─────────────────────────────────────\n\n");
output.push_str("Performance:\n");
output.push_str("─────────────────────────────────────\n");
output.push_str(&format!(
" Elapsed Time : {:>10.2} s\n",
stats.elapsed_seconds
));
let denom = if stats.elapsed_seconds > 0.0 {
stats.elapsed_seconds
} else {
1.0
};
let files_per_sec = stats.unique_files as f64 / denom;
output.push_str(&format!(" Files/sec : {:>10.1}\n", files_per_sec));
let total_lines = a.totals.total;
let lines_per_sec = total_lines as f64 / denom;
output.push_str(&format!(" Lines/sec : {:>10.0}\n", lines_per_sec));
output.push_str("─────────────────────────────────────\n\n");
}
let mut lang_w: usize = 12; let mut files_w: usize = 8; let mut code_w: usize = 10; let mut comm_w: usize = 10; let mut blank_w: usize = 10; let mut total_w: usize = 10;
let update_w = |w: &mut usize, val: usize| {
let l = format_num(val).len();
if l > *w {
*w = l;
}
};
lang_w = a
.per_lang
.keys()
.map(|s| s.len())
.chain(std::iter::once("Total".len()))
.max()
.unwrap_or(lang_w)
.max(12);
for c in a.per_lang.values() {
update_w(&mut files_w, c.files);
update_w(&mut code_w, c.code);
update_w(&mut comm_w, c.comment);
update_w(&mut blank_w, c.blank);
update_w(&mut total_w, c.total);
}
update_w(&mut files_w, a.totals.files);
update_w(&mut code_w, a.totals.code);
update_w(&mut comm_w, a.totals.comment);
update_w(&mut blank_w, a.totals.blank);
update_w(&mut total_w, a.totals.total);
let gutter: usize = 8; let sep = " ".repeat(gutter);
let widths = ColWidths {
lang: lang_w,
files: files_w,
blank: blank_w,
comm: comm_w,
code: code_w,
total: total_w,
};
let h_lang = format!("{:<w$}", "Language", w = widths.lang);
let h_files = format!("{:>w$}", "files", w = widths.files);
let h_blank = format!("{:>w$}", "blank", w = widths.blank);
let h_comm = format!("{:>w$}", "comment", w = widths.comm);
let h_code = format!("{:>w$}", "code", w = widths.code);
let h_total = format!("{:>w$}", "Total", w = widths.total);
let header = [h_lang, h_files, h_blank, h_comm, h_code, h_total].join(&sep);
let sep_len = widths.lang
+ widths.files
+ widths.blank
+ widths.comm
+ widths.code
+ widths.total
+ gutter * 5;
let separator = "-".repeat(sep_len);
let mut lines = Vec::new();
lines.push(header);
lines.push(separator.clone());
for (lang, counts) in &a.per_lang {
lines.push(format_row(lang, counts, &widths, &sep));
}
lines.push(separator.clone());
let total_line = format_row("Total", &a.totals, &widths, &sep);
lines.push(total_line);
lines.push(separator);
output.push_str(&lines.join("\n"));
output
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
#[test]
fn table_includes_new_languages() {
let mut per: IndexMap<String, FileCounts> = IndexMap::new();
per.insert(
"INI".to_string(),
FileCounts {
files: 3,
total: 9,
code: 6,
comment: 2,
blank: 1,
},
);
per.insert(
"Text".to_string(),
FileCounts {
files: 1,
total: 4,
code: 4,
comment: 0,
blank: 0,
},
);
let mut totals = FileCounts::default();
for v in per.values() {
totals.merge(v);
}
let a = AnalyzeResult {
per_lang: per,
totals,
files_analyzed: totals.files,
stats: None,
analyzed_path: None,
};
let out = format(&a);
assert!(out.contains("INI"));
assert!(out.contains("Text"));
assert!(out.contains("Total"));
}
}
struct ColWidths {
lang: usize,
files: usize,
blank: usize,
comm: usize,
code: usize,
total: usize,
}
fn format_row(lang: &str, c: &FileCounts, w: &ColWidths, sep: &str) -> String {
let name_plain = format!("{:<w$}", lang, w = w.lang);
let files_plain = format!("{:>w$}", format_num(c.files), w = w.files);
let blank_plain = format!("{:>w$}", format_num(c.blank), w = w.blank);
let comm_plain = format!("{:>w$}", format_num(c.comment), w = w.comm);
let code_plain = format!("{:>w$}", format_num(c.code), w = w.code);
let total_plain = format!("{:>w$}", format_num(c.total), w = w.total);
[
name_plain,
files_plain,
blank_plain,
comm_plain,
code_plain,
total_plain,
]
.join(sep)
}
fn format_num(n: usize) -> String {
let s = n.to_string();
let mut out = String::new();
let bytes = s.as_bytes();
let mut i = bytes.len() as isize - 1;
let mut count = 0;
while i >= 0 {
out.insert(0, bytes[i as usize] as char);
count += 1;
if count == 3 && i > 0 {
out.insert(0, ',');
count = 0;
}
i -= 1;
}
out
}