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(
17 table: &mut ExcelTable,
18 new_start_col: usize,
19 new_end_col: usize,
20 edit: &GridEdit,
21) {
22 if edit.insert {
23 if edit.at > table.start_col && edit.at <= table.end_col {
24 let offset = (edit.at - table.start_col).min(table.columns.len());
25 for _ in 0..edit.count {
26 table.columns.insert(offset, String::new());
27 }
28 }
29 } else {
30 let first = edit.at.max(table.start_col);
31 let last = (edit.at + edit.count).min(table.end_col + 1);
32 if first < last {
33 let lo = (first - table.start_col).min(table.columns.len());
34 let hi = (last - table.start_col).min(table.columns.len());
35 table.columns.drain(lo..hi);
36 }
37 }
38 table
39 .columns
40 .resize(new_end_col - new_start_col + 1, String::new());
41}
42
43pub struct SheetSummary {
45 pub name: String,
47 pub row_count: usize,
49 pub col_count: usize,
51 pub formula_count: usize,
53}
54
55pub struct WorkbookSummary {
57 pub file_name: String,
59 pub sheet_count: usize,
61 pub chart_count: usize,
63 pub sheets: Vec<SheetSummary>,
65}
66
67pub struct WorkbookManager {
83 pub sheets: Vec<Sheet>,
86 pub charts: Vec<Chart>,
89 pub pivot_tables: Vec<PivotTable>,
92 pub vba_project: Option<VbaProject>,
94 pub locale: Locale,
96}
97
98fn pivot_label_literal(text: &str) -> String {
99 if text.is_empty() {
100 String::new()
101 } else if text.starts_with('=')
102 || text.parse::<f64>().is_ok()
103 || text.eq_ignore_ascii_case("true")
104 || text.eq_ignore_ascii_case("false")
105 {
106 format!("\"{}\"", text)
107 } else {
108 text.to_string()
109 }
110}
111
112fn pivot_value_literal(v: &ResultData) -> String {
113 match v {
114 ResultData::Error(e) => e.clone(),
115 other => other.to_string(),
116 }
117}
118
119fn remove_pivot_field(fields: &mut Vec<PivotField>, column: &str) -> bool {
120 let before = fields.len();
121 fields.retain(|f| !f.column.eq_ignore_ascii_case(column));
122 before != fields.len()
123}
124
125impl WorkbookManager {
126 pub fn load_bytes(buffer: &[u8]) -> crate::Result<Self> {
128 let (imported_sheets, charts, pivot_tables, vba_project) =
129 import_xlsx_data(buffer, &[], |_, _, _| {})?;
130
131 let locale = Locale::default();
132 let mut sheets: Vec<Sheet> = imported_sheets.into_iter().map(|it| it.sheet).collect();
133 for sheet in &mut sheets {
134 sheet.locale = locale.clone();
135 }
136 Ok(Self {
137 sheets,
138 charts,
139 pivot_tables,
140 vba_project,
141 locale,
142 })
143 }
144
145 pub fn save_bytes(&self) -> crate::Result<Vec<u8>> {
151 export_xlsx_data(
152 &self.sheets,
153 &self.charts,
154 &self.pivot_tables,
155 self.vba_project.as_ref(),
156 )
157 }
158
159 pub fn new_empty() -> crate::Result<Self> {
161 let locale = Locale::default();
162 let mut wb = Self {
163 sheets: Vec::new(),
164 charts: Vec::new(),
165 pivot_tables: Vec::new(),
166 vba_project: None,
167 locale,
168 };
169 wb.add_sheet("Sheet1")?;
170 Ok(wb)
171 }
172
173 pub fn set_locale(&mut self, locale: Locale) {
175 self.locale = locale.clone();
176 for sheet in &mut self.sheets {
177 sheet.locale = locale.clone();
178 }
179 }
180
181 pub fn evaluate(&mut self) -> crate::Result<()> {
183 if self.sheets.is_empty() {
184 return Ok(());
185 }
186
187 let sheet_order: Vec<String> = self.sheets.iter().map(|s| s.name.clone()).collect();
188
189 for _pass in 0..3 {
190 for sheet in &mut self.sheets {
191 sheet.mark_all_dirty();
192 }
193 for i in 0..self.sheets.len() {
194 let (left, right) = self.sheets.split_at_mut(i);
195 let (target_sheet, right_tail) = right.split_first_mut().unwrap();
196
197 let mut context = Context::new();
198 for s in left.iter() {
199 context.add_table(s.name.clone(), s);
200 }
201 for s in right_tail.iter() {
202 context.add_table(s.name.clone(), s);
203 }
204 context.pivot_tables = &self.pivot_tables;
205 context.sheet_order = sheet_order.clone();
206
207 let _ = target_sheet.commit(Some(&context));
208 }
209 }
210
211 Ok(())
212 }
213
214 pub(crate) fn call_worksheet_function(
215 &self,
216 name: &str,
217 args: &[crate::core::parser::Expr],
218 ) -> Result<ResultData, crate::core::EngineError> {
219 let Some(host) = self.sheets.first() else {
220 return Err(crate::core::EngineError::EvalError(
221 crate::core::EvalError::UnknownFunction("no worksheets".to_string()),
222 ));
223 };
224 let mut context = Context::new();
225 for s in &self.sheets {
226 context.add_table(s.name.clone(), s);
227 }
228 context.pivot_tables = &self.pivot_tables;
229 context.sheet_order = self.sheets.iter().map(|s| s.name.clone()).collect();
230 host.call_worksheet_function(name, args, Some(&context))
231 }
232
233 pub fn find_sheet_index(&self, name_opt: Option<&str>) -> crate::Result<usize> {
235 if self.sheets.is_empty() {
236 return Err(Error::EmptyWorkbook);
237 }
238
239 match name_opt {
240 Some(name) => {
241 if let Some(idx) = self
242 .sheets
243 .iter()
244 .position(|s| s.name.eq_ignore_ascii_case(name))
245 {
246 Ok(idx)
247 } else {
248 let available: Vec<String> =
249 self.sheets.iter().map(|s| s.name.clone()).collect();
250 Err(Error::not_found_among(
251 ObjectKind::Sheet,
252 name.to_string(),
253 available,
254 ))
255 }
256 }
257 None => Ok(0),
258 }
259 }
260
261 pub fn get_summary(&self, file_name: &str) -> WorkbookSummary {
263 let sheet_summaries = self
264 .sheets
265 .iter()
266 .map(|sheet| {
267 let row_count = sheet.row_count();
268 let col_count = sheet.col_count();
269 let mut formula_count = 0;
270
271 for col in &sheet.columns {
272 for src in &col.src {
273 if src.starts_with('=') {
274 formula_count += 1;
275 }
276 }
277 }
278
279 SheetSummary {
280 name: sheet.name.clone(),
281 row_count,
282 col_count,
283 formula_count,
284 }
285 })
286 .collect();
287
288 WorkbookSummary {
289 file_name: file_name.to_string(),
290 sheet_count: self.sheets.len(),
291 chart_count: self.charts.len(),
292 sheets: sheet_summaries,
293 }
294 }
295
296 pub fn ensure_capacity(&mut self, sheet_idx: usize, target_row: usize, target_col: usize) {
298 if sheet_idx >= self.sheets.len() {
299 return;
300 }
301 self.sheets[sheet_idx].ensure_capacity(target_row, target_col);
302 }
303
304 pub fn set_cell_style(
311 &mut self,
312 sheet_name: Option<&str>,
313 row: usize,
314 col: usize,
315 style: crate::core::CellStyle,
316 ) -> crate::Result<()> {
317 let sheet_idx = self.find_sheet_index(sheet_name)?;
318 self.sheets[sheet_idx].update_cell_style(row, col, |s| s.merge(&style));
319 Ok(())
320 }
321
322 pub fn set_range_style(
324 &mut self,
325 sheet_name: Option<&str>,
326 start_row: usize,
327 start_col: usize,
328 end_row: usize,
329 end_col: usize,
330 style: crate::core::CellStyle,
331 ) -> crate::Result<()> {
332 if end_row < start_row || end_col < start_col {
333 return Err(Error::InvalidRange(
334 "range end must not precede its start".to_string(),
335 ));
336 }
337 let sheet_idx = self.find_sheet_index(sheet_name)?;
338 for r in start_row..=end_row {
339 for c in start_col..=end_col {
340 self.sheets[sheet_idx].update_cell_style(r, c, |s| s.merge(&style));
341 }
342 }
343 Ok(())
344 }
345
346 pub fn get_cell_style(
348 &self,
349 sheet_name: Option<&str>,
350 row: usize,
351 col: usize,
352 ) -> crate::Result<Option<crate::core::CellStyle>> {
353 let sheet_idx = self.find_sheet_index(sheet_name)?;
354 Ok(self.sheets[sheet_idx].get_cell_style(row, col).cloned())
355 }
356
357 pub fn set_table_style(&mut self, table_name: &str, style_name: &str) -> crate::Result<()> {
364 for sheet in &mut self.sheets {
365 for table in &mut sheet.tables {
366 if table.name.eq_ignore_ascii_case(table_name) {
367 table.set_style_name(Some(style_name.to_string()));
368 return Ok(());
369 }
370 }
371 }
372 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
373 }
374
375 pub fn get_table_style(&self, table_name: &str) -> crate::Result<Option<String>> {
381 for sheet in &self.sheets {
382 for table in &sheet.tables {
383 if table.name.eq_ignore_ascii_case(table_name) {
384 return Ok(table.style_name.clone());
385 }
386 }
387 }
388 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
389 }
390
391 pub fn set_cell(&mut self, sheet_idx: usize, row: usize, col: usize, value: String) {
393 self.ensure_capacity(sheet_idx, row, col);
394 let sheet = &mut self.sheets[sheet_idx];
395 sheet.set_cell_src(row, col, value);
396 }
397
398 pub fn set_cell_with_type(
400 &mut self,
401 sheet_idx: usize,
402 row: usize,
403 col: usize,
404 value: String,
405 cell_type: crate::core::CellType,
406 ) {
407 self.ensure_capacity(sheet_idx, row, col);
408 let sheet = &mut self.sheets[sheet_idx];
409 sheet.set_cell_with_type(row, col, value, cell_type);
410 }
411
412 pub fn set_cell_type(
414 &mut self,
415 sheet_idx: usize,
416 row: usize,
417 col: usize,
418 cell_type: crate::core::CellType,
419 ) {
420 self.ensure_capacity(sheet_idx, row, col);
421 let sheet = &mut self.sheets[sheet_idx];
422 sheet.set_cell_type(row, col, cell_type);
423 }
424
425 pub fn get_cell_type(&self, sheet_idx: usize, row: usize, col: usize) -> crate::core::CellType {
427 if let Some(sheet) = self.sheets.get(sheet_idx) {
428 sheet.get_cell_type(&crate::core::CellRef::new(row, col))
429 } else {
430 crate::core::CellType::Empty
431 }
432 }
433
434 pub fn insert_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
440 let sheet = &self.sheets[sheet_idx];
441 let at = row_idx.min(sheet.row_count());
442 let edit = GridEdit::insert_row(sheet.id, at);
443 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_row(at));
444 self.evaluate()
445 }
446
447 pub fn delete_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
452 let sheet = &self.sheets[sheet_idx];
453 if row_idx >= sheet.row_count() {
454 return Err(Error::OutOfBounds {
455 what: "row",
456 index: row_idx,
457 len: sheet.row_count(),
458 });
459 }
460 let edit = GridEdit::delete_row(sheet.id, row_idx);
461 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].delete_row(row_idx));
462 self.evaluate()
463 }
464
465 pub fn insert_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
467 let sheet = &self.sheets[sheet_idx];
468 let at = col_idx.min(sheet.col_count());
469 let edit = GridEdit::insert_col(sheet.id, at);
470 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_col(at));
471 self.evaluate()
472 }
473
474 pub fn delete_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
476 let sheet = &self.sheets[sheet_idx];
477 if col_idx >= sheet.col_count() {
478 return Err(Error::OutOfBounds {
479 what: "column",
480 index: col_idx,
481 len: sheet.col_count(),
482 });
483 }
484 let deleted_col_ids = vec![sheet.columns()[col_idx].id];
485 let edit = GridEdit::delete_col(sheet.id, col_idx);
486 self.apply_grid_edit(edit, &deleted_col_ids, |wb| {
487 wb.sheets[sheet_idx].delete_col(col_idx)
488 });
489 self.evaluate()
490 }
491
492 pub fn insert_cells_shift_down(
500 &mut self,
501 sheet_idx: usize,
502 row: usize,
503 first_col: usize,
504 last_col: usize,
505 count: usize,
506 ) -> crate::Result<()> {
507 let sheet = &self.sheets[sheet_idx];
508 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, true);
509 self.apply_grid_edit(edit, &[], |wb| {
510 wb.sheets[sheet_idx].insert_cells_shift_down(row, first_col, last_col, count)
511 });
512 self.evaluate()
513 }
514
515 pub fn delete_cells_shift_up(
518 &mut self,
519 sheet_idx: usize,
520 row: usize,
521 first_col: usize,
522 last_col: usize,
523 count: usize,
524 ) -> crate::Result<()> {
525 let sheet = &self.sheets[sheet_idx];
526 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, false);
527 self.apply_grid_edit(edit, &[], |wb| {
528 wb.sheets[sheet_idx].delete_cells_shift_up(row, first_col, last_col, count)
529 });
530 self.evaluate()
531 }
532
533 fn apply_grid_edit(
534 &mut self,
535 edit: GridEdit,
536 deleted_col_ids: &[u64],
537 apply: impl FnOnce(&mut Self),
538 ) {
539 let mut shifted: Vec<(usize, usize, usize, CompiledFormula)> = Vec::new();
540 for (sheet_idx, sheet) in self.sheets.iter().enumerate() {
541 for (col_idx, column) in sheet.columns().iter().enumerate() {
542 for row_idx in 0..column.len() {
543 let Some(src) = column.src(row_idx).filter(|s| s.starts_with('=')) else {
544 continue;
545 };
546 let compiled = crate::core::parser::compile_formula(src, &self.sheets);
547 if let Some(next) =
548 crate::core::grid_edit::shift_formula(&compiled, &edit, deleted_col_ids)
549 {
550 shifted.push((sheet_idx, col_idx, row_idx, next));
551 }
552 }
553 }
554 }
555
556 apply(self);
557 self.shift_table_and_pivot_ranges(&edit);
558
559 for (sheet_idx, col_idx, row_idx, compiled) in shifted {
560 let Some((row, col)) = self.moved_cell(&edit, sheet_idx, row_idx, col_idx) else {
561 continue;
562 };
563 let text = crate::core::parser::serialize_formula(&compiled, &self.sheets);
564 self.sheets[sheet_idx].set_cell_src(row, col, text);
565 }
566 }
567
568 fn moved_cell(
569 &self,
570 edit: &GridEdit,
571 sheet_idx: usize,
572 row: usize,
573 col: usize,
574 ) -> Option<(usize, usize)> {
575 if self.sheets[sheet_idx].id != edit.sheet_id || !edit.covers_columns(col, col) {
576 return Some((row, col));
577 }
578 let moved = |index: usize| {
579 crate::core::grid_edit::shift_point(index, edit.at, edit.count, edit.insert)
580 };
581 match edit.axis {
582 Axis::Row => Some((moved(row)?, col)),
583 Axis::Col => Some((row, moved(col)?)),
584 }
585 }
586
587 fn shift_table_and_pivot_ranges(&mut self, edit: &GridEdit) {
588 use crate::core::grid_edit::{shift_point, shift_rect};
589
590 for sheet in &mut self.sheets {
591 if sheet.id != edit.sheet_id {
592 continue;
593 }
594 sheet.tables.retain_mut(|table| {
595 if !edit.covers_columns(table.start_col, table.end_col) {
596 return true;
597 }
598 match shift_rect(
599 edit,
600 table.start_row,
601 table.start_col,
602 table.end_row,
603 table.end_col,
604 ) {
605 Some((r0, c0, r1, c1)) => {
606 if edit.axis == Axis::Col {
607 resize_table_columns(table, c0, c1, edit);
608 }
609 table.start_row = r0;
610 table.start_col = c0;
611 table.end_row = r1;
612 table.end_col = c1;
613 true
614 }
615 None => false,
616 }
617 });
618 }
619
620 for pivot in &mut self.pivot_tables {
621 if let PivotSource::Range {
622 sheet_id,
623 start_row,
624 start_col,
625 end_row,
626 end_col,
627 } = &mut pivot.source
628 && *sheet_id == edit.sheet_id
629 && edit.covers_columns(*start_col, *end_col)
630 && let Some((r0, c0, r1, c1)) =
631 shift_rect(edit, *start_row, *start_col, *end_row, *end_col)
632 {
633 *start_row = r0;
634 *start_col = c0;
635 *end_row = r1;
636 *end_col = c1;
637 }
638
639 if pivot.dest_sheet_id == edit.sheet_id
640 && edit.covers_columns(pivot.dest_col, pivot.dest_col)
641 {
642 match edit.axis {
643 Axis::Row => {
644 pivot.dest_row =
645 shift_point(pivot.dest_row, edit.at, edit.count, edit.insert)
646 .unwrap_or(edit.at);
647 }
648 Axis::Col => {
649 pivot.dest_col =
650 shift_point(pivot.dest_col, edit.at, edit.count, edit.insert)
651 .unwrap_or(edit.at);
652 }
653 }
654 pivot.last_output_end_row = None;
655 pivot.last_output_end_col = None;
656 }
657 }
658 }
659
660 pub fn add_sheet(&mut self, name: &str) -> crate::Result<()> {
662 if self
663 .sheets
664 .iter()
665 .any(|s| s.name.eq_ignore_ascii_case(name))
666 {
667 return Err(Error::AlreadyExists {
668 kind: ObjectKind::Sheet,
669 name: name.to_string(),
670 });
671 }
672
673 let mut columns = Vec::new();
674 for col_idx in 0..5 {
675 let mut col = DataColumn::new(10);
676 col.id = generate_unique_id();
677 col.name = col_idx_to_letters(col_idx);
678 columns.push(col);
679 }
680
681 let new_sheet = Sheet {
682 id: generate_unique_id(),
683 name: name.to_string(),
684 columns,
685 row_heights: vec![None; 10],
686 tables: Vec::new(),
687 dependencies: std::collections::HashMap::new(),
688 dependencies_rev: std::collections::HashMap::new(),
689 uncommitted_actions: Vec::new(),
690 locale: self.locale.clone(),
691 };
692
693 self.sheets.push(new_sheet);
694 Ok(())
695 }
696
697 pub fn delete_sheet(&mut self, name: &str) -> crate::Result<()> {
699 let idx = self.find_sheet_index(Some(name))?;
700 if self.sheets.len() <= 1 {
701 return Err(Error::LastSheetInWorkbook);
702 }
703 self.sheets.remove(idx);
704 Ok(())
705 }
706
707 pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
709 let idx = self.find_sheet_index(Some(old_name))?;
710 if self
711 .sheets
712 .iter()
713 .enumerate()
714 .any(|(i, s)| i != idx && s.name.eq_ignore_ascii_case(new_name))
715 {
716 return Err(Error::NameTaken {
717 kind: ObjectKind::Sheet,
718 name: new_name.to_string(),
719 });
720 }
721 self.sheets[idx].name = new_name.to_string();
722 Ok(())
723 }
724
725 #[allow(clippy::too_many_arguments)]
727 pub fn add_chart(
728 &mut self,
729 sheet_name: &str,
730 chart_type: ChartType,
731 range: String,
732 title: Option<String>,
733 anchor: Option<(usize, usize)>,
734 ) -> crate::Result<u64> {
735 let _ = self.find_sheet_index(Some(sheet_name))?;
736 let id = generate_unique_id();
737 let name = format!("Chart {}", self.charts.len() + 1);
738 let (anchor_row, anchor_col) = anchor.unwrap_or((0, 0));
739
740 let chart = Chart {
741 id,
742 name,
743 chart_type,
744 data_range: range,
745 title,
746 xlabel: None,
747 ylabel: None,
748 show_legend: true,
749 anchor_row,
750 anchor_col,
751 };
752
753 self.charts.push(chart);
754 Ok(id)
755 }
756
757 #[allow(clippy::too_many_arguments)]
762 pub fn edit_chart(
763 &mut self,
764 id: u64,
765 name: Option<String>,
766 chart_type: Option<ChartType>,
767 data_range: Option<String>,
768 title: Option<Option<String>>,
769 xlabel: Option<Option<String>>,
770 ylabel: Option<Option<String>>,
771 show_legend: Option<bool>,
772 anchor: Option<(usize, usize)>,
773 ) -> crate::Result<()> {
774 let chart = self
775 .charts
776 .iter_mut()
777 .find(|c| c.id == id)
778 .ok_or_else(|| Error::not_found(ObjectKind::Chart, id.to_string()))?;
779 if let Some(name) = name {
780 chart.name = name;
781 }
782 if let Some(chart_type) = chart_type {
783 chart.chart_type = chart_type;
784 }
785 if let Some(data_range) = data_range {
786 chart.data_range = data_range;
787 }
788 if let Some(title) = title {
789 chart.title = title;
790 }
791 if let Some(xlabel) = xlabel {
792 chart.xlabel = xlabel;
793 }
794 if let Some(ylabel) = ylabel {
795 chart.ylabel = ylabel;
796 }
797 if let Some(show_legend) = show_legend {
798 chart.show_legend = show_legend;
799 }
800 if let Some((anchor_row, anchor_col)) = anchor {
801 chart.anchor_row = anchor_row;
802 chart.anchor_col = anchor_col;
803 }
804 Ok(())
805 }
806
807 pub fn has_vba_project(&self) -> bool {
809 self.vba_project.is_some()
810 }
811
812 pub fn list_vba_modules(&self) -> Vec<&VbaModule> {
814 self.vba_project
815 .as_ref()
816 .map(|p| p.modules.iter().collect())
817 .unwrap_or_default()
818 }
819
820 pub fn ensure_vba_project(&mut self) -> crate::Result<()> {
824 if self.vba_project.is_some() {
825 return Ok(());
826 }
827 self.vba_project = Some(VbaProject::new_empty());
828 Ok(())
829 }
830
831 pub fn add_vba_module(
841 &mut self,
842 name: String,
843 kind: VbaModuleKind,
844 source: String,
845 bound_sheet_id: Option<u64>,
846 ) -> crate::Result<()> {
847 validate_vba_module_name(&name).map_err(|reason| Error::InvalidName {
848 kind: ObjectKind::VbaModule,
849 name: name.clone(),
850 reason,
851 })?;
852 let is_this_workbook = kind == VbaModuleKind::Document && name == "ThisWorkbook";
853 if kind == VbaModuleKind::Document && !is_this_workbook {
854 let sheet_id = bound_sheet_id
855 .ok_or_else(|| Error::Vba("document modules require a bound sheet".to_string()))?;
856 if !self.sheets.iter().any(|s| s.id == sheet_id) {
857 return Err(Error::not_found(ObjectKind::Sheet, sheet_id.to_string()));
858 }
859 }
860 self.ensure_vba_project()?;
861 let project = self.vba_project.as_mut().unwrap();
862 if project.module_name_taken(&name) {
863 return Err(Error::AlreadyExists {
864 kind: ObjectKind::VbaModule,
865 name: name.to_string(),
866 });
867 }
868 if kind == VbaModuleKind::Document
869 && bound_sheet_id.is_some()
870 && project
871 .modules
872 .iter()
873 .any(|m| m.kind == VbaModuleKind::Document && m.bound_sheet_id == bound_sheet_id)
874 {
875 return Err(Error::DocumentModuleExists);
876 }
877 let prefix_bytes = project
878 .modules
879 .first()
880 .map(|m| m.prefix_bytes.clone())
881 .unwrap_or_else(|| project.seed_prefix_bytes.clone());
882 let module_cookie = project
883 .modules
884 .first()
885 .map(|m| m.module_cookie)
886 .unwrap_or(project.seed_module_cookie);
887 let stored_bound_sheet_id = if kind == VbaModuleKind::Document && !is_this_workbook {
888 bound_sheet_id
889 } else {
890 None
891 };
892 project.modules.push(VbaModule {
893 name,
894 kind,
895 source,
896 bound_sheet_id: stored_bound_sheet_id,
897 prefix_bytes,
898 module_cookie,
899 cached_compressed_source: None,
900 });
901 Ok(())
902 }
903
904 pub fn remove_vba_module(&mut self, name: &str) -> crate::Result<()> {
911 let project = self
912 .vba_project
913 .as_mut()
914 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
915 let before = project.modules.len();
916 project
917 .modules
918 .retain(|m| !m.name.eq_ignore_ascii_case(name));
919 if project.modules.len() == before {
920 return Err(Error::not_found(ObjectKind::VbaModule, name.to_string()));
921 }
922 Ok(())
923 }
924
925 pub fn rename_vba_module(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
938 validate_vba_module_name(new_name).map_err(|reason| Error::InvalidName {
939 kind: ObjectKind::VbaModule,
940 name: new_name.to_string(),
941 reason,
942 })?;
943 let project = self
944 .vba_project
945 .as_mut()
946 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
947 if !old_name.eq_ignore_ascii_case(new_name) && project.module_name_taken(new_name) {
948 return Err(Error::AlreadyExists {
949 kind: ObjectKind::VbaModule,
950 name: new_name.to_string(),
951 });
952 }
953 let module = project
954 .find_module_mut(old_name)
955 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, old_name))?;
956 module.name = new_name.to_string();
957 Ok(())
958 }
959
960 pub fn set_vba_module_source(&mut self, name: &str, source: String) -> crate::Result<()> {
971 let project = self
972 .vba_project
973 .as_mut()
974 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
975 let module = project
976 .find_module_mut(name)
977 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, name))?;
978 module.source = source;
979 module.cached_compressed_source = None;
980 Ok(())
981 }
982
983 pub fn delete_chart(&mut self, id: u64) -> crate::Result<()> {
985 if let Some(pos) = self.charts.iter().position(|c| c.id == id) {
986 self.charts.remove(pos);
987 Ok(())
988 } else {
989 Err(Error::not_found(ObjectKind::Chart, id.to_string()))
990 }
991 }
992
993 pub fn find_table(&self, name: &str) -> Option<(&Sheet, &ExcelTable)> {
996 self.sheets
997 .iter()
998 .find_map(|s| s.find_table(name).map(|t| (s, t)))
999 }
1000
1001 pub fn list_tables(&self) -> Vec<(&str, &ExcelTable)> {
1004 self.sheets
1005 .iter()
1006 .flat_map(|s| s.tables.iter().map(move |t| (s.name.as_str(), t)))
1007 .collect()
1008 }
1009
1010 fn find_table_sheet_index(&self, name: &str) -> crate::Result<usize> {
1011 self.sheets
1012 .iter()
1013 .position(|s| s.find_table(name).is_some())
1014 .ok_or_else(|| Error::not_found(ObjectKind::Table, name))
1015 }
1016
1017 fn table_name_taken(&self, name: &str) -> bool {
1018 self.sheets
1019 .iter()
1020 .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(name)))
1021 }
1022
1023 #[allow(clippy::too_many_arguments)]
1027 pub fn add_table(
1028 &mut self,
1029 sheet_name: Option<&str>,
1030 name: &str,
1031 start_row: usize,
1032 start_col: usize,
1033 end_row: usize,
1034 end_col: usize,
1035 has_header_row: bool,
1036 has_totals_row: bool,
1037 ) -> crate::Result<u64> {
1038 if self.table_name_taken(name) {
1039 return Err(Error::AlreadyExists {
1040 kind: ObjectKind::Table,
1041 name: name.to_string(),
1042 });
1043 }
1044 let idx = self.find_sheet_index(sheet_name)?;
1045 self.sheets[idx]
1046 .add_table(
1047 name.to_string(),
1048 start_row,
1049 start_col,
1050 end_row,
1051 end_col,
1052 has_header_row,
1053 has_totals_row,
1054 )
1055 .map_err(Error::InvalidArgument)
1056 }
1057
1058 pub fn delete_table(&mut self, name: &str) -> crate::Result<()> {
1060 let idx = self.find_table_sheet_index(name)?;
1061 self.sheets[idx]
1062 .delete_table_by_name(name)
1063 .map_err(Error::InvalidArgument)
1064 }
1065
1066 pub fn rename_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1068 if !old_name.eq_ignore_ascii_case(new_name) && self.table_name_taken(new_name) {
1069 return Err(Error::NameTaken {
1070 kind: ObjectKind::Table,
1071 name: new_name.to_string(),
1072 });
1073 }
1074 let idx = self.find_table_sheet_index(old_name)?;
1075 self.sheets[idx]
1076 .rename_table(old_name, new_name)
1077 .map_err(Error::InvalidArgument)?;
1078 self.rewrite_table_references(old_name, Some(new_name), None);
1079 self.evaluate()
1080 }
1081
1082 fn rewrite_table_references(
1083 &mut self,
1084 table_name: &str,
1085 new_table_name: Option<&str>,
1086 col_rename: Option<(&str, &str)>,
1087 ) {
1088 for sheet in &mut self.sheets {
1089 for col_idx in 0..sheet.columns.len() {
1090 let row_count = sheet.columns[col_idx].src.len();
1091 for row_idx in 0..row_count {
1092 let src = sheet.columns[col_idx].src[row_idx].clone();
1093 if let Some(new_src) = crate::core::parser::rewrite_structured_table_reference(
1094 &src,
1095 table_name,
1096 new_table_name,
1097 col_rename,
1098 ) {
1099 sheet.set_cell_src(row_idx, col_idx, new_src);
1100 }
1101 }
1102 }
1103 }
1104 }
1105
1106 pub fn resize_table(
1108 &mut self,
1109 name: &str,
1110 new_end_row: usize,
1111 new_end_col: usize,
1112 ) -> crate::Result<()> {
1113 let idx = self.find_table_sheet_index(name)?;
1114 self.sheets[idx]
1115 .resize_table(name, new_end_row, new_end_col)
1116 .map_err(Error::InvalidArgument)
1117 }
1118
1119 pub fn rename_table_column(
1121 &mut self,
1122 table_name: &str,
1123 col_index: usize,
1124 new_name: &str,
1125 ) -> crate::Result<()> {
1126 let idx = self.find_table_sheet_index(table_name)?;
1127 let old_col_name = self.sheets[idx]
1128 .find_table(table_name)
1129 .and_then(|t| t.columns.get(col_index).cloned())
1130 .ok_or_else(|| {
1131 Error::InvalidArgument(format!(
1132 "column index {col_index} out of bounds for table '{table_name}'"
1133 ))
1134 })?;
1135 self.sheets[idx]
1136 .rename_table_column(table_name, col_index, new_name)
1137 .map_err(Error::InvalidArgument)?;
1138 self.rewrite_table_references(table_name, None, Some((&old_col_name, new_name)));
1139 self.evaluate()
1140 }
1141
1142 pub fn find_pivot_table(&self, name: &str) -> Option<&PivotTable> {
1144 self.pivot_tables
1145 .iter()
1146 .find(|p| p.name.eq_ignore_ascii_case(name))
1147 }
1148
1149 fn find_pivot_table_index(&self, name: &str) -> crate::Result<usize> {
1150 self.pivot_tables
1151 .iter()
1152 .position(|p| p.name.eq_ignore_ascii_case(name))
1153 .ok_or_else(|| Error::not_found(ObjectKind::PivotTable, name))
1154 }
1155
1156 pub fn list_pivot_tables(&self) -> &[PivotTable] {
1158 &self.pivot_tables
1159 }
1160
1161 fn pivot_table_name_taken(&self, name: &str) -> bool {
1162 self.pivot_tables
1163 .iter()
1164 .any(|p| p.name.eq_ignore_ascii_case(name))
1165 }
1166
1167 #[allow(clippy::too_many_arguments)]
1171 pub fn add_pivot_table_from_table(
1172 &mut self,
1173 name: &str,
1174 source_table_name: &str,
1175 dest_sheet_name: Option<&str>,
1176 dest_row: usize,
1177 dest_col: usize,
1178 grand_totals_row: bool,
1179 grand_totals_col: bool,
1180 ) -> crate::Result<u64> {
1181 if self.pivot_table_name_taken(name) {
1182 return Err(Error::AlreadyExists {
1183 kind: ObjectKind::PivotTable,
1184 name: name.to_string(),
1185 });
1186 }
1187 self.find_table(source_table_name)
1188 .ok_or_else(|| Error::not_found(ObjectKind::Table, source_table_name))?;
1189 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1190 let id = generate_unique_id();
1191 self.pivot_tables.push(PivotTable {
1192 id,
1193 name: name.to_string(),
1194 source: PivotSource::Table {
1195 name: source_table_name.to_string(),
1196 },
1197 dest_sheet_id: self.sheets[dest_idx].id,
1198 dest_row,
1199 dest_col,
1200 row_fields: Vec::new(),
1201 col_fields: Vec::new(),
1202 value_fields: Vec::new(),
1203 filter_fields: Vec::new(),
1204 grand_totals_row,
1205 grand_totals_col,
1206 last_output_end_row: None,
1207 last_output_end_col: None,
1208 });
1209 self.refresh_pivot_table(name)?;
1210 Ok(id)
1211 }
1212
1213 #[allow(clippy::too_many_arguments)]
1216 pub fn add_pivot_table_from_range(
1217 &mut self,
1218 name: &str,
1219 source_sheet_name: Option<&str>,
1220 start_row: usize,
1221 start_col: usize,
1222 end_row: usize,
1223 end_col: usize,
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 let src_idx = self.find_sheet_index(source_sheet_name)?;
1237 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1238 let id = generate_unique_id();
1239 self.pivot_tables.push(PivotTable {
1240 id,
1241 name: name.to_string(),
1242 source: PivotSource::Range {
1243 sheet_id: self.sheets[src_idx].id,
1244 start_row,
1245 start_col,
1246 end_row,
1247 end_col,
1248 },
1249 dest_sheet_id: self.sheets[dest_idx].id,
1250 dest_row,
1251 dest_col,
1252 row_fields: Vec::new(),
1253 col_fields: Vec::new(),
1254 value_fields: Vec::new(),
1255 filter_fields: Vec::new(),
1256 grand_totals_row,
1257 grand_totals_col,
1258 last_output_end_row: None,
1259 last_output_end_col: None,
1260 });
1261 self.refresh_pivot_table(name)?;
1262 Ok(id)
1263 }
1264
1265 pub fn delete_pivot_table(&mut self, name: &str) -> crate::Result<()> {
1268 let idx = self.find_pivot_table_index(name)?;
1269 let pivot = self.pivot_tables.remove(idx);
1270 if let (Some(end_row), Some(end_col)) =
1271 (pivot.last_output_end_row, pivot.last_output_end_col)
1272 && let Some(sheet_idx) = self.sheets.iter().position(|s| s.id == pivot.dest_sheet_id)
1273 {
1274 self.clear_range(sheet_idx, pivot.dest_row, pivot.dest_col, end_row, end_col);
1275 }
1276 Ok(())
1277 }
1278
1279 pub fn rename_pivot_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1281 if !old_name.eq_ignore_ascii_case(new_name) && self.pivot_table_name_taken(new_name) {
1282 return Err(Error::NameTaken {
1283 kind: ObjectKind::PivotTable,
1284 name: new_name.to_string(),
1285 });
1286 }
1287 let idx = self.find_pivot_table_index(old_name)?;
1288 self.pivot_tables[idx].name = new_name.to_string();
1289 Ok(())
1290 }
1291
1292 pub fn add_pivot_field(
1306 &mut self,
1307 pivot_name: &str,
1308 area: PivotArea,
1309 column: &str,
1310 aggregation: Option<PivotAggregation>,
1311 ) -> crate::Result<()> {
1312 let idx = self.find_pivot_table_index(pivot_name)?;
1313 if !matches!(area, PivotArea::Value) {
1314 let pivot = &mut self.pivot_tables[idx];
1315 remove_pivot_field(&mut pivot.row_fields, column);
1316 remove_pivot_field(&mut pivot.col_fields, column);
1317 pivot
1318 .filter_fields
1319 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1320 }
1321 match area {
1322 PivotArea::Row => self.pivot_tables[idx]
1323 .row_fields
1324 .push(PivotField::new(column)),
1325 PivotArea::Column => self.pivot_tables[idx]
1326 .col_fields
1327 .push(PivotField::new(column)),
1328 PivotArea::Value => {
1329 let agg = aggregation.unwrap_or(PivotAggregation::Sum);
1330 self.pivot_tables[idx]
1331 .value_fields
1332 .push(PivotValueField::new(column, agg));
1333 }
1334 PivotArea::Filter => self.pivot_tables[idx]
1335 .filter_fields
1336 .push(PivotFilterField::new(column)),
1337 }
1338 self.refresh_pivot_table(pivot_name)
1339 }
1340
1341 pub fn remove_pivot_field(
1344 &mut self,
1345 pivot_name: &str,
1346 area: PivotArea,
1347 column: &str,
1348 ) -> crate::Result<()> {
1349 let idx = self.find_pivot_table_index(pivot_name)?;
1350 let removed = match area {
1351 PivotArea::Row => remove_pivot_field(&mut self.pivot_tables[idx].row_fields, column),
1352 PivotArea::Column => remove_pivot_field(&mut self.pivot_tables[idx].col_fields, column),
1353 PivotArea::Value => {
1354 let before = self.pivot_tables[idx].value_fields.len();
1355 self.pivot_tables[idx]
1356 .value_fields
1357 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1358 before != self.pivot_tables[idx].value_fields.len()
1359 }
1360 PivotArea::Filter => {
1361 let before = self.pivot_tables[idx].filter_fields.len();
1362 self.pivot_tables[idx]
1363 .filter_fields
1364 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1365 before != self.pivot_tables[idx].filter_fields.len()
1366 }
1367 };
1368 if !removed {
1369 return Err(Error::not_found(
1370 ObjectKind::PivotField,
1371 format!("{column}' in pivot table '{pivot_name}"),
1372 ));
1373 }
1374 self.refresh_pivot_table(pivot_name)
1375 }
1376
1377 pub fn set_pivot_filter(
1380 &mut self,
1381 pivot_name: &str,
1382 column: &str,
1383 values: Option<Vec<String>>,
1384 ) -> crate::Result<()> {
1385 let idx = self.find_pivot_table_index(pivot_name)?;
1386 let field = self.pivot_tables[idx]
1387 .filter_fields
1388 .iter_mut()
1389 .find(|f| f.column.eq_ignore_ascii_case(column))
1390 .ok_or_else(|| {
1391 Error::not_found(
1392 ObjectKind::PivotField,
1393 format!("{column}' on pivot table '{pivot_name}"),
1394 )
1395 })?;
1396 field.selected_values = values;
1397 self.refresh_pivot_table(pivot_name)
1398 }
1399
1400 pub fn refresh_pivot_table(&mut self, pivot_name: &str) -> crate::Result<()> {
1405 let idx = self.find_pivot_table_index(pivot_name)?;
1406 let pivot = self.pivot_tables[idx].clone();
1407 let dest_idx = self
1408 .sheets
1409 .iter()
1410 .position(|s| s.id == pivot.dest_sheet_id)
1411 .ok_or_else(|| {
1412 Error::InvalidArgument(
1413 "pivot table's destination sheet no longer exists".to_string(),
1414 )
1415 })?;
1416
1417 let grid: Option<PivotGrid> = if pivot.value_fields.is_empty() {
1418 None
1419 } else {
1420 let sheet_refs: Vec<&Sheet> = self.sheets.iter().collect();
1421 Some(compute_pivot(&sheet_refs, &pivot).map_err(Error::InvalidArgument)?)
1422 };
1423
1424 if let (Some(old_end_row), Some(old_end_col)) =
1425 (pivot.last_output_end_row, pivot.last_output_end_col)
1426 {
1427 self.clear_range(
1428 dest_idx,
1429 pivot.dest_row,
1430 pivot.dest_col,
1431 old_end_row,
1432 old_end_col,
1433 );
1434 }
1435
1436 let new_bounds = grid.as_ref().map(|grid| {
1437 let height = grid.height();
1438 let width = grid.width.max(1);
1439 self.ensure_capacity(
1440 dest_idx,
1441 pivot.dest_row + height.saturating_sub(1),
1442 pivot.dest_col + width.saturating_sub(1),
1443 );
1444
1445 let mut r = pivot.dest_row;
1446 for (name, state) in &grid.filter_rows {
1447 self.set_cell(dest_idx, r, pivot.dest_col, pivot_label_literal(name));
1448 self.set_cell(dest_idx, r, pivot.dest_col + 1, pivot_label_literal(state));
1449 r += 1;
1450 }
1451 if !grid.filter_rows.is_empty() {
1452 r += 1;
1453 }
1454 for header in &grid.header_rows {
1455 for (c, text) in header.iter().enumerate() {
1456 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(text));
1457 }
1458 r += 1;
1459 }
1460 for body in &grid.body_rows {
1461 for (c, label) in body.row_labels.iter().enumerate() {
1462 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(label));
1463 }
1464 for (c, val) in body.values.iter().enumerate() {
1465 self.set_cell(
1466 dest_idx,
1467 r,
1468 pivot.dest_col + body.row_labels.len() + c,
1469 pivot_value_literal(val),
1470 );
1471 }
1472 r += 1;
1473 }
1474 (
1475 pivot.dest_row + height.saturating_sub(1),
1476 pivot.dest_col + width.saturating_sub(1),
1477 )
1478 });
1479
1480 self.pivot_tables[idx].last_output_end_row = new_bounds.map(|(r, _)| r);
1481 self.pivot_tables[idx].last_output_end_col = new_bounds.map(|(_, c)| c);
1482 self.evaluate()
1483 }
1484
1485 fn clear_range(
1486 &mut self,
1487 sheet_idx: usize,
1488 start_row: usize,
1489 start_col: usize,
1490 end_row: usize,
1491 end_col: usize,
1492 ) {
1493 if sheet_idx >= self.sheets.len() {
1494 return;
1495 }
1496 let (row_count, col_count) = {
1497 let s = &self.sheets[sheet_idx];
1498 (s.row_count(), s.col_count())
1499 };
1500 if row_count == 0 || col_count == 0 {
1501 return;
1502 }
1503 for r in start_row..=end_row.min(row_count - 1) {
1504 for c in start_col..=end_col.min(col_count - 1) {
1505 self.sheets[sheet_idx].set_cell_src(r, c, String::new());
1506 }
1507 }
1508 }
1509}