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 set_cell_with_type(
438 &mut self,
439 sheet_idx: usize,
440 row: usize,
441 col: usize,
442 value: String,
443 cell_type: crate::core::CellType,
444 ) {
445 self.ensure_capacity(sheet_idx, row, col);
446 let sheet = &mut self.sheets[sheet_idx];
447 sheet.set_cell_with_type(row, col, value, cell_type);
448 }
449
450 pub fn get_cell_type(&self, sheet_idx: usize, row: usize, col: usize) -> crate::core::CellType {
452 if let Some(sheet) = self.sheets.get(sheet_idx) {
453 sheet.get_cell_type(&crate::core::CellRef::new(row, col))
454 } else {
455 crate::core::CellType::Empty
456 }
457 }
458
459 pub fn insert_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
465 let sheet = &self.sheets[sheet_idx];
466 let at = row_idx.min(sheet.row_count());
469 let edit = GridEdit::insert_row(sheet.id, at);
470 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_row(at));
471 self.evaluate()
472 }
473
474 pub fn delete_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
479 let sheet = &self.sheets[sheet_idx];
480 if row_idx >= sheet.row_count() {
481 return Err(Error::OutOfBounds {
482 what: "row",
483 index: row_idx,
484 len: sheet.row_count(),
485 });
486 }
487 let edit = GridEdit::delete_row(sheet.id, row_idx);
488 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].delete_row(row_idx));
489 self.evaluate()
490 }
491
492 pub fn insert_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
494 let sheet = &self.sheets[sheet_idx];
495 let at = col_idx.min(sheet.col_count());
496 let edit = GridEdit::insert_col(sheet.id, at);
497 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_col(at));
498 self.evaluate()
499 }
500
501 pub fn delete_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
503 let sheet = &self.sheets[sheet_idx];
504 if col_idx >= sheet.col_count() {
505 return Err(Error::OutOfBounds {
506 what: "column",
507 index: col_idx,
508 len: sheet.col_count(),
509 });
510 }
511 let deleted_col_ids = vec![sheet.columns()[col_idx].id];
515 let edit = GridEdit::delete_col(sheet.id, col_idx);
516 self.apply_grid_edit(edit, &deleted_col_ids, |wb| {
517 wb.sheets[sheet_idx].delete_col(col_idx)
518 });
519 self.evaluate()
520 }
521
522 pub fn insert_cells_shift_down(
530 &mut self,
531 sheet_idx: usize,
532 row: usize,
533 first_col: usize,
534 last_col: usize,
535 count: usize,
536 ) -> crate::Result<()> {
537 let sheet = &self.sheets[sheet_idx];
538 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, true);
539 self.apply_grid_edit(edit, &[], |wb| {
540 wb.sheets[sheet_idx].insert_cells_shift_down(row, first_col, last_col, count)
541 });
542 self.evaluate()
543 }
544
545 pub fn delete_cells_shift_up(
548 &mut self,
549 sheet_idx: usize,
550 row: usize,
551 first_col: usize,
552 last_col: usize,
553 count: usize,
554 ) -> crate::Result<()> {
555 let sheet = &self.sheets[sheet_idx];
556 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, false);
557 self.apply_grid_edit(edit, &[], |wb| {
558 wb.sheets[sheet_idx].delete_cells_shift_up(row, first_col, last_col, count)
559 });
560 self.evaluate()
561 }
562
563 fn apply_grid_edit(
581 &mut self,
582 edit: GridEdit,
583 deleted_col_ids: &[u64],
584 apply: impl FnOnce(&mut Self),
585 ) {
586 let mut shifted: Vec<(usize, usize, usize, CompiledFormula)> = Vec::new();
590 for (sheet_idx, sheet) in self.sheets.iter().enumerate() {
591 for (col_idx, column) in sheet.columns().iter().enumerate() {
592 for row_idx in 0..column.len() {
593 let Some(src) = column.src(row_idx).filter(|s| s.starts_with('=')) else {
594 continue;
595 };
596 let compiled = crate::core::parser::compile_formula(src, &self.sheets);
597 if let Some(next) =
598 crate::core::grid_edit::shift_formula(&compiled, &edit, deleted_col_ids)
599 {
600 shifted.push((sheet_idx, col_idx, row_idx, next));
601 }
602 }
603 }
604 }
605
606 apply(self);
608 self.shift_table_and_pivot_ranges(&edit);
609
610 for (sheet_idx, col_idx, row_idx, compiled) in shifted {
612 let Some((row, col)) = self.moved_cell(&edit, sheet_idx, row_idx, col_idx) else {
613 continue;
615 };
616 let text = crate::core::parser::serialize_formula(&compiled, &self.sheets);
617 self.sheets[sheet_idx].set_cell_src(row, col, text);
618 }
619 }
620
621 fn moved_cell(
624 &self,
625 edit: &GridEdit,
626 sheet_idx: usize,
627 row: usize,
628 col: usize,
629 ) -> Option<(usize, usize)> {
630 if self.sheets[sheet_idx].id != edit.sheet_id || !edit.covers_columns(col, col) {
635 return Some((row, col));
636 }
637 let moved = |index: usize| {
638 crate::core::grid_edit::shift_point(index, edit.at, edit.count, edit.insert)
639 };
640 match edit.axis {
641 Axis::Row => Some((moved(row)?, col)),
642 Axis::Col => Some((row, moved(col)?)),
643 }
644 }
645
646 fn shift_table_and_pivot_ranges(&mut self, edit: &GridEdit) {
652 use crate::core::grid_edit::{shift_point, shift_rect};
653
654 for sheet in &mut self.sheets {
655 if sheet.id != edit.sheet_id {
656 continue;
657 }
658 sheet.tables.retain_mut(|table| {
659 if !edit.covers_columns(table.start_col, table.end_col) {
662 return true;
663 }
664 match shift_rect(
665 edit,
666 table.start_row,
667 table.start_col,
668 table.end_row,
669 table.end_col,
670 ) {
671 Some((r0, c0, r1, c1)) => {
672 if edit.axis == Axis::Col {
676 resize_table_columns(table, c0, c1, edit);
677 }
678 table.start_row = r0;
679 table.start_col = c0;
680 table.end_row = r1;
681 table.end_col = c1;
682 true
683 }
684 None => false,
685 }
686 });
687 }
688
689 for pivot in &mut self.pivot_tables {
690 if let PivotSource::Range {
691 sheet_id,
692 start_row,
693 start_col,
694 end_row,
695 end_col,
696 } = &mut pivot.source
697 && *sheet_id == edit.sheet_id
698 && edit.covers_columns(*start_col, *end_col)
699 && let Some((r0, c0, r1, c1)) =
700 shift_rect(edit, *start_row, *start_col, *end_row, *end_col)
701 {
702 *start_row = r0;
703 *start_col = c0;
704 *end_row = r1;
705 *end_col = c1;
706 }
707
708 if pivot.dest_sheet_id == edit.sheet_id
709 && edit.covers_columns(pivot.dest_col, pivot.dest_col)
710 {
711 match edit.axis {
716 Axis::Row => {
717 pivot.dest_row =
718 shift_point(pivot.dest_row, edit.at, edit.count, edit.insert)
719 .unwrap_or(edit.at);
720 }
721 Axis::Col => {
722 pivot.dest_col =
723 shift_point(pivot.dest_col, edit.at, edit.count, edit.insert)
724 .unwrap_or(edit.at);
725 }
726 }
727 pivot.last_output_end_row = None;
731 pivot.last_output_end_col = None;
732 }
733 }
734 }
735
736 pub fn add_sheet(&mut self, name: &str) -> crate::Result<()> {
738 if self
739 .sheets
740 .iter()
741 .any(|s| s.name.eq_ignore_ascii_case(name))
742 {
743 return Err(Error::AlreadyExists {
744 kind: ObjectKind::Sheet,
745 name: name.to_string(),
746 });
747 }
748
749 let mut columns = Vec::new();
750 for col_idx in 0..5 {
751 let mut col = DataColumn::new(10);
752 col.id = generate_unique_id();
753 col.name = col_idx_to_letters(col_idx);
754 columns.push(col);
755 }
756
757 let new_sheet = Sheet {
758 id: generate_unique_id(),
759 name: name.to_string(),
760 columns,
761 tables: Vec::new(),
762 dependencies: std::collections::HashMap::new(),
763 dependencies_rev: std::collections::HashMap::new(),
764 uncommitted_actions: Vec::new(),
765 };
766
767 self.sheets.push(new_sheet);
768 Ok(())
769 }
770
771 pub fn delete_sheet(&mut self, name: &str) -> crate::Result<()> {
773 let idx = self.find_sheet_index(Some(name))?;
774 if self.sheets.len() <= 1 {
775 return Err(Error::LastSheetInWorkbook);
776 }
777 self.sheets.remove(idx);
778 Ok(())
779 }
780
781 pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
783 let idx = self.find_sheet_index(Some(old_name))?;
784 if self
785 .sheets
786 .iter()
787 .enumerate()
788 .any(|(i, s)| i != idx && s.name.eq_ignore_ascii_case(new_name))
789 {
790 return Err(Error::NameTaken {
791 kind: ObjectKind::Sheet,
792 name: new_name.to_string(),
793 });
794 }
795 self.sheets[idx].name = new_name.to_string();
796 Ok(())
797 }
798
799 #[allow(clippy::too_many_arguments)]
801 pub fn add_chart(
802 &mut self,
803 sheet_name: &str,
804 chart_type: ChartType,
805 range: String,
806 title: Option<String>,
807 anchor: Option<(usize, usize)>,
808 ) -> crate::Result<u64> {
809 let _ = self.find_sheet_index(Some(sheet_name))?;
810 let id = generate_unique_id();
811 let name = format!("Chart {}", self.charts.len() + 1);
812 let (anchor_row, anchor_col) = anchor.unwrap_or((0, 0));
813
814 let chart = Chart {
815 id,
816 name,
817 chart_type,
818 data_range: range,
819 title,
820 xlabel: None,
821 ylabel: None,
822 show_legend: true,
823 anchor_row,
824 anchor_col,
825 };
826
827 self.charts.push(chart);
828 Ok(id)
829 }
830
831 #[allow(clippy::too_many_arguments)]
836 pub fn edit_chart(
837 &mut self,
838 id: u64,
839 name: Option<String>,
840 chart_type: Option<ChartType>,
841 data_range: Option<String>,
842 title: Option<Option<String>>,
843 xlabel: Option<Option<String>>,
844 ylabel: Option<Option<String>>,
845 show_legend: Option<bool>,
846 anchor: Option<(usize, usize)>,
847 ) -> crate::Result<()> {
848 let chart = self
849 .charts
850 .iter_mut()
851 .find(|c| c.id == id)
852 .ok_or_else(|| Error::not_found(ObjectKind::Chart, id.to_string()))?;
853 if let Some(name) = name {
854 chart.name = name;
855 }
856 if let Some(chart_type) = chart_type {
857 chart.chart_type = chart_type;
858 }
859 if let Some(data_range) = data_range {
860 chart.data_range = data_range;
861 }
862 if let Some(title) = title {
863 chart.title = title;
864 }
865 if let Some(xlabel) = xlabel {
866 chart.xlabel = xlabel;
867 }
868 if let Some(ylabel) = ylabel {
869 chart.ylabel = ylabel;
870 }
871 if let Some(show_legend) = show_legend {
872 chart.show_legend = show_legend;
873 }
874 if let Some((anchor_row, anchor_col)) = anchor {
875 chart.anchor_row = anchor_row;
876 chart.anchor_col = anchor_col;
877 }
878 Ok(())
879 }
880
881 pub fn has_vba_project(&self) -> bool {
883 self.vba_project.is_some()
884 }
885
886 pub fn list_vba_modules(&self) -> Vec<&VbaModule> {
888 self.vba_project
889 .as_ref()
890 .map(|p| p.modules.iter().collect())
891 .unwrap_or_default()
892 }
893
894 pub fn ensure_vba_project(&mut self) -> crate::Result<()> {
898 if self.vba_project.is_some() {
899 return Ok(());
900 }
901 self.vba_project = Some(VbaProject::new_empty());
902 Ok(())
903 }
904
905 pub fn add_vba_module(
915 &mut self,
916 name: String,
917 kind: VbaModuleKind,
918 source: String,
919 bound_sheet_id: Option<u64>,
920 ) -> crate::Result<()> {
921 validate_vba_module_name(&name).map_err(|reason| Error::InvalidName {
922 kind: ObjectKind::VbaModule,
923 name: name.clone(),
924 reason,
925 })?;
926 let is_this_workbook = kind == VbaModuleKind::Document && name == "ThisWorkbook";
927 if kind == VbaModuleKind::Document && !is_this_workbook {
928 let sheet_id = bound_sheet_id
929 .ok_or_else(|| Error::Vba("document modules require a bound sheet".to_string()))?;
930 if !self.sheets.iter().any(|s| s.id == sheet_id) {
931 return Err(Error::not_found(ObjectKind::Sheet, sheet_id.to_string()));
932 }
933 }
934 self.ensure_vba_project()?;
935 let project = self.vba_project.as_mut().unwrap();
936 if project.module_name_taken(&name) {
937 return Err(Error::AlreadyExists {
938 kind: ObjectKind::VbaModule,
939 name: name.to_string(),
940 });
941 }
942 if kind == VbaModuleKind::Document
943 && bound_sheet_id.is_some()
944 && project
945 .modules
946 .iter()
947 .any(|m| m.kind == VbaModuleKind::Document && m.bound_sheet_id == bound_sheet_id)
948 {
949 return Err(Error::DocumentModuleExists);
950 }
951 let prefix_bytes = project
959 .modules
960 .first()
961 .map(|m| m.prefix_bytes.clone())
962 .unwrap_or_else(|| project.seed_prefix_bytes.clone());
963 let module_cookie = project
964 .modules
965 .first()
966 .map(|m| m.module_cookie)
967 .unwrap_or(project.seed_module_cookie);
968 let stored_bound_sheet_id = if kind == VbaModuleKind::Document && !is_this_workbook {
969 bound_sheet_id
970 } else {
971 None
972 };
973 project.modules.push(VbaModule {
974 name,
975 kind,
976 source,
977 bound_sheet_id: stored_bound_sheet_id,
978 prefix_bytes,
979 module_cookie,
980 cached_compressed_source: None,
982 });
983 Ok(())
984 }
985
986 pub fn remove_vba_module(&mut self, name: &str) -> crate::Result<()> {
993 let project = self
994 .vba_project
995 .as_mut()
996 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
997 let before = project.modules.len();
998 project
999 .modules
1000 .retain(|m| !m.name.eq_ignore_ascii_case(name));
1001 if project.modules.len() == before {
1002 return Err(Error::not_found(ObjectKind::VbaModule, name.to_string()));
1003 }
1004 Ok(())
1005 }
1006
1007 pub fn rename_vba_module(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1020 validate_vba_module_name(new_name).map_err(|reason| Error::InvalidName {
1021 kind: ObjectKind::VbaModule,
1022 name: new_name.to_string(),
1023 reason,
1024 })?;
1025 let project = self
1026 .vba_project
1027 .as_mut()
1028 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1029 if !old_name.eq_ignore_ascii_case(new_name) && project.module_name_taken(new_name) {
1030 return Err(Error::AlreadyExists {
1031 kind: ObjectKind::VbaModule,
1032 name: new_name.to_string(),
1033 });
1034 }
1035 let module = project
1036 .find_module_mut(old_name)
1037 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, old_name))?;
1038 module.name = new_name.to_string();
1039 Ok(())
1040 }
1041
1042 pub fn set_vba_module_source(&mut self, name: &str, source: String) -> crate::Result<()> {
1053 let project = self
1054 .vba_project
1055 .as_mut()
1056 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1057 let module = project
1058 .find_module_mut(name)
1059 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, name))?;
1060 module.source = source;
1061 module.cached_compressed_source = None;
1065 Ok(())
1066 }
1067
1068 pub fn delete_chart(&mut self, id: u64) -> crate::Result<()> {
1070 if let Some(pos) = self.charts.iter().position(|c| c.id == id) {
1071 self.charts.remove(pos);
1072 Ok(())
1073 } else {
1074 Err(Error::not_found(ObjectKind::Chart, id.to_string()))
1075 }
1076 }
1077
1078 pub fn find_table(&self, name: &str) -> Option<(&Sheet, &ExcelTable)> {
1081 self.sheets
1082 .iter()
1083 .find_map(|s| s.find_table(name).map(|t| (s, t)))
1084 }
1085
1086 pub fn list_tables(&self) -> Vec<(&str, &ExcelTable)> {
1089 self.sheets
1090 .iter()
1091 .flat_map(|s| s.tables.iter().map(move |t| (s.name.as_str(), t)))
1092 .collect()
1093 }
1094
1095 fn find_table_sheet_index(&self, name: &str) -> crate::Result<usize> {
1096 self.sheets
1097 .iter()
1098 .position(|s| s.find_table(name).is_some())
1099 .ok_or_else(|| Error::not_found(ObjectKind::Table, name))
1100 }
1101
1102 fn table_name_taken(&self, name: &str) -> bool {
1103 self.sheets
1104 .iter()
1105 .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(name)))
1106 }
1107
1108 #[allow(clippy::too_many_arguments)]
1112 pub fn add_table(
1113 &mut self,
1114 sheet_name: Option<&str>,
1115 name: &str,
1116 start_row: usize,
1117 start_col: usize,
1118 end_row: usize,
1119 end_col: usize,
1120 has_header_row: bool,
1121 has_totals_row: bool,
1122 ) -> crate::Result<u64> {
1123 if self.table_name_taken(name) {
1124 return Err(Error::AlreadyExists {
1125 kind: ObjectKind::Table,
1126 name: name.to_string(),
1127 });
1128 }
1129 let idx = self.find_sheet_index(sheet_name)?;
1130 self.sheets[idx]
1131 .add_table(
1132 name.to_string(),
1133 start_row,
1134 start_col,
1135 end_row,
1136 end_col,
1137 has_header_row,
1138 has_totals_row,
1139 )
1140 .map_err(Error::InvalidArgument)
1141 }
1142
1143 pub fn delete_table(&mut self, name: &str) -> crate::Result<()> {
1145 let idx = self.find_table_sheet_index(name)?;
1146 self.sheets[idx]
1147 .delete_table_by_name(name)
1148 .map_err(Error::InvalidArgument)
1149 }
1150
1151 pub fn rename_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1153 if !old_name.eq_ignore_ascii_case(new_name) && self.table_name_taken(new_name) {
1154 return Err(Error::NameTaken {
1155 kind: ObjectKind::Table,
1156 name: new_name.to_string(),
1157 });
1158 }
1159 let idx = self.find_table_sheet_index(old_name)?;
1160 self.sheets[idx]
1161 .rename_table(old_name, new_name)
1162 .map_err(Error::InvalidArgument)?;
1163 self.rewrite_table_references(old_name, Some(new_name), None);
1167 self.evaluate()
1168 }
1169
1170 fn rewrite_table_references(
1175 &mut self,
1176 table_name: &str,
1177 new_table_name: Option<&str>,
1178 col_rename: Option<(&str, &str)>,
1179 ) {
1180 for sheet in &mut self.sheets {
1181 for col_idx in 0..sheet.columns.len() {
1182 let row_count = sheet.columns[col_idx].src.len();
1183 for row_idx in 0..row_count {
1184 let src = sheet.columns[col_idx].src[row_idx].clone();
1185 if let Some(new_src) = crate::core::parser::rewrite_structured_table_reference(
1186 &src,
1187 table_name,
1188 new_table_name,
1189 col_rename,
1190 ) {
1191 sheet.set_cell_src(row_idx, col_idx, new_src);
1192 }
1193 }
1194 }
1195 }
1196 }
1197
1198 pub fn resize_table(
1200 &mut self,
1201 name: &str,
1202 new_end_row: usize,
1203 new_end_col: usize,
1204 ) -> crate::Result<()> {
1205 let idx = self.find_table_sheet_index(name)?;
1206 self.sheets[idx]
1207 .resize_table(name, new_end_row, new_end_col)
1208 .map_err(Error::InvalidArgument)
1209 }
1210
1211 pub fn rename_table_column(
1213 &mut self,
1214 table_name: &str,
1215 col_index: usize,
1216 new_name: &str,
1217 ) -> crate::Result<()> {
1218 let idx = self.find_table_sheet_index(table_name)?;
1219 let old_col_name = self.sheets[idx]
1220 .find_table(table_name)
1221 .and_then(|t| t.columns.get(col_index).cloned())
1222 .ok_or_else(|| {
1223 Error::InvalidArgument(format!(
1224 "column index {col_index} out of bounds for table '{table_name}'"
1225 ))
1226 })?;
1227 self.sheets[idx]
1228 .rename_table_column(table_name, col_index, new_name)
1229 .map_err(Error::InvalidArgument)?;
1230 self.rewrite_table_references(table_name, None, Some((&old_col_name, new_name)));
1233 self.evaluate()
1234 }
1235
1236 pub fn find_pivot_table(&self, name: &str) -> Option<&PivotTable> {
1238 self.pivot_tables
1239 .iter()
1240 .find(|p| p.name.eq_ignore_ascii_case(name))
1241 }
1242
1243 fn find_pivot_table_index(&self, name: &str) -> crate::Result<usize> {
1244 self.pivot_tables
1245 .iter()
1246 .position(|p| p.name.eq_ignore_ascii_case(name))
1247 .ok_or_else(|| Error::not_found(ObjectKind::PivotTable, name))
1248 }
1249
1250 pub fn list_pivot_tables(&self) -> &[PivotTable] {
1252 &self.pivot_tables
1253 }
1254
1255 fn pivot_table_name_taken(&self, name: &str) -> bool {
1256 self.pivot_tables
1257 .iter()
1258 .any(|p| p.name.eq_ignore_ascii_case(name))
1259 }
1260
1261 #[allow(clippy::too_many_arguments)]
1265 pub fn add_pivot_table_from_table(
1266 &mut self,
1267 name: &str,
1268 source_table_name: &str,
1269 dest_sheet_name: Option<&str>,
1270 dest_row: usize,
1271 dest_col: usize,
1272 grand_totals_row: bool,
1273 grand_totals_col: bool,
1274 ) -> crate::Result<u64> {
1275 if self.pivot_table_name_taken(name) {
1276 return Err(Error::AlreadyExists {
1277 kind: ObjectKind::PivotTable,
1278 name: name.to_string(),
1279 });
1280 }
1281 self.find_table(source_table_name)
1282 .ok_or_else(|| Error::not_found(ObjectKind::Table, source_table_name))?;
1283 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1284 let id = generate_unique_id();
1285 self.pivot_tables.push(PivotTable {
1286 id,
1287 name: name.to_string(),
1288 source: PivotSource::Table {
1289 name: source_table_name.to_string(),
1290 },
1291 dest_sheet_id: self.sheets[dest_idx].id,
1292 dest_row,
1293 dest_col,
1294 row_fields: Vec::new(),
1295 col_fields: Vec::new(),
1296 value_fields: Vec::new(),
1297 filter_fields: Vec::new(),
1298 grand_totals_row,
1299 grand_totals_col,
1300 last_output_end_row: None,
1301 last_output_end_col: None,
1302 });
1303 self.refresh_pivot_table(name)?;
1304 Ok(id)
1305 }
1306
1307 #[allow(clippy::too_many_arguments)]
1310 pub fn add_pivot_table_from_range(
1311 &mut self,
1312 name: &str,
1313 source_sheet_name: Option<&str>,
1314 start_row: usize,
1315 start_col: usize,
1316 end_row: usize,
1317 end_col: usize,
1318 dest_sheet_name: Option<&str>,
1319 dest_row: usize,
1320 dest_col: usize,
1321 grand_totals_row: bool,
1322 grand_totals_col: bool,
1323 ) -> crate::Result<u64> {
1324 if self.pivot_table_name_taken(name) {
1325 return Err(Error::AlreadyExists {
1326 kind: ObjectKind::PivotTable,
1327 name: name.to_string(),
1328 });
1329 }
1330 let src_idx = self.find_sheet_index(source_sheet_name)?;
1331 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1332 let id = generate_unique_id();
1333 self.pivot_tables.push(PivotTable {
1334 id,
1335 name: name.to_string(),
1336 source: PivotSource::Range {
1337 sheet_id: self.sheets[src_idx].id,
1338 start_row,
1339 start_col,
1340 end_row,
1341 end_col,
1342 },
1343 dest_sheet_id: self.sheets[dest_idx].id,
1344 dest_row,
1345 dest_col,
1346 row_fields: Vec::new(),
1347 col_fields: Vec::new(),
1348 value_fields: Vec::new(),
1349 filter_fields: Vec::new(),
1350 grand_totals_row,
1351 grand_totals_col,
1352 last_output_end_row: None,
1353 last_output_end_col: None,
1354 });
1355 self.refresh_pivot_table(name)?;
1356 Ok(id)
1357 }
1358
1359 pub fn delete_pivot_table(&mut self, name: &str) -> crate::Result<()> {
1362 let idx = self.find_pivot_table_index(name)?;
1363 let pivot = self.pivot_tables.remove(idx);
1364 if let (Some(end_row), Some(end_col)) =
1365 (pivot.last_output_end_row, pivot.last_output_end_col)
1366 && let Some(sheet_idx) = self.sheets.iter().position(|s| s.id == pivot.dest_sheet_id)
1367 {
1368 self.clear_range(sheet_idx, pivot.dest_row, pivot.dest_col, end_row, end_col);
1369 }
1370 Ok(())
1371 }
1372
1373 pub fn rename_pivot_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1375 if !old_name.eq_ignore_ascii_case(new_name) && self.pivot_table_name_taken(new_name) {
1376 return Err(Error::NameTaken {
1377 kind: ObjectKind::PivotTable,
1378 name: new_name.to_string(),
1379 });
1380 }
1381 let idx = self.find_pivot_table_index(old_name)?;
1382 self.pivot_tables[idx].name = new_name.to_string();
1383 Ok(())
1384 }
1385
1386 pub fn add_pivot_field(
1400 &mut self,
1401 pivot_name: &str,
1402 area: PivotArea,
1403 column: &str,
1404 aggregation: Option<PivotAggregation>,
1405 ) -> crate::Result<()> {
1406 let idx = self.find_pivot_table_index(pivot_name)?;
1407 if !matches!(area, PivotArea::Value) {
1408 let pivot = &mut self.pivot_tables[idx];
1409 remove_pivot_field(&mut pivot.row_fields, column);
1410 remove_pivot_field(&mut pivot.col_fields, column);
1411 pivot
1412 .filter_fields
1413 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1414 }
1415 match area {
1416 PivotArea::Row => self.pivot_tables[idx]
1417 .row_fields
1418 .push(PivotField::new(column)),
1419 PivotArea::Column => self.pivot_tables[idx]
1420 .col_fields
1421 .push(PivotField::new(column)),
1422 PivotArea::Value => {
1423 let agg = aggregation.unwrap_or(PivotAggregation::Sum);
1424 self.pivot_tables[idx]
1425 .value_fields
1426 .push(PivotValueField::new(column, agg));
1427 }
1428 PivotArea::Filter => self.pivot_tables[idx]
1429 .filter_fields
1430 .push(PivotFilterField::new(column)),
1431 }
1432 self.refresh_pivot_table(pivot_name)
1433 }
1434
1435 pub fn remove_pivot_field(
1438 &mut self,
1439 pivot_name: &str,
1440 area: PivotArea,
1441 column: &str,
1442 ) -> crate::Result<()> {
1443 let idx = self.find_pivot_table_index(pivot_name)?;
1444 let removed = match area {
1445 PivotArea::Row => remove_pivot_field(&mut self.pivot_tables[idx].row_fields, column),
1446 PivotArea::Column => remove_pivot_field(&mut self.pivot_tables[idx].col_fields, column),
1447 PivotArea::Value => {
1448 let before = self.pivot_tables[idx].value_fields.len();
1449 self.pivot_tables[idx]
1450 .value_fields
1451 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1452 before != self.pivot_tables[idx].value_fields.len()
1453 }
1454 PivotArea::Filter => {
1455 let before = self.pivot_tables[idx].filter_fields.len();
1456 self.pivot_tables[idx]
1457 .filter_fields
1458 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1459 before != self.pivot_tables[idx].filter_fields.len()
1460 }
1461 };
1462 if !removed {
1463 return Err(Error::not_found(
1464 ObjectKind::PivotField,
1465 format!("{column}' in pivot table '{pivot_name}"),
1466 ));
1467 }
1468 self.refresh_pivot_table(pivot_name)
1469 }
1470
1471 pub fn set_pivot_filter(
1474 &mut self,
1475 pivot_name: &str,
1476 column: &str,
1477 values: Option<Vec<String>>,
1478 ) -> crate::Result<()> {
1479 let idx = self.find_pivot_table_index(pivot_name)?;
1480 let field = self.pivot_tables[idx]
1481 .filter_fields
1482 .iter_mut()
1483 .find(|f| f.column.eq_ignore_ascii_case(column))
1484 .ok_or_else(|| {
1485 Error::not_found(
1486 ObjectKind::PivotField,
1487 format!("{column}' on pivot table '{pivot_name}"),
1488 )
1489 })?;
1490 field.selected_values = values;
1491 self.refresh_pivot_table(pivot_name)
1492 }
1493
1494 pub fn refresh_pivot_table(&mut self, pivot_name: &str) -> crate::Result<()> {
1499 let idx = self.find_pivot_table_index(pivot_name)?;
1500 let pivot = self.pivot_tables[idx].clone();
1501 let dest_idx = self
1502 .sheets
1503 .iter()
1504 .position(|s| s.id == pivot.dest_sheet_id)
1505 .ok_or_else(|| {
1506 Error::InvalidArgument(
1507 "pivot table's destination sheet no longer exists".to_string(),
1508 )
1509 })?;
1510
1511 let grid: Option<PivotGrid> = if pivot.value_fields.is_empty() {
1512 None
1513 } else {
1514 let sheet_refs: Vec<&Sheet> = self.sheets.iter().collect();
1515 Some(compute_pivot(&sheet_refs, &pivot).map_err(Error::InvalidArgument)?)
1516 };
1517
1518 if let (Some(old_end_row), Some(old_end_col)) =
1521 (pivot.last_output_end_row, pivot.last_output_end_col)
1522 {
1523 self.clear_range(
1524 dest_idx,
1525 pivot.dest_row,
1526 pivot.dest_col,
1527 old_end_row,
1528 old_end_col,
1529 );
1530 }
1531
1532 let new_bounds = grid.as_ref().map(|grid| {
1533 let height = grid.height();
1534 let width = grid.width.max(1);
1535 self.ensure_capacity(
1536 dest_idx,
1537 pivot.dest_row + height.saturating_sub(1),
1538 pivot.dest_col + width.saturating_sub(1),
1539 );
1540
1541 let mut r = pivot.dest_row;
1542 for (name, state) in &grid.filter_rows {
1543 self.set_cell(dest_idx, r, pivot.dest_col, pivot_label_literal(name));
1544 self.set_cell(dest_idx, r, pivot.dest_col + 1, pivot_label_literal(state));
1545 r += 1;
1546 }
1547 if !grid.filter_rows.is_empty() {
1548 r += 1; }
1550 for header in &grid.header_rows {
1551 for (c, text) in header.iter().enumerate() {
1552 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(text));
1553 }
1554 r += 1;
1555 }
1556 for body in &grid.body_rows {
1557 for (c, label) in body.row_labels.iter().enumerate() {
1558 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(label));
1559 }
1560 for (c, val) in body.values.iter().enumerate() {
1561 self.set_cell(
1562 dest_idx,
1563 r,
1564 pivot.dest_col + body.row_labels.len() + c,
1565 pivot_value_literal(val),
1566 );
1567 }
1568 r += 1;
1569 }
1570 (
1571 pivot.dest_row + height.saturating_sub(1),
1572 pivot.dest_col + width.saturating_sub(1),
1573 )
1574 });
1575
1576 self.pivot_tables[idx].last_output_end_row = new_bounds.map(|(r, _)| r);
1577 self.pivot_tables[idx].last_output_end_col = new_bounds.map(|(_, c)| c);
1578 self.evaluate()
1579 }
1580
1581 fn clear_range(
1585 &mut self,
1586 sheet_idx: usize,
1587 start_row: usize,
1588 start_col: usize,
1589 end_row: usize,
1590 end_col: usize,
1591 ) {
1592 if sheet_idx >= self.sheets.len() {
1593 return;
1594 }
1595 let (row_count, col_count) = {
1596 let s = &self.sheets[sheet_idx];
1597 (s.row_count(), s.col_count())
1598 };
1599 if row_count == 0 || col_count == 0 {
1600 return;
1601 }
1602 for r in start_row..=end_row.min(row_count - 1) {
1603 for c in start_col..=end_col.min(col_count - 1) {
1604 self.sheets[sheet_idx].set_cell_src(r, c, String::new());
1605 }
1606 }
1607 }
1608}