use glob::{glob, GlobError};
use std::env;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use calamine::{open_workbook_auto, Data, Error, Reader};
#[derive(Debug)]
#[allow(dead_code)]
enum FileStatus {
VbaError(Error),
RangeError(Error),
WorkbookOpenError(Error),
Glob(GlobError),
}
fn main() -> Result<(), FileStatus> {
let search_dir = env::args().nth(1).unwrap_or_else(|| ".".to_string());
let file_pattern = format!("{search_dir}/**/*.xl*");
let mut file_count = 0;
let mut output_filename = file_pattern
.chars()
.take_while(|c| *c != '*')
.filter_map(|c| match c {
':' => None,
'/' | '\\' | ' ' => Some('_'),
c => Some(c),
})
.collect::<String>();
output_filename.push_str("errors.csv");
if search_dir == "." {
output_filename = "errors.csv".to_string();
}
let mut output_file = BufWriter::new(File::create(&output_filename).unwrap());
for file in glob(&file_pattern).unwrap() {
file_count += 1;
let file = file.map_err(FileStatus::Glob)?;
match analyze(&file) {
Ok((missing_vba_refs, cell_errors)) => {
writeln!(
output_file,
"{file:?}: Missing VBA refs = {missing_vba_refs:?}. Cell errors = {cell_errors}."
)
}
Err(e) => writeln!(output_file, "{file:?}: Error = {e:?}."),
}
.unwrap_or_else(|e| println!("{e:?}"));
}
println!("Analyzed {file_count} excel files. See '{output_filename}' for analysis.");
Ok(())
}
fn analyze(file: &PathBuf) -> Result<(Option<usize>, usize), FileStatus> {
let mut workbook = open_workbook_auto(file).map_err(FileStatus::WorkbookOpenError)?;
let mut num_cell_errors = 0;
let mut num_missing_vba_refs = None;
if let Some(vba) = workbook.vba_project().map_err(FileStatus::VbaError)? {
num_missing_vba_refs = Some(
vba.get_references()
.iter()
.filter(|r| r.is_missing())
.count(),
);
}
for sheet_name in workbook.sheet_names() {
let range = workbook
.worksheet_range(&sheet_name)
.map_err(FileStatus::RangeError)?;
num_cell_errors += range
.rows()
.flat_map(|r| r.iter().filter(|c| matches!(**c, Data::Error(_))))
.count();
}
Ok((num_missing_vba_refs, num_cell_errors))
}