Skip to main content

visi_core/core/
workbook.rs

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}