use crate::cell::CellValue;
use crate::sheet::Sheet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SheetClass {
Empty,
Readme,
Summary,
Data,
}
impl SheetClass {
pub fn as_str(&self) -> &'static str {
match self {
SheetClass::Empty => "empty",
SheetClass::Readme => "readme",
SheetClass::Summary => "summary",
SheetClass::Data => "data",
}
}
}
#[derive(Debug, Clone)]
pub struct SheetMap {
pub name: String,
pub rows: usize,
pub cols: usize,
pub class: SheetClass,
pub headers: Vec<String>,
pub tables: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct WorkbookMap {
pub path: String,
pub sheets: Vec<SheetMap>,
pub named_ranges: Vec<(String, String)>,
}
pub fn classify_sheet(sheet: &Sheet) -> SheetClass {
let (rows, cols) = sheet.dimensions();
if rows == 0 || cols == 0 {
return SheetClass::Empty;
}
if cols == 1 {
return SheetClass::Readme;
}
let total = rows * cols;
let non_empty: usize = sheet
.rows()
.iter()
.map(|row| {
row.iter()
.filter(|c| !matches!(c.value, CellValue::Empty))
.count()
})
.sum();
let density = non_empty as f64 / total as f64;
if rows <= 20 && cols <= 10 && density < 0.4 {
return SheetClass::Summary;
}
SheetClass::Data
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cell::Cell;
fn cell(s: &str) -> Cell {
Cell {
value: CellValue::String(s.to_string()),
number_format: None,
}
}
fn empty() -> Cell {
Cell::empty()
}
#[test]
fn empty_sheet_is_classified_empty() {
let sheet = Sheet::from_rows_for_test("blank", vec![]);
assert_eq!(classify_sheet(&sheet), SheetClass::Empty);
}
#[test]
fn single_column_sheet_is_classified_readme() {
let rows = (0..15)
.map(|i| vec![cell(&format!("note line {i}"))])
.collect();
let sheet = Sheet::from_rows_for_test("Notes", rows);
assert_eq!(classify_sheet(&sheet), SheetClass::Readme);
}
#[test]
fn small_sparse_sheet_is_classified_summary() {
let mut rows = vec![vec![empty(); 5]; 5];
rows[0][0] = cell("Q1 2026 Summary");
rows[2][1] = cell("$1.2M");
let sheet = Sheet::from_rows_for_test("Summary", rows);
assert_eq!(classify_sheet(&sheet), SheetClass::Summary);
}
#[test]
fn dense_rectangular_sheet_is_classified_data() {
let rows = (0..10)
.map(|r| (0..5).map(|c| cell(&format!("r{r}c{c}"))).collect())
.collect();
let sheet = Sheet::from_rows_for_test("Ledger", rows);
assert_eq!(classify_sheet(&sheet), SheetClass::Data);
}
#[test]
fn large_sheet_skips_summary_branch_even_if_sparse() {
let mut rows = vec![vec![empty(); 15]; 50];
rows[0][0] = cell("seed");
let sheet = Sheet::from_rows_for_test("Big", rows);
assert_eq!(classify_sheet(&sheet), SheetClass::Data);
}
}