use super::{merge_cell, ExistingCell, MergeResult};
use crate::ast::Node;
use crate::{Cell, Result, Runtime, Template};
use a1_notation::Address;
use std::ffi;
use std::path;
use umya_spreadsheet as u;
mod cell_validation;
mod compilation_target;
mod excel_modifier;
use cell_validation::CellValidation;
type ExcelValue = u::Cell;
#[derive(Debug)]
pub(crate) struct Excel<'a> {
path: path::PathBuf,
runtime: &'a Runtime,
}
impl<'a> Excel<'a> {
pub(crate) fn new(runtime: &'a Runtime, path: path::PathBuf) -> Self {
Self { path, runtime }
}
pub(crate) fn supports_extension(os_str: &ffi::OsStr) -> bool {
os_str.eq_ignore_ascii_case("xlsx")
|| os_str.eq_ignore_ascii_case("xlsm")
|| os_str.eq_ignore_ascii_case("xltx")
|| os_str.eq_ignore_ascii_case("xltm")
}
fn build_worksheet(&self, template: &Template, worksheet: &mut u::Worksheet) -> Result<()> {
let s = template.spreadsheet.borrow();
let mut cell_validations = vec![];
for row in &s.rows {
for cell in &row.cells {
let merged_cell = merge_cell(
&self.get_existing_cell(cell.position, worksheet),
Some(cell),
&self.runtime.options,
);
match merged_cell {
MergeResult::Existing(_) | MergeResult::Empty => (),
MergeResult::New(cell) => {
let e = worksheet.get_cell_mut(cell.position.to_string());
self.set_value(e, &cell);
if let Some(style) = self.build_style(&cell) {
e.set_style(style);
}
if let Some(n) = &cell.modifier.note {
self.set_comment(worksheet, &cell, n);
}
if let Some(data_validation) = cell.modifier.data_validation {
cell_validations.push(CellValidation(cell.position, data_validation));
}
}
}
}
}
self.set_data_validations(worksheet, cell_validations);
Ok(())
}
fn set_data_validations(
&self,
worksheet: &mut u::Worksheet,
cell_validations: Vec<CellValidation>,
) {
let mut validations = u::DataValidations::default();
if cell_validations.is_empty() {
return;
}
validations
.set_data_validation_list(cell_validations.into_iter().map(|dv| dv.into()).collect());
worksheet.set_data_validations(validations);
}
fn set_comment(&self, worksheet: &mut u::Worksheet, cell: &Cell, note: &str) {
let mut comment = u::Comment::default();
comment.set_author("csvpp");
let rt = comment.get_text_mut();
rt.set_text(note);
let coord = comment.get_coordinate_mut();
coord.set_col_num(cell.position.column.x as u32);
coord.set_row_num(cell.position.row.y as u32);
worksheet.add_comments(comment);
}
fn set_value(&self, existing_cell: &mut u::Cell, cell: &Cell) {
if let Some(ast) = &cell.ast {
match *ast.clone() {
Node::Boolean(b) => existing_cell.set_value_bool(b),
Node::Text(t) => existing_cell.set_value_string(t),
Node::Float(f) => existing_cell.set_value_number(f),
Node::Integer(i) => existing_cell.set_value_number(i as f64),
_ => existing_cell.set_formula(ast.to_string()),
};
} else if !cell.value.is_empty() {
existing_cell.set_value_string(cell.value.clone());
}
}
fn build_style(&self, cell: &Cell) -> Option<u::Style> {
let modifier = cell.modifier.clone();
if modifier.is_empty() {
return None;
}
Some(excel_modifier::ExcelModifier(modifier).into())
}
fn get_existing_cell(
&self,
position: Address,
worksheet: &u::Worksheet,
) -> ExistingCell<ExcelValue> {
let cell_value = worksheet.get_cell(position.to_string());
if let Some(cell) = cell_value {
ExistingCell::Value(cell.clone())
} else {
ExistingCell::Empty
}
}
fn open_spreadsheet(&self) -> Result<u::Spreadsheet> {
if self.path.exists() {
u::reader::xlsx::read(self.path.as_path()).map_err(|e| {
self.runtime
.output
.clone()
.into_error(format!("Unable to open target file: {e}"))
})
} else {
Ok(u::new_file_empty_worksheet())
}
}
fn create_worksheet(&self, spreadsheet: &mut u::Spreadsheet) -> Result<()> {
let sheet_name = self.runtime.options.sheet_name.clone();
let existing = spreadsheet.get_sheet_by_name(&sheet_name);
if existing.is_err() {
spreadsheet.new_sheet(&sheet_name).map_err(|e| {
self.runtime.output.clone().into_error(format!(
"Unable to create new worksheet {sheet_name} in target file: {e}"
))
})?;
}
Ok(())
}
fn get_worksheet_mut(
&'a self,
spreadsheet: &'a mut u::Spreadsheet,
) -> Result<&'a mut u::Worksheet> {
let sheet_name = &self.runtime.options.sheet_name;
spreadsheet.get_sheet_by_name_mut(sheet_name).map_err(|e| {
self.runtime.output.clone().into_error(format!(
"Unable to open worksheet {sheet_name} in target file: {e}"
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn supports_extension_true() {
assert!(Excel::supports_extension(ffi::OsStr::new("xlsx")));
assert!(Excel::supports_extension(ffi::OsStr::new("XLSX")));
assert!(Excel::supports_extension(ffi::OsStr::new("xlsm")));
assert!(Excel::supports_extension(ffi::OsStr::new("xltm")));
assert!(Excel::supports_extension(ffi::OsStr::new("xltx")));
}
#[test]
fn supports_extension_false() {
assert!(!Excel::supports_extension(ffi::OsStr::new("foo")));
assert!(!Excel::supports_extension(ffi::OsStr::new("csv")));
}
}