1use crate::core::formula::CompiledFormula;
2use crate::core::grid_edit::{Axis, GridEdit};
3use crate::core::locale::Locale;
4use crate::core::parser::col_idx_to_letters;
5use crate::core::xlsx::{export_xlsx_data, import_xlsx_data};
6use crate::core::{
7 ExcelTable, PivotAggregation, PivotArea, PivotField, PivotFilterField, PivotGrid, PivotSource,
8 PivotTable, PivotValueField, VbaModule, VbaModuleKind, VbaProject,
9 chart::{Chart, ChartType},
10 compute_pivot,
11 engine::{Context, DataColumn, ResultData, Sheet, generate_unique_id},
12 validate_vba_module_name,
13};
14use crate::{Error, ObjectKind};
15
16fn resize_table_columns(
25 table: &mut ExcelTable,
26 new_start_col: usize,
27 new_end_col: usize,
28 edit: &GridEdit,
29) {
30 if edit.insert {
31 if edit.at > table.start_col && edit.at <= table.end_col {
32 let offset = (edit.at - table.start_col).min(table.columns.len());
33 for _ in 0..edit.count {
34 table.columns.insert(offset, String::new());
35 }
36 }
37 } else {
38 let first = edit.at.max(table.start_col);
39 let last = (edit.at + edit.count).min(table.end_col + 1);
40 if first < last {
41 let lo = (first - table.start_col).min(table.columns.len());
42 let hi = (last - table.start_col).min(table.columns.len());
43 table.columns.drain(lo..hi);
44 }
45 }
46 table
47 .columns
48 .resize(new_end_col - new_start_col + 1, String::new());
49}
50
51pub struct SheetSummary {
53 pub name: String,
55 pub row_count: usize,
57 pub col_count: usize,
59 pub formula_count: usize,
61}
62
63pub struct WorkbookSummary {
65 pub file_name: String,
67 pub sheet_count: usize,
69 pub chart_count: usize,
71 pub sheets: Vec<SheetSummary>,
73}
74
75pub struct WorkbookManager {
91 pub sheets: Vec<Sheet>,
94 pub charts: Vec<Chart>,
97 pub pivot_tables: Vec<PivotTable>,
100 pub vba_project: Option<VbaProject>,
102 pub locale: Locale,
104}
105
106fn pivot_label_literal(text: &str) -> String {
110 if text.is_empty() {
111 String::new()
112 } else if text.starts_with('=')
113 || text.parse::<f64>().is_ok()
114 || text.eq_ignore_ascii_case("true")
115 || text.eq_ignore_ascii_case("false")
116 {
117 format!("\"{}\"", text)
118 } else {
119 text.to_string()
120 }
121}
122
123fn pivot_value_literal(v: &ResultData) -> String {
127 match v {
128 ResultData::Error(e) => e.clone(),
129 other => other.to_string(),
130 }
131}
132
133fn remove_pivot_field(fields: &mut Vec<PivotField>, column: &str) -> bool {
134 let before = fields.len();
135 fields.retain(|f| !f.column.eq_ignore_ascii_case(column));
136 before != fields.len()
137}
138
139impl WorkbookManager {
140 pub fn load_bytes(buffer: &[u8]) -> crate::Result<Self> {
142 let (imported_sheets, charts, pivot_tables, vba_project) =
143 import_xlsx_data(buffer, &[], |_, _, _| {})?;
144
145 let locale = Locale::default();
146 let mut sheets: Vec<Sheet> = imported_sheets.into_iter().map(|it| it.sheet).collect();
147 for sheet in &mut sheets {
148 sheet.locale = locale.clone();
149 }
150 Ok(Self {
151 sheets,
152 charts,
153 pivot_tables,
154 vba_project,
155 locale,
156 })
157 }
158
159 pub fn save_bytes(&self) -> crate::Result<Vec<u8>> {
165 export_xlsx_data(
166 &self.sheets,
167 &self.charts,
168 &self.pivot_tables,
169 self.vba_project.as_ref(),
170 )
171 }
172
173 pub fn new_empty() -> crate::Result<Self> {
175 let locale = Locale::default();
176 let mut wb = Self {
177 sheets: Vec::new(),
178 charts: Vec::new(),
179 pivot_tables: Vec::new(),
180 vba_project: None,
181 locale,
182 };
183 wb.add_sheet("Sheet1")?;
184 Ok(wb)
185 }
186
187 pub fn set_locale(&mut self, locale: Locale) {
189 self.locale = locale.clone();
190 for sheet in &mut self.sheets {
191 sheet.locale = locale.clone();
192 }
193 }
194
195 pub fn evaluate(&mut self) -> crate::Result<()> {
197 if self.sheets.is_empty() {
198 return Ok(());
199 }
200
201 let sheet_order: Vec<String> = self.sheets.iter().map(|s| s.name.clone()).collect();
202
203 for _pass in 0..3 {
204 for sheet in &mut self.sheets {
205 sheet.mark_all_dirty();
206 }
207 for i in 0..self.sheets.len() {
208 let (left, right) = self.sheets.split_at_mut(i);
209 let (target_sheet, right_tail) = right.split_first_mut().unwrap();
210
211 let mut context = Context::new();
212 for s in left.iter() {
213 context.add_table(s.name.clone(), s);
214 }
215 for s in right_tail.iter() {
216 context.add_table(s.name.clone(), s);
217 }
218 context.pivot_tables = &self.pivot_tables;
219 context.sheet_order = sheet_order.clone();
220
221 let _ = target_sheet.commit(Some(&context));
222 }
223 }
224
225 Ok(())
226 }
227
228 pub(crate) fn call_worksheet_function(
236 &self,
237 name: &str,
238 args: &[crate::core::parser::Expr],
239 ) -> Result<ResultData, crate::core::EngineError> {
240 let Some(host) = self.sheets.first() else {
241 return Err(crate::core::EngineError::EvalError(
242 crate::core::EvalError::UnknownFunction("no worksheets".to_string()),
243 ));
244 };
245 let mut context = Context::new();
246 for s in &self.sheets {
247 context.add_table(s.name.clone(), s);
248 }
249 context.pivot_tables = &self.pivot_tables;
250 context.sheet_order = self.sheets.iter().map(|s| s.name.clone()).collect();
251 host.call_worksheet_function(name, args, Some(&context))
252 }
253
254 pub fn find_sheet_index(&self, name_opt: Option<&str>) -> crate::Result<usize> {
256 if self.sheets.is_empty() {
257 return Err(Error::EmptyWorkbook);
258 }
259
260 match name_opt {
261 Some(name) => {
262 if let Some(idx) = self
263 .sheets
264 .iter()
265 .position(|s| s.name.eq_ignore_ascii_case(name))
266 {
267 Ok(idx)
268 } else {
269 let available: Vec<String> =
270 self.sheets.iter().map(|s| s.name.clone()).collect();
271 Err(Error::not_found_among(
272 ObjectKind::Sheet,
273 name.to_string(),
274 available,
275 ))
276 }
277 }
278 None => Ok(0),
279 }
280 }
281
282 pub fn get_summary(&self, file_name: &str) -> WorkbookSummary {
284 let sheet_summaries = self
285 .sheets
286 .iter()
287 .map(|sheet| {
288 let row_count = sheet.row_count();
289 let col_count = sheet.col_count();
290 let mut formula_count = 0;
291
292 for col in &sheet.columns {
293 for src in &col.src {
294 if src.starts_with('=') {
295 formula_count += 1;
296 }
297 }
298 }
299
300 SheetSummary {
301 name: sheet.name.clone(),
302 row_count,
303 col_count,
304 formula_count,
305 }
306 })
307 .collect();
308
309 WorkbookSummary {
310 file_name: file_name.to_string(),
311 sheet_count: self.sheets.len(),
312 chart_count: self.charts.len(),
313 sheets: sheet_summaries,
314 }
315 }
316
317 pub fn ensure_capacity(&mut self, sheet_idx: usize, target_row: usize, target_col: usize) {
319 if sheet_idx >= self.sheets.len() {
320 return;
321 }
322 self.sheets[sheet_idx].ensure_capacity(target_row, target_col);
323 }
324
325 pub fn set_cell_style(
332 &mut self,
333 sheet_name: Option<&str>,
334 row: usize,
335 col: usize,
336 style: crate::core::CellStyle,
337 ) -> crate::Result<()> {
338 let sheet_idx = self.find_sheet_index(sheet_name)?;
339 self.sheets[sheet_idx].update_cell_style(row, col, |s| s.merge(&style));
340 Ok(())
341 }
342
343 pub fn set_range_style(
345 &mut self,
346 sheet_name: Option<&str>,
347 start_row: usize,
348 start_col: usize,
349 end_row: usize,
350 end_col: usize,
351 style: crate::core::CellStyle,
352 ) -> crate::Result<()> {
353 if end_row < start_row || end_col < start_col {
354 return Err(Error::InvalidRange(
355 "range end must not precede its start".to_string(),
356 ));
357 }
358 let sheet_idx = self.find_sheet_index(sheet_name)?;
359 for r in start_row..=end_row {
360 for c in start_col..=end_col {
361 self.sheets[sheet_idx].update_cell_style(r, c, |s| s.merge(&style));
362 }
363 }
364 Ok(())
365 }
366
367 pub fn get_cell_style(
369 &self,
370 sheet_name: Option<&str>,
371 row: usize,
372 col: usize,
373 ) -> crate::Result<Option<crate::core::CellStyle>> {
374 let sheet_idx = self.find_sheet_index(sheet_name)?;
375 Ok(self.sheets[sheet_idx].get_cell_style(row, col).cloned())
376 }
377
378 pub fn set_table_style(&mut self, table_name: &str, style_name: &str) -> crate::Result<()> {
385 for sheet in &mut self.sheets {
386 for table in &mut sheet.tables {
387 if table.name.eq_ignore_ascii_case(table_name) {
388 table.set_style_name(Some(style_name.to_string()));
389 return Ok(());
390 }
391 }
392 }
393 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
394 }
395
396 pub fn get_table_style(&self, table_name: &str) -> crate::Result<Option<String>> {
402 for sheet in &self.sheets {
403 for table in &sheet.tables {
404 if table.name.eq_ignore_ascii_case(table_name) {
405 return Ok(table.style_name.clone());
406 }
407 }
408 }
409 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
410 }
411
412 pub fn set_cell(&mut self, sheet_idx: usize, row: usize, col: usize, value: String) {
414 self.ensure_capacity(sheet_idx, row, col);
415 let sheet = &mut self.sheets[sheet_idx];
416 sheet.set_cell_src(row, col, value);
417 }
418
419 pub fn set_cell_with_type(
421 &mut self,
422 sheet_idx: usize,
423 row: usize,
424 col: usize,
425 value: String,
426 cell_type: crate::core::CellType,
427 ) {
428 self.ensure_capacity(sheet_idx, row, col);
429 let sheet = &mut self.sheets[sheet_idx];
430 sheet.set_cell_with_type(row, col, value, cell_type);
431 }
432
433 pub fn set_cell_type(
435 &mut self,
436 sheet_idx: usize,
437 row: usize,
438 col: usize,
439 cell_type: crate::core::CellType,
440 ) {
441 self.ensure_capacity(sheet_idx, row, col);
442 let sheet = &mut self.sheets[sheet_idx];
443 sheet.set_cell_type(row, col, cell_type);
444 }
445
446 pub fn get_cell_type(&self, sheet_idx: usize, row: usize, col: usize) -> crate::core::CellType {
448 if let Some(sheet) = self.sheets.get(sheet_idx) {
449 sheet.get_cell_type(&crate::core::CellRef::new(row, col))
450 } else {
451 crate::core::CellType::Empty
452 }
453 }
454
455 pub fn insert_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
461 let sheet = &self.sheets[sheet_idx];
462 let at = row_idx.min(sheet.row_count());
463 let edit = GridEdit::insert_row(sheet.id, at);
464 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_row(at));
465 self.evaluate()
466 }
467
468 pub fn delete_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
473 let sheet = &self.sheets[sheet_idx];
474 if row_idx >= sheet.row_count() {
475 return Err(Error::OutOfBounds {
476 what: "row",
477 index: row_idx,
478 len: sheet.row_count(),
479 });
480 }
481 let edit = GridEdit::delete_row(sheet.id, row_idx);
482 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].delete_row(row_idx));
483 self.evaluate()
484 }
485
486 pub fn insert_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
488 let sheet = &self.sheets[sheet_idx];
489 let at = col_idx.min(sheet.col_count());
490 let edit = GridEdit::insert_col(sheet.id, at);
491 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_col(at));
492 self.evaluate()
493 }
494
495 pub fn delete_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
497 let sheet = &self.sheets[sheet_idx];
498 if col_idx >= sheet.col_count() {
499 return Err(Error::OutOfBounds {
500 what: "column",
501 index: col_idx,
502 len: sheet.col_count(),
503 });
504 }
505 let deleted_col_ids = vec![sheet.columns()[col_idx].id];
506 let edit = GridEdit::delete_col(sheet.id, col_idx);
507 self.apply_grid_edit(edit, &deleted_col_ids, |wb| {
508 wb.sheets[sheet_idx].delete_col(col_idx)
509 });
510 self.evaluate()
511 }
512
513 pub fn insert_cells_shift_down(
521 &mut self,
522 sheet_idx: usize,
523 row: usize,
524 first_col: usize,
525 last_col: usize,
526 count: usize,
527 ) -> crate::Result<()> {
528 let sheet = &self.sheets[sheet_idx];
529 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, true);
530 self.apply_grid_edit(edit, &[], |wb| {
531 wb.sheets[sheet_idx].insert_cells_shift_down(row, first_col, last_col, count)
532 });
533 self.evaluate()
534 }
535
536 pub fn delete_cells_shift_up(
539 &mut self,
540 sheet_idx: usize,
541 row: usize,
542 first_col: usize,
543 last_col: usize,
544 count: usize,
545 ) -> crate::Result<()> {
546 let sheet = &self.sheets[sheet_idx];
547 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, false);
548 self.apply_grid_edit(edit, &[], |wb| {
549 wb.sheets[sheet_idx].delete_cells_shift_up(row, first_col, last_col, count)
550 });
551 self.evaluate()
552 }
553
554 fn apply_grid_edit(
572 &mut self,
573 edit: GridEdit,
574 deleted_col_ids: &[u64],
575 apply: impl FnOnce(&mut Self),
576 ) {
577 let mut shifted: Vec<(usize, usize, usize, CompiledFormula)> = Vec::new();
578 for (sheet_idx, sheet) in self.sheets.iter().enumerate() {
579 for (col_idx, column) in sheet.columns().iter().enumerate() {
580 for row_idx in 0..column.len() {
581 let Some(src) = column.src(row_idx).filter(|s| s.starts_with('=')) else {
582 continue;
583 };
584 let compiled = crate::core::parser::compile_formula(src, &self.sheets);
585 if let Some(next) =
586 crate::core::grid_edit::shift_formula(&compiled, &edit, deleted_col_ids)
587 {
588 shifted.push((sheet_idx, col_idx, row_idx, next));
589 }
590 }
591 }
592 }
593
594 apply(self);
595 self.shift_table_and_pivot_ranges(&edit);
596
597 for (sheet_idx, col_idx, row_idx, compiled) in shifted {
598 let Some((row, col)) = self.moved_cell(&edit, sheet_idx, row_idx, col_idx) else {
599 continue;
600 };
601 let text = crate::core::parser::serialize_formula(&compiled, &self.sheets);
602 self.sheets[sheet_idx].set_cell_src(row, col, text);
603 }
604 }
605
606 fn moved_cell(
609 &self,
610 edit: &GridEdit,
611 sheet_idx: usize,
612 row: usize,
613 col: usize,
614 ) -> Option<(usize, usize)> {
615 if self.sheets[sheet_idx].id != edit.sheet_id || !edit.covers_columns(col, col) {
616 return Some((row, col));
617 }
618 let moved = |index: usize| {
619 crate::core::grid_edit::shift_point(index, edit.at, edit.count, edit.insert)
620 };
621 match edit.axis {
622 Axis::Row => Some((moved(row)?, col)),
623 Axis::Col => Some((row, moved(col)?)),
624 }
625 }
626
627 fn shift_table_and_pivot_ranges(&mut self, edit: &GridEdit) {
633 use crate::core::grid_edit::{shift_point, shift_rect};
634
635 for sheet in &mut self.sheets {
636 if sheet.id != edit.sheet_id {
637 continue;
638 }
639 sheet.tables.retain_mut(|table| {
640 if !edit.covers_columns(table.start_col, table.end_col) {
641 return true;
642 }
643 match shift_rect(
644 edit,
645 table.start_row,
646 table.start_col,
647 table.end_row,
648 table.end_col,
649 ) {
650 Some((r0, c0, r1, c1)) => {
651 if edit.axis == Axis::Col {
652 resize_table_columns(table, c0, c1, edit);
653 }
654 table.start_row = r0;
655 table.start_col = c0;
656 table.end_row = r1;
657 table.end_col = c1;
658 true
659 }
660 None => false,
661 }
662 });
663 }
664
665 for pivot in &mut self.pivot_tables {
666 if let PivotSource::Range {
667 sheet_id,
668 start_row,
669 start_col,
670 end_row,
671 end_col,
672 } = &mut pivot.source
673 && *sheet_id == edit.sheet_id
674 && edit.covers_columns(*start_col, *end_col)
675 && let Some((r0, c0, r1, c1)) =
676 shift_rect(edit, *start_row, *start_col, *end_row, *end_col)
677 {
678 *start_row = r0;
679 *start_col = c0;
680 *end_row = r1;
681 *end_col = c1;
682 }
683
684 if pivot.dest_sheet_id == edit.sheet_id
685 && edit.covers_columns(pivot.dest_col, pivot.dest_col)
686 {
687 match edit.axis {
688 Axis::Row => {
689 pivot.dest_row =
690 shift_point(pivot.dest_row, edit.at, edit.count, edit.insert)
691 .unwrap_or(edit.at);
692 }
693 Axis::Col => {
694 pivot.dest_col =
695 shift_point(pivot.dest_col, edit.at, edit.count, edit.insert)
696 .unwrap_or(edit.at);
697 }
698 }
699 pivot.last_output_end_row = None;
700 pivot.last_output_end_col = None;
701 }
702 }
703 }
704
705 pub fn add_sheet(&mut self, name: &str) -> crate::Result<()> {
707 if self
708 .sheets
709 .iter()
710 .any(|s| s.name.eq_ignore_ascii_case(name))
711 {
712 return Err(Error::AlreadyExists {
713 kind: ObjectKind::Sheet,
714 name: name.to_string(),
715 });
716 }
717
718 let mut columns = Vec::new();
719 for col_idx in 0..5 {
720 let mut col = DataColumn::new(10);
721 col.id = generate_unique_id();
722 col.name = col_idx_to_letters(col_idx);
723 columns.push(col);
724 }
725
726 let new_sheet = Sheet {
727 id: generate_unique_id(),
728 name: name.to_string(),
729 columns,
730 row_heights: vec![None; 10],
731 tables: Vec::new(),
732 dependencies: std::collections::HashMap::new(),
733 dependencies_rev: std::collections::HashMap::new(),
734 uncommitted_actions: Vec::new(),
735 locale: self.locale.clone(),
736 };
737
738 self.sheets.push(new_sheet);
739 Ok(())
740 }
741
742 pub fn delete_sheet(&mut self, name: &str) -> crate::Result<()> {
744 let idx = self.find_sheet_index(Some(name))?;
745 if self.sheets.len() <= 1 {
746 return Err(Error::LastSheetInWorkbook);
747 }
748 self.sheets.remove(idx);
749 Ok(())
750 }
751
752 pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
754 let idx = self.find_sheet_index(Some(old_name))?;
755 if self
756 .sheets
757 .iter()
758 .enumerate()
759 .any(|(i, s)| i != idx && s.name.eq_ignore_ascii_case(new_name))
760 {
761 return Err(Error::NameTaken {
762 kind: ObjectKind::Sheet,
763 name: new_name.to_string(),
764 });
765 }
766 self.sheets[idx].name = new_name.to_string();
767 Ok(())
768 }
769
770 #[allow(clippy::too_many_arguments)]
772 pub fn add_chart(
773 &mut self,
774 sheet_name: &str,
775 chart_type: ChartType,
776 range: String,
777 title: Option<String>,
778 anchor: Option<(usize, usize)>,
779 ) -> crate::Result<u64> {
780 let _ = self.find_sheet_index(Some(sheet_name))?;
781 let id = generate_unique_id();
782 let name = format!("Chart {}", self.charts.len() + 1);
783 let (anchor_row, anchor_col) = anchor.unwrap_or((0, 0));
784
785 let chart = Chart {
786 id,
787 name,
788 chart_type,
789 data_range: range,
790 title,
791 xlabel: None,
792 ylabel: None,
793 show_legend: true,
794 anchor_row,
795 anchor_col,
796 };
797
798 self.charts.push(chart);
799 Ok(id)
800 }
801
802 #[allow(clippy::too_many_arguments)]
807 pub fn edit_chart(
808 &mut self,
809 id: u64,
810 name: Option<String>,
811 chart_type: Option<ChartType>,
812 data_range: Option<String>,
813 title: Option<Option<String>>,
814 xlabel: Option<Option<String>>,
815 ylabel: Option<Option<String>>,
816 show_legend: Option<bool>,
817 anchor: Option<(usize, usize)>,
818 ) -> crate::Result<()> {
819 let chart = self
820 .charts
821 .iter_mut()
822 .find(|c| c.id == id)
823 .ok_or_else(|| Error::not_found(ObjectKind::Chart, id.to_string()))?;
824 if let Some(name) = name {
825 chart.name = name;
826 }
827 if let Some(chart_type) = chart_type {
828 chart.chart_type = chart_type;
829 }
830 if let Some(data_range) = data_range {
831 chart.data_range = data_range;
832 }
833 if let Some(title) = title {
834 chart.title = title;
835 }
836 if let Some(xlabel) = xlabel {
837 chart.xlabel = xlabel;
838 }
839 if let Some(ylabel) = ylabel {
840 chart.ylabel = ylabel;
841 }
842 if let Some(show_legend) = show_legend {
843 chart.show_legend = show_legend;
844 }
845 if let Some((anchor_row, anchor_col)) = anchor {
846 chart.anchor_row = anchor_row;
847 chart.anchor_col = anchor_col;
848 }
849 Ok(())
850 }
851
852 pub fn has_vba_project(&self) -> bool {
854 self.vba_project.is_some()
855 }
856
857 pub fn list_vba_modules(&self) -> Vec<&VbaModule> {
859 self.vba_project
860 .as_ref()
861 .map(|p| p.modules.iter().collect())
862 .unwrap_or_default()
863 }
864
865 pub fn ensure_vba_project(&mut self) -> crate::Result<()> {
869 if self.vba_project.is_some() {
870 return Ok(());
871 }
872 self.vba_project = Some(VbaProject::new_empty());
873 Ok(())
874 }
875
876 pub fn add_vba_module(
886 &mut self,
887 name: String,
888 kind: VbaModuleKind,
889 source: String,
890 bound_sheet_id: Option<u64>,
891 ) -> crate::Result<()> {
892 validate_vba_module_name(&name).map_err(|reason| Error::InvalidName {
893 kind: ObjectKind::VbaModule,
894 name: name.clone(),
895 reason,
896 })?;
897 let is_this_workbook = kind == VbaModuleKind::Document && name == "ThisWorkbook";
898 if kind == VbaModuleKind::Document && !is_this_workbook {
899 let sheet_id = bound_sheet_id
900 .ok_or_else(|| Error::Vba("document modules require a bound sheet".to_string()))?;
901 if !self.sheets.iter().any(|s| s.id == sheet_id) {
902 return Err(Error::not_found(ObjectKind::Sheet, sheet_id.to_string()));
903 }
904 }
905 self.ensure_vba_project()?;
906 let project = self.vba_project.as_mut().unwrap();
907 if project.module_name_taken(&name) {
908 return Err(Error::AlreadyExists {
909 kind: ObjectKind::VbaModule,
910 name: name.to_string(),
911 });
912 }
913 if kind == VbaModuleKind::Document
914 && bound_sheet_id.is_some()
915 && project
916 .modules
917 .iter()
918 .any(|m| m.kind == VbaModuleKind::Document && m.bound_sheet_id == bound_sheet_id)
919 {
920 return Err(Error::DocumentModuleExists);
921 }
922 let prefix_bytes = project
923 .modules
924 .first()
925 .map(|m| m.prefix_bytes.clone())
926 .unwrap_or_else(|| project.seed_prefix_bytes.clone());
927 let module_cookie = project
928 .modules
929 .first()
930 .map(|m| m.module_cookie)
931 .unwrap_or(project.seed_module_cookie);
932 let stored_bound_sheet_id = if kind == VbaModuleKind::Document && !is_this_workbook {
933 bound_sheet_id
934 } else {
935 None
936 };
937 project.modules.push(VbaModule {
938 name,
939 kind,
940 source,
941 bound_sheet_id: stored_bound_sheet_id,
942 prefix_bytes,
943 module_cookie,
944 cached_compressed_source: None,
945 });
946 Ok(())
947 }
948
949 pub fn remove_vba_module(&mut self, name: &str) -> crate::Result<()> {
956 let project = self
957 .vba_project
958 .as_mut()
959 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
960 let before = project.modules.len();
961 project
962 .modules
963 .retain(|m| !m.name.eq_ignore_ascii_case(name));
964 if project.modules.len() == before {
965 return Err(Error::not_found(ObjectKind::VbaModule, name.to_string()));
966 }
967 Ok(())
968 }
969
970 pub fn rename_vba_module(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
983 validate_vba_module_name(new_name).map_err(|reason| Error::InvalidName {
984 kind: ObjectKind::VbaModule,
985 name: new_name.to_string(),
986 reason,
987 })?;
988 let project = self
989 .vba_project
990 .as_mut()
991 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
992 if !old_name.eq_ignore_ascii_case(new_name) && project.module_name_taken(new_name) {
993 return Err(Error::AlreadyExists {
994 kind: ObjectKind::VbaModule,
995 name: new_name.to_string(),
996 });
997 }
998 let module = project
999 .find_module_mut(old_name)
1000 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, old_name))?;
1001 module.name = new_name.to_string();
1002 Ok(())
1003 }
1004
1005 pub fn set_vba_module_source(&mut self, name: &str, source: String) -> crate::Result<()> {
1016 let project = self
1017 .vba_project
1018 .as_mut()
1019 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1020 let module = project
1021 .find_module_mut(name)
1022 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, name))?;
1023 module.source = source;
1024 module.cached_compressed_source = None;
1025 Ok(())
1026 }
1027
1028 pub fn delete_chart(&mut self, id: u64) -> crate::Result<()> {
1030 if let Some(pos) = self.charts.iter().position(|c| c.id == id) {
1031 self.charts.remove(pos);
1032 Ok(())
1033 } else {
1034 Err(Error::not_found(ObjectKind::Chart, id.to_string()))
1035 }
1036 }
1037
1038 pub fn find_table(&self, name: &str) -> Option<(&Sheet, &ExcelTable)> {
1041 self.sheets
1042 .iter()
1043 .find_map(|s| s.find_table(name).map(|t| (s, t)))
1044 }
1045
1046 pub fn list_tables(&self) -> Vec<(&str, &ExcelTable)> {
1049 self.sheets
1050 .iter()
1051 .flat_map(|s| s.tables.iter().map(move |t| (s.name.as_str(), t)))
1052 .collect()
1053 }
1054
1055 fn find_table_sheet_index(&self, name: &str) -> crate::Result<usize> {
1056 self.sheets
1057 .iter()
1058 .position(|s| s.find_table(name).is_some())
1059 .ok_or_else(|| Error::not_found(ObjectKind::Table, name))
1060 }
1061
1062 fn table_name_taken(&self, name: &str) -> bool {
1063 self.sheets
1064 .iter()
1065 .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(name)))
1066 }
1067
1068 #[allow(clippy::too_many_arguments)]
1072 pub fn add_table(
1073 &mut self,
1074 sheet_name: Option<&str>,
1075 name: &str,
1076 start_row: usize,
1077 start_col: usize,
1078 end_row: usize,
1079 end_col: usize,
1080 has_header_row: bool,
1081 has_totals_row: bool,
1082 ) -> crate::Result<u64> {
1083 if self.table_name_taken(name) {
1084 return Err(Error::AlreadyExists {
1085 kind: ObjectKind::Table,
1086 name: name.to_string(),
1087 });
1088 }
1089 let idx = self.find_sheet_index(sheet_name)?;
1090 self.sheets[idx]
1091 .add_table(
1092 name.to_string(),
1093 start_row,
1094 start_col,
1095 end_row,
1096 end_col,
1097 has_header_row,
1098 has_totals_row,
1099 )
1100 .map_err(Error::InvalidArgument)
1101 }
1102
1103 pub fn delete_table(&mut self, name: &str) -> crate::Result<()> {
1105 let idx = self.find_table_sheet_index(name)?;
1106 self.sheets[idx]
1107 .delete_table_by_name(name)
1108 .map_err(Error::InvalidArgument)
1109 }
1110
1111 pub fn rename_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1113 if !old_name.eq_ignore_ascii_case(new_name) && self.table_name_taken(new_name) {
1114 return Err(Error::NameTaken {
1115 kind: ObjectKind::Table,
1116 name: new_name.to_string(),
1117 });
1118 }
1119 let idx = self.find_table_sheet_index(old_name)?;
1120 self.sheets[idx]
1121 .rename_table(old_name, new_name)
1122 .map_err(Error::InvalidArgument)?;
1123 self.rewrite_table_references(old_name, Some(new_name), None);
1124 self.evaluate()
1125 }
1126
1127 fn rewrite_table_references(
1132 &mut self,
1133 table_name: &str,
1134 new_table_name: Option<&str>,
1135 col_rename: Option<(&str, &str)>,
1136 ) {
1137 for sheet in &mut self.sheets {
1138 for col_idx in 0..sheet.columns.len() {
1139 let row_count = sheet.columns[col_idx].src.len();
1140 for row_idx in 0..row_count {
1141 let src = sheet.columns[col_idx].src[row_idx].clone();
1142 if let Some(new_src) = crate::core::parser::rewrite_structured_table_reference(
1143 &src,
1144 table_name,
1145 new_table_name,
1146 col_rename,
1147 ) {
1148 sheet.set_cell_src(row_idx, col_idx, new_src);
1149 }
1150 }
1151 }
1152 }
1153 }
1154
1155 pub fn resize_table(
1157 &mut self,
1158 name: &str,
1159 new_end_row: usize,
1160 new_end_col: usize,
1161 ) -> crate::Result<()> {
1162 let idx = self.find_table_sheet_index(name)?;
1163 self.sheets[idx]
1164 .resize_table(name, new_end_row, new_end_col)
1165 .map_err(Error::InvalidArgument)
1166 }
1167
1168 pub fn rename_table_column(
1170 &mut self,
1171 table_name: &str,
1172 col_index: usize,
1173 new_name: &str,
1174 ) -> crate::Result<()> {
1175 let idx = self.find_table_sheet_index(table_name)?;
1176 let old_col_name = self.sheets[idx]
1177 .find_table(table_name)
1178 .and_then(|t| t.columns.get(col_index).cloned())
1179 .ok_or_else(|| {
1180 Error::InvalidArgument(format!(
1181 "column index {col_index} out of bounds for table '{table_name}'"
1182 ))
1183 })?;
1184 self.sheets[idx]
1185 .rename_table_column(table_name, col_index, new_name)
1186 .map_err(Error::InvalidArgument)?;
1187 self.rewrite_table_references(table_name, None, Some((&old_col_name, new_name)));
1188 self.evaluate()
1189 }
1190
1191 pub fn find_pivot_table(&self, name: &str) -> Option<&PivotTable> {
1193 self.pivot_tables
1194 .iter()
1195 .find(|p| p.name.eq_ignore_ascii_case(name))
1196 }
1197
1198 fn find_pivot_table_index(&self, name: &str) -> crate::Result<usize> {
1199 self.pivot_tables
1200 .iter()
1201 .position(|p| p.name.eq_ignore_ascii_case(name))
1202 .ok_or_else(|| Error::not_found(ObjectKind::PivotTable, name))
1203 }
1204
1205 pub fn list_pivot_tables(&self) -> &[PivotTable] {
1207 &self.pivot_tables
1208 }
1209
1210 fn pivot_table_name_taken(&self, name: &str) -> bool {
1211 self.pivot_tables
1212 .iter()
1213 .any(|p| p.name.eq_ignore_ascii_case(name))
1214 }
1215
1216 #[allow(clippy::too_many_arguments)]
1220 pub fn add_pivot_table_from_table(
1221 &mut self,
1222 name: &str,
1223 source_table_name: &str,
1224 dest_sheet_name: Option<&str>,
1225 dest_row: usize,
1226 dest_col: usize,
1227 grand_totals_row: bool,
1228 grand_totals_col: bool,
1229 ) -> crate::Result<u64> {
1230 if self.pivot_table_name_taken(name) {
1231 return Err(Error::AlreadyExists {
1232 kind: ObjectKind::PivotTable,
1233 name: name.to_string(),
1234 });
1235 }
1236 self.find_table(source_table_name)
1237 .ok_or_else(|| Error::not_found(ObjectKind::Table, source_table_name))?;
1238 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1239 let id = generate_unique_id();
1240 self.pivot_tables.push(PivotTable {
1241 id,
1242 name: name.to_string(),
1243 source: PivotSource::Table {
1244 name: source_table_name.to_string(),
1245 },
1246 dest_sheet_id: self.sheets[dest_idx].id,
1247 dest_row,
1248 dest_col,
1249 row_fields: Vec::new(),
1250 col_fields: Vec::new(),
1251 value_fields: Vec::new(),
1252 filter_fields: Vec::new(),
1253 grand_totals_row,
1254 grand_totals_col,
1255 last_output_end_row: None,
1256 last_output_end_col: None,
1257 });
1258 self.refresh_pivot_table(name)?;
1259 Ok(id)
1260 }
1261
1262 #[allow(clippy::too_many_arguments)]
1265 pub fn add_pivot_table_from_range(
1266 &mut self,
1267 name: &str,
1268 source_sheet_name: Option<&str>,
1269 start_row: usize,
1270 start_col: usize,
1271 end_row: usize,
1272 end_col: usize,
1273 dest_sheet_name: Option<&str>,
1274 dest_row: usize,
1275 dest_col: usize,
1276 grand_totals_row: bool,
1277 grand_totals_col: bool,
1278 ) -> crate::Result<u64> {
1279 if self.pivot_table_name_taken(name) {
1280 return Err(Error::AlreadyExists {
1281 kind: ObjectKind::PivotTable,
1282 name: name.to_string(),
1283 });
1284 }
1285 let src_idx = self.find_sheet_index(source_sheet_name)?;
1286 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1287 let id = generate_unique_id();
1288 self.pivot_tables.push(PivotTable {
1289 id,
1290 name: name.to_string(),
1291 source: PivotSource::Range {
1292 sheet_id: self.sheets[src_idx].id,
1293 start_row,
1294 start_col,
1295 end_row,
1296 end_col,
1297 },
1298 dest_sheet_id: self.sheets[dest_idx].id,
1299 dest_row,
1300 dest_col,
1301 row_fields: Vec::new(),
1302 col_fields: Vec::new(),
1303 value_fields: Vec::new(),
1304 filter_fields: Vec::new(),
1305 grand_totals_row,
1306 grand_totals_col,
1307 last_output_end_row: None,
1308 last_output_end_col: None,
1309 });
1310 self.refresh_pivot_table(name)?;
1311 Ok(id)
1312 }
1313
1314 pub fn delete_pivot_table(&mut self, name: &str) -> crate::Result<()> {
1317 let idx = self.find_pivot_table_index(name)?;
1318 let pivot = self.pivot_tables.remove(idx);
1319 if let (Some(end_row), Some(end_col)) =
1320 (pivot.last_output_end_row, pivot.last_output_end_col)
1321 && let Some(sheet_idx) = self.sheets.iter().position(|s| s.id == pivot.dest_sheet_id)
1322 {
1323 self.clear_range(sheet_idx, pivot.dest_row, pivot.dest_col, end_row, end_col);
1324 }
1325 Ok(())
1326 }
1327
1328 pub fn rename_pivot_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1330 if !old_name.eq_ignore_ascii_case(new_name) && self.pivot_table_name_taken(new_name) {
1331 return Err(Error::NameTaken {
1332 kind: ObjectKind::PivotTable,
1333 name: new_name.to_string(),
1334 });
1335 }
1336 let idx = self.find_pivot_table_index(old_name)?;
1337 self.pivot_tables[idx].name = new_name.to_string();
1338 Ok(())
1339 }
1340
1341 pub fn add_pivot_field(
1355 &mut self,
1356 pivot_name: &str,
1357 area: PivotArea,
1358 column: &str,
1359 aggregation: Option<PivotAggregation>,
1360 ) -> crate::Result<()> {
1361 let idx = self.find_pivot_table_index(pivot_name)?;
1362 if !matches!(area, PivotArea::Value) {
1363 let pivot = &mut self.pivot_tables[idx];
1364 remove_pivot_field(&mut pivot.row_fields, column);
1365 remove_pivot_field(&mut pivot.col_fields, column);
1366 pivot
1367 .filter_fields
1368 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1369 }
1370 match area {
1371 PivotArea::Row => self.pivot_tables[idx]
1372 .row_fields
1373 .push(PivotField::new(column)),
1374 PivotArea::Column => self.pivot_tables[idx]
1375 .col_fields
1376 .push(PivotField::new(column)),
1377 PivotArea::Value => {
1378 let agg = aggregation.unwrap_or(PivotAggregation::Sum);
1379 self.pivot_tables[idx]
1380 .value_fields
1381 .push(PivotValueField::new(column, agg));
1382 }
1383 PivotArea::Filter => self.pivot_tables[idx]
1384 .filter_fields
1385 .push(PivotFilterField::new(column)),
1386 }
1387 self.refresh_pivot_table(pivot_name)
1388 }
1389
1390 pub fn remove_pivot_field(
1393 &mut self,
1394 pivot_name: &str,
1395 area: PivotArea,
1396 column: &str,
1397 ) -> crate::Result<()> {
1398 let idx = self.find_pivot_table_index(pivot_name)?;
1399 let removed = match area {
1400 PivotArea::Row => remove_pivot_field(&mut self.pivot_tables[idx].row_fields, column),
1401 PivotArea::Column => remove_pivot_field(&mut self.pivot_tables[idx].col_fields, column),
1402 PivotArea::Value => {
1403 let before = self.pivot_tables[idx].value_fields.len();
1404 self.pivot_tables[idx]
1405 .value_fields
1406 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1407 before != self.pivot_tables[idx].value_fields.len()
1408 }
1409 PivotArea::Filter => {
1410 let before = self.pivot_tables[idx].filter_fields.len();
1411 self.pivot_tables[idx]
1412 .filter_fields
1413 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1414 before != self.pivot_tables[idx].filter_fields.len()
1415 }
1416 };
1417 if !removed {
1418 return Err(Error::not_found(
1419 ObjectKind::PivotField,
1420 format!("{column}' in pivot table '{pivot_name}"),
1421 ));
1422 }
1423 self.refresh_pivot_table(pivot_name)
1424 }
1425
1426 pub fn set_pivot_filter(
1429 &mut self,
1430 pivot_name: &str,
1431 column: &str,
1432 values: Option<Vec<String>>,
1433 ) -> crate::Result<()> {
1434 let idx = self.find_pivot_table_index(pivot_name)?;
1435 let field = self.pivot_tables[idx]
1436 .filter_fields
1437 .iter_mut()
1438 .find(|f| f.column.eq_ignore_ascii_case(column))
1439 .ok_or_else(|| {
1440 Error::not_found(
1441 ObjectKind::PivotField,
1442 format!("{column}' on pivot table '{pivot_name}"),
1443 )
1444 })?;
1445 field.selected_values = values;
1446 self.refresh_pivot_table(pivot_name)
1447 }
1448
1449 pub fn refresh_pivot_table(&mut self, pivot_name: &str) -> crate::Result<()> {
1454 let idx = self.find_pivot_table_index(pivot_name)?;
1455 let pivot = self.pivot_tables[idx].clone();
1456 let dest_idx = self
1457 .sheets
1458 .iter()
1459 .position(|s| s.id == pivot.dest_sheet_id)
1460 .ok_or_else(|| {
1461 Error::InvalidArgument(
1462 "pivot table's destination sheet no longer exists".to_string(),
1463 )
1464 })?;
1465
1466 let grid: Option<PivotGrid> = if pivot.value_fields.is_empty() {
1467 None
1468 } else {
1469 let sheet_refs: Vec<&Sheet> = self.sheets.iter().collect();
1470 Some(compute_pivot(&sheet_refs, &pivot).map_err(Error::InvalidArgument)?)
1471 };
1472
1473 if let (Some(old_end_row), Some(old_end_col)) =
1474 (pivot.last_output_end_row, pivot.last_output_end_col)
1475 {
1476 self.clear_range(
1477 dest_idx,
1478 pivot.dest_row,
1479 pivot.dest_col,
1480 old_end_row,
1481 old_end_col,
1482 );
1483 }
1484
1485 let new_bounds = grid.as_ref().map(|grid| {
1486 let height = grid.height();
1487 let width = grid.width.max(1);
1488 self.ensure_capacity(
1489 dest_idx,
1490 pivot.dest_row + height.saturating_sub(1),
1491 pivot.dest_col + width.saturating_sub(1),
1492 );
1493
1494 let mut r = pivot.dest_row;
1495 for (name, state) in &grid.filter_rows {
1496 self.set_cell(dest_idx, r, pivot.dest_col, pivot_label_literal(name));
1497 self.set_cell(dest_idx, r, pivot.dest_col + 1, pivot_label_literal(state));
1498 r += 1;
1499 }
1500 if !grid.filter_rows.is_empty() {
1501 r += 1;
1502 }
1503 for header in &grid.header_rows {
1504 for (c, text) in header.iter().enumerate() {
1505 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(text));
1506 }
1507 r += 1;
1508 }
1509 for body in &grid.body_rows {
1510 for (c, label) in body.row_labels.iter().enumerate() {
1511 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(label));
1512 }
1513 for (c, val) in body.values.iter().enumerate() {
1514 self.set_cell(
1515 dest_idx,
1516 r,
1517 pivot.dest_col + body.row_labels.len() + c,
1518 pivot_value_literal(val),
1519 );
1520 }
1521 r += 1;
1522 }
1523 (
1524 pivot.dest_row + height.saturating_sub(1),
1525 pivot.dest_col + width.saturating_sub(1),
1526 )
1527 });
1528
1529 self.pivot_tables[idx].last_output_end_row = new_bounds.map(|(r, _)| r);
1530 self.pivot_tables[idx].last_output_end_col = new_bounds.map(|(_, c)| c);
1531 self.evaluate()
1532 }
1533
1534 fn clear_range(
1538 &mut self,
1539 sheet_idx: usize,
1540 start_row: usize,
1541 start_col: usize,
1542 end_row: usize,
1543 end_col: usize,
1544 ) {
1545 if sheet_idx >= self.sheets.len() {
1546 return;
1547 }
1548 let (row_count, col_count) = {
1549 let s = &self.sheets[sheet_idx];
1550 (s.row_count(), s.col_count())
1551 };
1552 if row_count == 0 || col_count == 0 {
1553 return;
1554 }
1555 for r in start_row..=end_row.min(row_count - 1) {
1556 for c in start_col..=end_col.min(col_count - 1) {
1557 self.sheets[sheet_idx].set_cell_src(r, c, String::new());
1558 }
1559 }
1560 }
1561}