excel-cli 1.3.2

Excel CLI for AI, scripting, and terminal users. Headless JSON API for automation, plus a Vim-like TUI for interactive browsing and editing.
Documentation
use anyhow::Result;
use chrono::Local;
use rust_xlsxwriter::{Format, Workbook as XlsxWorkbook, Worksheet};
use std::path::{Path, PathBuf};

use super::Workbook;
use crate::excel::{Cell, CellType, Sheet};

impl Workbook {
    pub fn save(&mut self) -> Result<()> {
        if !self.is_modified {
            println!("No changes to save.");
            return Ok(());
        }

        self.ensure_all_sheets_loaded()?;

        let mut workbook = XlsxWorkbook::new();
        let new_filepath = timestamped_save_path(&self.file_path);
        let number_format = Format::new().set_num_format("General");
        let date_format = Format::new().set_num_format("yyyy-mm-dd");

        for sheet in &self.sheets {
            write_sheet(&mut workbook, sheet, &number_format, &date_format)?;
        }

        workbook.save(&new_filepath)?;
        self.is_modified = false;

        Ok(())
    }
}

fn timestamped_save_path(file_path: &str) -> PathBuf {
    let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
    let path = Path::new(file_path);
    let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("sheet");
    let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("xlsx");
    let parent_dir = path.parent().unwrap_or_else(|| Path::new(""));
    parent_dir.join(format!("{file_stem}_{timestamp}.{extension}"))
}

fn write_sheet(
    workbook: &mut XlsxWorkbook,
    sheet: &Sheet,
    number_format: &Format,
    date_format: &Format,
) -> Result<()> {
    let worksheet = workbook.add_worksheet().set_name(&sheet.name)?;

    if sheet.freeze_panes.is_frozen() {
        worksheet.set_freeze_panes(
            sheet.freeze_panes.rows as u32,
            sheet.freeze_panes.cols as u16,
        )?;
    }

    for col in 0..sheet.max_cols {
        worksheet.set_column_width(col as u16, 15)?;
    }

    for row in 1..sheet.data.len() {
        if row > sheet.max_rows {
            continue;
        }

        for col in 1..sheet.data[0].len() {
            if col > sheet.max_cols {
                continue;
            }

            let cell = &sheet.data[row][col];
            if cell.value.is_empty() {
                continue;
            }

            let row_idx = (row - 1) as u32;
            let col_idx = (col - 1) as u16;
            write_cell(
                worksheet,
                cell,
                row_idx,
                col_idx,
                number_format,
                date_format,
            )?;
        }
    }

    Ok(())
}

fn write_cell(
    worksheet: &mut Worksheet,
    cell: &Cell,
    row_idx: u32,
    col_idx: u16,
    number_format: &Format,
    date_format: &Format,
) -> Result<()> {
    if cell.is_formula {
        let formula_text = cell.formula.as_deref().unwrap_or(cell.value.as_str());
        let formula = rust_xlsxwriter::Formula::new(formula_text);
        worksheet.write_formula(row_idx, col_idx, formula)?;
        if !cell.value.is_empty() && cell.value != formula_text {
            worksheet.set_formula_result(row_idx, col_idx, &cell.value);
        }
        return Ok(());
    }

    match cell.cell_type {
        CellType::Number => {
            if let Ok(num) = cell.value.parse::<f64>() {
                worksheet.write_number_with_format(row_idx, col_idx, num, number_format)?;
            } else {
                worksheet.write_string(row_idx, col_idx, &cell.value)?;
            }
        }
        CellType::Date => {
            worksheet.write_string_with_format(row_idx, col_idx, &cell.value, date_format)?;
        }
        CellType::Boolean => {
            if let Ok(b) = cell.value.parse::<bool>() {
                worksheet.write_boolean(row_idx, col_idx, b)?;
            } else {
                worksheet.write_string(row_idx, col_idx, &cell.value)?;
            }
        }
        CellType::Text => {
            worksheet.write_string(row_idx, col_idx, &cell.value)?;
        }
        CellType::Empty => {}
    }

    Ok(())
}