1use crate::core::parser::col_idx_to_letters;
19use crate::core::xlsx::{export_xlsx_data, import_xlsx_data};
20use crate::core::{
21 ExcelTable, PivotAggregation, PivotArea, PivotField, PivotFilterField, PivotGrid, PivotSource,
22 PivotTable, PivotValueField, VbaModule, VbaModuleKind, VbaProject,
23 chart::{Chart, ChartType},
24 compute_pivot,
25 engine::{Context, DataColumn, ResultData, Sheet, generate_unique_id},
26 validate_vba_module_name,
27};
28use crate::{Error, ObjectKind};
29
30pub struct SheetSummary {
32 pub name: String,
34 pub row_count: usize,
36 pub col_count: usize,
38 pub formula_count: usize,
40}
41
42pub struct WorkbookSummary {
44 pub file_name: String,
46 pub sheet_count: usize,
48 pub chart_count: usize,
50 pub sheets: Vec<SheetSummary>,
52}
53
54pub struct WorkbookManager {
70 pub sheets: Vec<Sheet>,
73 pub charts: Vec<Chart>,
76 pub pivot_tables: Vec<PivotTable>,
79 pub vba_project: Option<VbaProject>,
81}
82
83fn pivot_label_literal(text: &str) -> String {
87 if text.is_empty() {
88 String::new()
89 } else if text.starts_with('=')
90 || text.parse::<f64>().is_ok()
91 || text.eq_ignore_ascii_case("true")
92 || text.eq_ignore_ascii_case("false")
93 {
94 format!("\"{}\"", text)
95 } else {
96 text.to_string()
97 }
98}
99
100fn pivot_value_literal(v: &ResultData) -> String {
104 match v {
105 ResultData::Error(e) => e.clone(),
106 other => other.to_string(),
107 }
108}
109
110fn remove_pivot_field(fields: &mut Vec<PivotField>, column: &str) -> bool {
111 let before = fields.len();
112 fields.retain(|f| !f.column.eq_ignore_ascii_case(column));
113 before != fields.len()
114}
115
116impl WorkbookManager {
117 pub fn load_bytes(buffer: &[u8]) -> crate::Result<Self> {
119 let (imported_tables, charts, pivot_tables, vba_project) =
120 import_xlsx_data(buffer, &[], |_, _, _| {})?;
121
122 let sheets = imported_tables.into_iter().map(|it| it.sheet).collect();
123 Ok(Self {
124 sheets,
125 charts,
126 pivot_tables,
127 vba_project,
128 })
129 }
130
131 pub fn save_bytes(&self) -> crate::Result<Vec<u8>> {
137 export_xlsx_data(
138 &self.sheets,
139 &self.charts,
140 &self.pivot_tables,
141 self.vba_project.as_ref(),
142 )
143 }
144
145 pub fn new_empty() -> crate::Result<Self> {
147 let mut wb = Self {
148 sheets: Vec::new(),
149 charts: Vec::new(),
150 pivot_tables: Vec::new(),
151 vba_project: None,
152 };
153 wb.add_sheet("Sheet1")?;
154 Ok(wb)
155 }
156
157 pub fn evaluate(&mut self) -> crate::Result<()> {
159 if self.sheets.is_empty() {
160 return Ok(());
161 }
162
163 let sheet_order: Vec<String> = self.sheets.iter().map(|s| s.name.clone()).collect();
167
168 for _pass in 0..3 {
179 for sheet in &mut self.sheets {
180 sheet.mark_all_dirty();
181 }
182 for i in 0..self.sheets.len() {
183 let (left, right) = self.sheets.split_at_mut(i);
184 let (target_sheet, right_tail) = right.split_first_mut().unwrap();
185
186 let mut context = Context::new();
187 for s in left.iter() {
188 context.add_table(s.name.clone(), s);
189 }
190 for s in right_tail.iter() {
191 context.add_table(s.name.clone(), s);
192 }
193 context.pivot_tables = &self.pivot_tables;
194 context.sheet_order = sheet_order.clone();
195
196 let _ = target_sheet.commit(Some(&context));
197 }
198 }
199
200 Ok(())
201 }
202
203 pub fn find_sheet_index(&self, name_opt: Option<&str>) -> crate::Result<usize> {
205 if self.sheets.is_empty() {
206 return Err(Error::EmptyWorkbook);
207 }
208
209 match name_opt {
210 Some(name) => {
211 if let Some(idx) = self
212 .sheets
213 .iter()
214 .position(|s| s.name.eq_ignore_ascii_case(name))
215 {
216 Ok(idx)
217 } else {
218 let available: Vec<String> =
219 self.sheets.iter().map(|s| s.name.clone()).collect();
220 Err(Error::not_found_among(
221 ObjectKind::Sheet,
222 name.to_string(),
223 available,
224 ))
225 }
226 }
227 None => Ok(0),
228 }
229 }
230
231 pub fn get_summary(&self, file_name: &str) -> WorkbookSummary {
233 let sheet_summaries = self
234 .sheets
235 .iter()
236 .map(|sheet| {
237 let row_count = sheet.row_count();
238 let col_count = sheet.col_count();
239 let mut formula_count = 0;
240
241 for col in &sheet.columns {
242 for src in &col.src {
243 if src.starts_with('=') {
244 formula_count += 1;
245 }
246 }
247 }
248
249 SheetSummary {
250 name: sheet.name.clone(),
251 row_count,
252 col_count,
253 formula_count,
254 }
255 })
256 .collect();
257
258 WorkbookSummary {
259 file_name: file_name.to_string(),
260 sheet_count: self.sheets.len(),
261 chart_count: self.charts.len(),
262 sheets: sheet_summaries,
263 }
264 }
265
266 pub fn ensure_capacity(&mut self, sheet_idx: usize, target_row: usize, target_col: usize) {
268 if sheet_idx >= self.sheets.len() {
269 return;
270 }
271 self.sheets[sheet_idx].ensure_capacity(target_row, target_col);
272 }
273
274 pub fn set_cell_style(
281 &mut self,
282 sheet_name: Option<&str>,
283 row: usize,
284 col: usize,
285 style: crate::core::CellStyle,
286 ) -> crate::Result<()> {
287 let sheet_idx = self.find_sheet_index(sheet_name)?;
288 self.sheets[sheet_idx].update_cell_style(row, col, |s| s.merge(&style));
289 Ok(())
290 }
291
292 pub fn set_range_style(
294 &mut self,
295 sheet_name: Option<&str>,
296 start_row: usize,
297 start_col: usize,
298 end_row: usize,
299 end_col: usize,
300 style: crate::core::CellStyle,
301 ) -> crate::Result<()> {
302 if end_row < start_row || end_col < start_col {
303 return Err(Error::InvalidRange(
304 "range end must not precede its start".to_string(),
305 ));
306 }
307 let sheet_idx = self.find_sheet_index(sheet_name)?;
308 for r in start_row..=end_row {
309 for c in start_col..=end_col {
310 self.sheets[sheet_idx].update_cell_style(r, c, |s| s.merge(&style));
311 }
312 }
313 Ok(())
314 }
315
316 pub fn get_cell_style(
318 &self,
319 sheet_name: Option<&str>,
320 row: usize,
321 col: usize,
322 ) -> crate::Result<Option<crate::core::CellStyle>> {
323 let sheet_idx = self.find_sheet_index(sheet_name)?;
324 Ok(self.sheets[sheet_idx].get_cell_style(row, col).cloned())
325 }
326
327 pub fn set_table_style(&mut self, table_name: &str, style_name: &str) -> crate::Result<()> {
334 for sheet in &mut self.sheets {
335 for table in &mut sheet.tables {
336 if table.name.eq_ignore_ascii_case(table_name) {
337 table.set_style_name(Some(style_name.to_string()));
338 return Ok(());
339 }
340 }
341 }
342 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
343 }
344
345 pub fn get_table_style(&self, table_name: &str) -> crate::Result<Option<String>> {
351 for sheet in &self.sheets {
352 for table in &sheet.tables {
353 if table.name.eq_ignore_ascii_case(table_name) {
354 return Ok(table.style_name.clone());
355 }
356 }
357 }
358 Err(Error::not_found(ObjectKind::Table, table_name.to_string()))
359 }
360
361 pub fn set_cell(&mut self, sheet_idx: usize, row: usize, col: usize, value: String) {
363 self.ensure_capacity(sheet_idx, row, col);
364 let sheet = &mut self.sheets[sheet_idx];
365 sheet.set_cell_src(row, col, value);
366 }
367
368 pub fn insert_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
370 let sheet = &mut self.sheets[sheet_idx];
371 sheet.insert_row(row_idx);
372 Ok(())
373 }
374
375 pub fn delete_row(&mut self, sheet_idx: usize, row_idx: usize) -> crate::Result<()> {
377 let sheet = &mut self.sheets[sheet_idx];
378 if row_idx >= sheet.row_count() {
379 return Err(Error::OutOfBounds {
380 what: "row",
381 index: row_idx,
382 len: sheet.row_count(),
383 });
384 }
385 sheet.delete_row(row_idx);
386 Ok(())
387 }
388
389 pub fn insert_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
391 let sheet = &mut self.sheets[sheet_idx];
392 sheet.insert_col(col_idx);
393 Ok(())
394 }
395
396 pub fn delete_col(&mut self, sheet_idx: usize, col_idx: usize) -> crate::Result<()> {
398 let sheet = &mut self.sheets[sheet_idx];
399 if col_idx >= sheet.col_count() {
400 return Err(Error::OutOfBounds {
401 what: "column",
402 index: col_idx,
403 len: sheet.col_count(),
404 });
405 }
406 sheet.delete_col(col_idx);
407 Ok(())
408 }
409
410 pub fn add_sheet(&mut self, name: &str) -> crate::Result<()> {
412 if self
413 .sheets
414 .iter()
415 .any(|s| s.name.eq_ignore_ascii_case(name))
416 {
417 return Err(Error::AlreadyExists {
418 kind: ObjectKind::Sheet,
419 name: name.to_string(),
420 });
421 }
422
423 let mut columns = Vec::new();
424 for col_idx in 0..5 {
425 let mut col = DataColumn::new(10);
426 col.id = generate_unique_id();
427 col.name = col_idx_to_letters(col_idx);
428 columns.push(col);
429 }
430
431 let new_sheet = Sheet {
432 id: generate_unique_id(),
433 name: name.to_string(),
434 columns,
435 tables: Vec::new(),
436 dependencies: std::collections::HashMap::new(),
437 dependencies_rev: std::collections::HashMap::new(),
438 uncommitted_actions: Vec::new(),
439 };
440
441 self.sheets.push(new_sheet);
442 Ok(())
443 }
444
445 pub fn delete_sheet(&mut self, name: &str) -> crate::Result<()> {
447 let idx = self.find_sheet_index(Some(name))?;
448 if self.sheets.len() <= 1 {
449 return Err(Error::LastSheetInWorkbook);
450 }
451 self.sheets.remove(idx);
452 Ok(())
453 }
454
455 pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
457 let idx = self.find_sheet_index(Some(old_name))?;
458 if self
459 .sheets
460 .iter()
461 .enumerate()
462 .any(|(i, s)| i != idx && s.name.eq_ignore_ascii_case(new_name))
463 {
464 return Err(Error::NameTaken {
465 kind: ObjectKind::Sheet,
466 name: new_name.to_string(),
467 });
468 }
469 self.sheets[idx].name = new_name.to_string();
470 Ok(())
471 }
472
473 #[allow(clippy::too_many_arguments)]
475 pub fn add_chart(
476 &mut self,
477 sheet_name: &str,
478 chart_type: ChartType,
479 range: String,
480 title: Option<String>,
481 anchor: Option<(usize, usize)>,
482 ) -> crate::Result<u64> {
483 let _ = self.find_sheet_index(Some(sheet_name))?;
484 let id = generate_unique_id();
485 let name = format!("Chart {}", self.charts.len() + 1);
486 let (anchor_row, anchor_col) = anchor.unwrap_or((0, 0));
487
488 let chart = Chart {
489 id,
490 name,
491 chart_type,
492 data_range: range,
493 title,
494 xlabel: None,
495 ylabel: None,
496 show_legend: true,
497 anchor_row,
498 anchor_col,
499 };
500
501 self.charts.push(chart);
502 Ok(id)
503 }
504
505 #[allow(clippy::too_many_arguments)]
510 pub fn edit_chart(
511 &mut self,
512 id: u64,
513 name: Option<String>,
514 chart_type: Option<ChartType>,
515 data_range: Option<String>,
516 title: Option<Option<String>>,
517 xlabel: Option<Option<String>>,
518 ylabel: Option<Option<String>>,
519 show_legend: Option<bool>,
520 anchor: Option<(usize, usize)>,
521 ) -> crate::Result<()> {
522 let chart = self
523 .charts
524 .iter_mut()
525 .find(|c| c.id == id)
526 .ok_or_else(|| Error::not_found(ObjectKind::Chart, id.to_string()))?;
527 if let Some(name) = name {
528 chart.name = name;
529 }
530 if let Some(chart_type) = chart_type {
531 chart.chart_type = chart_type;
532 }
533 if let Some(data_range) = data_range {
534 chart.data_range = data_range;
535 }
536 if let Some(title) = title {
537 chart.title = title;
538 }
539 if let Some(xlabel) = xlabel {
540 chart.xlabel = xlabel;
541 }
542 if let Some(ylabel) = ylabel {
543 chart.ylabel = ylabel;
544 }
545 if let Some(show_legend) = show_legend {
546 chart.show_legend = show_legend;
547 }
548 if let Some((anchor_row, anchor_col)) = anchor {
549 chart.anchor_row = anchor_row;
550 chart.anchor_col = anchor_col;
551 }
552 Ok(())
553 }
554
555 pub fn has_vba_project(&self) -> bool {
557 self.vba_project.is_some()
558 }
559
560 pub fn list_vba_modules(&self) -> Vec<&VbaModule> {
562 self.vba_project
563 .as_ref()
564 .map(|p| p.modules.iter().collect())
565 .unwrap_or_default()
566 }
567
568 pub fn ensure_vba_project(&mut self) -> crate::Result<()> {
572 if self.vba_project.is_some() {
573 return Ok(());
574 }
575 self.vba_project = Some(VbaProject::new_empty());
576 Ok(())
577 }
578
579 pub fn add_vba_module(
589 &mut self,
590 name: String,
591 kind: VbaModuleKind,
592 source: String,
593 bound_sheet_id: Option<u64>,
594 ) -> crate::Result<()> {
595 validate_vba_module_name(&name).map_err(|reason| Error::InvalidName {
596 kind: ObjectKind::VbaModule,
597 name: name.clone(),
598 reason,
599 })?;
600 let is_this_workbook = kind == VbaModuleKind::Document && name == "ThisWorkbook";
601 if kind == VbaModuleKind::Document && !is_this_workbook {
602 let sheet_id = bound_sheet_id
603 .ok_or_else(|| Error::Vba("document modules require a bound sheet".to_string()))?;
604 if !self.sheets.iter().any(|s| s.id == sheet_id) {
605 return Err(Error::not_found(ObjectKind::Sheet, sheet_id.to_string()));
606 }
607 }
608 self.ensure_vba_project()?;
609 let project = self.vba_project.as_mut().unwrap();
610 if project.module_name_taken(&name) {
611 return Err(Error::AlreadyExists {
612 kind: ObjectKind::VbaModule,
613 name: name.to_string(),
614 });
615 }
616 if kind == VbaModuleKind::Document
617 && bound_sheet_id.is_some()
618 && project
619 .modules
620 .iter()
621 .any(|m| m.kind == VbaModuleKind::Document && m.bound_sheet_id == bound_sheet_id)
622 {
623 return Err(Error::DocumentModuleExists);
624 }
625 let prefix_bytes = project
633 .modules
634 .first()
635 .map(|m| m.prefix_bytes.clone())
636 .unwrap_or_else(|| project.seed_prefix_bytes.clone());
637 let module_cookie = project
638 .modules
639 .first()
640 .map(|m| m.module_cookie)
641 .unwrap_or(project.seed_module_cookie);
642 let stored_bound_sheet_id = if kind == VbaModuleKind::Document && !is_this_workbook {
643 bound_sheet_id
644 } else {
645 None
646 };
647 project.modules.push(VbaModule {
648 name,
649 kind,
650 source,
651 bound_sheet_id: stored_bound_sheet_id,
652 prefix_bytes,
653 module_cookie,
654 cached_compressed_source: None,
656 });
657 Ok(())
658 }
659
660 pub fn remove_vba_module(&mut self, name: &str) -> crate::Result<()> {
667 let project = self
668 .vba_project
669 .as_mut()
670 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
671 let before = project.modules.len();
672 project
673 .modules
674 .retain(|m| !m.name.eq_ignore_ascii_case(name));
675 if project.modules.len() == before {
676 return Err(Error::not_found(ObjectKind::VbaModule, name.to_string()));
677 }
678 Ok(())
679 }
680
681 pub fn rename_vba_module(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
694 validate_vba_module_name(new_name).map_err(|reason| Error::InvalidName {
695 kind: ObjectKind::VbaModule,
696 name: new_name.to_string(),
697 reason,
698 })?;
699 let project = self
700 .vba_project
701 .as_mut()
702 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
703 if !old_name.eq_ignore_ascii_case(new_name) && project.module_name_taken(new_name) {
704 return Err(Error::AlreadyExists {
705 kind: ObjectKind::VbaModule,
706 name: new_name.to_string(),
707 });
708 }
709 let module = project
710 .find_module_mut(old_name)
711 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, old_name))?;
712 module.name = new_name.to_string();
713 Ok(())
714 }
715
716 pub fn set_vba_module_source(&mut self, name: &str, source: String) -> crate::Result<()> {
727 let project = self
728 .vba_project
729 .as_mut()
730 .ok_or_else(|| Error::Vba("workbook has no VBA project".to_string()))?;
731 let module = project
732 .find_module_mut(name)
733 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, name))?;
734 module.source = source;
735 module.cached_compressed_source = None;
739 Ok(())
740 }
741
742 pub fn delete_chart(&mut self, id: u64) -> crate::Result<()> {
744 if let Some(pos) = self.charts.iter().position(|c| c.id == id) {
745 self.charts.remove(pos);
746 Ok(())
747 } else {
748 Err(Error::not_found(ObjectKind::Chart, id.to_string()))
749 }
750 }
751
752 pub fn find_table(&self, name: &str) -> Option<(&Sheet, &ExcelTable)> {
755 self.sheets
756 .iter()
757 .find_map(|s| s.find_table(name).map(|t| (s, t)))
758 }
759
760 pub fn list_tables(&self) -> Vec<(&str, &ExcelTable)> {
763 self.sheets
764 .iter()
765 .flat_map(|s| s.tables.iter().map(move |t| (s.name.as_str(), t)))
766 .collect()
767 }
768
769 fn find_table_sheet_index(&self, name: &str) -> crate::Result<usize> {
770 self.sheets
771 .iter()
772 .position(|s| s.find_table(name).is_some())
773 .ok_or_else(|| Error::not_found(ObjectKind::Table, name))
774 }
775
776 fn table_name_taken(&self, name: &str) -> bool {
777 self.sheets
778 .iter()
779 .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(name)))
780 }
781
782 #[allow(clippy::too_many_arguments)]
786 pub fn add_table(
787 &mut self,
788 sheet_name: Option<&str>,
789 name: &str,
790 start_row: usize,
791 start_col: usize,
792 end_row: usize,
793 end_col: usize,
794 has_header_row: bool,
795 has_totals_row: bool,
796 ) -> crate::Result<u64> {
797 if self.table_name_taken(name) {
798 return Err(Error::AlreadyExists {
799 kind: ObjectKind::Table,
800 name: name.to_string(),
801 });
802 }
803 let idx = self.find_sheet_index(sheet_name)?;
804 self.sheets[idx]
805 .add_table(
806 name.to_string(),
807 start_row,
808 start_col,
809 end_row,
810 end_col,
811 has_header_row,
812 has_totals_row,
813 )
814 .map_err(Error::InvalidArgument)
815 }
816
817 pub fn delete_table(&mut self, name: &str) -> crate::Result<()> {
819 let idx = self.find_table_sheet_index(name)?;
820 self.sheets[idx]
821 .delete_table_by_name(name)
822 .map_err(Error::InvalidArgument)
823 }
824
825 pub fn rename_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
827 if !old_name.eq_ignore_ascii_case(new_name) && self.table_name_taken(new_name) {
828 return Err(Error::NameTaken {
829 kind: ObjectKind::Table,
830 name: new_name.to_string(),
831 });
832 }
833 let idx = self.find_table_sheet_index(old_name)?;
834 self.sheets[idx]
835 .rename_table(old_name, new_name)
836 .map_err(Error::InvalidArgument)?;
837 self.rewrite_table_references(old_name, Some(new_name), None);
841 self.evaluate()
842 }
843
844 fn rewrite_table_references(
849 &mut self,
850 table_name: &str,
851 new_table_name: Option<&str>,
852 col_rename: Option<(&str, &str)>,
853 ) {
854 for sheet in &mut self.sheets {
855 for col_idx in 0..sheet.columns.len() {
856 let row_count = sheet.columns[col_idx].src.len();
857 for row_idx in 0..row_count {
858 let src = sheet.columns[col_idx].src[row_idx].clone();
859 if let Some(new_src) = crate::core::parser::rewrite_structured_table_reference(
860 &src,
861 table_name,
862 new_table_name,
863 col_rename,
864 ) {
865 sheet.set_cell_src(row_idx, col_idx, new_src);
866 }
867 }
868 }
869 }
870 }
871
872 pub fn resize_table(
874 &mut self,
875 name: &str,
876 new_end_row: usize,
877 new_end_col: usize,
878 ) -> crate::Result<()> {
879 let idx = self.find_table_sheet_index(name)?;
880 self.sheets[idx]
881 .resize_table(name, new_end_row, new_end_col)
882 .map_err(Error::InvalidArgument)
883 }
884
885 pub fn rename_table_column(
887 &mut self,
888 table_name: &str,
889 col_index: usize,
890 new_name: &str,
891 ) -> crate::Result<()> {
892 let idx = self.find_table_sheet_index(table_name)?;
893 let old_col_name = self.sheets[idx]
894 .find_table(table_name)
895 .and_then(|t| t.columns.get(col_index).cloned())
896 .ok_or_else(|| {
897 Error::InvalidArgument(format!(
898 "column index {col_index} out of bounds for table '{table_name}'"
899 ))
900 })?;
901 self.sheets[idx]
902 .rename_table_column(table_name, col_index, new_name)
903 .map_err(Error::InvalidArgument)?;
904 self.rewrite_table_references(table_name, None, Some((&old_col_name, new_name)));
907 self.evaluate()
908 }
909
910 pub fn find_pivot_table(&self, name: &str) -> Option<&PivotTable> {
912 self.pivot_tables
913 .iter()
914 .find(|p| p.name.eq_ignore_ascii_case(name))
915 }
916
917 fn find_pivot_table_index(&self, name: &str) -> crate::Result<usize> {
918 self.pivot_tables
919 .iter()
920 .position(|p| p.name.eq_ignore_ascii_case(name))
921 .ok_or_else(|| Error::not_found(ObjectKind::PivotTable, name))
922 }
923
924 pub fn list_pivot_tables(&self) -> &[PivotTable] {
926 &self.pivot_tables
927 }
928
929 fn pivot_table_name_taken(&self, name: &str) -> bool {
930 self.pivot_tables
931 .iter()
932 .any(|p| p.name.eq_ignore_ascii_case(name))
933 }
934
935 #[allow(clippy::too_many_arguments)]
939 pub fn add_pivot_table_from_table(
940 &mut self,
941 name: &str,
942 source_table_name: &str,
943 dest_sheet_name: Option<&str>,
944 dest_row: usize,
945 dest_col: usize,
946 grand_totals_row: bool,
947 grand_totals_col: bool,
948 ) -> crate::Result<u64> {
949 if self.pivot_table_name_taken(name) {
950 return Err(Error::AlreadyExists {
951 kind: ObjectKind::PivotTable,
952 name: name.to_string(),
953 });
954 }
955 self.find_table(source_table_name)
956 .ok_or_else(|| Error::not_found(ObjectKind::Table, source_table_name))?;
957 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
958 let id = generate_unique_id();
959 self.pivot_tables.push(PivotTable {
960 id,
961 name: name.to_string(),
962 source: PivotSource::Table {
963 name: source_table_name.to_string(),
964 },
965 dest_sheet_id: self.sheets[dest_idx].id,
966 dest_row,
967 dest_col,
968 row_fields: Vec::new(),
969 col_fields: Vec::new(),
970 value_fields: Vec::new(),
971 filter_fields: Vec::new(),
972 grand_totals_row,
973 grand_totals_col,
974 last_output_end_row: None,
975 last_output_end_col: None,
976 });
977 self.refresh_pivot_table(name)?;
978 Ok(id)
979 }
980
981 #[allow(clippy::too_many_arguments)]
984 pub fn add_pivot_table_from_range(
985 &mut self,
986 name: &str,
987 source_sheet_name: Option<&str>,
988 start_row: usize,
989 start_col: usize,
990 end_row: usize,
991 end_col: usize,
992 dest_sheet_name: Option<&str>,
993 dest_row: usize,
994 dest_col: usize,
995 grand_totals_row: bool,
996 grand_totals_col: bool,
997 ) -> crate::Result<u64> {
998 if self.pivot_table_name_taken(name) {
999 return Err(Error::AlreadyExists {
1000 kind: ObjectKind::PivotTable,
1001 name: name.to_string(),
1002 });
1003 }
1004 let src_idx = self.find_sheet_index(source_sheet_name)?;
1005 let dest_idx = self.find_sheet_index(dest_sheet_name)?;
1006 let id = generate_unique_id();
1007 self.pivot_tables.push(PivotTable {
1008 id,
1009 name: name.to_string(),
1010 source: PivotSource::Range {
1011 sheet_id: self.sheets[src_idx].id,
1012 start_row,
1013 start_col,
1014 end_row,
1015 end_col,
1016 },
1017 dest_sheet_id: self.sheets[dest_idx].id,
1018 dest_row,
1019 dest_col,
1020 row_fields: Vec::new(),
1021 col_fields: Vec::new(),
1022 value_fields: Vec::new(),
1023 filter_fields: Vec::new(),
1024 grand_totals_row,
1025 grand_totals_col,
1026 last_output_end_row: None,
1027 last_output_end_col: None,
1028 });
1029 self.refresh_pivot_table(name)?;
1030 Ok(id)
1031 }
1032
1033 pub fn delete_pivot_table(&mut self, name: &str) -> crate::Result<()> {
1036 let idx = self.find_pivot_table_index(name)?;
1037 let pivot = self.pivot_tables.remove(idx);
1038 if let (Some(end_row), Some(end_col)) =
1039 (pivot.last_output_end_row, pivot.last_output_end_col)
1040 && let Some(sheet_idx) = self.sheets.iter().position(|s| s.id == pivot.dest_sheet_id)
1041 {
1042 self.clear_range(sheet_idx, pivot.dest_row, pivot.dest_col, end_row, end_col);
1043 }
1044 Ok(())
1045 }
1046
1047 pub fn rename_pivot_table(&mut self, old_name: &str, new_name: &str) -> crate::Result<()> {
1049 if !old_name.eq_ignore_ascii_case(new_name) && self.pivot_table_name_taken(new_name) {
1050 return Err(Error::NameTaken {
1051 kind: ObjectKind::PivotTable,
1052 name: new_name.to_string(),
1053 });
1054 }
1055 let idx = self.find_pivot_table_index(old_name)?;
1056 self.pivot_tables[idx].name = new_name.to_string();
1057 Ok(())
1058 }
1059
1060 pub fn add_pivot_field(
1074 &mut self,
1075 pivot_name: &str,
1076 area: PivotArea,
1077 column: &str,
1078 aggregation: Option<PivotAggregation>,
1079 ) -> crate::Result<()> {
1080 let idx = self.find_pivot_table_index(pivot_name)?;
1081 if !matches!(area, PivotArea::Value) {
1082 let pivot = &mut self.pivot_tables[idx];
1083 remove_pivot_field(&mut pivot.row_fields, column);
1084 remove_pivot_field(&mut pivot.col_fields, column);
1085 pivot
1086 .filter_fields
1087 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1088 }
1089 match area {
1090 PivotArea::Row => self.pivot_tables[idx]
1091 .row_fields
1092 .push(PivotField::new(column)),
1093 PivotArea::Column => self.pivot_tables[idx]
1094 .col_fields
1095 .push(PivotField::new(column)),
1096 PivotArea::Value => {
1097 let agg = aggregation.unwrap_or(PivotAggregation::Sum);
1098 self.pivot_tables[idx]
1099 .value_fields
1100 .push(PivotValueField::new(column, agg));
1101 }
1102 PivotArea::Filter => self.pivot_tables[idx]
1103 .filter_fields
1104 .push(PivotFilterField::new(column)),
1105 }
1106 self.refresh_pivot_table(pivot_name)
1107 }
1108
1109 pub fn remove_pivot_field(
1112 &mut self,
1113 pivot_name: &str,
1114 area: PivotArea,
1115 column: &str,
1116 ) -> crate::Result<()> {
1117 let idx = self.find_pivot_table_index(pivot_name)?;
1118 let removed = match area {
1119 PivotArea::Row => remove_pivot_field(&mut self.pivot_tables[idx].row_fields, column),
1120 PivotArea::Column => remove_pivot_field(&mut self.pivot_tables[idx].col_fields, column),
1121 PivotArea::Value => {
1122 let before = self.pivot_tables[idx].value_fields.len();
1123 self.pivot_tables[idx]
1124 .value_fields
1125 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1126 before != self.pivot_tables[idx].value_fields.len()
1127 }
1128 PivotArea::Filter => {
1129 let before = self.pivot_tables[idx].filter_fields.len();
1130 self.pivot_tables[idx]
1131 .filter_fields
1132 .retain(|f| !f.column.eq_ignore_ascii_case(column));
1133 before != self.pivot_tables[idx].filter_fields.len()
1134 }
1135 };
1136 if !removed {
1137 return Err(Error::not_found(
1138 ObjectKind::PivotField,
1139 format!("{column}' in pivot table '{pivot_name}"),
1140 ));
1141 }
1142 self.refresh_pivot_table(pivot_name)
1143 }
1144
1145 pub fn set_pivot_filter(
1148 &mut self,
1149 pivot_name: &str,
1150 column: &str,
1151 values: Option<Vec<String>>,
1152 ) -> crate::Result<()> {
1153 let idx = self.find_pivot_table_index(pivot_name)?;
1154 let field = self.pivot_tables[idx]
1155 .filter_fields
1156 .iter_mut()
1157 .find(|f| f.column.eq_ignore_ascii_case(column))
1158 .ok_or_else(|| {
1159 Error::not_found(
1160 ObjectKind::PivotField,
1161 format!("{column}' on pivot table '{pivot_name}"),
1162 )
1163 })?;
1164 field.selected_values = values;
1165 self.refresh_pivot_table(pivot_name)
1166 }
1167
1168 pub fn refresh_pivot_table(&mut self, pivot_name: &str) -> crate::Result<()> {
1173 let idx = self.find_pivot_table_index(pivot_name)?;
1174 let pivot = self.pivot_tables[idx].clone();
1175 let dest_idx = self
1176 .sheets
1177 .iter()
1178 .position(|s| s.id == pivot.dest_sheet_id)
1179 .ok_or_else(|| {
1180 Error::InvalidArgument(
1181 "pivot table's destination sheet no longer exists".to_string(),
1182 )
1183 })?;
1184
1185 let grid: Option<PivotGrid> = if pivot.value_fields.is_empty() {
1186 None
1187 } else {
1188 let sheet_refs: Vec<&Sheet> = self.sheets.iter().collect();
1189 Some(compute_pivot(&sheet_refs, &pivot).map_err(Error::InvalidArgument)?)
1190 };
1191
1192 if let (Some(old_end_row), Some(old_end_col)) =
1195 (pivot.last_output_end_row, pivot.last_output_end_col)
1196 {
1197 self.clear_range(
1198 dest_idx,
1199 pivot.dest_row,
1200 pivot.dest_col,
1201 old_end_row,
1202 old_end_col,
1203 );
1204 }
1205
1206 let new_bounds = grid.as_ref().map(|grid| {
1207 let height = grid.height();
1208 let width = grid.width.max(1);
1209 self.ensure_capacity(
1210 dest_idx,
1211 pivot.dest_row + height.saturating_sub(1),
1212 pivot.dest_col + width.saturating_sub(1),
1213 );
1214
1215 let mut r = pivot.dest_row;
1216 for (name, state) in &grid.filter_rows {
1217 self.set_cell(dest_idx, r, pivot.dest_col, pivot_label_literal(name));
1218 self.set_cell(dest_idx, r, pivot.dest_col + 1, pivot_label_literal(state));
1219 r += 1;
1220 }
1221 if !grid.filter_rows.is_empty() {
1222 r += 1; }
1224 for header in &grid.header_rows {
1225 for (c, text) in header.iter().enumerate() {
1226 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(text));
1227 }
1228 r += 1;
1229 }
1230 for body in &grid.body_rows {
1231 for (c, label) in body.row_labels.iter().enumerate() {
1232 self.set_cell(dest_idx, r, pivot.dest_col + c, pivot_label_literal(label));
1233 }
1234 for (c, val) in body.values.iter().enumerate() {
1235 self.set_cell(
1236 dest_idx,
1237 r,
1238 pivot.dest_col + body.row_labels.len() + c,
1239 pivot_value_literal(val),
1240 );
1241 }
1242 r += 1;
1243 }
1244 (
1245 pivot.dest_row + height.saturating_sub(1),
1246 pivot.dest_col + width.saturating_sub(1),
1247 )
1248 });
1249
1250 self.pivot_tables[idx].last_output_end_row = new_bounds.map(|(r, _)| r);
1251 self.pivot_tables[idx].last_output_end_col = new_bounds.map(|(_, c)| c);
1252 self.evaluate()
1253 }
1254
1255 fn clear_range(
1259 &mut self,
1260 sheet_idx: usize,
1261 start_row: usize,
1262 start_col: usize,
1263 end_row: usize,
1264 end_col: usize,
1265 ) {
1266 if sheet_idx >= self.sheets.len() {
1267 return;
1268 }
1269 let (row_count, col_count) = {
1270 let s = &self.sheets[sheet_idx];
1271 (s.row_count(), s.col_count())
1272 };
1273 if row_count == 0 || col_count == 0 {
1274 return;
1275 }
1276 for r in start_row..=end_row.min(row_count - 1) {
1277 for c in start_col..=end_col.min(col_count - 1) {
1278 self.sheets[sheet_idx].set_cell_src(r, c, String::new());
1279 }
1280 }
1281 }
1282}