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