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 {
35 let offset = (edit.at - table.start_col).min(table.columns.len());
36 for _ in 0..edit.count {
37 table.columns.insert(offset, String::new());
38 }
39 }
40 } else {
41 let first = edit.at.max(table.start_col);
42 let last = (edit.at + edit.count).min(table.end_col + 1);
43 if first < last {
44 let lo = (first - table.start_col).min(table.columns.len());
45 let hi = (last - table.start_col).min(table.columns.len());
46 table.columns.drain(lo..hi);
47 }
48 }
49 table
52 .columns
53 .resize(new_end_col - new_start_col + 1, String::new());
54}
55
56pub struct SheetSummary {
58 pub name: String,
60 pub row_count: usize,
62 pub col_count: usize,
64 pub formula_count: usize,
66}
67
68pub struct WorkbookSummary {
70 pub file_name: String,
72 pub sheet_count: usize,
74 pub chart_count: usize,
76 pub sheets: Vec<SheetSummary>,
78}
79
80pub struct WorkbookManager {
96 pub sheets: Vec<Sheet>,
99 pub charts: Vec<Chart>,
102 pub pivot_tables: Vec<PivotTable>,
105 pub vba_project: Option<VbaProject>,
107 pub locale: Locale,
109}
110
111fn pivot_label_literal(text: &str) -> String {
115 if text.is_empty() {
116 String::new()
117 } else if text.starts_with('=')
118 || text.parse::<f64>().is_ok()
119 || text.eq_ignore_ascii_case("true")
120 || text.eq_ignore_ascii_case("false")
121 {
122 format!("\"{}\"", text)
123 } else {
124 text.to_string()
125 }
126}
127
128fn pivot_value_literal(v: &ResultData) -> String {
132 match v {
133 ResultData::Error(e) => e.clone(),
134 other => other.to_string(),
135 }
136}
137
138fn remove_pivot_field(fields: &mut Vec<PivotField>, column: &str) -> bool {
139 let before = fields.len();
140 fields.retain(|f| !f.column.eq_ignore_ascii_case(column));
141 before != fields.len()
142}
143
144impl WorkbookManager {
145 pub fn load_bytes(buffer: &[u8]) -> crate::Result<Self> {
147 let (imported_sheets, charts, pivot_tables, vba_project) =
148 import_xlsx_data(buffer, &[], |_, _, _| {})?;
149
150 let locale = Locale::default();
151 let mut sheets: Vec<Sheet> = imported_sheets.into_iter().map(|it| it.sheet).collect();
152 for sheet in &mut sheets {
153 sheet.locale = locale.clone();
154 }
155 Ok(Self {
156 sheets,
157 charts,
158 pivot_tables,
159 vba_project,
160 locale,
161 })
162 }
163
164 pub fn save_bytes(&self) -> crate::Result<Vec<u8>> {
170 export_xlsx_data(
171 &self.sheets,
172 &self.charts,
173 &self.pivot_tables,
174 self.vba_project.as_ref(),
175 )
176 }
177
178 pub fn new_empty() -> crate::Result<Self> {
180 let locale = Locale::default();
181 let mut wb = Self {
182 sheets: Vec::new(),
183 charts: Vec::new(),
184 pivot_tables: Vec::new(),
185 vba_project: None,
186 locale,
187 };
188 wb.add_sheet("Sheet1")?;
189 Ok(wb)
190 }
191
192 pub fn set_locale(&mut self, locale: Locale) {
194 self.locale = locale.clone();
195 for sheet in &mut self.sheets {
196 sheet.locale = locale.clone();
197 }
198 }
199
200 pub fn evaluate(&mut self) -> crate::Result<()> {
202 if self.sheets.is_empty() {
203 return Ok(());
204 }
205
206 let sheet_order: Vec<String> = self.sheets.iter().map(|s| s.name.clone()).collect();
210
211 for _pass in 0..3 {
217 for sheet in &mut self.sheets {
218 sheet.mark_all_dirty();
219 }
220 for i in 0..self.sheets.len() {
221 let (left, right) = self.sheets.split_at_mut(i);
222 let (target_sheet, right_tail) = right.split_first_mut().unwrap();
223
224 let mut context = Context::new();
225 for s in left.iter() {
226 context.add_table(s.name.clone(), s);
227 }
228 for s in right_tail.iter() {
229 context.add_table(s.name.clone(), s);
230 }
231 context.pivot_tables = &self.pivot_tables;
232 context.sheet_order = sheet_order.clone();
233
234 let _ = target_sheet.commit(Some(&context));
235 }
236 }
237
238 Ok(())
239 }
240
241 pub(crate) fn call_worksheet_function(
249 &self,
250 name: &str,
251 args: &[crate::core::parser::Expr],
252 ) -> Result<ResultData, crate::core::EngineError> {
253 let Some(host) = self.sheets.first() else {
254 return Err(crate::core::EngineError::EvalError(
255 crate::core::EvalError::UnknownFunction("no worksheets".to_string()),
256 ));
257 };
258 let mut context = Context::new();
259 for s in &self.sheets {
260 context.add_table(s.name.clone(), s);
261 }
262 context.pivot_tables = &self.pivot_tables;
263 context.sheet_order = self.sheets.iter().map(|s| s.name.clone()).collect();
264 host.call_worksheet_function(name, args, Some(&context))
265 }
266
267 pub fn find_sheet_index(&self, name_opt: Option<&str>) -> crate::Result<usize> {
269 if self.sheets.is_empty() {
270 return Err(Error::EmptyWorkbook);
271 }
272
273 match name_opt {
274 Some(name) => {
275 if let Some(idx) = self
276 .sheets
277 .iter()
278 .position(|s| s.name.eq_ignore_ascii_case(name))
279 {
280 Ok(idx)
281 } else {
282 let available: Vec<String> =
283 self.sheets.iter().map(|s| s.name.clone()).collect();
284 Err(Error::not_found_among(
285 ObjectKind::Sheet,
286 name.to_string(),
287 available,
288 ))
289 }
290 }
291 None => Ok(0),
292 }
293 }
294
295 pub fn get_summary(&self, file_name: &str) -> WorkbookSummary {
297 let sheet_summaries = self
298 .sheets
299 .iter()
300 .map(|sheet| {
301 let row_count = sheet.row_count();
302 let col_count = sheet.col_count();
303 let mut formula_count = 0;
304
305 for col in &sheet.columns {
306 for src in &col.src {
307 if src.starts_with('=') {
308 formula_count += 1;
309 }
310 }
311 }
312
313 SheetSummary {
314 name: sheet.name.clone(),
315 row_count,
316 col_count,
317 formula_count,
318 }
319 })
320 .collect();
321
322 WorkbookSummary {
323 file_name: file_name.to_string(),
324 sheet_count: self.sheets.len(),
325 chart_count: self.charts.len(),
326 sheets: sheet_summaries,
327 }
328 }
329
330 pub fn ensure_capacity(&mut self, sheet_idx: usize, target_row: usize, target_col: usize) {
332 if sheet_idx >= self.sheets.len() {
333 return;
334 }
335 self.sheets[sheet_idx].ensure_capacity(target_row, target_col);
336 }
337
338 pub fn set_cell_style(
345 &mut self,
346 sheet_name: Option<&str>,
347 row: usize,
348 col: usize,
349 style: crate::core::CellStyle,
350 ) -> crate::Result<()> {
351 let sheet_idx = self.find_sheet_index(sheet_name)?;
352 self.sheets[sheet_idx].update_cell_style(row, col, |s| s.merge(&style));
353 Ok(())
354 }
355
356 pub fn set_range_style(
358 &mut self,
359 sheet_name: Option<&str>,
360 start_row: usize,
361 start_col: usize,
362 end_row: usize,
363 end_col: usize,
364 style: crate::core::CellStyle,
365 ) -> crate::Result<()> {
366 if end_row < start_row || end_col < start_col {
367 return Err(Error::InvalidRange(
368 "range end must not precede its start".to_string(),
369 ));
370 }
371 let sheet_idx = self.find_sheet_index(sheet_name)?;
372 for r in start_row..=end_row {
373 for c in start_col..=end_col {
374 self.sheets[sheet_idx].update_cell_style(r, c, |s| s.merge(&style));
375 }
376 }
377 Ok(())
378 }
379
380 pub fn get_cell_style(
382 &self,
383 sheet_name: Option<&str>,
384 row: usize,
385 col: usize,
386 ) -> crate::Result<Option<crate::core::CellStyle>> {
387 let sheet_idx = self.find_sheet_index(sheet_name)?;
388 Ok(self.sheets[sheet_idx].get_cell_style(row, col).cloned())
389 }
390
391 pub fn set_table_style(&mut self, table_name: &str, style_name: &str) -> crate::Result<()> {
398 for sheet in &mut self.sheets {
399 for table in &mut sheet.tables {
400 if table.name.eq_ignore_ascii_case(table_name) {
401 table.set_style_name(Some(style_name.to_string()));
402 return Ok(());
403 }
404 }
405 }
406 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
407 }
408
409 pub fn get_table_style(&self, table_name: &str) -> crate::Result<Option<String>> {
415 for sheet in &self.sheets {
416 for table in &sheet.tables {
417 if table.name.eq_ignore_ascii_case(table_name) {
418 return Ok(table.style_name.clone());
419 }
420 }
421 }
422 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
423 }
424
425 pub fn set_cell(&mut self, sheet_idx: usize, row: usize, col: usize, value: String) {
427 self.ensure_capacity(sheet_idx, row, col);
428 let sheet = &mut self.sheets[sheet_idx];
429 sheet.set_cell_src(row, col, value);
430 }
431
432 pub fn set_cell_with_type(
434 &mut self,
435 sheet_idx: usize,
436 row: usize,
437 col: usize,
438 value: String,
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_with_type(row, col, value, cell_type);
444 }
445
446 pub fn set_cell_type(
448 &mut self,
449 sheet_idx: usize,
450 row: usize,
451 col: usize,
452 cell_type: crate::core::CellType,
453 ) {
454 self.ensure_capacity(sheet_idx, row, col);
455 let sheet = &mut self.sheets[sheet_idx];
456 sheet.set_cell_type(row, col, cell_type);
457 }
458
459 pub fn get_cell_type(&self, sheet_idx: usize, row: usize, col: usize) -> crate::core::CellType {
461 if let Some(sheet) = self.sheets.get(sheet_idx) {
462 sheet.get_cell_type(&crate::core::CellRef::new(row, col))
463 } else {
464 crate::core::CellType::Empty
465 }
466 }
467
468 pub fn insert_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
474 let sheet = &self.sheets[sheet_idx];
475 let at = row_idx.min(sheet.row_count());
478 let edit = GridEdit::insert_row(sheet.id, at);
479 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_row(at));
480 self.evaluate()
481 }
482
483 pub fn delete_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
488 let sheet = &self.sheets[sheet_idx];
489 if row_idx >= sheet.row_count() {
490 return Err(Error::OutOfBounds {
491 what: "row",
492 index: row_idx,
493 len: sheet.row_count(),
494 });
495 }
496 let edit = GridEdit::delete_row(sheet.id, row_idx);
497 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].delete_row(row_idx));
498 self.evaluate()
499 }
500
501 pub fn insert_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
503 let sheet = &self.sheets[sheet_idx];
504 let at = col_idx.min(sheet.col_count());
505 let edit = GridEdit::insert_col(sheet.id, at);
506 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_col(at));
507 self.evaluate()
508 }
509
510 pub fn delete_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
512 let sheet = &self.sheets[sheet_idx];
513 if col_idx >= sheet.col_count() {
514 return Err(Error::OutOfBounds {
515 what: "column",
516 index: col_idx,
517 len: sheet.col_count(),
518 });
519 }
520 let deleted_col_ids = vec![sheet.columns()[col_idx].id];
524 let edit = GridEdit::delete_col(sheet.id, col_idx);
525 self.apply_grid_edit(edit, &deleted_col_ids, |wb| {
526 wb.sheets[sheet_idx].delete_col(col_idx)
527 });
528 self.evaluate()
529 }
530
531 pub fn insert_cells_shift_down(
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, true);
548 self.apply_grid_edit(edit, &[], |wb| {
549 wb.sheets[sheet_idx].insert_cells_shift_down(row, first_col, last_col, count)
550 });
551 self.evaluate()
552 }
553
554 pub fn delete_cells_shift_up(
557 &mut self,
558 sheet_idx: usize,
559 row: usize,
560 first_col: usize,
561 last_col: usize,
562 count: usize,
563 ) -> crate::Result<()> {
564 let sheet = &self.sheets[sheet_idx];
565 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, false);
566 self.apply_grid_edit(edit, &[], |wb| {
567 wb.sheets[sheet_idx].delete_cells_shift_up(row, first_col, last_col, count)
568 });
569 self.evaluate()
570 }
571
572 fn apply_grid_edit(
590 &mut self,
591 edit: GridEdit,
592 deleted_col_ids: &[u64],
593 apply: impl FnOnce(&mut Self),
594 ) {
595 let mut shifted: Vec<(usize, usize, usize, CompiledFormula)> = Vec::new();
599 for (sheet_idx, sheet) in self.sheets.iter().enumerate() {
600 for (col_idx, column) in sheet.columns().iter().enumerate() {
601 for row_idx in 0..column.len() {
602 let Some(src) = column.src(row_idx).filter(|s| s.starts_with('=')) else {
603 continue;
604 };
605 let compiled = crate::core::parser::compile_formula(src, &self.sheets);
606 if let Some(next) =
607 crate::core::grid_edit::shift_formula(&compiled, &edit, deleted_col_ids)
608 {
609 shifted.push((sheet_idx, col_idx, row_idx, next));
610 }
611 }
612 }
613 }
614
615 apply(self);
617 self.shift_table_and_pivot_ranges(&edit);
618
619 for (sheet_idx, col_idx, row_idx, compiled) in shifted {
621 let Some((row, col)) = self.moved_cell(&edit, sheet_idx, row_idx, col_idx) else {
622 continue;
624 };
625 let text = crate::core::parser::serialize_formula(&compiled, &self.sheets);
626 self.sheets[sheet_idx].set_cell_src(row, col, text);
627 }
628 }
629
630 fn moved_cell(
633 &self,
634 edit: &GridEdit,
635 sheet_idx: usize,
636 row: usize,
637 col: usize,
638 ) -> Option<(usize, usize)> {
639 if self.sheets[sheet_idx].id != edit.sheet_id || !edit.covers_columns(col, col) {
644 return Some((row, col));
645 }
646 let moved = |index: usize| {
647 crate::core::grid_edit::shift_point(index, edit.at, edit.count, edit.insert)
648 };
649 match edit.axis {
650 Axis::Row => Some((moved(row)?, col)),
651 Axis::Col => Some((row, moved(col)?)),
652 }
653 }
654
655 fn shift_table_and_pivot_ranges(&mut self, edit: &GridEdit) {
661 use crate::core::grid_edit::{shift_point, shift_rect};
662
663 for sheet in &mut self.sheets {
664 if sheet.id != edit.sheet_id {
665 continue;
666 }
667 sheet.tables.retain_mut(|table| {
668 if !edit.covers_columns(table.start_col, table.end_col) {
671 return true;
672 }
673 match shift_rect(
674 edit,
675 table.start_row,
676 table.start_col,
677 table.end_row,
678 table.end_col,
679 ) {
680 Some((r0, c0, r1, c1)) => {
681 if edit.axis == Axis::Col {
685 resize_table_columns(table, c0, c1, edit);
686 }
687 table.start_row = r0;
688 table.start_col = c0;
689 table.end_row = r1;
690 table.end_col = c1;
691 true
692 }
693 None => false,
694 }
695 });
696 }
697
698 for pivot in &mut self.pivot_tables {
699 if let PivotSource::Range {
700 sheet_id,
701 start_row,
702 start_col,
703 end_row,
704 end_col,
705 } = &mut pivot.source
706 && *sheet_id == edit.sheet_id
707 && edit.covers_columns(*start_col, *end_col)
708 && let Some((r0, c0, r1, c1)) =
709 shift_rect(edit, *start_row, *start_col, *end_row, *end_col)
710 {
711 *start_row = r0;
712 *start_col = c0;
713 *end_row = r1;
714 *end_col = c1;
715 }
716
717 if pivot.dest_sheet_id == edit.sheet_id
718 && edit.covers_columns(pivot.dest_col, pivot.dest_col)
719 {
720 match edit.axis {
725 Axis::Row => {
726 pivot.dest_row =
727 shift_point(pivot.dest_row, edit.at, edit.count, edit.insert)
728 .unwrap_or(edit.at);
729 }
730 Axis::Col => {
731 pivot.dest_col =
732 shift_point(pivot.dest_col, edit.at, edit.count, edit.insert)
733 .unwrap_or(edit.at);
734 }
735 }
736 pivot.last_output_end_row = None;
740 pivot.last_output_end_col = None;
741 }
742 }
743 }
744
745 pub fn add_sheet(&mut self, name: &str) -> crate::Result<()> {
747 if self
748 .sheets
749 .iter()
750 .any(|s| s.name.eq_ignore_ascii_case(name))
751 {
752 return Err(Error::AlreadyExists {
753 kind: ObjectKind::Sheet,
754 name: name.to_string(),
755 });
756 }
757
758 let mut columns = Vec::new();
759 for col_idx in 0..5 {
760 let mut col = DataColumn::new(10);
761 col.id = generate_unique_id();
762 col.name = col_idx_to_letters(col_idx);
763 columns.push(col);
764 }
765
766 let new_sheet = Sheet {
767 id: generate_unique_id(),
768 name: name.to_string(),
769 columns,
770 tables: Vec::new(),
771 dependencies: std::collections::HashMap::new(),
772 dependencies_rev: std::collections::HashMap::new(),
773 uncommitted_actions: Vec::new(),
774 locale: self.locale.clone(),
775 };
776
777 self.sheets.push(new_sheet);
778 Ok(())
779 }
780
781 pub fn delete_sheet(&mut self, name: &str) -> crate::Result<()> {
783 let idx = self.find_sheet_index(Some(name))?;
784 if self.sheets.len() <= 1 {
785 return Err(Error::LastSheetInWorkbook);
786 }
787 self.sheets.remove(idx);
788 Ok(())
789 }
790
791 pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
793 let idx = self.find_sheet_index(Some(old_name))?;
794 if self
795 .sheets
796 .iter()
797 .enumerate()
798 .any(|(i, s)| i != idx && s.name.eq_ignore_ascii_case(new_name))
799 {
800 return Err(Error::NameTaken {
801 kind: ObjectKind::Sheet,
802 name: new_name.to_string(),
803 });
804 }
805 self.sheets[idx].name = new_name.to_string();
806 Ok(())
807 }
808
809 #[allow(clippy::too_many_arguments)]
811 pub fn add_chart(
812 &mut self,
813 sheet_name: &str,
814 chart_type: ChartType,
815 range: String,
816 title: Option<String>,
817 anchor: Option<(usize, usize)>,
818 ) -> crate::Result<u64> {
819 let _ = self.find_sheet_index(Some(sheet_name))?;
820 let id = generate_unique_id();
821 let name = format!("Chart {}", self.charts.len() + 1);
822 let (anchor_row, anchor_col) = anchor.unwrap_or((0, 0));
823
824 let chart = Chart {
825 id,
826 name,
827 chart_type,
828 data_range: range,
829 title,
830 xlabel: None,
831 ylabel: None,
832 show_legend: true,
833 anchor_row,
834 anchor_col,
835 };
836
837 self.charts.push(chart);
838 Ok(id)
839 }
840
841 #[allow(clippy::too_many_arguments)]
846 pub fn edit_chart(
847 &mut self,
848 id: u64,
849 name: Option<String>,
850 chart_type: Option<ChartType>,
851 data_range: Option<String>,
852 title: Option<Option<String>>,
853 xlabel: Option<Option<String>>,
854 ylabel: Option<Option<String>>,
855 show_legend: Option<bool>,
856 anchor: Option<(usize, usize)>,
857 ) -> crate::Result<()> {
858 let chart = self
859 .charts
860 .iter_mut()
861 .find(|c| c.id == id)
862 .ok_or_else(|| Error::not_found(ObjectKind::Chart, id.to_string()))?;
863 if let Some(name) = name {
864 chart.name = name;
865 }
866 if let Some(chart_type) = chart_type {
867 chart.chart_type = chart_type;
868 }
869 if let Some(data_range) = data_range {
870 chart.data_range = data_range;
871 }
872 if let Some(title) = title {
873 chart.title = title;
874 }
875 if let Some(xlabel) = xlabel {
876 chart.xlabel = xlabel;
877 }
878 if let Some(ylabel) = ylabel {
879 chart.ylabel = ylabel;
880 }
881 if let Some(show_legend) = show_legend {
882 chart.show_legend = show_legend;
883 }
884 if let Some((anchor_row, anchor_col)) = anchor {
885 chart.anchor_row = anchor_row;
886 chart.anchor_col = anchor_col;
887 }
888 Ok(())
889 }
890
891 pub fn has_vba_project(&self) -> bool {
893 self.vba_project.is_some()
894 }
895
896 pub fn list_vba_modules(&self) -> Vec<&VbaModule> {
898 self.vba_project
899 .as_ref()
900 .map(|p| p.modules.iter().collect())
901 .unwrap_or_default()
902 }
903
904 pub fn ensure_vba_project(&mut self) -> crate::Result<()> {
908 if self.vba_project.is_some() {
909 return Ok(());
910 }
911 self.vba_project = Some(VbaProject::new_empty());
912 Ok(())
913 }
914
915 pub fn add_vba_module(
925 &mut self,
926 name: String,
927 kind: VbaModuleKind,
928 source: String,
929 bound_sheet_id: Option<u64>,
930 ) -> crate::Result<()> {
931 validate_vba_module_name(&name).map_err(|reason| Error::InvalidName {
932 kind: ObjectKind::VbaModule,
933 name: name.clone(),
934 reason,
935 })?;
936 let is_this_workbook = kind == VbaModuleKind::Document && name == "ThisWorkbook";
937 if kind == VbaModuleKind::Document && !is_this_workbook {
938 let sheet_id = bound_sheet_id
939 .ok_or_else(|| Error::Vba("document modules require a bound sheet".to_string()))?;
940 if !self.sheets.iter().any(|s| s.id == sheet_id) {
941 return Err(Error::not_found(ObjectKind::Sheet, sheet_id.to_string()));
942 }
943 }
944 self.ensure_vba_project()?;
945 let project = self.vba_project.as_mut().unwrap();
946 if project.module_name_taken(&name) {
947 return Err(Error::AlreadyExists {
948 kind: ObjectKind::VbaModule,
949 name: name.to_string(),
950 });
951 }
952 if kind == VbaModuleKind::Document
953 && bound_sheet_id.is_some()
954 && project
955 .modules
956 .iter()
957 .any(|m| m.kind == VbaModuleKind::Document && m.bound_sheet_id == bound_sheet_id)
958 {
959 return Err(Error::DocumentModuleExists);
960 }
961 let prefix_bytes = project
969 .modules
970 .first()
971 .map(|m| m.prefix_bytes.clone())
972 .unwrap_or_else(|| project.seed_prefix_bytes.clone());
973 let module_cookie = project
974 .modules
975 .first()
976 .map(|m| m.module_cookie)
977 .unwrap_or(project.seed_module_cookie);
978 let stored_bound_sheet_id = if kind == VbaModuleKind::Document && !is_this_workbook {
979 bound_sheet_id
980 } else {
981 None
982 };
983 project.modules.push(VbaModule {
984 name,
985 kind,
986 source,
987 bound_sheet_id: stored_bound_sheet_id,
988 prefix_bytes,
989 module_cookie,
990 cached_compressed_source: None,
992 });
993 Ok(())
994 }
995
996 pub fn remove_vba_module(&mut self, name: &str) -> crate::Result<()> {
1003 let project = self
1004 .vba_project
1005 .as_mut()
1006 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1007 let before = project.modules.len();
1008 project
1009 .modules
1010 .retain(|m| !m.name.eq_ignore_ascii_case(name));
1011 if project.modules.len() == before {
1012 return Err(Error::not_found(ObjectKind::VbaModule, name.to_string()));
1013 }
1014 Ok(())
1015 }
1016
1017 pub fn rename_vba_module(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1030 validate_vba_module_name(new_name).map_err(|reason| Error::InvalidName {
1031 kind: ObjectKind::VbaModule,
1032 name: new_name.to_string(),
1033 reason,
1034 })?;
1035 let project = self
1036 .vba_project
1037 .as_mut()
1038 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1039 if !old_name.eq_ignore_ascii_case(new_name) && project.module_name_taken(new_name) {
1040 return Err(Error::AlreadyExists {
1041 kind: ObjectKind::VbaModule,
1042 name: new_name.to_string(),
1043 });
1044 }
1045 let module = project
1046 .find_module_mut(old_name)
1047 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, old_name))?;
1048 module.name = new_name.to_string();
1049 Ok(())
1050 }
1051
1052 pub fn set_vba_module_source(&mut self, name: &str, source: String) -> crate::Result<()> {
1063 let project = self
1064 .vba_project
1065 .as_mut()
1066 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1067 let module = project
1068 .find_module_mut(name)
1069 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, name))?;
1070 module.source = source;
1071 module.cached_compressed_source = None;
1075 Ok(())
1076 }
1077
1078 pub fn delete_chart(&mut self, id: u64) -> crate::Result<()> {
1080 if let Some(pos) = self.charts.iter().position(|c| c.id == id) {
1081 self.charts.remove(pos);
1082 Ok(())
1083 } else {
1084 Err(Error::not_found(ObjectKind::Chart, id.to_string()))
1085 }
1086 }
1087
1088 pub fn find_table(&self, name: &str) -> Option<(&Sheet, &ExcelTable)> {
1091 self.sheets
1092 .iter()
1093 .find_map(|s| s.find_table(name).map(|t| (s, t)))
1094 }
1095
1096 pub fn list_tables(&self) -> Vec<(&str, &ExcelTable)> {
1099 self.sheets
1100 .iter()
1101 .flat_map(|s| s.tables.iter().map(move |t| (s.name.as_str(), t)))
1102 .collect()
1103 }
1104
1105 fn find_table_sheet_index(&self, name: &str) -> crate::Result<usize> {
1106 self.sheets
1107 .iter()
1108 .position(|s| s.find_table(name).is_some())
1109 .ok_or_else(|| Error::not_found(ObjectKind::Table, name))
1110 }
1111
1112 fn table_name_taken(&self, name: &str) -> bool {
1113 self.sheets
1114 .iter()
1115 .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(name)))
1116 }
1117
1118 #[allow(clippy::too_many_arguments)]
1122 pub fn add_table(
1123 &mut self,
1124 sheet_name: Option<&str>,
1125 name: &str,
1126 start_row: usize,
1127 start_col: usize,
1128 end_row: usize,
1129 end_col: usize,
1130 has_header_row: bool,
1131 has_totals_row: bool,
1132 ) -> crate::Result<u64> {
1133 if self.table_name_taken(name) {
1134 return Err(Error::AlreadyExists {
1135 kind: ObjectKind::Table,
1136 name: name.to_string(),
1137 });
1138 }
1139 let idx = self.find_sheet_index(sheet_name)?;
1140 self.sheets[idx]
1141 .add_table(
1142 name.to_string(),
1143 start_row,
1144 start_col,
1145 end_row,
1146 end_col,
1147 has_header_row,
1148 has_totals_row,
1149 )
1150 .map_err(Error::InvalidArgument)
1151 }
1152
1153 pub fn delete_table(&mut self, name: &str) -> crate::Result<()> {
1155 let idx = self.find_table_sheet_index(name)?;
1156 self.sheets[idx]
1157 .delete_table_by_name(name)
1158 .map_err(Error::InvalidArgument)
1159 }
1160
1161 pub fn rename_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1163 if !old_name.eq_ignore_ascii_case(new_name) && self.table_name_taken(new_name) {
1164 return Err(Error::NameTaken {
1165 kind: ObjectKind::Table,
1166 name: new_name.to_string(),
1167 });
1168 }
1169 let idx = self.find_table_sheet_index(old_name)?;
1170 self.sheets[idx]
1171 .rename_table(old_name, new_name)
1172 .map_err(Error::InvalidArgument)?;
1173 self.rewrite_table_references(old_name, Some(new_name), None);
1177 self.evaluate()
1178 }
1179
1180 fn rewrite_table_references(
1185 &mut self,
1186 table_name: &str,
1187 new_table_name: Option<&str>,
1188 col_rename: Option<(&str, &str)>,
1189 ) {
1190 for sheet in &mut self.sheets {
1191 for col_idx in 0..sheet.columns.len() {
1192 let row_count = sheet.columns[col_idx].src.len();
1193 for row_idx in 0..row_count {
1194 let src = sheet.columns[col_idx].src[row_idx].clone();
1195 if let Some(new_src) = crate::core::parser::rewrite_structured_table_reference(
1196 &src,
1197 table_name,
1198 new_table_name,
1199 col_rename,
1200 ) {
1201 sheet.set_cell_src(row_idx, col_idx, new_src);
1202 }
1203 }
1204 }
1205 }
1206 }
1207
1208 pub fn resize_table(
1210 &mut self,
1211 name: &str,
1212 new_end_row: usize,
1213 new_end_col: usize,
1214 ) -> crate::Result<()> {
1215 let idx = self.find_table_sheet_index(name)?;
1216 self.sheets[idx]
1217 .resize_table(name, new_end_row, new_end_col)
1218 .map_err(Error::InvalidArgument)
1219 }
1220
1221 pub fn rename_table_column(
1223 &mut self,
1224 table_name: &str,
1225 col_index: usize,
1226 new_name: &str,
1227 ) -> crate::Result<()> {
1228 let idx = self.find_table_sheet_index(table_name)?;
1229 let old_col_name = self.sheets[idx]
1230 .find_table(table_name)
1231 .and_then(|t| t.columns.get(col_index).cloned())
1232 .ok_or_else(|| {
1233 Error::InvalidArgument(format!(
1234 "column index {col_index} out of bounds for table '{table_name}'"
1235 ))
1236 })?;
1237 self.sheets[idx]
1238 .rename_table_column(table_name, col_index, new_name)
1239 .map_err(Error::InvalidArgument)?;
1240 self.rewrite_table_references(table_name, None, Some((&old_col_name, new_name)));
1243 self.evaluate()
1244 }
1245
1246 pub fn find_pivot_table(&self, name: &str) -> Option<&PivotTable> {
1248 self.pivot_tables
1249 .iter()
1250 .find(|p| p.name.eq_ignore_ascii_case(name))
1251 }
1252
1253 fn find_pivot_table_index(&self, name: &str) -> crate::Result<usize> {
1254 self.pivot_tables
1255 .iter()
1256 .position(|p| p.name.eq_ignore_ascii_case(name))
1257 .ok_or_else(|| Error::not_found(ObjectKind::PivotTable, name))
1258 }
1259
1260 pub fn list_pivot_tables(&self) -> &[PivotTable] {
1262 &self.pivot_tables
1263 }
1264
1265 fn pivot_table_name_taken(&self, name: &str) -> bool {
1266 self.pivot_tables
1267 .iter()
1268 .any(|p| p.name.eq_ignore_ascii_case(name))
1269 }
1270
1271 #[allow(clippy::too_many_arguments)]
1275 pub fn add_pivot_table_from_table(
1276 &mut self,
1277 name: &str,
1278 source_table_name: &str,
1279 dest_sheet_name: Option<&str>,
1280 dest_row: usize,
1281 dest_col: usize,
1282 grand_totals_row: bool,
1283 grand_totals_col: bool,
1284 ) -> crate::Result<u64> {
1285 if self.pivot_table_name_taken(name) {
1286 return Err(Error::AlreadyExists {
1287 kind: ObjectKind::PivotTable,
1288 name: name.to_string(),
1289 });
1290 }
1291 self.find_table(source_table_name)
1292 .ok_or_else(|| Error::not_found(ObjectKind::Table, source_table_name))?;
1293 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1294 let id = generate_unique_id();
1295 self.pivot_tables.push(PivotTable {
1296 id,
1297 name: name.to_string(),
1298 source: PivotSource::Table {
1299 name: source_table_name.to_string(),
1300 },
1301 dest_sheet_id: self.sheets[dest_idx].id,
1302 dest_row,
1303 dest_col,
1304 row_fields: Vec::new(),
1305 col_fields: Vec::new(),
1306 value_fields: Vec::new(),
1307 filter_fields: Vec::new(),
1308 grand_totals_row,
1309 grand_totals_col,
1310 last_output_end_row: None,
1311 last_output_end_col: None,
1312 });
1313 self.refresh_pivot_table(name)?;
1314 Ok(id)
1315 }
1316
1317 #[allow(clippy::too_many_arguments)]
1320 pub fn add_pivot_table_from_range(
1321 &mut self,
1322 name: &str,
1323 source_sheet_name: Option<&str>,
1324 start_row: usize,
1325 start_col: usize,
1326 end_row: usize,
1327 end_col: usize,
1328 dest_sheet_name: Option<&str>,
1329 dest_row: usize,
1330 dest_col: usize,
1331 grand_totals_row: bool,
1332 grand_totals_col: bool,
1333 ) -> crate::Result<u64> {
1334 if self.pivot_table_name_taken(name) {
1335 return Err(Error::AlreadyExists {
1336 kind: ObjectKind::PivotTable,
1337 name: name.to_string(),
1338 });
1339 }
1340 let src_idx = self.find_sheet_index(source_sheet_name)?;
1341 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1342 let id = generate_unique_id();
1343 self.pivot_tables.push(PivotTable {
1344 id,
1345 name: name.to_string(),
1346 source: PivotSource::Range {
1347 sheet_id: self.sheets[src_idx].id,
1348 start_row,
1349 start_col,
1350 end_row,
1351 end_col,
1352 },
1353 dest_sheet_id: self.sheets[dest_idx].id,
1354 dest_row,
1355 dest_col,
1356 row_fields: Vec::new(),
1357 col_fields: Vec::new(),
1358 value_fields: Vec::new(),
1359 filter_fields: Vec::new(),
1360 grand_totals_row,
1361 grand_totals_col,
1362 last_output_end_row: None,
1363 last_output_end_col: None,
1364 });
1365 self.refresh_pivot_table(name)?;
1366 Ok(id)
1367 }
1368
1369 pub fn delete_pivot_table(&mut self, name: &str) -> crate::Result<()> {
1372 let idx = self.find_pivot_table_index(name)?;
1373 let pivot = self.pivot_tables.remove(idx);
1374 if let (Some(end_row), Some(end_col)) =
1375 (pivot.last_output_end_row, pivot.last_output_end_col)
1376 && let Some(sheet_idx) = self.sheets.iter().position(|s| s.id == pivot.dest_sheet_id)
1377 {
1378 self.clear_range(sheet_idx, pivot.dest_row, pivot.dest_col, end_row, end_col);
1379 }
1380 Ok(())
1381 }
1382
1383 pub fn rename_pivot_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1385 if !old_name.eq_ignore_ascii_case(new_name) && self.pivot_table_name_taken(new_name) {
1386 return Err(Error::NameTaken {
1387 kind: ObjectKind::PivotTable,
1388 name: new_name.to_string(),
1389 });
1390 }
1391 let idx = self.find_pivot_table_index(old_name)?;
1392 self.pivot_tables[idx].name = new_name.to_string();
1393 Ok(())
1394 }
1395
1396 pub fn add_pivot_field(
1410 &mut self,
1411 pivot_name: &str,
1412 area: PivotArea,
1413 column: &str,
1414 aggregation: Option<PivotAggregation>,
1415 ) -> crate::Result<()> {
1416 let idx = self.find_pivot_table_index(pivot_name)?;
1417 if !matches!(area, PivotArea::Value) {
1418 let pivot = &mut self.pivot_tables[idx];
1419 remove_pivot_field(&mut pivot.row_fields, column);
1420 remove_pivot_field(&mut pivot.col_fields, column);
1421 pivot
1422 .filter_fields
1423 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1424 }
1425 match area {
1426 PivotArea::Row => self.pivot_tables[idx]
1427 .row_fields
1428 .push(PivotField::new(column)),
1429 PivotArea::Column => self.pivot_tables[idx]
1430 .col_fields
1431 .push(PivotField::new(column)),
1432 PivotArea::Value => {
1433 let agg = aggregation.unwrap_or(PivotAggregation::Sum);
1434 self.pivot_tables[idx]
1435 .value_fields
1436 .push(PivotValueField::new(column, agg));
1437 }
1438 PivotArea::Filter => self.pivot_tables[idx]
1439 .filter_fields
1440 .push(PivotFilterField::new(column)),
1441 }
1442 self.refresh_pivot_table(pivot_name)
1443 }
1444
1445 pub fn remove_pivot_field(
1448 &mut self,
1449 pivot_name: &str,
1450 area: PivotArea,
1451 column: &str,
1452 ) -> crate::Result<()> {
1453 let idx = self.find_pivot_table_index(pivot_name)?;
1454 let removed = match area {
1455 PivotArea::Row => remove_pivot_field(&mut self.pivot_tables[idx].row_fields, column),
1456 PivotArea::Column => remove_pivot_field(&mut self.pivot_tables[idx].col_fields, column),
1457 PivotArea::Value => {
1458 let before = self.pivot_tables[idx].value_fields.len();
1459 self.pivot_tables[idx]
1460 .value_fields
1461 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1462 before != self.pivot_tables[idx].value_fields.len()
1463 }
1464 PivotArea::Filter => {
1465 let before = self.pivot_tables[idx].filter_fields.len();
1466 self.pivot_tables[idx]
1467 .filter_fields
1468 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1469 before != self.pivot_tables[idx].filter_fields.len()
1470 }
1471 };
1472 if !removed {
1473 return Err(Error::not_found(
1474 ObjectKind::PivotField,
1475 format!("{column}' in pivot table '{pivot_name}"),
1476 ));
1477 }
1478 self.refresh_pivot_table(pivot_name)
1479 }
1480
1481 pub fn set_pivot_filter(
1484 &mut self,
1485 pivot_name: &str,
1486 column: &str,
1487 values: Option<Vec<String>>,
1488 ) -> crate::Result<()> {
1489 let idx = self.find_pivot_table_index(pivot_name)?;
1490 let field = self.pivot_tables[idx]
1491 .filter_fields
1492 .iter_mut()
1493 .find(|f| f.column.eq_ignore_ascii_case(column))
1494 .ok_or_else(|| {
1495 Error::not_found(
1496 ObjectKind::PivotField,
1497 format!("{column}' on pivot table '{pivot_name}"),
1498 )
1499 })?;
1500 field.selected_values = values;
1501 self.refresh_pivot_table(pivot_name)
1502 }
1503
1504 pub fn refresh_pivot_table(&mut self, pivot_name: &str) -> crate::Result<()> {
1509 let idx = self.find_pivot_table_index(pivot_name)?;
1510 let pivot = self.pivot_tables[idx].clone();
1511 let dest_idx = self
1512 .sheets
1513 .iter()
1514 .position(|s| s.id == pivot.dest_sheet_id)
1515 .ok_or_else(|| {
1516 Error::InvalidArgument(
1517 "pivot table's destination sheet no longer exists".to_string(),
1518 )
1519 })?;
1520
1521 let grid: Option<PivotGrid> = if pivot.value_fields.is_empty() {
1522 None
1523 } else {
1524 let sheet_refs: Vec<&Sheet> = self.sheets.iter().collect();
1525 Some(compute_pivot(&sheet_refs, &pivot).map_err(Error::InvalidArgument)?)
1526 };
1527
1528 if let (Some(old_end_row), Some(old_end_col)) =
1531 (pivot.last_output_end_row, pivot.last_output_end_col)
1532 {
1533 self.clear_range(
1534 dest_idx,
1535 pivot.dest_row,
1536 pivot.dest_col,
1537 old_end_row,
1538 old_end_col,
1539 );
1540 }
1541
1542 let new_bounds = grid.as_ref().map(|grid| {
1543 let height = grid.height();
1544 let width = grid.width.max(1);
1545 self.ensure_capacity(
1546 dest_idx,
1547 pivot.dest_row + height.saturating_sub(1),
1548 pivot.dest_col + width.saturating_sub(1),
1549 );
1550
1551 let mut r = pivot.dest_row;
1552 for (name, state) in &grid.filter_rows {
1553 self.set_cell(dest_idx, r, pivot.dest_col, pivot_label_literal(name));
1554 self.set_cell(dest_idx, r, pivot.dest_col + 1, pivot_label_literal(state));
1555 r += 1;
1556 }
1557 if !grid.filter_rows.is_empty() {
1558 r += 1; }
1560 for header in &grid.header_rows {
1561 for (c, text) in header.iter().enumerate() {
1562 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(text));
1563 }
1564 r += 1;
1565 }
1566 for body in &grid.body_rows {
1567 for (c, label) in body.row_labels.iter().enumerate() {
1568 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(label));
1569 }
1570 for (c, val) in body.values.iter().enumerate() {
1571 self.set_cell(
1572 dest_idx,
1573 r,
1574 pivot.dest_col + body.row_labels.len() + c,
1575 pivot_value_literal(val),
1576 );
1577 }
1578 r += 1;
1579 }
1580 (
1581 pivot.dest_row + height.saturating_sub(1),
1582 pivot.dest_col + width.saturating_sub(1),
1583 )
1584 });
1585
1586 self.pivot_tables[idx].last_output_end_row = new_bounds.map(|(r, _)| r);
1587 self.pivot_tables[idx].last_output_end_col = new_bounds.map(|(_, c)| c);
1588 self.evaluate()
1589 }
1590
1591 fn clear_range(
1595 &mut self,
1596 sheet_idx: usize,
1597 start_row: usize,
1598 start_col: usize,
1599 end_row: usize,
1600 end_col: usize,
1601 ) {
1602 if sheet_idx >= self.sheets.len() {
1603 return;
1604 }
1605 let (row_count, col_count) = {
1606 let s = &self.sheets[sheet_idx];
1607 (s.row_count(), s.col_count())
1608 };
1609 if row_count == 0 || col_count == 0 {
1610 return;
1611 }
1612 for r in start_row..=end_row.min(row_count - 1) {
1613 for c in start_col..=end_col.min(col_count - 1) {
1614 self.sheets[sheet_idx].set_cell_src(r, c, String::new());
1615 }
1616 }
1617 }
1618}