2024-11-16 10:51:38 -05:00
|
|
|
use std::cmp::max;
|
2024-11-14 18:24:29 -05:00
|
|
|
|
2024-11-11 19:27:59 -05:00
|
|
|
use anyhow::{anyhow, Result};
|
2024-11-14 18:24:29 -05:00
|
|
|
use ironcalc::{
|
|
|
|
base::{
|
2024-11-16 10:51:38 -05:00
|
|
|
types::{SheetData, Worksheet},
|
|
|
|
worksheet::WorksheetDimension,
|
|
|
|
Model,
|
2024-11-14 18:24:29 -05:00
|
|
|
},
|
2024-11-18 18:22:36 -05:00
|
|
|
export::save_xlsx_to_writer,
|
2024-11-14 18:24:29 -05:00
|
|
|
import::load_from_xlsx,
|
2024-11-13 19:39:47 -05:00
|
|
|
};
|
2024-11-11 19:27:59 -05:00
|
|
|
|
2024-11-16 10:51:38 -05:00
|
|
|
use crate::ui::Address;
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test;
|
|
|
|
|
2024-11-13 19:02:41 -05:00
|
|
|
/// A spreadsheet book with some internal state tracking.
|
2024-11-11 19:27:59 -05:00
|
|
|
pub struct Book {
|
2024-11-16 10:51:38 -05:00
|
|
|
pub(crate) model: Model,
|
|
|
|
pub current_sheet: u32,
|
|
|
|
pub location: crate::ui::Address,
|
|
|
|
// TODO(zaphar): Because the ironcalc model is sparse we need to track our render size
|
|
|
|
// separately
|
2024-11-11 19:27:59 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Book {
|
2024-11-14 18:24:29 -05:00
|
|
|
/// Construct a new book from a Model
|
|
|
|
pub fn new(model: Model) -> Self {
|
|
|
|
Self {
|
|
|
|
model,
|
|
|
|
current_sheet: 0,
|
2024-11-16 10:51:38 -05:00
|
|
|
location: Address::default(),
|
2024-11-14 18:24:29 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-16 10:51:38 -05:00
|
|
|
pub fn new_from_xlsx(path: &str) -> Result<Self> {
|
|
|
|
Ok(Self::new(load_from_xlsx(path, "en", "America/New_York")?))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn evaluate(&mut self) {
|
|
|
|
self.model.evaluate();
|
|
|
|
}
|
|
|
|
|
2024-11-14 18:24:29 -05:00
|
|
|
// TODO(zaphar): Should I support ICalc?
|
|
|
|
/// Construct a new book from a path.
|
2024-11-16 10:51:38 -05:00
|
|
|
pub fn new_from_xlsx_with_locale(path: &str, locale: &str, tz: &str) -> Result<Self> {
|
2024-11-14 18:24:29 -05:00
|
|
|
Ok(Self::new(load_from_xlsx(path, locale, tz)?))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Save book to an xlsx file.
|
|
|
|
pub fn save_to_xlsx(&self, path: &str) -> Result<()> {
|
2024-11-18 18:22:36 -05:00
|
|
|
// TODO(zaphar): Currently overwrites. Should we prompty in this case?
|
|
|
|
let file_path = std::path::Path::new(path);
|
|
|
|
let file = std::fs::File::create(file_path)?;
|
|
|
|
let writer = std::io::BufWriter::new(file);
|
|
|
|
save_xlsx_to_writer(&self.model, writer)?;
|
2024-11-14 18:24:29 -05:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-11-16 10:51:38 -05:00
|
|
|
pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> {
|
|
|
|
self.model
|
|
|
|
.workbook
|
|
|
|
.worksheets
|
|
|
|
.iter()
|
|
|
|
.map(|sheet| (sheet.get_name(), sheet.get_sheet_id()))
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
2024-11-13 19:02:41 -05:00
|
|
|
/// Get the currently set sheets name.
|
2024-11-11 19:27:59 -05:00
|
|
|
pub fn get_sheet_name(&self) -> Result<&str> {
|
|
|
|
Ok(&self.get_sheet()?.name)
|
|
|
|
}
|
|
|
|
|
2024-11-13 19:02:41 -05:00
|
|
|
/// Get the sheet data for the current worksheet.
|
2024-11-11 19:27:59 -05:00
|
|
|
pub fn get_sheet_data(&self) -> Result<&SheetData> {
|
|
|
|
Ok(&self.get_sheet()?.sheet_data)
|
|
|
|
}
|
2024-11-13 19:02:41 -05:00
|
|
|
|
2024-11-16 10:51:38 -05:00
|
|
|
pub fn move_to(&mut self, loc: Address) -> Result<()> {
|
|
|
|
// FIXME(zaphar): Check that this is safe first.
|
|
|
|
self.location.row = loc.row;
|
|
|
|
self.location.col = loc.col;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-11-13 19:02:41 -05:00
|
|
|
/// Get a cells formatted content.
|
2024-11-16 10:51:38 -05:00
|
|
|
pub fn get_current_cell_rendered(&self) -> Result<String> {
|
|
|
|
Ok(self.get_cell_addr_rendered(self.location.row, self.location.col)?)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(zaphar): Use Address here too
|
|
|
|
pub fn get_cell_addr_rendered(&self, row: usize, col: usize) -> Result<String> {
|
2024-11-13 19:39:47 -05:00
|
|
|
Ok(self
|
|
|
|
.model
|
2024-11-16 10:51:38 -05:00
|
|
|
.get_formatted_cell_value(self.current_sheet, row as i32, col as i32)
|
2024-11-13 19:02:41 -05:00
|
|
|
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
|
|
|
|
}
|
2024-11-13 19:39:47 -05:00
|
|
|
|
2024-11-13 19:02:41 -05:00
|
|
|
/// Get a cells actual content as a string.
|
2024-11-16 10:51:38 -05:00
|
|
|
pub fn get_current_cell_contents(&self) -> Result<String> {
|
2024-11-13 19:39:47 -05:00
|
|
|
Ok(self
|
|
|
|
.model
|
|
|
|
.get_cell_content(
|
|
|
|
self.current_sheet,
|
2024-11-16 10:51:38 -05:00
|
|
|
self.location.row as i32,
|
|
|
|
self.location.col as i32,
|
2024-11-13 19:39:47 -05:00
|
|
|
)
|
2024-11-13 19:02:41 -05:00
|
|
|
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
|
|
|
|
}
|
|
|
|
|
2024-11-16 10:51:38 -05:00
|
|
|
/// Update the current cell in a book.
|
|
|
|
/// This update won't be reflected until you call `Book::evaluate`.
|
|
|
|
pub fn edit_current_cell<S: Into<String>>(&mut self, value: S) -> Result<()> {
|
|
|
|
self.update_entry(&self.location.clone(), value)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Update an entry in the current sheet for a book.
|
|
|
|
/// This update won't be reflected until you call `Book::evaluate`.
|
|
|
|
pub fn update_entry<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> {
|
2024-11-13 19:39:47 -05:00
|
|
|
self.model
|
|
|
|
.set_user_input(
|
|
|
|
self.current_sheet,
|
2024-11-16 10:51:38 -05:00
|
|
|
location.row as i32,
|
|
|
|
location.col as i32,
|
|
|
|
value.into(),
|
2024-11-13 19:39:47 -05:00
|
|
|
)
|
|
|
|
.map_err(|e| anyhow!("Invalid cell contents: {}", e))?;
|
2024-11-13 19:02:41 -05:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-11-16 10:51:38 -05:00
|
|
|
pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> {
|
2024-11-16 20:25:35 -05:00
|
|
|
self.model
|
2024-11-16 10:51:38 -05:00
|
|
|
.insert_rows(self.current_sheet, row_idx as i32, count as i32)
|
2024-11-16 20:25:35 -05:00
|
|
|
.map_err(|e| anyhow!("Unable to insert row(s): {}", e))?;
|
2024-11-18 18:00:16 -05:00
|
|
|
if self.location.row >= row_idx {
|
|
|
|
self.move_to(Address {
|
|
|
|
row: self.location.row + count,
|
|
|
|
col: self.location.col,
|
|
|
|
})?;
|
|
|
|
}
|
2024-11-16 20:25:35 -05:00
|
|
|
Ok(())
|
2024-11-16 10:51:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> {
|
2024-11-16 20:25:35 -05:00
|
|
|
self.model
|
2024-11-16 10:51:38 -05:00
|
|
|
.insert_columns(self.current_sheet, col_idx as i32, count as i32)
|
2024-11-16 20:25:35 -05:00
|
|
|
.map_err(|e| anyhow!("Unable to insert column(s): {}", e))?;
|
2024-11-18 18:00:16 -05:00
|
|
|
if self.location.col >= col_idx {
|
|
|
|
self.move_to(Address {
|
|
|
|
row: self.location.row,
|
|
|
|
col: self.location.col + count,
|
|
|
|
})?;
|
|
|
|
}
|
2024-11-16 20:25:35 -05:00
|
|
|
Ok(())
|
2024-11-16 10:51:38 -05:00
|
|
|
}
|
|
|
|
|
2024-11-13 19:02:41 -05:00
|
|
|
/// Get the current sheets dimensions. This is a somewhat expensive calculation.
|
2024-11-16 10:51:38 -05:00
|
|
|
pub fn get_dimensions(&self) -> Result<WorksheetDimension> {
|
|
|
|
Ok(self.get_sheet()?.dimension())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the size of the current sheet as a `(row_count, column_count)`
|
|
|
|
pub fn get_size(&self) -> Result<(usize, usize)> {
|
|
|
|
let sheet = &self.get_sheet()?.sheet_data;
|
|
|
|
let mut row_count = 0 as i32;
|
|
|
|
let mut col_count = 0 as i32;
|
|
|
|
for (ri, cols) in sheet.iter() {
|
2024-11-16 20:25:35 -05:00
|
|
|
row_count = max(*ri, row_count);
|
2024-11-16 10:51:38 -05:00
|
|
|
for (ci, _) in cols.iter() {
|
2024-11-16 20:25:35 -05:00
|
|
|
col_count = max(*ci, col_count);
|
2024-11-16 10:51:38 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok((row_count as usize, col_count as usize))
|
2024-11-13 19:02:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Select a sheet by name.
|
|
|
|
pub fn select_sheet_by_name(&mut self, name: &str) -> bool {
|
2024-11-13 19:39:47 -05:00
|
|
|
if let Some(sheet) = self
|
|
|
|
.model
|
|
|
|
.workbook
|
|
|
|
.worksheets
|
|
|
|
.iter()
|
|
|
|
.find(|sheet| sheet.name == name)
|
|
|
|
{
|
2024-11-13 19:02:41 -05:00
|
|
|
self.current_sheet = sheet.sheet_id;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
2024-11-13 19:49:31 -05:00
|
|
|
/// Get all sheet names
|
2024-11-13 19:02:41 -05:00
|
|
|
pub fn get_sheet_names(&self) -> Vec<String> {
|
|
|
|
self.model.workbook.get_worksheet_names()
|
|
|
|
}
|
2024-11-13 19:39:47 -05:00
|
|
|
|
2024-11-13 19:02:41 -05:00
|
|
|
/// Select a sheet by id.
|
|
|
|
pub fn select_sheet_by_id(&mut self, id: u32) -> bool {
|
2024-11-13 19:39:47 -05:00
|
|
|
if let Some(sheet) = self
|
|
|
|
.model
|
|
|
|
.workbook
|
|
|
|
.worksheets
|
|
|
|
.iter()
|
|
|
|
.find(|sheet| sheet.sheet_id == id)
|
|
|
|
{
|
2024-11-13 19:02:41 -05:00
|
|
|
self.current_sheet = sheet.sheet_id;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
2024-11-13 19:39:47 -05:00
|
|
|
|
2024-11-16 10:51:38 -05:00
|
|
|
pub(crate) fn get_sheet(&self) -> Result<&Worksheet> {
|
2024-11-13 19:39:47 -05:00
|
|
|
Ok(self
|
|
|
|
.model
|
|
|
|
.workbook
|
|
|
|
.worksheet(self.current_sheet)
|
2024-11-11 19:27:59 -05:00
|
|
|
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
|
|
|
|
}
|
2024-11-16 10:51:38 -05:00
|
|
|
}
|
2024-11-13 19:39:47 -05:00
|
|
|
|
2024-11-16 10:51:38 -05:00
|
|
|
impl Default for Book {
|
|
|
|
fn default() -> Self {
|
2024-11-18 18:00:16 -05:00
|
|
|
let mut book =
|
|
|
|
Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap());
|
|
|
|
book.update_entry(&Address { row: 1, col: 1 }, "").unwrap();
|
2024-11-16 20:25:35 -05:00
|
|
|
book
|
2024-11-11 19:27:59 -05:00
|
|
|
}
|
|
|
|
}
|