1use crate::core::formula::CompiledFormula;
19use crate::core::grid_edit::{Axis, GridEdit};
20use crate::core::parser::col_idx_to_letters;
21use crate::core::xlsx::{export_xlsx_data, import_xlsx_data};
22use crate::core::{
23 ExcelTable, PivotAggregation, PivotArea, PivotField, PivotFilterField, PivotGrid, PivotSource,
24 PivotTable, PivotValueField, VbaModule, VbaModuleKind, VbaProject,
25 chart::{Chart, ChartType},
26 compute_pivot,
27 engine::{Context, DataColumn, ResultData, Sheet, generate_unique_id},
28 validate_vba_module_name,
29};
30use crate::{Error, ObjectKind};
31
32fn resize_table_columns(
41 table: &mut ExcelTable,
42 new_start_col: usize,
43 new_end_col: usize,
44 edit: &GridEdit,
45) {
46 if edit.insert {
47 if edit.at > table.start_col && edit.at <= table.end_col {
51 let offset = (edit.at - table.start_col).min(table.columns.len());
52 for _ in 0..edit.count {
53 table.columns.insert(offset, String::new());
54 }
55 }
56 } else {
57 let first = edit.at.max(table.start_col);
58 let last = (edit.at + edit.count).min(table.end_col + 1);
59 if first < last {
60 let lo = (first - table.start_col).min(table.columns.len());
61 let hi = (last - table.start_col).min(table.columns.len());
62 table.columns.drain(lo..hi);
63 }
64 }
65 table
68 .columns
69 .resize(new_end_col - new_start_col + 1, String::new());
70}
71
72pub struct SheetSummary {
74 pub name: String,
76 pub row_count: usize,
78 pub col_count: usize,
80 pub formula_count: usize,
82}
83
84pub struct WorkbookSummary {
86 pub file_name: String,
88 pub sheet_count: usize,
90 pub chart_count: usize,
92 pub sheets: Vec<SheetSummary>,
94}
95
96pub struct WorkbookManager {
112 pub sheets: Vec<Sheet>,
115 pub charts: Vec<Chart>,
118 pub pivot_tables: Vec<PivotTable>,
121 pub vba_project: Option<VbaProject>,
123}
124
125fn pivot_label_literal(text: &str) -> String {
129 if text.is_empty() {
130 String::new()
131 } else if text.starts_with('=')
132 || text.parse::<f64>().is_ok()
133 || text.eq_ignore_ascii_case("true")
134 || text.eq_ignore_ascii_case("false")
135 {
136 format!("\"{}\"", text)
137 } else {
138 text.to_string()
139 }
140}
141
142fn pivot_value_literal(v: &ResultData) -> String {
146 match v {
147 ResultData::Error(e) => e.clone(),
148 other => other.to_string(),
149 }
150}
151
152fn remove_pivot_field(fields: &mut Vec<PivotField>, column: &str) -> bool {
153 let before = fields.len();
154 fields.retain(|f| !f.column.eq_ignore_ascii_case(column));
155 before != fields.len()
156}
157
158impl WorkbookManager {
159 pub fn load_bytes(buffer: &[u8]) -> crate::Result<Self> {
161 let (imported_tables, charts, pivot_tables, vba_project) =
162 import_xlsx_data(buffer, &[], |_, _, _| {})?;
163
164 let sheets = imported_tables.into_iter().map(|it| it.sheet).collect();
165 Ok(Self {
166 sheets,
167 charts,
168 pivot_tables,
169 vba_project,
170 })
171 }
172
173 pub fn save_bytes(&self) -> crate::Result<Vec<u8>> {
179 export_xlsx_data(
180 &self.sheets,
181 &self.charts,
182 &self.pivot_tables,
183 self.vba_project.as_ref(),
184 )
185 }
186
187 pub fn new_empty() -> crate::Result<Self> {
189 let mut wb = Self {
190 sheets: Vec::new(),
191 charts: Vec::new(),
192 pivot_tables: Vec::new(),
193 vba_project: None,
194 };
195 wb.add_sheet("Sheet1")?;
196 Ok(wb)
197 }
198
199 pub fn evaluate(&mut self) -> crate::Result<()> {
201 if self.sheets.is_empty() {
202 return Ok(());
203 }
204
205 let sheet_order: Vec<String> = self.sheets.iter().map(|s| s.name.clone()).collect();
209
210 for _pass in 0..3 {
221 for sheet in &mut self.sheets {
222 sheet.mark_all_dirty();
223 }
224 for i in 0..self.sheets.len() {
225 let (left, right) = self.sheets.split_at_mut(i);
226 let (target_sheet, right_tail) = right.split_first_mut().unwrap();
227
228 let mut context = Context::new();
229 for s in left.iter() {
230 context.add_table(s.name.clone(), s);
231 }
232 for s in right_tail.iter() {
233 context.add_table(s.name.clone(), s);
234 }
235 context.pivot_tables = &self.pivot_tables;
236 context.sheet_order = sheet_order.clone();
237
238 let _ = target_sheet.commit(Some(&context));
239 }
240 }
241
242 Ok(())
243 }
244
245 pub(crate) fn call_worksheet_function(
253 &self,
254 name: &str,
255 args: &[crate::core::parser::Expr],
256 ) -> Result<ResultData, crate::core::EngineError> {
257 let Some(host) = self.sheets.first() else {
258 return Err(crate::core::EngineError::EvalError(
259 crate::core::EvalError::UnknownFunction("no worksheets".to_string()),
260 ));
261 };
262 let mut context = Context::new();
263 for s in &self.sheets {
264 context.add_table(s.name.clone(), s);
265 }
266 context.pivot_tables = &self.pivot_tables;
267 context.sheet_order = self.sheets.iter().map(|s| s.name.clone()).collect();
268 host.call_worksheet_function(name, args, Some(&context))
269 }
270
271 pub fn find_sheet_index(&self, name_opt: Option<&str>) -> crate::Result<usize> {
273 if self.sheets.is_empty() {
274 return Err(Error::EmptyWorkbook);
275 }
276
277 match name_opt {
278 Some(name) => {
279 if let Some(idx) = self
280 .sheets
281 .iter()
282 .position(|s| s.name.eq_ignore_ascii_case(name))
283 {
284 Ok(idx)
285 } else {
286 let available: Vec<String> =
287 self.sheets.iter().map(|s| s.name.clone()).collect();
288 Err(Error::not_found_among(
289 ObjectKind::Sheet,
290 name.to_string(),
291 available,
292 ))
293 }
294 }
295 None => Ok(0),
296 }
297 }
298
299 pub fn get_summary(&self, file_name: &str) -> WorkbookSummary {
301 let sheet_summaries = self
302 .sheets
303 .iter()
304 .map(|sheet| {
305 let row_count = sheet.row_count();
306 let col_count = sheet.col_count();
307 let mut formula_count = 0;
308
309 for col in &sheet.columns {
310 for src in &col.src {
311 if src.starts_with('=') {
312 formula_count += 1;
313 }
314 }
315 }
316
317 SheetSummary {
318 name: sheet.name.clone(),
319 row_count,
320 col_count,
321 formula_count,
322 }
323 })
324 .collect();
325
326 WorkbookSummary {
327 file_name: file_name.to_string(),
328 sheet_count: self.sheets.len(),
329 chart_count: self.charts.len(),
330 sheets: sheet_summaries,
331 }
332 }
333
334 pub fn ensure_capacity(&mut self, sheet_idx: usize, target_row: usize, target_col: usize) {
336 if sheet_idx >= self.sheets.len() {
337 return;
338 }
339 self.sheets[sheet_idx].ensure_capacity(target_row, target_col);
340 }
341
342 pub fn set_cell_style(
349 &mut self,
350 sheet_name: Option<&str>,
351 row: usize,
352 col: usize,
353 style: crate::core::CellStyle,
354 ) -> crate::Result<()> {
355 let sheet_idx = self.find_sheet_index(sheet_name)?;
356 self.sheets[sheet_idx].update_cell_style(row, col, |s| s.merge(&style));
357 Ok(())
358 }
359
360 pub fn set_range_style(
362 &mut self,
363 sheet_name: Option<&str>,
364 start_row: usize,
365 start_col: usize,
366 end_row: usize,
367 end_col: usize,
368 style: crate::core::CellStyle,
369 ) -> crate::Result<()> {
370 if end_row < start_row || end_col < start_col {
371 return Err(Error::InvalidRange(
372 "range end must not precede its start".to_string(),
373 ));
374 }
375 let sheet_idx = self.find_sheet_index(sheet_name)?;
376 for r in start_row..=end_row {
377 for c in start_col..=end_col {
378 self.sheets[sheet_idx].update_cell_style(r, c, |s| s.merge(&style));
379 }
380 }
381 Ok(())
382 }
383
384 pub fn get_cell_style(
386 &self,
387 sheet_name: Option<&str>,
388 row: usize,
389 col: usize,
390 ) -> crate::Result<Option<crate::core::CellStyle>> {
391 let sheet_idx = self.find_sheet_index(sheet_name)?;
392 Ok(self.sheets[sheet_idx].get_cell_style(row, col).cloned())
393 }
394
395 pub fn set_table_style(&mut self, table_name: &str, style_name: &str) -> crate::Result<()> {
402 for sheet in &mut self.sheets {
403 for table in &mut sheet.tables {
404 if table.name.eq_ignore_ascii_case(table_name) {
405 table.set_style_name(Some(style_name.to_string()));
406 return Ok(());
407 }
408 }
409 }
410 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
411 }
412
413 pub fn get_table_style(&self, table_name: &str) -> crate::Result<Option<String>> {
419 for sheet in &self.sheets {
420 for table in &sheet.tables {
421 if table.name.eq_ignore_ascii_case(table_name) {
422 return Ok(table.style_name.clone());
423 }
424 }
425 }
426 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
427 }
428
429 pub fn set_cell(&mut self, sheet_idx: usize, row: usize, col: usize, value: String) {
431 self.ensure_capacity(sheet_idx, row, col);
432 let sheet = &mut self.sheets[sheet_idx];
433 sheet.set_cell_src(row, col, value);
434 }
435
436 pub fn insert_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
442 let sheet = &self.sheets[sheet_idx];
443 let at = row_idx.min(sheet.row_count());
446 let edit = GridEdit::insert_row(sheet.id, at);
447 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_row(at));
448 self.evaluate()
449 }
450
451 pub fn delete_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
456 let sheet = &self.sheets[sheet_idx];
457 if row_idx >= sheet.row_count() {
458 return Err(Error::OutOfBounds {
459 what: "row",
460 index: row_idx,
461 len: sheet.row_count(),
462 });
463 }
464 let edit = GridEdit::delete_row(sheet.id, row_idx);
465 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].delete_row(row_idx));
466 self.evaluate()
467 }
468
469 pub fn insert_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
471 let sheet = &self.sheets[sheet_idx];
472 let at = col_idx.min(sheet.col_count());
473 let edit = GridEdit::insert_col(sheet.id, at);
474 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_col(at));
475 self.evaluate()
476 }
477
478 pub fn delete_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
480 let sheet = &self.sheets[sheet_idx];
481 if col_idx >= sheet.col_count() {
482 return Err(Error::OutOfBounds {
483 what: "column",
484 index: col_idx,
485 len: sheet.col_count(),
486 });
487 }
488 let deleted_col_ids = vec![sheet.columns()[col_idx].id];
492 let edit = GridEdit::delete_col(sheet.id, col_idx);
493 self.apply_grid_edit(edit, &deleted_col_ids, |wb| {
494 wb.sheets[sheet_idx].delete_col(col_idx)
495 });
496 self.evaluate()
497 }
498
499 pub fn insert_cells_shift_down(
507 &mut self,
508 sheet_idx: usize,
509 row: usize,
510 first_col: usize,
511 last_col: usize,
512 count: usize,
513 ) -> crate::Result<()> {
514 let sheet = &self.sheets[sheet_idx];
515 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, true);
516 self.apply_grid_edit(edit, &[], |wb| {
517 wb.sheets[sheet_idx].insert_cells_shift_down(row, first_col, last_col, count)
518 });
519 self.evaluate()
520 }
521
522 pub fn delete_cells_shift_up(
525 &mut self,
526 sheet_idx: usize,
527 row: usize,
528 first_col: usize,
529 last_col: usize,
530 count: usize,
531 ) -> crate::Result<()> {
532 let sheet = &self.sheets[sheet_idx];
533 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, false);
534 self.apply_grid_edit(edit, &[], |wb| {
535 wb.sheets[sheet_idx].delete_cells_shift_up(row, first_col, last_col, count)
536 });
537 self.evaluate()
538 }
539
540 fn apply_grid_edit(
558 &mut self,
559 edit: GridEdit,
560 deleted_col_ids: &[u64],
561 apply: impl FnOnce(&mut Self),
562 ) {
563 let mut shifted: Vec<(usize, usize, usize, CompiledFormula)> = Vec::new();
567 for (sheet_idx, sheet) in self.sheets.iter().enumerate() {
568 for (col_idx, column) in sheet.columns().iter().enumerate() {
569 for row_idx in 0..column.len() {
570 let Some(src) = column.src(row_idx).filter(|s| s.starts_with('=')) else {
571 continue;
572 };
573 let compiled = crate::core::parser::compile_formula(src, &self.sheets);
574 if let Some(next) =
575 crate::core::grid_edit::shift_formula(&compiled, &edit, deleted_col_ids)
576 {
577 shifted.push((sheet_idx, col_idx, row_idx, next));
578 }
579 }
580 }
581 }
582
583 apply(self);
585 self.shift_table_and_pivot_ranges(&edit);
586
587 for (sheet_idx, col_idx, row_idx, compiled) in shifted {
589 let Some((row, col)) = self.moved_cell(&edit, sheet_idx, row_idx, col_idx) else {
590 continue;
592 };
593 let text = crate::core::parser::serialize_formula(&compiled, &self.sheets);
594 self.sheets[sheet_idx].set_cell_src(row, col, text);
595 }
596 }
597
598 fn moved_cell(
601 &self,
602 edit: &GridEdit,
603 sheet_idx: usize,
604 row: usize,
605 col: usize,
606 ) -> Option<(usize, usize)> {
607 if self.sheets[sheet_idx].id != edit.sheet_id || !edit.covers_columns(col, col) {
612 return Some((row, col));
613 }
614 let moved = |index: usize| {
615 crate::core::grid_edit::shift_point(index, edit.at, edit.count, edit.insert)
616 };
617 match edit.axis {
618 Axis::Row => Some((moved(row)?, col)),
619 Axis::Col => Some((row, moved(col)?)),
620 }
621 }
622
623 fn shift_table_and_pivot_ranges(&mut self, edit: &GridEdit) {
629 use crate::core::grid_edit::{shift_point, shift_rect};
630
631 for sheet in &mut self.sheets {
632 if sheet.id != edit.sheet_id {
633 continue;
634 }
635 sheet.tables.retain_mut(|table| {
636 if !edit.covers_columns(table.start_col, table.end_col) {
639 return true;
640 }
641 match shift_rect(
642 edit,
643 table.start_row,
644 table.start_col,
645 table.end_row,
646 table.end_col,
647 ) {
648 Some((r0, c0, r1, c1)) => {
649 if edit.axis == Axis::Col {
653 resize_table_columns(table, c0, c1, edit);
654 }
655 table.start_row = r0;
656 table.start_col = c0;
657 table.end_row = r1;
658 table.end_col = c1;
659 true
660 }
661 None => false,
662 }
663 });
664 }
665
666 for pivot in &mut self.pivot_tables {
667 if let PivotSource::Range {
668 sheet_id,
669 start_row,
670 start_col,
671 end_row,
672 end_col,
673 } = &mut pivot.source
674 && *sheet_id == edit.sheet_id
675 && edit.covers_columns(*start_col, *end_col)
676 && let Some((r0, c0, r1, c1)) =
677 shift_rect(edit, *start_row, *start_col, *end_row, *end_col)
678 {
679 *start_row = r0;
680 *start_col = c0;
681 *end_row = r1;
682 *end_col = c1;
683 }
684
685 if pivot.dest_sheet_id == edit.sheet_id
686 && edit.covers_columns(pivot.dest_col, pivot.dest_col)
687 {
688 match edit.axis {
693 Axis::Row => {
694 pivot.dest_row =
695 shift_point(pivot.dest_row, edit.at, edit.count, edit.insert)
696 .unwrap_or(edit.at);
697 }
698 Axis::Col => {
699 pivot.dest_col =
700 shift_point(pivot.dest_col, edit.at, edit.count, edit.insert)
701 .unwrap_or(edit.at);
702 }
703 }
704 pivot.last_output_end_row = None;
708 pivot.last_output_end_col = None;
709 }
710 }
711 }
712
713 pub fn add_sheet(&mut self, name: &str) -> crate::Result<()> {
715 if self
716 .sheets
717 .iter()
718 .any(|s| s.name.eq_ignore_ascii_case(name))
719 {
720 return Err(Error::AlreadyExists {
721 kind: ObjectKind::Sheet,
722 name: name.to_string(),
723 });
724 }
725
726 let mut columns = Vec::new();
727 for col_idx in 0..5 {
728 let mut col = DataColumn::new(10);
729 col.id = generate_unique_id();
730 col.name = col_idx_to_letters(col_idx);
731 columns.push(col);
732 }
733
734 let new_sheet = Sheet {
735 id: generate_unique_id(),
736 name: name.to_string(),
737 columns,
738 tables: Vec::new(),
739 dependencies: std::collections::HashMap::new(),
740 dependencies_rev: std::collections::HashMap::new(),
741 uncommitted_actions: Vec::new(),
742 };
743
744 self.sheets.push(new_sheet);
745 Ok(())
746 }
747
748 pub fn delete_sheet(&mut self, name: &str) -> crate::Result<()> {
750 let idx = self.find_sheet_index(Some(name))?;
751 if self.sheets.len() <= 1 {
752 return Err(Error::LastSheetInWorkbook);
753 }
754 self.sheets.remove(idx);
755 Ok(())
756 }
757
758 pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
760 let idx = self.find_sheet_index(Some(old_name))?;
761 if self
762 .sheets
763 .iter()
764 .enumerate()
765 .any(|(i, s)| i != idx && s.name.eq_ignore_ascii_case(new_name))
766 {
767 return Err(Error::NameTaken {
768 kind: ObjectKind::Sheet,
769 name: new_name.to_string(),
770 });
771 }
772 self.sheets[idx].name = new_name.to_string();
773 Ok(())
774 }
775
776 #[allow(clippy::too_many_arguments)]
778 pub fn add_chart(
779 &mut self,
780 sheet_name: &str,
781 chart_type: ChartType,
782 range: String,
783 title: Option<String>,
784 anchor: Option<(usize, usize)>,
785 ) -> crate::Result<u64> {
786 let _ = self.find_sheet_index(Some(sheet_name))?;
787 let id = generate_unique_id();
788 let name = format!("Chart {}", self.charts.len() + 1);
789 let (anchor_row, anchor_col) = anchor.unwrap_or((0, 0));
790
791 let chart = Chart {
792 id,
793 name,
794 chart_type,
795 data_range: range,
796 title,
797 xlabel: None,
798 ylabel: None,
799 show_legend: true,
800 anchor_row,
801 anchor_col,
802 };
803
804 self.charts.push(chart);
805 Ok(id)
806 }
807
808 #[allow(clippy::too_many_arguments)]
813 pub fn edit_chart(
814 &mut self,
815 id: u64,
816 name: Option<String>,
817 chart_type: Option<ChartType>,
818 data_range: Option<String>,
819 title: Option<Option<String>>,
820 xlabel: Option<Option<String>>,
821 ylabel: Option<Option<String>>,
822 show_legend: Option<bool>,
823 anchor: Option<(usize, usize)>,
824 ) -> crate::Result<()> {
825 let chart = self
826 .charts
827 .iter_mut()
828 .find(|c| c.id == id)
829 .ok_or_else(|| Error::not_found(ObjectKind::Chart, id.to_string()))?;
830 if let Some(name) = name {
831 chart.name = name;
832 }
833 if let Some(chart_type) = chart_type {
834 chart.chart_type = chart_type;
835 }
836 if let Some(data_range) = data_range {
837 chart.data_range = data_range;
838 }
839 if let Some(title) = title {
840 chart.title = title;
841 }
842 if let Some(xlabel) = xlabel {
843 chart.xlabel = xlabel;
844 }
845 if let Some(ylabel) = ylabel {
846 chart.ylabel = ylabel;
847 }
848 if let Some(show_legend) = show_legend {
849 chart.show_legend = show_legend;
850 }
851 if let Some((anchor_row, anchor_col)) = anchor {
852 chart.anchor_row = anchor_row;
853 chart.anchor_col = anchor_col;
854 }
855 Ok(())
856 }
857
858 pub fn has_vba_project(&self) -> bool {
860 self.vba_project.is_some()
861 }
862
863 pub fn list_vba_modules(&self) -> Vec<&VbaModule> {
865 self.vba_project
866 .as_ref()
867 .map(|p| p.modules.iter().collect())
868 .unwrap_or_default()
869 }
870
871 pub fn ensure_vba_project(&mut self) -> crate::Result<()> {
875 if self.vba_project.is_some() {
876 return Ok(());
877 }
878 self.vba_project = Some(VbaProject::new_empty());
879 Ok(())
880 }
881
882 pub fn add_vba_module(
892 &mut self,
893 name: String,
894 kind: VbaModuleKind,
895 source: String,
896 bound_sheet_id: Option<u64>,
897 ) -> crate::Result<()> {
898 validate_vba_module_name(&name).map_err(|reason| Error::InvalidName {
899 kind: ObjectKind::VbaModule,
900 name: name.clone(),
901 reason,
902 })?;
903 let is_this_workbook = kind == VbaModuleKind::Document && name == "ThisWorkbook";
904 if kind == VbaModuleKind::Document && !is_this_workbook {
905 let sheet_id = bound_sheet_id
906 .ok_or_else(|| Error::Vba("document modules require a bound sheet".to_string()))?;
907 if !self.sheets.iter().any(|s| s.id == sheet_id) {
908 return Err(Error::not_found(ObjectKind::Sheet, sheet_id.to_string()));
909 }
910 }
911 self.ensure_vba_project()?;
912 let project = self.vba_project.as_mut().unwrap();
913 if project.module_name_taken(&name) {
914 return Err(Error::AlreadyExists {
915 kind: ObjectKind::VbaModule,
916 name: name.to_string(),
917 });
918 }
919 if kind == VbaModuleKind::Document
920 && bound_sheet_id.is_some()
921 && project
922 .modules
923 .iter()
924 .any(|m| m.kind == VbaModuleKind::Document && m.bound_sheet_id == bound_sheet_id)
925 {
926 return Err(Error::DocumentModuleExists);
927 }
928 let prefix_bytes = project
936 .modules
937 .first()
938 .map(|m| m.prefix_bytes.clone())
939 .unwrap_or_else(|| project.seed_prefix_bytes.clone());
940 let module_cookie = project
941 .modules
942 .first()
943 .map(|m| m.module_cookie)
944 .unwrap_or(project.seed_module_cookie);
945 let stored_bound_sheet_id = if kind == VbaModuleKind::Document && !is_this_workbook {
946 bound_sheet_id
947 } else {
948 None
949 };
950 project.modules.push(VbaModule {
951 name,
952 kind,
953 source,
954 bound_sheet_id: stored_bound_sheet_id,
955 prefix_bytes,
956 module_cookie,
957 cached_compressed_source: None,
959 });
960 Ok(())
961 }
962
963 pub fn remove_vba_module(&mut self, name: &str) -> crate::Result<()> {
970 let project = self
971 .vba_project
972 .as_mut()
973 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
974 let before = project.modules.len();
975 project
976 .modules
977 .retain(|m| !m.name.eq_ignore_ascii_case(name));
978 if project.modules.len() == before {
979 return Err(Error::not_found(ObjectKind::VbaModule, name.to_string()));
980 }
981 Ok(())
982 }
983
984 pub fn rename_vba_module(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
997 validate_vba_module_name(new_name).map_err(|reason| Error::InvalidName {
998 kind: ObjectKind::VbaModule,
999 name: new_name.to_string(),
1000 reason,
1001 })?;
1002 let project = self
1003 .vba_project
1004 .as_mut()
1005 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1006 if !old_name.eq_ignore_ascii_case(new_name) && project.module_name_taken(new_name) {
1007 return Err(Error::AlreadyExists {
1008 kind: ObjectKind::VbaModule,
1009 name: new_name.to_string(),
1010 });
1011 }
1012 let module = project
1013 .find_module_mut(old_name)
1014 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, old_name))?;
1015 module.name = new_name.to_string();
1016 Ok(())
1017 }
1018
1019 pub fn set_vba_module_source(&mut self, name: &str, source: String) -> crate::Result<()> {
1030 let project = self
1031 .vba_project
1032 .as_mut()
1033 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1034 let module = project
1035 .find_module_mut(name)
1036 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, name))?;
1037 module.source = source;
1038 module.cached_compressed_source = None;
1042 Ok(())
1043 }
1044
1045 pub fn delete_chart(&mut self, id: u64) -> crate::Result<()> {
1047 if let Some(pos) = self.charts.iter().position(|c| c.id == id) {
1048 self.charts.remove(pos);
1049 Ok(())
1050 } else {
1051 Err(Error::not_found(ObjectKind::Chart, id.to_string()))
1052 }
1053 }
1054
1055 pub fn find_table(&self, name: &str) -> Option<(&Sheet, &ExcelTable)> {
1058 self.sheets
1059 .iter()
1060 .find_map(|s| s.find_table(name).map(|t| (s, t)))
1061 }
1062
1063 pub fn list_tables(&self) -> Vec<(&str, &ExcelTable)> {
1066 self.sheets
1067 .iter()
1068 .flat_map(|s| s.tables.iter().map(move |t| (s.name.as_str(), t)))
1069 .collect()
1070 }
1071
1072 fn find_table_sheet_index(&self, name: &str) -> crate::Result<usize> {
1073 self.sheets
1074 .iter()
1075 .position(|s| s.find_table(name).is_some())
1076 .ok_or_else(|| Error::not_found(ObjectKind::Table, name))
1077 }
1078
1079 fn table_name_taken(&self, name: &str) -> bool {
1080 self.sheets
1081 .iter()
1082 .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(name)))
1083 }
1084
1085 #[allow(clippy::too_many_arguments)]
1089 pub fn add_table(
1090 &mut self,
1091 sheet_name: Option<&str>,
1092 name: &str,
1093 start_row: usize,
1094 start_col: usize,
1095 end_row: usize,
1096 end_col: usize,
1097 has_header_row: bool,
1098 has_totals_row: bool,
1099 ) -> crate::Result<u64> {
1100 if self.table_name_taken(name) {
1101 return Err(Error::AlreadyExists {
1102 kind: ObjectKind::Table,
1103 name: name.to_string(),
1104 });
1105 }
1106 let idx = self.find_sheet_index(sheet_name)?;
1107 self.sheets[idx]
1108 .add_table(
1109 name.to_string(),
1110 start_row,
1111 start_col,
1112 end_row,
1113 end_col,
1114 has_header_row,
1115 has_totals_row,
1116 )
1117 .map_err(Error::InvalidArgument)
1118 }
1119
1120 pub fn delete_table(&mut self, name: &str) -> crate::Result<()> {
1122 let idx = self.find_table_sheet_index(name)?;
1123 self.sheets[idx]
1124 .delete_table_by_name(name)
1125 .map_err(Error::InvalidArgument)
1126 }
1127
1128 pub fn rename_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1130 if !old_name.eq_ignore_ascii_case(new_name) && self.table_name_taken(new_name) {
1131 return Err(Error::NameTaken {
1132 kind: ObjectKind::Table,
1133 name: new_name.to_string(),
1134 });
1135 }
1136 let idx = self.find_table_sheet_index(old_name)?;
1137 self.sheets[idx]
1138 .rename_table(old_name, new_name)
1139 .map_err(Error::InvalidArgument)?;
1140 self.rewrite_table_references(old_name, Some(new_name), None);
1144 self.evaluate()
1145 }
1146
1147 fn rewrite_table_references(
1152 &mut self,
1153 table_name: &str,
1154 new_table_name: Option<&str>,
1155 col_rename: Option<(&str, &str)>,
1156 ) {
1157 for sheet in &mut self.sheets {
1158 for col_idx in 0..sheet.columns.len() {
1159 let row_count = sheet.columns[col_idx].src.len();
1160 for row_idx in 0..row_count {
1161 let src = sheet.columns[col_idx].src[row_idx].clone();
1162 if let Some(new_src) = crate::core::parser::rewrite_structured_table_reference(
1163 &src,
1164 table_name,
1165 new_table_name,
1166 col_rename,
1167 ) {
1168 sheet.set_cell_src(row_idx, col_idx, new_src);
1169 }
1170 }
1171 }
1172 }
1173 }
1174
1175 pub fn resize_table(
1177 &mut self,
1178 name: &str,
1179 new_end_row: usize,
1180 new_end_col: usize,
1181 ) -> crate::Result<()> {
1182 let idx = self.find_table_sheet_index(name)?;
1183 self.sheets[idx]
1184 .resize_table(name, new_end_row, new_end_col)
1185 .map_err(Error::InvalidArgument)
1186 }
1187
1188 pub fn rename_table_column(
1190 &mut self,
1191 table_name: &str,
1192 col_index: usize,
1193 new_name: &str,
1194 ) -> crate::Result<()> {
1195 let idx = self.find_table_sheet_index(table_name)?;
1196 let old_col_name = self.sheets[idx]
1197 .find_table(table_name)
1198 .and_then(|t| t.columns.get(col_index).cloned())
1199 .ok_or_else(|| {
1200 Error::InvalidArgument(format!(
1201 "column index {col_index} out of bounds for table '{table_name}'"
1202 ))
1203 })?;
1204 self.sheets[idx]
1205 .rename_table_column(table_name, col_index, new_name)
1206 .map_err(Error::InvalidArgument)?;
1207 self.rewrite_table_references(table_name, None, Some((&old_col_name, new_name)));
1210 self.evaluate()
1211 }
1212
1213 pub fn find_pivot_table(&self, name: &str) -> Option<&PivotTable> {
1215 self.pivot_tables
1216 .iter()
1217 .find(|p| p.name.eq_ignore_ascii_case(name))
1218 }
1219
1220 fn find_pivot_table_index(&self, name: &str) -> crate::Result<usize> {
1221 self.pivot_tables
1222 .iter()
1223 .position(|p| p.name.eq_ignore_ascii_case(name))
1224 .ok_or_else(|| Error::not_found(ObjectKind::PivotTable, name))
1225 }
1226
1227 pub fn list_pivot_tables(&self) -> &[PivotTable] {
1229 &self.pivot_tables
1230 }
1231
1232 fn pivot_table_name_taken(&self, name: &str) -> bool {
1233 self.pivot_tables
1234 .iter()
1235 .any(|p| p.name.eq_ignore_ascii_case(name))
1236 }
1237
1238 #[allow(clippy::too_many_arguments)]
1242 pub fn add_pivot_table_from_table(
1243 &mut self,
1244 name: &str,
1245 source_table_name: &str,
1246 dest_sheet_name: Option<&str>,
1247 dest_row: usize,
1248 dest_col: usize,
1249 grand_totals_row: bool,
1250 grand_totals_col: bool,
1251 ) -> crate::Result<u64> {
1252 if self.pivot_table_name_taken(name) {
1253 return Err(Error::AlreadyExists {
1254 kind: ObjectKind::PivotTable,
1255 name: name.to_string(),
1256 });
1257 }
1258 self.find_table(source_table_name)
1259 .ok_or_else(|| Error::not_found(ObjectKind::Table, source_table_name))?;
1260 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1261 let id = generate_unique_id();
1262 self.pivot_tables.push(PivotTable {
1263 id,
1264 name: name.to_string(),
1265 source: PivotSource::Table {
1266 name: source_table_name.to_string(),
1267 },
1268 dest_sheet_id: self.sheets[dest_idx].id,
1269 dest_row,
1270 dest_col,
1271 row_fields: Vec::new(),
1272 col_fields: Vec::new(),
1273 value_fields: Vec::new(),
1274 filter_fields: Vec::new(),
1275 grand_totals_row,
1276 grand_totals_col,
1277 last_output_end_row: None,
1278 last_output_end_col: None,
1279 });
1280 self.refresh_pivot_table(name)?;
1281 Ok(id)
1282 }
1283
1284 #[allow(clippy::too_many_arguments)]
1287 pub fn add_pivot_table_from_range(
1288 &mut self,
1289 name: &str,
1290 source_sheet_name: Option<&str>,
1291 start_row: usize,
1292 start_col: usize,
1293 end_row: usize,
1294 end_col: usize,
1295 dest_sheet_name: Option<&str>,
1296 dest_row: usize,
1297 dest_col: usize,
1298 grand_totals_row: bool,
1299 grand_totals_col: bool,
1300 ) -> crate::Result<u64> {
1301 if self.pivot_table_name_taken(name) {
1302 return Err(Error::AlreadyExists {
1303 kind: ObjectKind::PivotTable,
1304 name: name.to_string(),
1305 });
1306 }
1307 let src_idx = self.find_sheet_index(source_sheet_name)?;
1308 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1309 let id = generate_unique_id();
1310 self.pivot_tables.push(PivotTable {
1311 id,
1312 name: name.to_string(),
1313 source: PivotSource::Range {
1314 sheet_id: self.sheets[src_idx].id,
1315 start_row,
1316 start_col,
1317 end_row,
1318 end_col,
1319 },
1320 dest_sheet_id: self.sheets[dest_idx].id,
1321 dest_row,
1322 dest_col,
1323 row_fields: Vec::new(),
1324 col_fields: Vec::new(),
1325 value_fields: Vec::new(),
1326 filter_fields: Vec::new(),
1327 grand_totals_row,
1328 grand_totals_col,
1329 last_output_end_row: None,
1330 last_output_end_col: None,
1331 });
1332 self.refresh_pivot_table(name)?;
1333 Ok(id)
1334 }
1335
1336 pub fn delete_pivot_table(&mut self, name: &str) -> crate::Result<()> {
1339 let idx = self.find_pivot_table_index(name)?;
1340 let pivot = self.pivot_tables.remove(idx);
1341 if let (Some(end_row), Some(end_col)) =
1342 (pivot.last_output_end_row, pivot.last_output_end_col)
1343 && let Some(sheet_idx) = self.sheets.iter().position(|s| s.id == pivot.dest_sheet_id)
1344 {
1345 self.clear_range(sheet_idx, pivot.dest_row, pivot.dest_col, end_row, end_col);
1346 }
1347 Ok(())
1348 }
1349
1350 pub fn rename_pivot_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1352 if !old_name.eq_ignore_ascii_case(new_name) && self.pivot_table_name_taken(new_name) {
1353 return Err(Error::NameTaken {
1354 kind: ObjectKind::PivotTable,
1355 name: new_name.to_string(),
1356 });
1357 }
1358 let idx = self.find_pivot_table_index(old_name)?;
1359 self.pivot_tables[idx].name = new_name.to_string();
1360 Ok(())
1361 }
1362
1363 pub fn add_pivot_field(
1377 &mut self,
1378 pivot_name: &str,
1379 area: PivotArea,
1380 column: &str,
1381 aggregation: Option<PivotAggregation>,
1382 ) -> crate::Result<()> {
1383 let idx = self.find_pivot_table_index(pivot_name)?;
1384 if !matches!(area, PivotArea::Value) {
1385 let pivot = &mut self.pivot_tables[idx];
1386 remove_pivot_field(&mut pivot.row_fields, column);
1387 remove_pivot_field(&mut pivot.col_fields, column);
1388 pivot
1389 .filter_fields
1390 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1391 }
1392 match area {
1393 PivotArea::Row => self.pivot_tables[idx]
1394 .row_fields
1395 .push(PivotField::new(column)),
1396 PivotArea::Column => self.pivot_tables[idx]
1397 .col_fields
1398 .push(PivotField::new(column)),
1399 PivotArea::Value => {
1400 let agg = aggregation.unwrap_or(PivotAggregation::Sum);
1401 self.pivot_tables[idx]
1402 .value_fields
1403 .push(PivotValueField::new(column, agg));
1404 }
1405 PivotArea::Filter => self.pivot_tables[idx]
1406 .filter_fields
1407 .push(PivotFilterField::new(column)),
1408 }
1409 self.refresh_pivot_table(pivot_name)
1410 }
1411
1412 pub fn remove_pivot_field(
1415 &mut self,
1416 pivot_name: &str,
1417 area: PivotArea,
1418 column: &str,
1419 ) -> crate::Result<()> {
1420 let idx = self.find_pivot_table_index(pivot_name)?;
1421 let removed = match area {
1422 PivotArea::Row => remove_pivot_field(&mut self.pivot_tables[idx].row_fields, column),
1423 PivotArea::Column => remove_pivot_field(&mut self.pivot_tables[idx].col_fields, column),
1424 PivotArea::Value => {
1425 let before = self.pivot_tables[idx].value_fields.len();
1426 self.pivot_tables[idx]
1427 .value_fields
1428 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1429 before != self.pivot_tables[idx].value_fields.len()
1430 }
1431 PivotArea::Filter => {
1432 let before = self.pivot_tables[idx].filter_fields.len();
1433 self.pivot_tables[idx]
1434 .filter_fields
1435 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1436 before != self.pivot_tables[idx].filter_fields.len()
1437 }
1438 };
1439 if !removed {
1440 return Err(Error::not_found(
1441 ObjectKind::PivotField,
1442 format!("{column}' in pivot table '{pivot_name}"),
1443 ));
1444 }
1445 self.refresh_pivot_table(pivot_name)
1446 }
1447
1448 pub fn set_pivot_filter(
1451 &mut self,
1452 pivot_name: &str,
1453 column: &str,
1454 values: Option<Vec<String>>,
1455 ) -> crate::Result<()> {
1456 let idx = self.find_pivot_table_index(pivot_name)?;
1457 let field = self.pivot_tables[idx]
1458 .filter_fields
1459 .iter_mut()
1460 .find(|f| f.column.eq_ignore_ascii_case(column))
1461 .ok_or_else(|| {
1462 Error::not_found(
1463 ObjectKind::PivotField,
1464 format!("{column}' on pivot table '{pivot_name}"),
1465 )
1466 })?;
1467 field.selected_values = values;
1468 self.refresh_pivot_table(pivot_name)
1469 }
1470
1471 pub fn refresh_pivot_table(&mut self, pivot_name: &str) -> crate::Result<()> {
1476 let idx = self.find_pivot_table_index(pivot_name)?;
1477 let pivot = self.pivot_tables[idx].clone();
1478 let dest_idx = self
1479 .sheets
1480 .iter()
1481 .position(|s| s.id == pivot.dest_sheet_id)
1482 .ok_or_else(|| {
1483 Error::InvalidArgument(
1484 "pivot table's destination sheet no longer exists".to_string(),
1485 )
1486 })?;
1487
1488 let grid: Option<PivotGrid> = if pivot.value_fields.is_empty() {
1489 None
1490 } else {
1491 let sheet_refs: Vec<&Sheet> = self.sheets.iter().collect();
1492 Some(compute_pivot(&sheet_refs, &pivot).map_err(Error::InvalidArgument)?)
1493 };
1494
1495 if let (Some(old_end_row), Some(old_end_col)) =
1498 (pivot.last_output_end_row, pivot.last_output_end_col)
1499 {
1500 self.clear_range(
1501 dest_idx,
1502 pivot.dest_row,
1503 pivot.dest_col,
1504 old_end_row,
1505 old_end_col,
1506 );
1507 }
1508
1509 let new_bounds = grid.as_ref().map(|grid| {
1510 let height = grid.height();
1511 let width = grid.width.max(1);
1512 self.ensure_capacity(
1513 dest_idx,
1514 pivot.dest_row + height.saturating_sub(1),
1515 pivot.dest_col + width.saturating_sub(1),
1516 );
1517
1518 let mut r = pivot.dest_row;
1519 for (name, state) in &grid.filter_rows {
1520 self.set_cell(dest_idx, r, pivot.dest_col, pivot_label_literal(name));
1521 self.set_cell(dest_idx, r, pivot.dest_col + 1, pivot_label_literal(state));
1522 r += 1;
1523 }
1524 if !grid.filter_rows.is_empty() {
1525 r += 1; }
1527 for header in &grid.header_rows {
1528 for (c, text) in header.iter().enumerate() {
1529 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(text));
1530 }
1531 r += 1;
1532 }
1533 for body in &grid.body_rows {
1534 for (c, label) in body.row_labels.iter().enumerate() {
1535 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(label));
1536 }
1537 for (c, val) in body.values.iter().enumerate() {
1538 self.set_cell(
1539 dest_idx,
1540 r,
1541 pivot.dest_col + body.row_labels.len() + c,
1542 pivot_value_literal(val),
1543 );
1544 }
1545 r += 1;
1546 }
1547 (
1548 pivot.dest_row + height.saturating_sub(1),
1549 pivot.dest_col + width.saturating_sub(1),
1550 )
1551 });
1552
1553 self.pivot_tables[idx].last_output_end_row = new_bounds.map(|(r, _)| r);
1554 self.pivot_tables[idx].last_output_end_col = new_bounds.map(|(_, c)| c);
1555 self.evaluate()
1556 }
1557
1558 fn clear_range(
1562 &mut self,
1563 sheet_idx: usize,
1564 start_row: usize,
1565 start_col: usize,
1566 end_row: usize,
1567 end_col: usize,
1568 ) {
1569 if sheet_idx >= self.sheets.len() {
1570 return;
1571 }
1572 let (row_count, col_count) = {
1573 let s = &self.sheets[sheet_idx];
1574 (s.row_count(), s.col_count())
1575 };
1576 if row_count == 0 || col_count == 0 {
1577 return;
1578 }
1579 for r in start_row..=end_row.min(row_count - 1) {
1580 for c in start_col..=end_col.min(col_count - 1) {
1581 self.sheets[sheet_idx].set_cell_src(r, c, String::new());
1582 }
1583 }
1584 }
1585}