use std::cmp::max; use anyhow::{anyhow, Result}; use ironcalc::{ base::{ calc_result::Range, expressions::types::Area, types::{Border, Col, Fill, Font, Row, SheetData, Style, Worksheet}, worksheet::WorksheetDimension, Model, UserModel }, export::save_xlsx_to_writer, import::load_from_xlsx, }; use crate::ui::Address; #[cfg(test)] mod test; const COL_PIXELS: f64 = 5.0; #[derive(Debug, Clone)] pub struct AddressRange<'book> { pub start: &'book Address, pub end: &'book Address, } impl<'book> AddressRange<'book> { pub fn as_rows(&self) -> Vec> { let (row_range, col_range) = self.get_ranges(); let mut rows = Vec::with_capacity(row_range.len()); for ri in row_range.iter() { let mut row = Vec::with_capacity(col_range.len()); for ci in col_range.iter() { row.push(Address { row: *ri, col: *ci }); } rows.push(row); } rows } pub fn as_series(&self) -> Vec
{ let (row_range, col_range) = self.get_ranges(); let mut rows = Vec::with_capacity(row_range.len() * col_range.len()); for ri in row_range.iter() { for ci in col_range.iter() { rows.push(Address { row: *ri, col: *ci }); } } rows } fn get_ranges(&self) -> (Vec, Vec) { let row_range = if self.start.row <= self.end.row { (self.start.row..=self.end.row) .into_iter() .collect::>() } else { let mut v = (self.start.row..=self.end.row) .into_iter() .collect::>(); v.reverse(); v }; let col_range = if self.start.col <= self.end.col { (self.start.col..=self.end.col) .into_iter() .collect::>() } else { let mut v = (self.start.col..=self.end.col) .into_iter() .collect::>(); v.reverse(); v }; (row_range, col_range) } } /// A spreadsheet book with some internal state tracking. pub struct Book { pub(crate) model: UserModel, pub current_sheet: u32, pub location: crate::ui::Address, } impl Book { /// Construct a new book from a Model pub fn new(model: UserModel) -> Self { Self { model, current_sheet: 0, location: Address::default(), } } pub fn from_model(model: Model) -> Self { Self::new(UserModel::from_model(model)) } /// Construct a new book from an xlsx file. pub fn new_from_xlsx(path: &str) -> Result { Ok(Self::from_model(load_from_xlsx( path, "en", "America/New_York", )?)) } /// Evaluate the spreadsheet calculating formulas and style changes. /// This can be an expensive operation. pub fn evaluate(&mut self) { self.model.evaluate(); } // TODO(zaphar): Should I support ICalc? /// Construct a new book from a path. pub fn new_from_xlsx_with_locale(path: &str, locale: &str, tz: &str) -> Result { Ok(Self::from_model(load_from_xlsx(path, locale, tz)?)) } /// Save book to an xlsx file. pub fn save_to_xlsx(&self, path: &str) -> Result<()> { // 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.get_model(), writer)?; Ok(()) } /// Get all the sheet identiers a `Vec<(String, u32)>` where the string /// is the sheet name and the u32 is the sheet index. pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> { self.model .get_worksheets_properties() .iter() .map(|sheet| (sheet.name.to_owned(), sheet.sheet_id)) .collect() } /// Get the current sheets name. pub fn get_sheet_name(&self) -> Result<&str> { Ok(&self.get_sheet()?.name) } pub fn set_sheet_name(&mut self, idx: u32, sheet_name: &str) -> Result<()> { self.model.rename_sheet(idx, sheet_name).map_err(|e| anyhow!(e))?; Ok(()) } pub fn new_sheet(&mut self, sheet_name: Option<&str>) -> Result<()> { self.model.new_sheet().map_err(|e| anyhow!(e))?; let idx = self.model.get_selected_sheet(); if let Some(name) = sheet_name { self.set_sheet_name(idx, name)?; } self.model.set_selected_sheet(self.current_sheet).map_err(|e| anyhow!(e))?; Ok(()) } /// Get the sheet data for the current worksheet. pub fn get_sheet_data(&self) -> Result<&SheetData> { Ok(&self.get_sheet()?.sheet_data) } /// Move to a specific sheet location in the current sheet pub fn move_to(&mut self, Address { row, col }: &Address) -> Result<()> { // FIXME(zaphar): Check that this is safe first. self.location.row = *row; self.location.col = *col; Ok(()) } /// Extend a cell to the rest of the range. pub fn extend_to(&mut self, from: &Address, to: &Address) -> Result<()> { for cell in (AddressRange { start: from, end: to, }) .as_series() .iter() .skip(1) { // TODO(jeremy): Is there a better way to do this using UserModel? let contents = self .model .get_model() .extend_to( self.current_sheet, from.row as i32, from.col as i32, cell.row as i32, cell.col as i32, ) .map_err(|e| anyhow!(e))?; self.model .set_user_input( self.current_sheet, cell.row as i32, cell.col as i32, &contents, ) .map_err(|e| anyhow!(e))?; } self.evaluate(); Ok(()) } pub fn clear_current_cell(&mut self) -> Result<()> { self.clear_cell_contents(self.current_sheet as u32, self.location.clone()) } pub fn clear_current_cell_all(&mut self) -> Result<()> { self.clear_cell_all(self.current_sheet as u32, self.location.clone()) } pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> { Ok(self .model .range_clear_contents(&Area { sheet, row: row as i32, column: col as i32, width: 1, height: 1, }) .map_err(|s| anyhow!("Unable to clear cell contents {}", s))?) } pub fn clear_cell_range(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> { let area = Area { sheet, row: start.row as i32, column: start.col as i32, width: (end.row - start.row) as i32, height: (end.col - end.row) as i32, }; self.model .range_clear_contents(&area) .map_err(|s| anyhow!("Unable to clear cell contents {}", s))?; Ok(()) } pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> { Ok(self .model .range_clear_all(&Area { sheet, row: row as i32, column: col as i32, width: 1, height: 1, }) .map_err(|s| anyhow!("Unable to clear cell contents {}", s))?) } pub fn clear_cell_range_all(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> { let area = Area { sheet, row: start.row as i32, column: start.col as i32, width: (end.row - start.row) as i32, height: (end.col - end.row) as i32, }; self.model.range_clear_all(&area) .map_err(|s| anyhow!("Unable to clear cell contents {}", s))?; Ok(()) } /// Get a cells formatted content. pub fn get_current_cell_rendered(&self) -> Result { Ok(self.get_cell_addr_rendered(&self.location)?) } pub fn get_cell_style(&self, sheet: u32, cell: &Address) -> Option