1use crate::core::formula::CompiledFormula;
19use crate::core::grid_edit::{Axis, GridEdit};
20use crate::core::locale::Locale;
21use crate::core::parser::col_idx_to_letters;
22use crate::core::xlsx::{export_xlsx_data, import_xlsx_data};
23use crate::core::{
24 ExcelTable, PivotAggregation, PivotArea, PivotField, PivotFilterField, PivotGrid, PivotSource,
25 PivotTable, PivotValueField, VbaModule, VbaModuleKind, VbaProject,
26 chart::{Chart, ChartType},
27 compute_pivot,
28 engine::{Context, DataColumn, ResultData, Sheet, generate_unique_id},
29 validate_vba_module_name,
30};
31use crate::{Error, ObjectKind};
32
33fn resize_table_columns(
42 table: &mut ExcelTable,
43 new_start_col: usize,
44 new_end_col: usize,
45 edit: &GridEdit,
46) {
47 if edit.insert {
48 if edit.at > table.start_col && edit.at <= table.end_col {
52 let offset = (edit.at - table.start_col).min(table.columns.len());
53 for _ in 0..edit.count {
54 table.columns.insert(offset, String::new());
55 }
56 }
57 } else {
58 let first = edit.at.max(table.start_col);
59 let last = (edit.at + edit.count).min(table.end_col + 1);
60 if first < last {
61 let lo = (first - table.start_col).min(table.columns.len());
62 let hi = (last - table.start_col).min(table.columns.len());
63 table.columns.drain(lo..hi);
64 }
65 }
66 table
69 .columns
70 .resize(new_end_col - new_start_col + 1, String::new());
71}
72
73pub struct SheetSummary {
75 pub name: String,
77 pub row_count: usize,
79 pub col_count: usize,
81 pub formula_count: usize,
83}
84
85pub struct WorkbookSummary {
87 pub file_name: String,
89 pub sheet_count: usize,
91 pub chart_count: usize,
93 pub sheets: Vec<SheetSummary>,
95}
96
97pub struct WorkbookManager {
113 pub sheets: Vec<Sheet>,
116 pub charts: Vec<Chart>,
119 pub pivot_tables: Vec<PivotTable>,
122 pub vba_project: Option<VbaProject>,
124 pub locale: Locale,
126}
127
128fn pivot_label_literal(text: &str) -> String {
132 if text.is_empty() {
133 String::new()
134 } else if text.starts_with('=')
135 || text.parse::<f64>().is_ok()
136 || text.eq_ignore_ascii_case("true")
137 || text.eq_ignore_ascii_case("false")
138 {
139 format!("\"{}\"", text)
140 } else {
141 text.to_string()
142 }
143}
144
145fn pivot_value_literal(v: &ResultData) -> String {
149 match v {
150 ResultData::Error(e) => e.clone(),
151 other => other.to_string(),
152 }
153}
154
155fn remove_pivot_field(fields: &mut Vec<PivotField>, column: &str) -> bool {
156 let before = fields.len();
157 fields.retain(|f| !f.column.eq_ignore_ascii_case(column));
158 before != fields.len()
159}
160
161impl WorkbookManager {
162 pub fn load_bytes(buffer: &[u8]) -> crate::Result<Self> {
164 let (imported_tables, charts, pivot_tables, vba_project) =
165 import_xlsx_data(buffer, &[], |_, _, _| {})?;
166
167 let locale = Locale::default();
168 let mut sheets: Vec<Sheet> = imported_tables.into_iter().map(|it| it.sheet).collect();
169 for sheet in &mut sheets {
170 sheet.locale = locale.clone();
171 }
172 Ok(Self {
173 sheets,
174 charts,
175 pivot_tables,
176 vba_project,
177 locale,
178 })
179 }
180
181 pub fn save_bytes(&self) -> crate::Result<Vec<u8>> {
187 export_xlsx_data(
188 &self.sheets,
189 &self.charts,
190 &self.pivot_tables,
191 self.vba_project.as_ref(),
192 )
193 }
194
195 pub fn new_empty() -> crate::Result<Self> {
197 let locale = Locale::default();
198 let mut wb = Self {
199 sheets: Vec::new(),
200 charts: Vec::new(),
201 pivot_tables: Vec::new(),
202 vba_project: None,
203 locale,
204 };
205 wb.add_sheet("Sheet1")?;
206 Ok(wb)
207 }
208
209 pub fn set_locale(&mut self, locale: Locale) {
211 self.locale = locale.clone();
212 for sheet in &mut self.sheets {
213 sheet.locale = locale.clone();
214 }
215 }
216
217 pub fn evaluate(&mut self) -> crate::Result<()> {
219 if self.sheets.is_empty() {
220 return Ok(());
221 }
222
223 let sheet_order: Vec<String> = self.sheets.iter().map(|s| s.name.clone()).collect();
227
228 for _pass in 0..3 {
239 for sheet in &mut self.sheets {
240 sheet.mark_all_dirty();
241 }
242 for i in 0..self.sheets.len() {
243 let (left, right) = self.sheets.split_at_mut(i);
244 let (target_sheet, right_tail) = right.split_first_mut().unwrap();
245
246 let mut context = Context::new();
247 for s in left.iter() {
248 context.add_table(s.name.clone(), s);
249 }
250 for s in right_tail.iter() {
251 context.add_table(s.name.clone(), s);
252 }
253 context.pivot_tables = &self.pivot_tables;
254 context.sheet_order = sheet_order.clone();
255
256 let _ = target_sheet.commit(Some(&context));
257 }
258 }
259
260 Ok(())
261 }
262
263 pub(crate) fn call_worksheet_function(
271 &self,
272 name: &str,
273 args: &[crate::core::parser::Expr],
274 ) -> Result<ResultData, crate::core::EngineError> {
275 let Some(host) = self.sheets.first() else {
276 return Err(crate::core::EngineError::EvalError(
277 crate::core::EvalError::UnknownFunction("no worksheets".to_string()),
278 ));
279 };
280 let mut context = Context::new();
281 for s in &self.sheets {
282 context.add_table(s.name.clone(), s);
283 }
284 context.pivot_tables = &self.pivot_tables;
285 context.sheet_order = self.sheets.iter().map(|s| s.name.clone()).collect();
286 host.call_worksheet_function(name, args, Some(&context))
287 }
288
289 pub fn find_sheet_index(&self, name_opt: Option<&str>) -> crate::Result<usize> {
291 if self.sheets.is_empty() {
292 return Err(Error::EmptyWorkbook);
293 }
294
295 match name_opt {
296 Some(name) => {
297 if let Some(idx) = self
298 .sheets
299 .iter()
300 .position(|s| s.name.eq_ignore_ascii_case(name))
301 {
302 Ok(idx)
303 } else {
304 let available: Vec<String> =
305 self.sheets.iter().map(|s| s.name.clone()).collect();
306 Err(Error::not_found_among(
307 ObjectKind::Sheet,
308 name.to_string(),
309 available,
310 ))
311 }
312 }
313 None => Ok(0),
314 }
315 }
316
317 pub fn get_summary(&self, file_name: &str) -> WorkbookSummary {
319 let sheet_summaries = self
320 .sheets
321 .iter()
322 .map(|sheet| {
323 let row_count = sheet.row_count();
324 let col_count = sheet.col_count();
325 let mut formula_count = 0;
326
327 for col in &sheet.columns {
328 for src in &col.src {
329 if src.starts_with('=') {
330 formula_count += 1;
331 }
332 }
333 }
334
335 SheetSummary {
336 name: sheet.name.clone(),
337 row_count,
338 col_count,
339 formula_count,
340 }
341 })
342 .collect();
343
344 WorkbookSummary {
345 file_name: file_name.to_string(),
346 sheet_count: self.sheets.len(),
347 chart_count: self.charts.len(),
348 sheets: sheet_summaries,
349 }
350 }
351
352 pub fn ensure_capacity(&mut self, sheet_idx: usize, target_row: usize, target_col: usize) {
354 if sheet_idx >= self.sheets.len() {
355 return;
356 }
357 self.sheets[sheet_idx].ensure_capacity(target_row, target_col);
358 }
359
360 pub fn set_cell_style(
367 &mut self,
368 sheet_name: Option<&str>,
369 row: usize,
370 col: usize,
371 style: crate::core::CellStyle,
372 ) -> crate::Result<()> {
373 let sheet_idx = self.find_sheet_index(sheet_name)?;
374 self.sheets[sheet_idx].update_cell_style(row, col, |s| s.merge(&style));
375 Ok(())
376 }
377
378 pub fn set_range_style(
380 &mut self,
381 sheet_name: Option<&str>,
382 start_row: usize,
383 start_col: usize,
384 end_row: usize,
385 end_col: usize,
386 style: crate::core::CellStyle,
387 ) -> crate::Result<()> {
388 if end_row < start_row || end_col < start_col {
389 return Err(Error::InvalidRange(
390 "range end must not precede its start".to_string(),
391 ));
392 }
393 let sheet_idx = self.find_sheet_index(sheet_name)?;
394 for r in start_row..=end_row {
395 for c in start_col..=end_col {
396 self.sheets[sheet_idx].update_cell_style(r, c, |s| s.merge(&style));
397 }
398 }
399 Ok(())
400 }
401
402 pub fn get_cell_style(
404 &self,
405 sheet_name: Option<&str>,
406 row: usize,
407 col: usize,
408 ) -> crate::Result<Option<crate::core::CellStyle>> {
409 let sheet_idx = self.find_sheet_index(sheet_name)?;
410 Ok(self.sheets[sheet_idx].get_cell_style(row, col).cloned())
411 }
412
413 pub fn set_table_style(&mut self, table_name: &str, style_name: &str) -> crate::Result<()> {
420 for sheet in &mut self.sheets {
421 for table in &mut sheet.tables {
422 if table.name.eq_ignore_ascii_case(table_name) {
423 table.set_style_name(Some(style_name.to_string()));
424 return Ok(());
425 }
426 }
427 }
428 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
429 }
430
431 pub fn get_table_style(&self, table_name: &str) -> crate::Result<Option<String>> {
437 for sheet in &self.sheets {
438 for table in &sheet.tables {
439 if table.name.eq_ignore_ascii_case(table_name) {
440 return Ok(table.style_name.clone());
441 }
442 }
443 }
444 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
445 }
446
447 pub fn set_cell(&mut self, sheet_idx: usize, row: usize, col: usize, value: String) {
449 self.ensure_capacity(sheet_idx, row, col);
450 let sheet = &mut self.sheets[sheet_idx];
451 sheet.set_cell_src(row, col, value);
452 }
453
454 pub fn set_cell_with_type(
456 &mut self,
457 sheet_idx: usize,
458 row: usize,
459 col: usize,
460 value: String,
461 cell_type: crate::core::CellType,
462 ) {
463 self.ensure_capacity(sheet_idx, row, col);
464 let sheet = &mut self.sheets[sheet_idx];
465 sheet.set_cell_with_type(row, col, value, cell_type);
466 }
467
468 pub fn set_cell_type(
470 &mut self,
471 sheet_idx: usize,
472 row: usize,
473 col: usize,
474 cell_type: crate::core::CellType,
475 ) {
476 self.ensure_capacity(sheet_idx, row, col);
477 let sheet = &mut self.sheets[sheet_idx];
478 sheet.set_cell_type(row, col, cell_type);
479 }
480
481 pub fn get_cell_type(&self, sheet_idx: usize, row: usize, col: usize) -> crate::core::CellType {
483 if let Some(sheet) = self.sheets.get(sheet_idx) {
484 sheet.get_cell_type(&crate::core::CellRef::new(row, col))
485 } else {
486 crate::core::CellType::Empty
487 }
488 }
489
490 pub fn insert_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
496 let sheet = &self.sheets[sheet_idx];
497 let at = row_idx.min(sheet.row_count());
500 let edit = GridEdit::insert_row(sheet.id, at);
501 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_row(at));
502 self.evaluate()
503 }
504
505 pub fn delete_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
510 let sheet = &self.sheets[sheet_idx];
511 if row_idx >= sheet.row_count() {
512 return Err(Error::OutOfBounds {
513 what: "row",
514 index: row_idx,
515 len: sheet.row_count(),
516 });
517 }
518 let edit = GridEdit::delete_row(sheet.id, row_idx);
519 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].delete_row(row_idx));
520 self.evaluate()
521 }
522
523 pub fn insert_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
525 let sheet = &self.sheets[sheet_idx];
526 let at = col_idx.min(sheet.col_count());
527 let edit = GridEdit::insert_col(sheet.id, at);
528 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_col(at));
529 self.evaluate()
530 }
531
532 pub fn delete_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
534 let sheet = &self.sheets[sheet_idx];
535 if col_idx >= sheet.col_count() {
536 return Err(Error::OutOfBounds {
537 what: "column",
538 index: col_idx,
539 len: sheet.col_count(),
540 });
541 }
542 let deleted_col_ids = vec![sheet.columns()[col_idx].id];
546 let edit = GridEdit::delete_col(sheet.id, col_idx);
547 self.apply_grid_edit(edit, &deleted_col_ids, |wb| {
548 wb.sheets[sheet_idx].delete_col(col_idx)
549 });
550 self.evaluate()
551 }
552
553 pub fn insert_cells_shift_down(
561 &mut self,
562 sheet_idx: usize,
563 row: usize,
564 first_col: usize,
565 last_col: usize,
566 count: usize,
567 ) -> crate::Result<()> {
568 let sheet = &self.sheets[sheet_idx];
569 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, true);
570 self.apply_grid_edit(edit, &[], |wb| {
571 wb.sheets[sheet_idx].insert_cells_shift_down(row, first_col, last_col, count)
572 });
573 self.evaluate()
574 }
575
576 pub fn delete_cells_shift_up(
579 &mut self,
580 sheet_idx: usize,
581 row: usize,
582 first_col: usize,
583 last_col: usize,
584 count: usize,
585 ) -> crate::Result<()> {
586 let sheet = &self.sheets[sheet_idx];
587 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, false);
588 self.apply_grid_edit(edit, &[], |wb| {
589 wb.sheets[sheet_idx].delete_cells_shift_up(row, first_col, last_col, count)
590 });
591 self.evaluate()
592 }
593
594 fn apply_grid_edit(
612 &mut self,
613 edit: GridEdit,
614 deleted_col_ids: &[u64],
615 apply: impl FnOnce(&mut Self),
616 ) {
617 let mut shifted: Vec<(usize, usize, usize, CompiledFormula)> = Vec::new();
621 for (sheet_idx, sheet) in self.sheets.iter().enumerate() {
622 for (col_idx, column) in sheet.columns().iter().enumerate() {
623 for row_idx in 0..column.len() {
624 let Some(src) = column.src(row_idx).filter(|s| s.starts_with('=')) else {
625 continue;
626 };
627 let compiled = crate::core::parser::compile_formula(src, &self.sheets);
628 if let Some(next) =
629 crate::core::grid_edit::shift_formula(&compiled, &edit, deleted_col_ids)
630 {
631 shifted.push((sheet_idx, col_idx, row_idx, next));
632 }
633 }
634 }
635 }
636
637 apply(self);
639 self.shift_table_and_pivot_ranges(&edit);
640
641 for (sheet_idx, col_idx, row_idx, compiled) in shifted {
643 let Some((row, col)) = self.moved_cell(&edit, sheet_idx, row_idx, col_idx) else {
644 continue;
646 };
647 let text = crate::core::parser::serialize_formula(&compiled, &self.sheets);
648 self.sheets[sheet_idx].set_cell_src(row, col, text);
649 }
650 }
651
652 fn moved_cell(
655 &self,
656 edit: &GridEdit,
657 sheet_idx: usize,
658 row: usize,
659 col: usize,
660 ) -> Option<(usize, usize)> {
661 if self.sheets[sheet_idx].id != edit.sheet_id || !edit.covers_columns(col, col) {
666 return Some((row, col));
667 }
668 let moved = |index: usize| {
669 crate::core::grid_edit::shift_point(index, edit.at, edit.count, edit.insert)
670 };
671 match edit.axis {
672 Axis::Row => Some((moved(row)?, col)),
673 Axis::Col => Some((row, moved(col)?)),
674 }
675 }
676
677 fn shift_table_and_pivot_ranges(&mut self, edit: &GridEdit) {
683 use crate::core::grid_edit::{shift_point, shift_rect};
684
685 for sheet in &mut self.sheets {
686 if sheet.id != edit.sheet_id {
687 continue;
688 }
689 sheet.tables.retain_mut(|table| {
690 if !edit.covers_columns(table.start_col, table.end_col) {
693 return true;
694 }
695 match shift_rect(
696 edit,
697 table.start_row,
698 table.start_col,
699 table.end_row,
700 table.end_col,
701 ) {
702 Some((r0, c0, r1, c1)) => {
703 if edit.axis == Axis::Col {
707 resize_table_columns(table, c0, c1, edit);
708 }
709 table.start_row = r0;
710 table.start_col = c0;
711 table.end_row = r1;
712 table.end_col = c1;
713 true
714 }
715 None => false,
716 }
717 });
718 }
719
720 for pivot in &mut self.pivot_tables {
721 if let PivotSource::Range {
722 sheet_id,
723 start_row,
724 start_col,
725 end_row,
726 end_col,
727 } = &mut pivot.source
728 && *sheet_id == edit.sheet_id
729 && edit.covers_columns(*start_col, *end_col)
730 && let Some((r0, c0, r1, c1)) =
731 shift_rect(edit, *start_row, *start_col, *end_row, *end_col)
732 {
733 *start_row = r0;
734 *start_col = c0;
735 *end_row = r1;
736 *end_col = c1;
737 }
738
739 if pivot.dest_sheet_id == edit.sheet_id
740 && edit.covers_columns(pivot.dest_col, pivot.dest_col)
741 {
742 match edit.axis {
747 Axis::Row => {
748 pivot.dest_row =
749 shift_point(pivot.dest_row, edit.at, edit.count, edit.insert)
750 .unwrap_or(edit.at);
751 }
752 Axis::Col => {
753 pivot.dest_col =
754 shift_point(pivot.dest_col, edit.at, edit.count, edit.insert)
755 .unwrap_or(edit.at);
756 }
757 }
758 pivot.last_output_end_row = None;
762 pivot.last_output_end_col = None;
763 }
764 }
765 }
766
767 pub fn add_sheet(&mut self, name: &str) -> crate::Result<()> {
769 if self
770 .sheets
771 .iter()
772 .any(|s| s.name.eq_ignore_ascii_case(name))
773 {
774 return Err(Error::AlreadyExists {
775 kind: ObjectKind::Sheet,
776 name: name.to_string(),
777 });
778 }
779
780 let mut columns = Vec::new();
781 for col_idx in 0..5 {
782 let mut col = DataColumn::new(10);
783 col.id = generate_unique_id();
784 col.name = col_idx_to_letters(col_idx);
785 columns.push(col);
786 }
787
788 let new_sheet = Sheet {
789 id: generate_unique_id(),
790 name: name.to_string(),
791 columns,
792 tables: Vec::new(),
793 dependencies: std::collections::HashMap::new(),
794 dependencies_rev: std::collections::HashMap::new(),
795 uncommitted_actions: Vec::new(),
796 locale: self.locale.clone(),
797 };
798
799 self.sheets.push(new_sheet);
800 Ok(())
801 }
802
803 pub fn delete_sheet(&mut self, name: &str) -> crate::Result<()> {
805 let idx = self.find_sheet_index(Some(name))?;
806 if self.sheets.len() <= 1 {
807 return Err(Error::LastSheetInWorkbook);
808 }
809 self.sheets.remove(idx);
810 Ok(())
811 }
812
813 pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
815 let idx = self.find_sheet_index(Some(old_name))?;
816 if self
817 .sheets
818 .iter()
819 .enumerate()
820 .any(|(i, s)| i != idx && s.name.eq_ignore_ascii_case(new_name))
821 {
822 return Err(Error::NameTaken {
823 kind: ObjectKind::Sheet,
824 name: new_name.to_string(),
825 });
826 }
827 self.sheets[idx].name = new_name.to_string();
828 Ok(())
829 }
830
831 #[allow(clippy::too_many_arguments)]
833 pub fn add_chart(
834 &mut self,
835 sheet_name: &str,
836 chart_type: ChartType,
837 range: String,
838 title: Option<String>,
839 anchor: Option<(usize, usize)>,
840 ) -> crate::Result<u64> {
841 let _ = self.find_sheet_index(Some(sheet_name))?;
842 let id = generate_unique_id();
843 let name = format!("Chart {}", self.charts.len() + 1);
844 let (anchor_row, anchor_col) = anchor.unwrap_or((0, 0));
845
846 let chart = Chart {
847 id,
848 name,
849 chart_type,
850 data_range: range,
851 title,
852 xlabel: None,
853 ylabel: None,
854 show_legend: true,
855 anchor_row,
856 anchor_col,
857 };
858
859 self.charts.push(chart);
860 Ok(id)
861 }
862
863 #[allow(clippy::too_many_arguments)]
868 pub fn edit_chart(
869 &mut self,
870 id: u64,
871 name: Option<String>,
872 chart_type: Option<ChartType>,
873 data_range: Option<String>,
874 title: Option<Option<String>>,
875 xlabel: Option<Option<String>>,
876 ylabel: Option<Option<String>>,
877 show_legend: Option<bool>,
878 anchor: Option<(usize, usize)>,
879 ) -> crate::Result<()> {
880 let chart = self
881 .charts
882 .iter_mut()
883 .find(|c| c.id == id)
884 .ok_or_else(|| Error::not_found(ObjectKind::Chart, id.to_string()))?;
885 if let Some(name) = name {
886 chart.name = name;
887 }
888 if let Some(chart_type) = chart_type {
889 chart.chart_type = chart_type;
890 }
891 if let Some(data_range) = data_range {
892 chart.data_range = data_range;
893 }
894 if let Some(title) = title {
895 chart.title = title;
896 }
897 if let Some(xlabel) = xlabel {
898 chart.xlabel = xlabel;
899 }
900 if let Some(ylabel) = ylabel {
901 chart.ylabel = ylabel;
902 }
903 if let Some(show_legend) = show_legend {
904 chart.show_legend = show_legend;
905 }
906 if let Some((anchor_row, anchor_col)) = anchor {
907 chart.anchor_row = anchor_row;
908 chart.anchor_col = anchor_col;
909 }
910 Ok(())
911 }
912
913 pub fn has_vba_project(&self) -> bool {
915 self.vba_project.is_some()
916 }
917
918 pub fn list_vba_modules(&self) -> Vec<&VbaModule> {
920 self.vba_project
921 .as_ref()
922 .map(|p| p.modules.iter().collect())
923 .unwrap_or_default()
924 }
925
926 pub fn ensure_vba_project(&mut self) -> crate::Result<()> {
930 if self.vba_project.is_some() {
931 return Ok(());
932 }
933 self.vba_project = Some(VbaProject::new_empty());
934 Ok(())
935 }
936
937 pub fn add_vba_module(
947 &mut self,
948 name: String,
949 kind: VbaModuleKind,
950 source: String,
951 bound_sheet_id: Option<u64>,
952 ) -> crate::Result<()> {
953 validate_vba_module_name(&name).map_err(|reason| Error::InvalidName {
954 kind: ObjectKind::VbaModule,
955 name: name.clone(),
956 reason,
957 })?;
958 let is_this_workbook = kind == VbaModuleKind::Document && name == "ThisWorkbook";
959 if kind == VbaModuleKind::Document && !is_this_workbook {
960 let sheet_id = bound_sheet_id
961 .ok_or_else(|| Error::Vba("document modules require a bound sheet".to_string()))?;
962 if !self.sheets.iter().any(|s| s.id == sheet_id) {
963 return Err(Error::not_found(ObjectKind::Sheet, sheet_id.to_string()));
964 }
965 }
966 self.ensure_vba_project()?;
967 let project = self.vba_project.as_mut().unwrap();
968 if project.module_name_taken(&name) {
969 return Err(Error::AlreadyExists {
970 kind: ObjectKind::VbaModule,
971 name: name.to_string(),
972 });
973 }
974 if kind == VbaModuleKind::Document
975 && bound_sheet_id.is_some()
976 && project
977 .modules
978 .iter()
979 .any(|m| m.kind == VbaModuleKind::Document && m.bound_sheet_id == bound_sheet_id)
980 {
981 return Err(Error::DocumentModuleExists);
982 }
983 let prefix_bytes = project
991 .modules
992 .first()
993 .map(|m| m.prefix_bytes.clone())
994 .unwrap_or_else(|| project.seed_prefix_bytes.clone());
995 let module_cookie = project
996 .modules
997 .first()
998 .map(|m| m.module_cookie)
999 .unwrap_or(project.seed_module_cookie);
1000 let stored_bound_sheet_id = if kind == VbaModuleKind::Document && !is_this_workbook {
1001 bound_sheet_id
1002 } else {
1003 None
1004 };
1005 project.modules.push(VbaModule {
1006 name,
1007 kind,
1008 source,
1009 bound_sheet_id: stored_bound_sheet_id,
1010 prefix_bytes,
1011 module_cookie,
1012 cached_compressed_source: None,
1014 });
1015 Ok(())
1016 }
1017
1018 pub fn remove_vba_module(&mut self, name: &str) -> crate::Result<()> {
1025 let project = self
1026 .vba_project
1027 .as_mut()
1028 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1029 let before = project.modules.len();
1030 project
1031 .modules
1032 .retain(|m| !m.name.eq_ignore_ascii_case(name));
1033 if project.modules.len() == before {
1034 return Err(Error::not_found(ObjectKind::VbaModule, name.to_string()));
1035 }
1036 Ok(())
1037 }
1038
1039 pub fn rename_vba_module(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1052 validate_vba_module_name(new_name).map_err(|reason| Error::InvalidName {
1053 kind: ObjectKind::VbaModule,
1054 name: new_name.to_string(),
1055 reason,
1056 })?;
1057 let project = self
1058 .vba_project
1059 .as_mut()
1060 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1061 if !old_name.eq_ignore_ascii_case(new_name) && project.module_name_taken(new_name) {
1062 return Err(Error::AlreadyExists {
1063 kind: ObjectKind::VbaModule,
1064 name: new_name.to_string(),
1065 });
1066 }
1067 let module = project
1068 .find_module_mut(old_name)
1069 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, old_name))?;
1070 module.name = new_name.to_string();
1071 Ok(())
1072 }
1073
1074 pub fn set_vba_module_source(&mut self, name: &str, source: String) -> crate::Result<()> {
1085 let project = self
1086 .vba_project
1087 .as_mut()
1088 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1089 let module = project
1090 .find_module_mut(name)
1091 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, name))?;
1092 module.source = source;
1093 module.cached_compressed_source = None;
1097 Ok(())
1098 }
1099
1100 pub fn delete_chart(&mut self, id: u64) -> crate::Result<()> {
1102 if let Some(pos) = self.charts.iter().position(|c| c.id == id) {
1103 self.charts.remove(pos);
1104 Ok(())
1105 } else {
1106 Err(Error::not_found(ObjectKind::Chart, id.to_string()))
1107 }
1108 }
1109
1110 pub fn find_table(&self, name: &str) -> Option<(&Sheet, &ExcelTable)> {
1113 self.sheets
1114 .iter()
1115 .find_map(|s| s.find_table(name).map(|t| (s, t)))
1116 }
1117
1118 pub fn list_tables(&self) -> Vec<(&str, &ExcelTable)> {
1121 self.sheets
1122 .iter()
1123 .flat_map(|s| s.tables.iter().map(move |t| (s.name.as_str(), t)))
1124 .collect()
1125 }
1126
1127 fn find_table_sheet_index(&self, name: &str) -> crate::Result<usize> {
1128 self.sheets
1129 .iter()
1130 .position(|s| s.find_table(name).is_some())
1131 .ok_or_else(|| Error::not_found(ObjectKind::Table, name))
1132 }
1133
1134 fn table_name_taken(&self, name: &str) -> bool {
1135 self.sheets
1136 .iter()
1137 .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(name)))
1138 }
1139
1140 #[allow(clippy::too_many_arguments)]
1144 pub fn add_table(
1145 &mut self,
1146 sheet_name: Option<&str>,
1147 name: &str,
1148 start_row: usize,
1149 start_col: usize,
1150 end_row: usize,
1151 end_col: usize,
1152 has_header_row: bool,
1153 has_totals_row: bool,
1154 ) -> crate::Result<u64> {
1155 if self.table_name_taken(name) {
1156 return Err(Error::AlreadyExists {
1157 kind: ObjectKind::Table,
1158 name: name.to_string(),
1159 });
1160 }
1161 let idx = self.find_sheet_index(sheet_name)?;
1162 self.sheets[idx]
1163 .add_table(
1164 name.to_string(),
1165 start_row,
1166 start_col,
1167 end_row,
1168 end_col,
1169 has_header_row,
1170 has_totals_row,
1171 )
1172 .map_err(Error::InvalidArgument)
1173 }
1174
1175 pub fn delete_table(&mut self, name: &str) -> crate::Result<()> {
1177 let idx = self.find_table_sheet_index(name)?;
1178 self.sheets[idx]
1179 .delete_table_by_name(name)
1180 .map_err(Error::InvalidArgument)
1181 }
1182
1183 pub fn rename_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1185 if !old_name.eq_ignore_ascii_case(new_name) && self.table_name_taken(new_name) {
1186 return Err(Error::NameTaken {
1187 kind: ObjectKind::Table,
1188 name: new_name.to_string(),
1189 });
1190 }
1191 let idx = self.find_table_sheet_index(old_name)?;
1192 self.sheets[idx]
1193 .rename_table(old_name, new_name)
1194 .map_err(Error::InvalidArgument)?;
1195 self.rewrite_table_references(old_name, Some(new_name), None);
1199 self.evaluate()
1200 }
1201
1202 fn rewrite_table_references(
1207 &mut self,
1208 table_name: &str,
1209 new_table_name: Option<&str>,
1210 col_rename: Option<(&str, &str)>,
1211 ) {
1212 for sheet in &mut self.sheets {
1213 for col_idx in 0..sheet.columns.len() {
1214 let row_count = sheet.columns[col_idx].src.len();
1215 for row_idx in 0..row_count {
1216 let src = sheet.columns[col_idx].src[row_idx].clone();
1217 if let Some(new_src) = crate::core::parser::rewrite_structured_table_reference(
1218 &src,
1219 table_name,
1220 new_table_name,
1221 col_rename,
1222 ) {
1223 sheet.set_cell_src(row_idx, col_idx, new_src);
1224 }
1225 }
1226 }
1227 }
1228 }
1229
1230 pub fn resize_table(
1232 &mut self,
1233 name: &str,
1234 new_end_row: usize,
1235 new_end_col: usize,
1236 ) -> crate::Result<()> {
1237 let idx = self.find_table_sheet_index(name)?;
1238 self.sheets[idx]
1239 .resize_table(name, new_end_row, new_end_col)
1240 .map_err(Error::InvalidArgument)
1241 }
1242
1243 pub fn rename_table_column(
1245 &mut self,
1246 table_name: &str,
1247 col_index: usize,
1248 new_name: &str,
1249 ) -> crate::Result<()> {
1250 let idx = self.find_table_sheet_index(table_name)?;
1251 let old_col_name = self.sheets[idx]
1252 .find_table(table_name)
1253 .and_then(|t| t.columns.get(col_index).cloned())
1254 .ok_or_else(|| {
1255 Error::InvalidArgument(format!(
1256 "column index {col_index} out of bounds for table '{table_name}'"
1257 ))
1258 })?;
1259 self.sheets[idx]
1260 .rename_table_column(table_name, col_index, new_name)
1261 .map_err(Error::InvalidArgument)?;
1262 self.rewrite_table_references(table_name, None, Some((&old_col_name, new_name)));
1265 self.evaluate()
1266 }
1267
1268 pub fn find_pivot_table(&self, name: &str) -> Option<&PivotTable> {
1270 self.pivot_tables
1271 .iter()
1272 .find(|p| p.name.eq_ignore_ascii_case(name))
1273 }
1274
1275 fn find_pivot_table_index(&self, name: &str) -> crate::Result<usize> {
1276 self.pivot_tables
1277 .iter()
1278 .position(|p| p.name.eq_ignore_ascii_case(name))
1279 .ok_or_else(|| Error::not_found(ObjectKind::PivotTable, name))
1280 }
1281
1282 pub fn list_pivot_tables(&self) -> &[PivotTable] {
1284 &self.pivot_tables
1285 }
1286
1287 fn pivot_table_name_taken(&self, name: &str) -> bool {
1288 self.pivot_tables
1289 .iter()
1290 .any(|p| p.name.eq_ignore_ascii_case(name))
1291 }
1292
1293 #[allow(clippy::too_many_arguments)]
1297 pub fn add_pivot_table_from_table(
1298 &mut self,
1299 name: &str,
1300 source_table_name: &str,
1301 dest_sheet_name: Option<&str>,
1302 dest_row: usize,
1303 dest_col: usize,
1304 grand_totals_row: bool,
1305 grand_totals_col: bool,
1306 ) -> crate::Result<u64> {
1307 if self.pivot_table_name_taken(name) {
1308 return Err(Error::AlreadyExists {
1309 kind: ObjectKind::PivotTable,
1310 name: name.to_string(),
1311 });
1312 }
1313 self.find_table(source_table_name)
1314 .ok_or_else(|| Error::not_found(ObjectKind::Table, source_table_name))?;
1315 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1316 let id = generate_unique_id();
1317 self.pivot_tables.push(PivotTable {
1318 id,
1319 name: name.to_string(),
1320 source: PivotSource::Table {
1321 name: source_table_name.to_string(),
1322 },
1323 dest_sheet_id: self.sheets[dest_idx].id,
1324 dest_row,
1325 dest_col,
1326 row_fields: Vec::new(),
1327 col_fields: Vec::new(),
1328 value_fields: Vec::new(),
1329 filter_fields: Vec::new(),
1330 grand_totals_row,
1331 grand_totals_col,
1332 last_output_end_row: None,
1333 last_output_end_col: None,
1334 });
1335 self.refresh_pivot_table(name)?;
1336 Ok(id)
1337 }
1338
1339 #[allow(clippy::too_many_arguments)]
1342 pub fn add_pivot_table_from_range(
1343 &mut self,
1344 name: &str,
1345 source_sheet_name: Option<&str>,
1346 start_row: usize,
1347 start_col: usize,
1348 end_row: usize,
1349 end_col: usize,
1350 dest_sheet_name: Option<&str>,
1351 dest_row: usize,
1352 dest_col: usize,
1353 grand_totals_row: bool,
1354 grand_totals_col: bool,
1355 ) -> crate::Result<u64> {
1356 if self.pivot_table_name_taken(name) {
1357 return Err(Error::AlreadyExists {
1358 kind: ObjectKind::PivotTable,
1359 name: name.to_string(),
1360 });
1361 }
1362 let src_idx = self.find_sheet_index(source_sheet_name)?;
1363 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1364 let id = generate_unique_id();
1365 self.pivot_tables.push(PivotTable {
1366 id,
1367 name: name.to_string(),
1368 source: PivotSource::Range {
1369 sheet_id: self.sheets[src_idx].id,
1370 start_row,
1371 start_col,
1372 end_row,
1373 end_col,
1374 },
1375 dest_sheet_id: self.sheets[dest_idx].id,
1376 dest_row,
1377 dest_col,
1378 row_fields: Vec::new(),
1379 col_fields: Vec::new(),
1380 value_fields: Vec::new(),
1381 filter_fields: Vec::new(),
1382 grand_totals_row,
1383 grand_totals_col,
1384 last_output_end_row: None,
1385 last_output_end_col: None,
1386 });
1387 self.refresh_pivot_table(name)?;
1388 Ok(id)
1389 }
1390
1391 pub fn delete_pivot_table(&mut self, name: &str) -> crate::Result<()> {
1394 let idx = self.find_pivot_table_index(name)?;
1395 let pivot = self.pivot_tables.remove(idx);
1396 if let (Some(end_row), Some(end_col)) =
1397 (pivot.last_output_end_row, pivot.last_output_end_col)
1398 && let Some(sheet_idx) = self.sheets.iter().position(|s| s.id == pivot.dest_sheet_id)
1399 {
1400 self.clear_range(sheet_idx, pivot.dest_row, pivot.dest_col, end_row, end_col);
1401 }
1402 Ok(())
1403 }
1404
1405 pub fn rename_pivot_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1407 if !old_name.eq_ignore_ascii_case(new_name) && self.pivot_table_name_taken(new_name) {
1408 return Err(Error::NameTaken {
1409 kind: ObjectKind::PivotTable,
1410 name: new_name.to_string(),
1411 });
1412 }
1413 let idx = self.find_pivot_table_index(old_name)?;
1414 self.pivot_tables[idx].name = new_name.to_string();
1415 Ok(())
1416 }
1417
1418 pub fn add_pivot_field(
1432 &mut self,
1433 pivot_name: &str,
1434 area: PivotArea,
1435 column: &str,
1436 aggregation: Option<PivotAggregation>,
1437 ) -> crate::Result<()> {
1438 let idx = self.find_pivot_table_index(pivot_name)?;
1439 if !matches!(area, PivotArea::Value) {
1440 let pivot = &mut self.pivot_tables[idx];
1441 remove_pivot_field(&mut pivot.row_fields, column);
1442 remove_pivot_field(&mut pivot.col_fields, column);
1443 pivot
1444 .filter_fields
1445 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1446 }
1447 match area {
1448 PivotArea::Row => self.pivot_tables[idx]
1449 .row_fields
1450 .push(PivotField::new(column)),
1451 PivotArea::Column => self.pivot_tables[idx]
1452 .col_fields
1453 .push(PivotField::new(column)),
1454 PivotArea::Value => {
1455 let agg = aggregation.unwrap_or(PivotAggregation::Sum);
1456 self.pivot_tables[idx]
1457 .value_fields
1458 .push(PivotValueField::new(column, agg));
1459 }
1460 PivotArea::Filter => self.pivot_tables[idx]
1461 .filter_fields
1462 .push(PivotFilterField::new(column)),
1463 }
1464 self.refresh_pivot_table(pivot_name)
1465 }
1466
1467 pub fn remove_pivot_field(
1470 &mut self,
1471 pivot_name: &str,
1472 area: PivotArea,
1473 column: &str,
1474 ) -> crate::Result<()> {
1475 let idx = self.find_pivot_table_index(pivot_name)?;
1476 let removed = match area {
1477 PivotArea::Row => remove_pivot_field(&mut self.pivot_tables[idx].row_fields, column),
1478 PivotArea::Column => remove_pivot_field(&mut self.pivot_tables[idx].col_fields, column),
1479 PivotArea::Value => {
1480 let before = self.pivot_tables[idx].value_fields.len();
1481 self.pivot_tables[idx]
1482 .value_fields
1483 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1484 before != self.pivot_tables[idx].value_fields.len()
1485 }
1486 PivotArea::Filter => {
1487 let before = self.pivot_tables[idx].filter_fields.len();
1488 self.pivot_tables[idx]
1489 .filter_fields
1490 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1491 before != self.pivot_tables[idx].filter_fields.len()
1492 }
1493 };
1494 if !removed {
1495 return Err(Error::not_found(
1496 ObjectKind::PivotField,
1497 format!("{column}' in pivot table '{pivot_name}"),
1498 ));
1499 }
1500 self.refresh_pivot_table(pivot_name)
1501 }
1502
1503 pub fn set_pivot_filter(
1506 &mut self,
1507 pivot_name: &str,
1508 column: &str,
1509 values: Option<Vec<String>>,
1510 ) -> crate::Result<()> {
1511 let idx = self.find_pivot_table_index(pivot_name)?;
1512 let field = self.pivot_tables[idx]
1513 .filter_fields
1514 .iter_mut()
1515 .find(|f| f.column.eq_ignore_ascii_case(column))
1516 .ok_or_else(|| {
1517 Error::not_found(
1518 ObjectKind::PivotField,
1519 format!("{column}' on pivot table '{pivot_name}"),
1520 )
1521 })?;
1522 field.selected_values = values;
1523 self.refresh_pivot_table(pivot_name)
1524 }
1525
1526 pub fn refresh_pivot_table(&mut self, pivot_name: &str) -> crate::Result<()> {
1531 let idx = self.find_pivot_table_index(pivot_name)?;
1532 let pivot = self.pivot_tables[idx].clone();
1533 let dest_idx = self
1534 .sheets
1535 .iter()
1536 .position(|s| s.id == pivot.dest_sheet_id)
1537 .ok_or_else(|| {
1538 Error::InvalidArgument(
1539 "pivot table's destination sheet no longer exists".to_string(),
1540 )
1541 })?;
1542
1543 let grid: Option<PivotGrid> = if pivot.value_fields.is_empty() {
1544 None
1545 } else {
1546 let sheet_refs: Vec<&Sheet> = self.sheets.iter().collect();
1547 Some(compute_pivot(&sheet_refs, &pivot).map_err(Error::InvalidArgument)?)
1548 };
1549
1550 if let (Some(old_end_row), Some(old_end_col)) =
1553 (pivot.last_output_end_row, pivot.last_output_end_col)
1554 {
1555 self.clear_range(
1556 dest_idx,
1557 pivot.dest_row,
1558 pivot.dest_col,
1559 old_end_row,
1560 old_end_col,
1561 );
1562 }
1563
1564 let new_bounds = grid.as_ref().map(|grid| {
1565 let height = grid.height();
1566 let width = grid.width.max(1);
1567 self.ensure_capacity(
1568 dest_idx,
1569 pivot.dest_row + height.saturating_sub(1),
1570 pivot.dest_col + width.saturating_sub(1),
1571 );
1572
1573 let mut r = pivot.dest_row;
1574 for (name, state) in &grid.filter_rows {
1575 self.set_cell(dest_idx, r, pivot.dest_col, pivot_label_literal(name));
1576 self.set_cell(dest_idx, r, pivot.dest_col + 1, pivot_label_literal(state));
1577 r += 1;
1578 }
1579 if !grid.filter_rows.is_empty() {
1580 r += 1; }
1582 for header in &grid.header_rows {
1583 for (c, text) in header.iter().enumerate() {
1584 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(text));
1585 }
1586 r += 1;
1587 }
1588 for body in &grid.body_rows {
1589 for (c, label) in body.row_labels.iter().enumerate() {
1590 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(label));
1591 }
1592 for (c, val) in body.values.iter().enumerate() {
1593 self.set_cell(
1594 dest_idx,
1595 r,
1596 pivot.dest_col + body.row_labels.len() + c,
1597 pivot_value_literal(val),
1598 );
1599 }
1600 r += 1;
1601 }
1602 (
1603 pivot.dest_row + height.saturating_sub(1),
1604 pivot.dest_col + width.saturating_sub(1),
1605 )
1606 });
1607
1608 self.pivot_tables[idx].last_output_end_row = new_bounds.map(|(r, _)| r);
1609 self.pivot_tables[idx].last_output_end_col = new_bounds.map(|(_, c)| c);
1610 self.evaluate()
1611 }
1612
1613 fn clear_range(
1617 &mut self,
1618 sheet_idx: usize,
1619 start_row: usize,
1620 start_col: usize,
1621 end_row: usize,
1622 end_col: usize,
1623 ) {
1624 if sheet_idx >= self.sheets.len() {
1625 return;
1626 }
1627 let (row_count, col_count) = {
1628 let s = &self.sheets[sheet_idx];
1629 (s.row_count(), s.col_count())
1630 };
1631 if row_count == 0 || col_count == 0 {
1632 return;
1633 }
1634 for r in start_row..=end_row.min(row_count - 1) {
1635 for c in start_col..=end_col.min(col_count - 1) {
1636 self.sheets[sheet_idx].set_cell_src(r, c, String::new());
1637 }
1638 }
1639 }
1640}