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 {
234 for sheet in &mut self.sheets {
235 sheet.mark_all_dirty();
236 }
237 for i in 0..self.sheets.len() {
238 let (left, right) = self.sheets.split_at_mut(i);
239 let (target_sheet, right_tail) = right.split_first_mut().unwrap();
240
241 let mut context = Context::new();
242 for s in left.iter() {
243 context.add_table(s.name.clone(), s);
244 }
245 for s in right_tail.iter() {
246 context.add_table(s.name.clone(), s);
247 }
248 context.pivot_tables = &self.pivot_tables;
249 context.sheet_order = sheet_order.clone();
250
251 let _ = target_sheet.commit(Some(&context));
252 }
253 }
254
255 Ok(())
256 }
257
258 pub(crate) fn call_worksheet_function(
266 &self,
267 name: &str,
268 args: &[crate::core::parser::Expr],
269 ) -> Result<ResultData, crate::core::EngineError> {
270 let Some(host) = self.sheets.first() else {
271 return Err(crate::core::EngineError::EvalError(
272 crate::core::EvalError::UnknownFunction("no worksheets".to_string()),
273 ));
274 };
275 let mut context = Context::new();
276 for s in &self.sheets {
277 context.add_table(s.name.clone(), s);
278 }
279 context.pivot_tables = &self.pivot_tables;
280 context.sheet_order = self.sheets.iter().map(|s| s.name.clone()).collect();
281 host.call_worksheet_function(name, args, Some(&context))
282 }
283
284 pub fn find_sheet_index(&self, name_opt: Option<&str>) -> crate::Result<usize> {
286 if self.sheets.is_empty() {
287 return Err(Error::EmptyWorkbook);
288 }
289
290 match name_opt {
291 Some(name) => {
292 if let Some(idx) = self
293 .sheets
294 .iter()
295 .position(|s| s.name.eq_ignore_ascii_case(name))
296 {
297 Ok(idx)
298 } else {
299 let available: Vec<String> =
300 self.sheets.iter().map(|s| s.name.clone()).collect();
301 Err(Error::not_found_among(
302 ObjectKind::Sheet,
303 name.to_string(),
304 available,
305 ))
306 }
307 }
308 None => Ok(0),
309 }
310 }
311
312 pub fn get_summary(&self, file_name: &str) -> WorkbookSummary {
314 let sheet_summaries = self
315 .sheets
316 .iter()
317 .map(|sheet| {
318 let row_count = sheet.row_count();
319 let col_count = sheet.col_count();
320 let mut formula_count = 0;
321
322 for col in &sheet.columns {
323 for src in &col.src {
324 if src.starts_with('=') {
325 formula_count += 1;
326 }
327 }
328 }
329
330 SheetSummary {
331 name: sheet.name.clone(),
332 row_count,
333 col_count,
334 formula_count,
335 }
336 })
337 .collect();
338
339 WorkbookSummary {
340 file_name: file_name.to_string(),
341 sheet_count: self.sheets.len(),
342 chart_count: self.charts.len(),
343 sheets: sheet_summaries,
344 }
345 }
346
347 pub fn ensure_capacity(&mut self, sheet_idx: usize, target_row: usize, target_col: usize) {
349 if sheet_idx >= self.sheets.len() {
350 return;
351 }
352 self.sheets[sheet_idx].ensure_capacity(target_row, target_col);
353 }
354
355 pub fn set_cell_style(
362 &mut self,
363 sheet_name: Option<&str>,
364 row: usize,
365 col: usize,
366 style: crate::core::CellStyle,
367 ) -> crate::Result<()> {
368 let sheet_idx = self.find_sheet_index(sheet_name)?;
369 self.sheets[sheet_idx].update_cell_style(row, col, |s| s.merge(&style));
370 Ok(())
371 }
372
373 pub fn set_range_style(
375 &mut self,
376 sheet_name: Option<&str>,
377 start_row: usize,
378 start_col: usize,
379 end_row: usize,
380 end_col: usize,
381 style: crate::core::CellStyle,
382 ) -> crate::Result<()> {
383 if end_row < start_row || end_col < start_col {
384 return Err(Error::InvalidRange(
385 "range end must not precede its start".to_string(),
386 ));
387 }
388 let sheet_idx = self.find_sheet_index(sheet_name)?;
389 for r in start_row..=end_row {
390 for c in start_col..=end_col {
391 self.sheets[sheet_idx].update_cell_style(r, c, |s| s.merge(&style));
392 }
393 }
394 Ok(())
395 }
396
397 pub fn get_cell_style(
399 &self,
400 sheet_name: Option<&str>,
401 row: usize,
402 col: usize,
403 ) -> crate::Result<Option<crate::core::CellStyle>> {
404 let sheet_idx = self.find_sheet_index(sheet_name)?;
405 Ok(self.sheets[sheet_idx].get_cell_style(row, col).cloned())
406 }
407
408 pub fn set_table_style(&mut self, table_name: &str, style_name: &str) -> crate::Result<()> {
415 for sheet in &mut self.sheets {
416 for table in &mut sheet.tables {
417 if table.name.eq_ignore_ascii_case(table_name) {
418 table.set_style_name(Some(style_name.to_string()));
419 return Ok(());
420 }
421 }
422 }
423 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
424 }
425
426 pub fn get_table_style(&self, table_name: &str) -> crate::Result<Option<String>> {
432 for sheet in &self.sheets {
433 for table in &sheet.tables {
434 if table.name.eq_ignore_ascii_case(table_name) {
435 return Ok(table.style_name.clone());
436 }
437 }
438 }
439 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
440 }
441
442 pub fn set_cell(&mut self, sheet_idx: usize, row: usize, col: usize, value: String) {
444 self.ensure_capacity(sheet_idx, row, col);
445 let sheet = &mut self.sheets[sheet_idx];
446 sheet.set_cell_src(row, col, value);
447 }
448
449 pub fn set_cell_with_type(
451 &mut self,
452 sheet_idx: usize,
453 row: usize,
454 col: usize,
455 value: String,
456 cell_type: crate::core::CellType,
457 ) {
458 self.ensure_capacity(sheet_idx, row, col);
459 let sheet = &mut self.sheets[sheet_idx];
460 sheet.set_cell_with_type(row, col, value, cell_type);
461 }
462
463 pub fn set_cell_type(
465 &mut self,
466 sheet_idx: usize,
467 row: usize,
468 col: usize,
469 cell_type: crate::core::CellType,
470 ) {
471 self.ensure_capacity(sheet_idx, row, col);
472 let sheet = &mut self.sheets[sheet_idx];
473 sheet.set_cell_type(row, col, cell_type);
474 }
475
476 pub fn get_cell_type(&self, sheet_idx: usize, row: usize, col: usize) -> crate::core::CellType {
478 if let Some(sheet) = self.sheets.get(sheet_idx) {
479 sheet.get_cell_type(&crate::core::CellRef::new(row, col))
480 } else {
481 crate::core::CellType::Empty
482 }
483 }
484
485 pub fn insert_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
491 let sheet = &self.sheets[sheet_idx];
492 let at = row_idx.min(sheet.row_count());
495 let edit = GridEdit::insert_row(sheet.id, at);
496 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_row(at));
497 self.evaluate()
498 }
499
500 pub fn delete_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
505 let sheet = &self.sheets[sheet_idx];
506 if row_idx >= sheet.row_count() {
507 return Err(Error::OutOfBounds {
508 what: "row",
509 index: row_idx,
510 len: sheet.row_count(),
511 });
512 }
513 let edit = GridEdit::delete_row(sheet.id, row_idx);
514 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].delete_row(row_idx));
515 self.evaluate()
516 }
517
518 pub fn insert_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
520 let sheet = &self.sheets[sheet_idx];
521 let at = col_idx.min(sheet.col_count());
522 let edit = GridEdit::insert_col(sheet.id, at);
523 self.apply_grid_edit(edit, &[], |wb| wb.sheets[sheet_idx].insert_col(at));
524 self.evaluate()
525 }
526
527 pub fn delete_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
529 let sheet = &self.sheets[sheet_idx];
530 if col_idx >= sheet.col_count() {
531 return Err(Error::OutOfBounds {
532 what: "column",
533 index: col_idx,
534 len: sheet.col_count(),
535 });
536 }
537 let deleted_col_ids = vec![sheet.columns()[col_idx].id];
541 let edit = GridEdit::delete_col(sheet.id, col_idx);
542 self.apply_grid_edit(edit, &deleted_col_ids, |wb| {
543 wb.sheets[sheet_idx].delete_col(col_idx)
544 });
545 self.evaluate()
546 }
547
548 pub fn insert_cells_shift_down(
556 &mut self,
557 sheet_idx: usize,
558 row: usize,
559 first_col: usize,
560 last_col: usize,
561 count: usize,
562 ) -> crate::Result<()> {
563 let sheet = &self.sheets[sheet_idx];
564 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, true);
565 self.apply_grid_edit(edit, &[], |wb| {
566 wb.sheets[sheet_idx].insert_cells_shift_down(row, first_col, last_col, count)
567 });
568 self.evaluate()
569 }
570
571 pub fn delete_cells_shift_up(
574 &mut self,
575 sheet_idx: usize,
576 row: usize,
577 first_col: usize,
578 last_col: usize,
579 count: usize,
580 ) -> crate::Result<()> {
581 let sheet = &self.sheets[sheet_idx];
582 let edit = GridEdit::band_rows(sheet.id, row, count, first_col, last_col, false);
583 self.apply_grid_edit(edit, &[], |wb| {
584 wb.sheets[sheet_idx].delete_cells_shift_up(row, first_col, last_col, count)
585 });
586 self.evaluate()
587 }
588
589 fn apply_grid_edit(
607 &mut self,
608 edit: GridEdit,
609 deleted_col_ids: &[u64],
610 apply: impl FnOnce(&mut Self),
611 ) {
612 let mut shifted: Vec<(usize, usize, usize, CompiledFormula)> = Vec::new();
616 for (sheet_idx, sheet) in self.sheets.iter().enumerate() {
617 for (col_idx, column) in sheet.columns().iter().enumerate() {
618 for row_idx in 0..column.len() {
619 let Some(src) = column.src(row_idx).filter(|s| s.starts_with('=')) else {
620 continue;
621 };
622 let compiled = crate::core::parser::compile_formula(src, &self.sheets);
623 if let Some(next) =
624 crate::core::grid_edit::shift_formula(&compiled, &edit, deleted_col_ids)
625 {
626 shifted.push((sheet_idx, col_idx, row_idx, next));
627 }
628 }
629 }
630 }
631
632 apply(self);
634 self.shift_table_and_pivot_ranges(&edit);
635
636 for (sheet_idx, col_idx, row_idx, compiled) in shifted {
638 let Some((row, col)) = self.moved_cell(&edit, sheet_idx, row_idx, col_idx) else {
639 continue;
641 };
642 let text = crate::core::parser::serialize_formula(&compiled, &self.sheets);
643 self.sheets[sheet_idx].set_cell_src(row, col, text);
644 }
645 }
646
647 fn moved_cell(
650 &self,
651 edit: &GridEdit,
652 sheet_idx: usize,
653 row: usize,
654 col: usize,
655 ) -> Option<(usize, usize)> {
656 if self.sheets[sheet_idx].id != edit.sheet_id || !edit.covers_columns(col, col) {
661 return Some((row, col));
662 }
663 let moved = |index: usize| {
664 crate::core::grid_edit::shift_point(index, edit.at, edit.count, edit.insert)
665 };
666 match edit.axis {
667 Axis::Row => Some((moved(row)?, col)),
668 Axis::Col => Some((row, moved(col)?)),
669 }
670 }
671
672 fn shift_table_and_pivot_ranges(&mut self, edit: &GridEdit) {
678 use crate::core::grid_edit::{shift_point, shift_rect};
679
680 for sheet in &mut self.sheets {
681 if sheet.id != edit.sheet_id {
682 continue;
683 }
684 sheet.tables.retain_mut(|table| {
685 if !edit.covers_columns(table.start_col, table.end_col) {
688 return true;
689 }
690 match shift_rect(
691 edit,
692 table.start_row,
693 table.start_col,
694 table.end_row,
695 table.end_col,
696 ) {
697 Some((r0, c0, r1, c1)) => {
698 if edit.axis == Axis::Col {
702 resize_table_columns(table, c0, c1, edit);
703 }
704 table.start_row = r0;
705 table.start_col = c0;
706 table.end_row = r1;
707 table.end_col = c1;
708 true
709 }
710 None => false,
711 }
712 });
713 }
714
715 for pivot in &mut self.pivot_tables {
716 if let PivotSource::Range {
717 sheet_id,
718 start_row,
719 start_col,
720 end_row,
721 end_col,
722 } = &mut pivot.source
723 && *sheet_id == edit.sheet_id
724 && edit.covers_columns(*start_col, *end_col)
725 && let Some((r0, c0, r1, c1)) =
726 shift_rect(edit, *start_row, *start_col, *end_row, *end_col)
727 {
728 *start_row = r0;
729 *start_col = c0;
730 *end_row = r1;
731 *end_col = c1;
732 }
733
734 if pivot.dest_sheet_id == edit.sheet_id
735 && edit.covers_columns(pivot.dest_col, pivot.dest_col)
736 {
737 match edit.axis {
742 Axis::Row => {
743 pivot.dest_row =
744 shift_point(pivot.dest_row, edit.at, edit.count, edit.insert)
745 .unwrap_or(edit.at);
746 }
747 Axis::Col => {
748 pivot.dest_col =
749 shift_point(pivot.dest_col, edit.at, edit.count, edit.insert)
750 .unwrap_or(edit.at);
751 }
752 }
753 pivot.last_output_end_row = None;
757 pivot.last_output_end_col = None;
758 }
759 }
760 }
761
762 pub fn add_sheet(&mut self, name: &str) -> crate::Result<()> {
764 if self
765 .sheets
766 .iter()
767 .any(|s| s.name.eq_ignore_ascii_case(name))
768 {
769 return Err(Error::AlreadyExists {
770 kind: ObjectKind::Sheet,
771 name: name.to_string(),
772 });
773 }
774
775 let mut columns = Vec::new();
776 for col_idx in 0..5 {
777 let mut col = DataColumn::new(10);
778 col.id = generate_unique_id();
779 col.name = col_idx_to_letters(col_idx);
780 columns.push(col);
781 }
782
783 let new_sheet = Sheet {
784 id: generate_unique_id(),
785 name: name.to_string(),
786 columns,
787 tables: Vec::new(),
788 dependencies: std::collections::HashMap::new(),
789 dependencies_rev: std::collections::HashMap::new(),
790 uncommitted_actions: Vec::new(),
791 locale: self.locale.clone(),
792 };
793
794 self.sheets.push(new_sheet);
795 Ok(())
796 }
797
798 pub fn delete_sheet(&mut self, name: &str) -> crate::Result<()> {
800 let idx = self.find_sheet_index(Some(name))?;
801 if self.sheets.len() <= 1 {
802 return Err(Error::LastSheetInWorkbook);
803 }
804 self.sheets.remove(idx);
805 Ok(())
806 }
807
808 pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
810 let idx = self.find_sheet_index(Some(old_name))?;
811 if self
812 .sheets
813 .iter()
814 .enumerate()
815 .any(|(i, s)| i != idx && s.name.eq_ignore_ascii_case(new_name))
816 {
817 return Err(Error::NameTaken {
818 kind: ObjectKind::Sheet,
819 name: new_name.to_string(),
820 });
821 }
822 self.sheets[idx].name = new_name.to_string();
823 Ok(())
824 }
825
826 #[allow(clippy::too_many_arguments)]
828 pub fn add_chart(
829 &mut self,
830 sheet_name: &str,
831 chart_type: ChartType,
832 range: String,
833 title: Option<String>,
834 anchor: Option<(usize, usize)>,
835 ) -> crate::Result<u64> {
836 let _ = self.find_sheet_index(Some(sheet_name))?;
837 let id = generate_unique_id();
838 let name = format!("Chart {}", self.charts.len() + 1);
839 let (anchor_row, anchor_col) = anchor.unwrap_or((0, 0));
840
841 let chart = Chart {
842 id,
843 name,
844 chart_type,
845 data_range: range,
846 title,
847 xlabel: None,
848 ylabel: None,
849 show_legend: true,
850 anchor_row,
851 anchor_col,
852 };
853
854 self.charts.push(chart);
855 Ok(id)
856 }
857
858 #[allow(clippy::too_many_arguments)]
863 pub fn edit_chart(
864 &mut self,
865 id: u64,
866 name: Option<String>,
867 chart_type: Option<ChartType>,
868 data_range: Option<String>,
869 title: Option<Option<String>>,
870 xlabel: Option<Option<String>>,
871 ylabel: Option<Option<String>>,
872 show_legend: Option<bool>,
873 anchor: Option<(usize, usize)>,
874 ) -> crate::Result<()> {
875 let chart = self
876 .charts
877 .iter_mut()
878 .find(|c| c.id == id)
879 .ok_or_else(|| Error::not_found(ObjectKind::Chart, id.to_string()))?;
880 if let Some(name) = name {
881 chart.name = name;
882 }
883 if let Some(chart_type) = chart_type {
884 chart.chart_type = chart_type;
885 }
886 if let Some(data_range) = data_range {
887 chart.data_range = data_range;
888 }
889 if let Some(title) = title {
890 chart.title = title;
891 }
892 if let Some(xlabel) = xlabel {
893 chart.xlabel = xlabel;
894 }
895 if let Some(ylabel) = ylabel {
896 chart.ylabel = ylabel;
897 }
898 if let Some(show_legend) = show_legend {
899 chart.show_legend = show_legend;
900 }
901 if let Some((anchor_row, anchor_col)) = anchor {
902 chart.anchor_row = anchor_row;
903 chart.anchor_col = anchor_col;
904 }
905 Ok(())
906 }
907
908 pub fn has_vba_project(&self) -> bool {
910 self.vba_project.is_some()
911 }
912
913 pub fn list_vba_modules(&self) -> Vec<&VbaModule> {
915 self.vba_project
916 .as_ref()
917 .map(|p| p.modules.iter().collect())
918 .unwrap_or_default()
919 }
920
921 pub fn ensure_vba_project(&mut self) -> crate::Result<()> {
925 if self.vba_project.is_some() {
926 return Ok(());
927 }
928 self.vba_project = Some(VbaProject::new_empty());
929 Ok(())
930 }
931
932 pub fn add_vba_module(
942 &mut self,
943 name: String,
944 kind: VbaModuleKind,
945 source: String,
946 bound_sheet_id: Option<u64>,
947 ) -> crate::Result<()> {
948 validate_vba_module_name(&name).map_err(|reason| Error::InvalidName {
949 kind: ObjectKind::VbaModule,
950 name: name.clone(),
951 reason,
952 })?;
953 let is_this_workbook = kind == VbaModuleKind::Document && name == "ThisWorkbook";
954 if kind == VbaModuleKind::Document && !is_this_workbook {
955 let sheet_id = bound_sheet_id
956 .ok_or_else(|| Error::Vba("document modules require a bound sheet".to_string()))?;
957 if !self.sheets.iter().any(|s| s.id == sheet_id) {
958 return Err(Error::not_found(ObjectKind::Sheet, sheet_id.to_string()));
959 }
960 }
961 self.ensure_vba_project()?;
962 let project = self.vba_project.as_mut().unwrap();
963 if project.module_name_taken(&name) {
964 return Err(Error::AlreadyExists {
965 kind: ObjectKind::VbaModule,
966 name: name.to_string(),
967 });
968 }
969 if kind == VbaModuleKind::Document
970 && bound_sheet_id.is_some()
971 && project
972 .modules
973 .iter()
974 .any(|m| m.kind == VbaModuleKind::Document && m.bound_sheet_id == bound_sheet_id)
975 {
976 return Err(Error::DocumentModuleExists);
977 }
978 let prefix_bytes = project
986 .modules
987 .first()
988 .map(|m| m.prefix_bytes.clone())
989 .unwrap_or_else(|| project.seed_prefix_bytes.clone());
990 let module_cookie = project
991 .modules
992 .first()
993 .map(|m| m.module_cookie)
994 .unwrap_or(project.seed_module_cookie);
995 let stored_bound_sheet_id = if kind == VbaModuleKind::Document && !is_this_workbook {
996 bound_sheet_id
997 } else {
998 None
999 };
1000 project.modules.push(VbaModule {
1001 name,
1002 kind,
1003 source,
1004 bound_sheet_id: stored_bound_sheet_id,
1005 prefix_bytes,
1006 module_cookie,
1007 cached_compressed_source: None,
1009 });
1010 Ok(())
1011 }
1012
1013 pub fn remove_vba_module(&mut self, name: &str) -> crate::Result<()> {
1020 let project = self
1021 .vba_project
1022 .as_mut()
1023 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1024 let before = project.modules.len();
1025 project
1026 .modules
1027 .retain(|m| !m.name.eq_ignore_ascii_case(name));
1028 if project.modules.len() == before {
1029 return Err(Error::not_found(ObjectKind::VbaModule, name.to_string()));
1030 }
1031 Ok(())
1032 }
1033
1034 pub fn rename_vba_module(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1047 validate_vba_module_name(new_name).map_err(|reason| Error::InvalidName {
1048 kind: ObjectKind::VbaModule,
1049 name: new_name.to_string(),
1050 reason,
1051 })?;
1052 let project = self
1053 .vba_project
1054 .as_mut()
1055 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1056 if !old_name.eq_ignore_ascii_case(new_name) && project.module_name_taken(new_name) {
1057 return Err(Error::AlreadyExists {
1058 kind: ObjectKind::VbaModule,
1059 name: new_name.to_string(),
1060 });
1061 }
1062 let module = project
1063 .find_module_mut(old_name)
1064 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, old_name))?;
1065 module.name = new_name.to_string();
1066 Ok(())
1067 }
1068
1069 pub fn set_vba_module_source(&mut self, name: &str, source: String) -> crate::Result<()> {
1080 let project = self
1081 .vba_project
1082 .as_mut()
1083 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
1084 let module = project
1085 .find_module_mut(name)
1086 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, name))?;
1087 module.source = source;
1088 module.cached_compressed_source = None;
1092 Ok(())
1093 }
1094
1095 pub fn delete_chart(&mut self, id: u64) -> crate::Result<()> {
1097 if let Some(pos) = self.charts.iter().position(|c| c.id == id) {
1098 self.charts.remove(pos);
1099 Ok(())
1100 } else {
1101 Err(Error::not_found(ObjectKind::Chart, id.to_string()))
1102 }
1103 }
1104
1105 pub fn find_table(&self, name: &str) -> Option<(&Sheet, &ExcelTable)> {
1108 self.sheets
1109 .iter()
1110 .find_map(|s| s.find_table(name).map(|t| (s, t)))
1111 }
1112
1113 pub fn list_tables(&self) -> Vec<(&str, &ExcelTable)> {
1116 self.sheets
1117 .iter()
1118 .flat_map(|s| s.tables.iter().map(move |t| (s.name.as_str(), t)))
1119 .collect()
1120 }
1121
1122 fn find_table_sheet_index(&self, name: &str) -> crate::Result<usize> {
1123 self.sheets
1124 .iter()
1125 .position(|s| s.find_table(name).is_some())
1126 .ok_or_else(|| Error::not_found(ObjectKind::Table, name))
1127 }
1128
1129 fn table_name_taken(&self, name: &str) -> bool {
1130 self.sheets
1131 .iter()
1132 .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(name)))
1133 }
1134
1135 #[allow(clippy::too_many_arguments)]
1139 pub fn add_table(
1140 &mut self,
1141 sheet_name: Option<&str>,
1142 name: &str,
1143 start_row: usize,
1144 start_col: usize,
1145 end_row: usize,
1146 end_col: usize,
1147 has_header_row: bool,
1148 has_totals_row: bool,
1149 ) -> crate::Result<u64> {
1150 if self.table_name_taken(name) {
1151 return Err(Error::AlreadyExists {
1152 kind: ObjectKind::Table,
1153 name: name.to_string(),
1154 });
1155 }
1156 let idx = self.find_sheet_index(sheet_name)?;
1157 self.sheets[idx]
1158 .add_table(
1159 name.to_string(),
1160 start_row,
1161 start_col,
1162 end_row,
1163 end_col,
1164 has_header_row,
1165 has_totals_row,
1166 )
1167 .map_err(Error::InvalidArgument)
1168 }
1169
1170 pub fn delete_table(&mut self, name: &str) -> crate::Result<()> {
1172 let idx = self.find_table_sheet_index(name)?;
1173 self.sheets[idx]
1174 .delete_table_by_name(name)
1175 .map_err(Error::InvalidArgument)
1176 }
1177
1178 pub fn rename_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1180 if !old_name.eq_ignore_ascii_case(new_name) && self.table_name_taken(new_name) {
1181 return Err(Error::NameTaken {
1182 kind: ObjectKind::Table,
1183 name: new_name.to_string(),
1184 });
1185 }
1186 let idx = self.find_table_sheet_index(old_name)?;
1187 self.sheets[idx]
1188 .rename_table(old_name, new_name)
1189 .map_err(Error::InvalidArgument)?;
1190 self.rewrite_table_references(old_name, Some(new_name), None);
1194 self.evaluate()
1195 }
1196
1197 fn rewrite_table_references(
1202 &mut self,
1203 table_name: &str,
1204 new_table_name: Option<&str>,
1205 col_rename: Option<(&str, &str)>,
1206 ) {
1207 for sheet in &mut self.sheets {
1208 for col_idx in 0..sheet.columns.len() {
1209 let row_count = sheet.columns[col_idx].src.len();
1210 for row_idx in 0..row_count {
1211 let src = sheet.columns[col_idx].src[row_idx].clone();
1212 if let Some(new_src) = crate::core::parser::rewrite_structured_table_reference(
1213 &src,
1214 table_name,
1215 new_table_name,
1216 col_rename,
1217 ) {
1218 sheet.set_cell_src(row_idx, col_idx, new_src);
1219 }
1220 }
1221 }
1222 }
1223 }
1224
1225 pub fn resize_table(
1227 &mut self,
1228 name: &str,
1229 new_end_row: usize,
1230 new_end_col: usize,
1231 ) -> crate::Result<()> {
1232 let idx = self.find_table_sheet_index(name)?;
1233 self.sheets[idx]
1234 .resize_table(name, new_end_row, new_end_col)
1235 .map_err(Error::InvalidArgument)
1236 }
1237
1238 pub fn rename_table_column(
1240 &mut self,
1241 table_name: &str,
1242 col_index: usize,
1243 new_name: &str,
1244 ) -> crate::Result<()> {
1245 let idx = self.find_table_sheet_index(table_name)?;
1246 let old_col_name = self.sheets[idx]
1247 .find_table(table_name)
1248 .and_then(|t| t.columns.get(col_index).cloned())
1249 .ok_or_else(|| {
1250 Error::InvalidArgument(format!(
1251 "column index {col_index} out of bounds for table '{table_name}'"
1252 ))
1253 })?;
1254 self.sheets[idx]
1255 .rename_table_column(table_name, col_index, new_name)
1256 .map_err(Error::InvalidArgument)?;
1257 self.rewrite_table_references(table_name, None, Some((&old_col_name, new_name)));
1260 self.evaluate()
1261 }
1262
1263 pub fn find_pivot_table(&self, name: &str) -> Option<&PivotTable> {
1265 self.pivot_tables
1266 .iter()
1267 .find(|p| p.name.eq_ignore_ascii_case(name))
1268 }
1269
1270 fn find_pivot_table_index(&self, name: &str) -> crate::Result<usize> {
1271 self.pivot_tables
1272 .iter()
1273 .position(|p| p.name.eq_ignore_ascii_case(name))
1274 .ok_or_else(|| Error::not_found(ObjectKind::PivotTable, name))
1275 }
1276
1277 pub fn list_pivot_tables(&self) -> &[PivotTable] {
1279 &self.pivot_tables
1280 }
1281
1282 fn pivot_table_name_taken(&self, name: &str) -> bool {
1283 self.pivot_tables
1284 .iter()
1285 .any(|p| p.name.eq_ignore_ascii_case(name))
1286 }
1287
1288 #[allow(clippy::too_many_arguments)]
1292 pub fn add_pivot_table_from_table(
1293 &mut self,
1294 name: &str,
1295 source_table_name: &str,
1296 dest_sheet_name: Option<&str>,
1297 dest_row: usize,
1298 dest_col: usize,
1299 grand_totals_row: bool,
1300 grand_totals_col: bool,
1301 ) -> crate::Result<u64> {
1302 if self.pivot_table_name_taken(name) {
1303 return Err(Error::AlreadyExists {
1304 kind: ObjectKind::PivotTable,
1305 name: name.to_string(),
1306 });
1307 }
1308 self.find_table(source_table_name)
1309 .ok_or_else(|| Error::not_found(ObjectKind::Table, source_table_name))?;
1310 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1311 let id = generate_unique_id();
1312 self.pivot_tables.push(PivotTable {
1313 id,
1314 name: name.to_string(),
1315 source: PivotSource::Table {
1316 name: source_table_name.to_string(),
1317 },
1318 dest_sheet_id: self.sheets[dest_idx].id,
1319 dest_row,
1320 dest_col,
1321 row_fields: Vec::new(),
1322 col_fields: Vec::new(),
1323 value_fields: Vec::new(),
1324 filter_fields: Vec::new(),
1325 grand_totals_row,
1326 grand_totals_col,
1327 last_output_end_row: None,
1328 last_output_end_col: None,
1329 });
1330 self.refresh_pivot_table(name)?;
1331 Ok(id)
1332 }
1333
1334 #[allow(clippy::too_many_arguments)]
1337 pub fn add_pivot_table_from_range(
1338 &mut self,
1339 name: &str,
1340 source_sheet_name: Option<&str>,
1341 start_row: usize,
1342 start_col: usize,
1343 end_row: usize,
1344 end_col: usize,
1345 dest_sheet_name: Option<&str>,
1346 dest_row: usize,
1347 dest_col: usize,
1348 grand_totals_row: bool,
1349 grand_totals_col: bool,
1350 ) -> crate::Result<u64> {
1351 if self.pivot_table_name_taken(name) {
1352 return Err(Error::AlreadyExists {
1353 kind: ObjectKind::PivotTable,
1354 name: name.to_string(),
1355 });
1356 }
1357 let src_idx = self.find_sheet_index(source_sheet_name)?;
1358 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1359 let id = generate_unique_id();
1360 self.pivot_tables.push(PivotTable {
1361 id,
1362 name: name.to_string(),
1363 source: PivotSource::Range {
1364 sheet_id: self.sheets[src_idx].id,
1365 start_row,
1366 start_col,
1367 end_row,
1368 end_col,
1369 },
1370 dest_sheet_id: self.sheets[dest_idx].id,
1371 dest_row,
1372 dest_col,
1373 row_fields: Vec::new(),
1374 col_fields: Vec::new(),
1375 value_fields: Vec::new(),
1376 filter_fields: Vec::new(),
1377 grand_totals_row,
1378 grand_totals_col,
1379 last_output_end_row: None,
1380 last_output_end_col: None,
1381 });
1382 self.refresh_pivot_table(name)?;
1383 Ok(id)
1384 }
1385
1386 pub fn delete_pivot_table(&mut self, name: &str) -> crate::Result<()> {
1389 let idx = self.find_pivot_table_index(name)?;
1390 let pivot = self.pivot_tables.remove(idx);
1391 if let (Some(end_row), Some(end_col)) =
1392 (pivot.last_output_end_row, pivot.last_output_end_col)
1393 && let Some(sheet_idx) = self.sheets.iter().position(|s| s.id == pivot.dest_sheet_id)
1394 {
1395 self.clear_range(sheet_idx, pivot.dest_row, pivot.dest_col, end_row, end_col);
1396 }
1397 Ok(())
1398 }
1399
1400 pub fn rename_pivot_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1402 if !old_name.eq_ignore_ascii_case(new_name) && self.pivot_table_name_taken(new_name) {
1403 return Err(Error::NameTaken {
1404 kind: ObjectKind::PivotTable,
1405 name: new_name.to_string(),
1406 });
1407 }
1408 let idx = self.find_pivot_table_index(old_name)?;
1409 self.pivot_tables[idx].name = new_name.to_string();
1410 Ok(())
1411 }
1412
1413 pub fn add_pivot_field(
1427 &mut self,
1428 pivot_name: &str,
1429 area: PivotArea,
1430 column: &str,
1431 aggregation: Option<PivotAggregation>,
1432 ) -> crate::Result<()> {
1433 let idx = self.find_pivot_table_index(pivot_name)?;
1434 if !matches!(area, PivotArea::Value) {
1435 let pivot = &mut self.pivot_tables[idx];
1436 remove_pivot_field(&mut pivot.row_fields, column);
1437 remove_pivot_field(&mut pivot.col_fields, column);
1438 pivot
1439 .filter_fields
1440 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1441 }
1442 match area {
1443 PivotArea::Row => self.pivot_tables[idx]
1444 .row_fields
1445 .push(PivotField::new(column)),
1446 PivotArea::Column => self.pivot_tables[idx]
1447 .col_fields
1448 .push(PivotField::new(column)),
1449 PivotArea::Value => {
1450 let agg = aggregation.unwrap_or(PivotAggregation::Sum);
1451 self.pivot_tables[idx]
1452 .value_fields
1453 .push(PivotValueField::new(column, agg));
1454 }
1455 PivotArea::Filter => self.pivot_tables[idx]
1456 .filter_fields
1457 .push(PivotFilterField::new(column)),
1458 }
1459 self.refresh_pivot_table(pivot_name)
1460 }
1461
1462 pub fn remove_pivot_field(
1465 &mut self,
1466 pivot_name: &str,
1467 area: PivotArea,
1468 column: &str,
1469 ) -> crate::Result<()> {
1470 let idx = self.find_pivot_table_index(pivot_name)?;
1471 let removed = match area {
1472 PivotArea::Row => remove_pivot_field(&mut self.pivot_tables[idx].row_fields, column),
1473 PivotArea::Column => remove_pivot_field(&mut self.pivot_tables[idx].col_fields, column),
1474 PivotArea::Value => {
1475 let before = self.pivot_tables[idx].value_fields.len();
1476 self.pivot_tables[idx]
1477 .value_fields
1478 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1479 before != self.pivot_tables[idx].value_fields.len()
1480 }
1481 PivotArea::Filter => {
1482 let before = self.pivot_tables[idx].filter_fields.len();
1483 self.pivot_tables[idx]
1484 .filter_fields
1485 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1486 before != self.pivot_tables[idx].filter_fields.len()
1487 }
1488 };
1489 if !removed {
1490 return Err(Error::not_found(
1491 ObjectKind::PivotField,
1492 format!("{column}' in pivot table '{pivot_name}"),
1493 ));
1494 }
1495 self.refresh_pivot_table(pivot_name)
1496 }
1497
1498 pub fn set_pivot_filter(
1501 &mut self,
1502 pivot_name: &str,
1503 column: &str,
1504 values: Option<Vec<String>>,
1505 ) -> crate::Result<()> {
1506 let idx = self.find_pivot_table_index(pivot_name)?;
1507 let field = self.pivot_tables[idx]
1508 .filter_fields
1509 .iter_mut()
1510 .find(|f| f.column.eq_ignore_ascii_case(column))
1511 .ok_or_else(|| {
1512 Error::not_found(
1513 ObjectKind::PivotField,
1514 format!("{column}' on pivot table '{pivot_name}"),
1515 )
1516 })?;
1517 field.selected_values = values;
1518 self.refresh_pivot_table(pivot_name)
1519 }
1520
1521 pub fn refresh_pivot_table(&mut self, pivot_name: &str) -> crate::Result<()> {
1526 let idx = self.find_pivot_table_index(pivot_name)?;
1527 let pivot = self.pivot_tables[idx].clone();
1528 let dest_idx = self
1529 .sheets
1530 .iter()
1531 .position(|s| s.id == pivot.dest_sheet_id)
1532 .ok_or_else(|| {
1533 Error::InvalidArgument(
1534 "pivot table's destination sheet no longer exists".to_string(),
1535 )
1536 })?;
1537
1538 let grid: Option<PivotGrid> = if pivot.value_fields.is_empty() {
1539 None
1540 } else {
1541 let sheet_refs: Vec<&Sheet> = self.sheets.iter().collect();
1542 Some(compute_pivot(&sheet_refs, &pivot).map_err(Error::InvalidArgument)?)
1543 };
1544
1545 if let (Some(old_end_row), Some(old_end_col)) =
1548 (pivot.last_output_end_row, pivot.last_output_end_col)
1549 {
1550 self.clear_range(
1551 dest_idx,
1552 pivot.dest_row,
1553 pivot.dest_col,
1554 old_end_row,
1555 old_end_col,
1556 );
1557 }
1558
1559 let new_bounds = grid.as_ref().map(|grid| {
1560 let height = grid.height();
1561 let width = grid.width.max(1);
1562 self.ensure_capacity(
1563 dest_idx,
1564 pivot.dest_row + height.saturating_sub(1),
1565 pivot.dest_col + width.saturating_sub(1),
1566 );
1567
1568 let mut r = pivot.dest_row;
1569 for (name, state) in &grid.filter_rows {
1570 self.set_cell(dest_idx, r, pivot.dest_col, pivot_label_literal(name));
1571 self.set_cell(dest_idx, r, pivot.dest_col + 1, pivot_label_literal(state));
1572 r += 1;
1573 }
1574 if !grid.filter_rows.is_empty() {
1575 r += 1; }
1577 for header in &grid.header_rows {
1578 for (c, text) in header.iter().enumerate() {
1579 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(text));
1580 }
1581 r += 1;
1582 }
1583 for body in &grid.body_rows {
1584 for (c, label) in body.row_labels.iter().enumerate() {
1585 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(label));
1586 }
1587 for (c, val) in body.values.iter().enumerate() {
1588 self.set_cell(
1589 dest_idx,
1590 r,
1591 pivot.dest_col + body.row_labels.len() + c,
1592 pivot_value_literal(val),
1593 );
1594 }
1595 r += 1;
1596 }
1597 (
1598 pivot.dest_row + height.saturating_sub(1),
1599 pivot.dest_col + width.saturating_sub(1),
1600 )
1601 });
1602
1603 self.pivot_tables[idx].last_output_end_row = new_bounds.map(|(r, _)| r);
1604 self.pivot_tables[idx].last_output_end_col = new_bounds.map(|(_, c)| c);
1605 self.evaluate()
1606 }
1607
1608 fn clear_range(
1612 &mut self,
1613 sheet_idx: usize,
1614 start_row: usize,
1615 start_col: usize,
1616 end_row: usize,
1617 end_col: usize,
1618 ) {
1619 if sheet_idx >= self.sheets.len() {
1620 return;
1621 }
1622 let (row_count, col_count) = {
1623 let s = &self.sheets[sheet_idx];
1624 (s.row_count(), s.col_count())
1625 };
1626 if row_count == 0 || col_count == 0 {
1627 return;
1628 }
1629 for r in start_row..=end_row.min(row_count - 1) {
1630 for c in start_col..=end_col.min(col_count - 1) {
1631 self.sheets[sheet_idx].set_cell_src(r, c, String::new());
1632 }
1633 }
1634 }
1635}