Skip to main content

sheetkit_core/workbook/
sheet_ops.rs

1use super::*;
2
3impl Workbook {
4    /// Return the names of all sheets in workbook order.
5    pub fn sheet_names(&self) -> Vec<&str> {
6        self.worksheets
7            .iter()
8            .map(|(name, _)| name.as_str())
9            .collect()
10    }
11
12    /// Create a new empty sheet with the given name. Returns the 0-based sheet index.
13    pub fn new_sheet(&mut self, name: &str) -> Result<usize> {
14        let idx = crate::sheet::add_sheet(
15            &mut self.workbook_xml,
16            &mut self.workbook_rels,
17            &mut self.content_types,
18            &mut self.worksheets,
19            name,
20            WorksheetXml::default(),
21        )?;
22        if self.sheet_comments.len() < self.worksheets.len() {
23            self.sheet_comments.push(None);
24        }
25        if self.sheet_sparklines.len() < self.worksheets.len() {
26            self.sheet_sparklines.push(vec![]);
27        }
28        if self.sheet_vml.len() < self.worksheets.len() {
29            self.sheet_vml.push(None);
30        }
31        Ok(idx)
32    }
33
34    /// Delete a sheet by name.
35    pub fn delete_sheet(&mut self, name: &str) -> Result<()> {
36        let idx = self.sheet_index(name)?;
37        crate::sheet::delete_sheet(
38            &mut self.workbook_xml,
39            &mut self.workbook_rels,
40            &mut self.content_types,
41            &mut self.worksheets,
42            name,
43        )?;
44
45        if idx < self.sheet_comments.len() {
46            self.sheet_comments.remove(idx);
47        }
48        if idx < self.sheet_sparklines.len() {
49            self.sheet_sparklines.remove(idx);
50        }
51        if idx < self.sheet_vml.len() {
52            self.sheet_vml.remove(idx);
53        }
54        self.reindex_sheet_maps_after_delete(idx);
55        Ok(())
56    }
57
58    /// Rename a sheet.
59    pub fn set_sheet_name(&mut self, old_name: &str, new_name: &str) -> Result<()> {
60        crate::sheet::rename_sheet(
61            &mut self.workbook_xml,
62            &mut self.worksheets,
63            old_name,
64            new_name,
65        )
66    }
67
68    /// Copy a sheet, returning the 0-based index of the new copy.
69    pub fn copy_sheet(&mut self, source: &str, target: &str) -> Result<usize> {
70        let idx = crate::sheet::copy_sheet(
71            &mut self.workbook_xml,
72            &mut self.workbook_rels,
73            &mut self.content_types,
74            &mut self.worksheets,
75            source,
76            target,
77        )?;
78        if self.sheet_comments.len() < self.worksheets.len() {
79            self.sheet_comments.push(None);
80        }
81        let source_sparklines = {
82            let src_idx = self.sheet_index(source).unwrap_or(0);
83            self.sheet_sparklines
84                .get(src_idx)
85                .cloned()
86                .unwrap_or_default()
87        };
88        if self.sheet_sparklines.len() < self.worksheets.len() {
89            self.sheet_sparklines.push(source_sparklines);
90        }
91        if self.sheet_vml.len() < self.worksheets.len() {
92            self.sheet_vml.push(None);
93        }
94        Ok(idx)
95    }
96
97    /// Get a sheet's 0-based index by name. Returns `None` if not found.
98    pub fn get_sheet_index(&self, name: &str) -> Option<usize> {
99        crate::sheet::find_sheet_index(&self.worksheets, name)
100    }
101
102    /// Get the name of the active sheet.
103    pub fn get_active_sheet(&self) -> &str {
104        let idx = crate::sheet::active_sheet_index(&self.workbook_xml);
105        self.worksheets
106            .get(idx)
107            .map(|(n, _)| n.as_str())
108            .unwrap_or_else(|| self.worksheets[0].0.as_str())
109    }
110
111    /// Set the active sheet by name.
112    pub fn set_active_sheet(&mut self, name: &str) -> Result<()> {
113        let idx = crate::sheet::find_sheet_index(&self.worksheets, name).ok_or_else(|| {
114            Error::SheetNotFound {
115                name: name.to_string(),
116            }
117        })?;
118        crate::sheet::set_active_sheet_index(&mut self.workbook_xml, idx as u32);
119        Ok(())
120    }
121
122    /// Create a [`StreamWriter`](crate::stream::StreamWriter) for a new sheet.
123    ///
124    /// The sheet will be added to the workbook when the StreamWriter is applied
125    /// via [`apply_stream_writer`](Self::apply_stream_writer).
126    pub fn new_stream_writer(&self, sheet_name: &str) -> Result<crate::stream::StreamWriter> {
127        crate::sheet::validate_sheet_name(sheet_name)?;
128        if self.worksheets.iter().any(|(n, _)| n == sheet_name) {
129            return Err(Error::SheetAlreadyExists {
130                name: sheet_name.to_string(),
131            });
132        }
133        Ok(crate::stream::StreamWriter::new(sheet_name))
134    }
135
136    /// Apply a completed [`StreamWriter`](crate::stream::StreamWriter) to the
137    /// workbook, adding it as a new sheet.
138    ///
139    /// Returns the 0-based index of the new sheet.
140    pub fn apply_stream_writer(&mut self, writer: crate::stream::StreamWriter) -> Result<usize> {
141        let sheet_name = writer.sheet_name().to_string();
142        let (xml_bytes, sst) = writer.into_parts()?;
143
144        // Parse the XML back into WorksheetXml
145        let mut ws: WorksheetXml = quick_xml::de::from_str(
146            &String::from_utf8(xml_bytes).map_err(|e| Error::Internal(e.to_string()))?,
147        )
148        .map_err(|e| Error::XmlDeserialize(e.to_string()))?;
149
150        // Merge SST entries and build index mapping (old_index -> new_index)
151        let mut sst_remap: Vec<usize> = Vec::with_capacity(sst.len());
152        for i in 0..sst.len() {
153            if let Some(s) = sst.get(i) {
154                let new_idx = self.sst_runtime.add(s);
155                sst_remap.push(new_idx);
156            }
157        }
158
159        // Remap SST indices in the worksheet cells
160        for row in &mut ws.sheet_data.rows {
161            for cell in &mut row.cells {
162                if cell.t.as_deref() == Some("s") {
163                    if let Some(ref v) = cell.v {
164                        if let Ok(old_idx) = v.parse::<usize>() {
165                            if let Some(&new_idx) = sst_remap.get(old_idx) {
166                                cell.v = Some(new_idx.to_string());
167                            }
168                        }
169                    }
170                }
171            }
172        }
173
174        // Add the sheet
175        let idx = crate::sheet::add_sheet(
176            &mut self.workbook_xml,
177            &mut self.workbook_rels,
178            &mut self.content_types,
179            &mut self.worksheets,
180            &sheet_name,
181            ws,
182        )?;
183        if self.sheet_comments.len() < self.worksheets.len() {
184            self.sheet_comments.push(None);
185        }
186        if self.sheet_vml.len() < self.worksheets.len() {
187            self.sheet_vml.push(None);
188        }
189        Ok(idx)
190    }
191
192    /// Insert `count` empty rows starting at `start_row` in the named sheet.
193    pub fn insert_rows(&mut self, sheet: &str, start_row: u32, count: u32) -> Result<()> {
194        let sheet_idx = self.sheet_index(sheet)?;
195        {
196            let ws = &mut self.worksheets[sheet_idx].1;
197            crate::row::insert_rows(ws, start_row, count)?;
198        }
199        self.apply_reference_shift_for_sheet(sheet_idx, |col, row| {
200            if row >= start_row {
201                (col, row + count)
202            } else {
203                (col, row)
204            }
205        })
206    }
207
208    /// Remove a single row from the named sheet, shifting rows below it up.
209    pub fn remove_row(&mut self, sheet: &str, row: u32) -> Result<()> {
210        let sheet_idx = self.sheet_index(sheet)?;
211        {
212            let ws = &mut self.worksheets[sheet_idx].1;
213            crate::row::remove_row(ws, row)?;
214        }
215        self.apply_reference_shift_for_sheet(sheet_idx, |col, r| {
216            if r > row {
217                (col, r - 1)
218            } else {
219                (col, r)
220            }
221        })
222    }
223
224    /// Duplicate a row, inserting the copy directly below.
225    pub fn duplicate_row(&mut self, sheet: &str, row: u32) -> Result<()> {
226        let ws = self.worksheet_mut(sheet)?;
227        crate::row::duplicate_row(ws, row)
228    }
229
230    /// Set the height of a row in points.
231    pub fn set_row_height(&mut self, sheet: &str, row: u32, height: f64) -> Result<()> {
232        let ws = self.worksheet_mut(sheet)?;
233        crate::row::set_row_height(ws, row, height)
234    }
235
236    /// Get the height of a row.
237    pub fn get_row_height(&self, sheet: &str, row: u32) -> Result<Option<f64>> {
238        let ws = self.worksheet_ref(sheet)?;
239        Ok(crate::row::get_row_height(ws, row))
240    }
241
242    /// Set the visibility of a row.
243    pub fn set_row_visible(&mut self, sheet: &str, row: u32, visible: bool) -> Result<()> {
244        let ws = self.worksheet_mut(sheet)?;
245        crate::row::set_row_visible(ws, row, visible)
246    }
247
248    /// Get the visibility of a row. Returns true if visible (not hidden).
249    pub fn get_row_visible(&self, sheet: &str, row: u32) -> Result<bool> {
250        let ws = self.worksheet_ref(sheet)?;
251        Ok(crate::row::get_row_visible(ws, row))
252    }
253
254    /// Set the outline level of a row.
255    pub fn set_row_outline_level(&mut self, sheet: &str, row: u32, level: u8) -> Result<()> {
256        let ws = self.worksheet_mut(sheet)?;
257        crate::row::set_row_outline_level(ws, row, level)
258    }
259
260    /// Get the outline level of a row. Returns 0 if not set.
261    pub fn get_row_outline_level(&self, sheet: &str, row: u32) -> Result<u8> {
262        let ws = self.worksheet_ref(sheet)?;
263        Ok(crate::row::get_row_outline_level(ws, row))
264    }
265
266    /// Set the style for an entire row.
267    ///
268    /// The `style_id` must be a valid index in cellXfs (returned by `add_style`).
269    pub fn set_row_style(&mut self, sheet: &str, row: u32, style_id: u32) -> Result<()> {
270        if style_id as usize >= self.stylesheet.cell_xfs.xfs.len() {
271            return Err(Error::StyleNotFound { id: style_id });
272        }
273        let ws = self.worksheet_mut(sheet)?;
274        crate::row::set_row_style(ws, row, style_id)
275    }
276
277    /// Get the style ID for a row. Returns 0 (default) if not set.
278    pub fn get_row_style(&self, sheet: &str, row: u32) -> Result<u32> {
279        let ws = self.worksheet_ref(sheet)?;
280        Ok(crate::row::get_row_style(ws, row))
281    }
282
283    /// Get all rows with their data from a sheet.
284    ///
285    /// Returns a Vec of `(row_number, Vec<(column_name, CellValue)>)` tuples.
286    /// Only rows that contain at least one cell are included (sparse).
287    #[allow(clippy::type_complexity)]
288    pub fn get_rows(&self, sheet: &str) -> Result<Vec<(u32, Vec<(String, CellValue)>)>> {
289        let ws = self.worksheet_ref(sheet)?;
290        crate::row::get_rows(ws, &self.sst_runtime)
291    }
292
293    /// Get all columns with their data from a sheet.
294    ///
295    /// Returns a Vec of `(column_name, Vec<(row_number, CellValue)>)` tuples.
296    /// Only columns that have data are included (sparse).
297    #[allow(clippy::type_complexity)]
298    pub fn get_cols(&self, sheet: &str) -> Result<Vec<(String, Vec<(u32, CellValue)>)>> {
299        let ws = self.worksheet_ref(sheet)?;
300        crate::col::get_cols(ws, &self.sst_runtime)
301    }
302
303    /// Set the width of a column.
304    pub fn set_col_width(&mut self, sheet: &str, col: &str, width: f64) -> Result<()> {
305        let ws = self.worksheet_mut(sheet)?;
306        crate::col::set_col_width(ws, col, width)
307    }
308
309    /// Get the width of a column.
310    pub fn get_col_width(&self, sheet: &str, col: &str) -> Result<Option<f64>> {
311        let ws = self.worksheet_ref(sheet)?;
312        Ok(crate::col::get_col_width(ws, col))
313    }
314
315    /// Set the visibility of a column.
316    pub fn set_col_visible(&mut self, sheet: &str, col: &str, visible: bool) -> Result<()> {
317        let ws = self.worksheet_mut(sheet)?;
318        crate::col::set_col_visible(ws, col, visible)
319    }
320
321    /// Get the visibility of a column. Returns true if visible (not hidden).
322    pub fn get_col_visible(&self, sheet: &str, col: &str) -> Result<bool> {
323        let ws = self.worksheet_ref(sheet)?;
324        crate::col::get_col_visible(ws, col)
325    }
326
327    /// Set the outline level of a column.
328    pub fn set_col_outline_level(&mut self, sheet: &str, col: &str, level: u8) -> Result<()> {
329        let ws = self.worksheet_mut(sheet)?;
330        crate::col::set_col_outline_level(ws, col, level)
331    }
332
333    /// Get the outline level of a column. Returns 0 if not set.
334    pub fn get_col_outline_level(&self, sheet: &str, col: &str) -> Result<u8> {
335        let ws = self.worksheet_ref(sheet)?;
336        crate::col::get_col_outline_level(ws, col)
337    }
338
339    /// Set the style for an entire column.
340    ///
341    /// The `style_id` must be a valid index in cellXfs (returned by `add_style`).
342    pub fn set_col_style(&mut self, sheet: &str, col: &str, style_id: u32) -> Result<()> {
343        if style_id as usize >= self.stylesheet.cell_xfs.xfs.len() {
344            return Err(Error::StyleNotFound { id: style_id });
345        }
346        let ws = self.worksheet_mut(sheet)?;
347        crate::col::set_col_style(ws, col, style_id)
348    }
349
350    /// Get the style ID for a column. Returns 0 (default) if not set.
351    pub fn get_col_style(&self, sheet: &str, col: &str) -> Result<u32> {
352        let ws = self.worksheet_ref(sheet)?;
353        crate::col::get_col_style(ws, col)
354    }
355
356    /// Insert `count` columns starting at `col` in the named sheet.
357    pub fn insert_cols(&mut self, sheet: &str, col: &str, count: u32) -> Result<()> {
358        let sheet_idx = self.sheet_index(sheet)?;
359        let start_col = column_name_to_number(col)?;
360        {
361            let ws = &mut self.worksheets[sheet_idx].1;
362            crate::col::insert_cols(ws, col, count)?;
363        }
364        self.apply_reference_shift_for_sheet(sheet_idx, |c, row| {
365            if c >= start_col {
366                (c + count, row)
367            } else {
368                (c, row)
369            }
370        })
371    }
372
373    /// Remove a single column from the named sheet.
374    pub fn remove_col(&mut self, sheet: &str, col: &str) -> Result<()> {
375        let sheet_idx = self.sheet_index(sheet)?;
376        let col_num = column_name_to_number(col)?;
377        {
378            let ws = &mut self.worksheets[sheet_idx].1;
379            crate::col::remove_col(ws, col)?;
380        }
381        self.apply_reference_shift_for_sheet(sheet_idx, |c, row| {
382            if c > col_num {
383                (c - 1, row)
384            } else {
385                (c, row)
386            }
387        })
388    }
389
390    /// Reindex per-sheet maps after deleting a sheet.
391    pub(crate) fn reindex_sheet_maps_after_delete(&mut self, removed_idx: usize) {
392        self.worksheet_rels = self
393            .worksheet_rels
394            .iter()
395            .filter_map(|(idx, rels)| {
396                if *idx == removed_idx {
397                    None
398                } else if *idx > removed_idx {
399                    Some((idx - 1, rels.clone()))
400                } else {
401                    Some((*idx, rels.clone()))
402                }
403            })
404            .collect();
405
406        self.worksheet_drawings = self
407            .worksheet_drawings
408            .iter()
409            .filter_map(|(idx, drawing_idx)| {
410                if *idx == removed_idx {
411                    None
412                } else if *idx > removed_idx {
413                    Some((idx - 1, *drawing_idx))
414                } else {
415                    Some((*idx, *drawing_idx))
416                }
417            })
418            .collect();
419    }
420
421    /// Apply a cell-reference shift transformation to sheet-scoped structures.
422    pub(crate) fn apply_reference_shift_for_sheet<F>(
423        &mut self,
424        sheet_idx: usize,
425        shift_cell: F,
426    ) -> Result<()>
427    where
428        F: Fn(u32, u32) -> (u32, u32) + Copy,
429    {
430        {
431            let ws = &mut self.worksheets[sheet_idx].1;
432
433            // Cell formulas.
434            for row in &mut ws.sheet_data.rows {
435                for cell in &mut row.cells {
436                    if let Some(ref mut f) = cell.f {
437                        if let Some(ref mut expr) = f.value {
438                            *expr = shift_cell_references_in_text(expr, shift_cell)?;
439                        }
440                    }
441                }
442            }
443
444            // Merged ranges.
445            if let Some(ref mut merges) = ws.merge_cells {
446                for mc in &mut merges.merge_cells {
447                    mc.reference = shift_cell_references_in_text(&mc.reference, shift_cell)?;
448                }
449            }
450
451            // Auto-filter.
452            if let Some(ref mut af) = ws.auto_filter {
453                af.reference = shift_cell_references_in_text(&af.reference, shift_cell)?;
454            }
455
456            // Data validations.
457            if let Some(ref mut dvs) = ws.data_validations {
458                for dv in &mut dvs.data_validations {
459                    dv.sqref = shift_cell_references_in_text(&dv.sqref, shift_cell)?;
460                    if let Some(ref mut f1) = dv.formula1 {
461                        *f1 = shift_cell_references_in_text(f1, shift_cell)?;
462                    }
463                    if let Some(ref mut f2) = dv.formula2 {
464                        *f2 = shift_cell_references_in_text(f2, shift_cell)?;
465                    }
466                }
467            }
468
469            // Conditional formatting ranges/formulas.
470            for cf in &mut ws.conditional_formatting {
471                cf.sqref = shift_cell_references_in_text(&cf.sqref, shift_cell)?;
472                for rule in &mut cf.cf_rules {
473                    for f in &mut rule.formulas {
474                        *f = shift_cell_references_in_text(f, shift_cell)?;
475                    }
476                }
477            }
478
479            // Hyperlinks.
480            if let Some(ref mut hyperlinks) = ws.hyperlinks {
481                for hl in &mut hyperlinks.hyperlinks {
482                    hl.reference = shift_cell_references_in_text(&hl.reference, shift_cell)?;
483                    if let Some(ref mut loc) = hl.location {
484                        *loc = shift_cell_references_in_text(loc, shift_cell)?;
485                    }
486                }
487            }
488
489            // Pane/selection references.
490            if let Some(ref mut views) = ws.sheet_views {
491                for view in &mut views.sheet_views {
492                    if let Some(ref mut pane) = view.pane {
493                        if let Some(ref mut top_left) = pane.top_left_cell {
494                            *top_left = shift_cell_references_in_text(top_left, shift_cell)?;
495                        }
496                    }
497                    for sel in &mut view.selection {
498                        if let Some(ref mut ac) = sel.active_cell {
499                            *ac = shift_cell_references_in_text(ac, shift_cell)?;
500                        }
501                        if let Some(ref mut sqref) = sel.sqref {
502                            *sqref = shift_cell_references_in_text(sqref, shift_cell)?;
503                        }
504                    }
505                }
506            }
507        }
508
509        // Drawing anchors attached to this sheet.
510        if let Some(&drawing_idx) = self.worksheet_drawings.get(&sheet_idx) {
511            if let Some((_, drawing)) = self.drawings.get_mut(drawing_idx) {
512                for anchor in &mut drawing.one_cell_anchors {
513                    let (new_col, new_row) = shift_cell(anchor.from.col + 1, anchor.from.row + 1);
514                    anchor.from.col = new_col - 1;
515                    anchor.from.row = new_row - 1;
516                }
517                for anchor in &mut drawing.two_cell_anchors {
518                    let (from_col, from_row) = shift_cell(anchor.from.col + 1, anchor.from.row + 1);
519                    anchor.from.col = from_col - 1;
520                    anchor.from.row = from_row - 1;
521                    let (to_col, to_row) = shift_cell(anchor.to.col + 1, anchor.to.row + 1);
522                    anchor.to.col = to_col - 1;
523                    anchor.to.row = to_row - 1;
524                }
525            }
526        }
527
528        Ok(())
529    }
530
531    /// Ensure a drawing exists for the given sheet index, creating one if needed.
532    /// Returns the drawing index.
533    pub(crate) fn ensure_drawing_for_sheet(&mut self, sheet_idx: usize) -> usize {
534        if let Some(&idx) = self.worksheet_drawings.get(&sheet_idx) {
535            return idx;
536        }
537
538        let idx = self.drawings.len();
539        let drawing_path = format!("xl/drawings/drawing{}.xml", idx + 1);
540        self.drawings.push((drawing_path, WsDr::default()));
541        self.worksheet_drawings.insert(sheet_idx, idx);
542
543        // Add drawing reference to the worksheet.
544        let ws_rid = self.next_worksheet_rid(sheet_idx);
545        self.worksheets[sheet_idx].1.drawing = Some(DrawingRef {
546            r_id: ws_rid.clone(),
547        });
548
549        // Add worksheet->drawing relationship.
550        let drawing_rel_target = format!("../drawings/drawing{}.xml", idx + 1);
551        let ws_rels = self
552            .worksheet_rels
553            .entry(sheet_idx)
554            .or_insert_with(|| Relationships {
555                xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
556                relationships: vec![],
557            });
558        ws_rels.relationships.push(Relationship {
559            id: ws_rid,
560            rel_type: rel_types::DRAWING.to_string(),
561            target: drawing_rel_target,
562            target_mode: None,
563        });
564
565        // Add content type for the drawing.
566        self.content_types.overrides.push(ContentTypeOverride {
567            part_name: format!("/xl/drawings/drawing{}.xml", idx + 1),
568            content_type: mime_types::DRAWING.to_string(),
569        });
570
571        idx
572    }
573
574    /// Generate the next relationship ID for a worksheet's rels.
575    pub(crate) fn next_worksheet_rid(&self, sheet_idx: usize) -> String {
576        let existing = self
577            .worksheet_rels
578            .get(&sheet_idx)
579            .map(|r| r.relationships.as_slice())
580            .unwrap_or(&[]);
581        crate::sheet::next_rid(existing)
582    }
583
584    /// Generate the next relationship ID for a drawing's rels.
585    pub(crate) fn next_drawing_rid(&self, drawing_idx: usize) -> String {
586        let existing = self
587            .drawing_rels
588            .get(&drawing_idx)
589            .map(|r| r.relationships.as_slice())
590            .unwrap_or(&[]);
591        crate::sheet::next_rid(existing)
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598    use tempfile::TempDir;
599
600    #[test]
601    fn test_new_sheet_basic() {
602        let mut wb = Workbook::new();
603        let idx = wb.new_sheet("Sheet2").unwrap();
604        assert_eq!(idx, 1);
605        assert_eq!(wb.sheet_names(), vec!["Sheet1", "Sheet2"]);
606    }
607
608    #[test]
609    fn test_new_sheet_duplicate_returns_error() {
610        let mut wb = Workbook::new();
611        let result = wb.new_sheet("Sheet1");
612        assert!(result.is_err());
613        assert!(matches!(
614            result.unwrap_err(),
615            Error::SheetAlreadyExists { .. }
616        ));
617    }
618
619    #[test]
620    fn test_new_sheet_invalid_name_returns_error() {
621        let mut wb = Workbook::new();
622        let result = wb.new_sheet("Bad/Name");
623        assert!(result.is_err());
624        assert!(matches!(result.unwrap_err(), Error::InvalidSheetName(_)));
625    }
626
627    #[test]
628    fn test_delete_sheet_basic() {
629        let mut wb = Workbook::new();
630        wb.new_sheet("Sheet2").unwrap();
631        wb.delete_sheet("Sheet1").unwrap();
632        assert_eq!(wb.sheet_names(), vec!["Sheet2"]);
633    }
634
635    #[test]
636    fn test_delete_last_sheet_returns_error() {
637        let mut wb = Workbook::new();
638        let result = wb.delete_sheet("Sheet1");
639        assert!(result.is_err());
640    }
641
642    #[test]
643    fn test_delete_nonexistent_sheet_returns_error() {
644        let mut wb = Workbook::new();
645        let result = wb.delete_sheet("NoSuchSheet");
646        assert!(result.is_err());
647        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
648    }
649
650    #[test]
651    fn test_set_sheet_name_basic() {
652        let mut wb = Workbook::new();
653        wb.set_sheet_name("Sheet1", "Renamed").unwrap();
654        assert_eq!(wb.sheet_names(), vec!["Renamed"]);
655    }
656
657    #[test]
658    fn test_set_sheet_name_to_existing_returns_error() {
659        let mut wb = Workbook::new();
660        wb.new_sheet("Sheet2").unwrap();
661        let result = wb.set_sheet_name("Sheet1", "Sheet2");
662        assert!(result.is_err());
663        assert!(matches!(
664            result.unwrap_err(),
665            Error::SheetAlreadyExists { .. }
666        ));
667    }
668
669    #[test]
670    fn test_copy_sheet_basic() {
671        let mut wb = Workbook::new();
672        let idx = wb.copy_sheet("Sheet1", "Sheet1 Copy").unwrap();
673        assert_eq!(idx, 1);
674        assert_eq!(wb.sheet_names(), vec!["Sheet1", "Sheet1 Copy"]);
675    }
676
677    #[test]
678    fn test_get_sheet_index() {
679        let mut wb = Workbook::new();
680        wb.new_sheet("Sheet2").unwrap();
681        assert_eq!(wb.get_sheet_index("Sheet1"), Some(0));
682        assert_eq!(wb.get_sheet_index("Sheet2"), Some(1));
683        assert_eq!(wb.get_sheet_index("Nonexistent"), None);
684    }
685
686    #[test]
687    fn test_get_active_sheet_default() {
688        let wb = Workbook::new();
689        assert_eq!(wb.get_active_sheet(), "Sheet1");
690    }
691
692    #[test]
693    fn test_set_active_sheet() {
694        let mut wb = Workbook::new();
695        wb.new_sheet("Sheet2").unwrap();
696        wb.set_active_sheet("Sheet2").unwrap();
697        assert_eq!(wb.get_active_sheet(), "Sheet2");
698    }
699
700    #[test]
701    fn test_set_active_sheet_not_found() {
702        let mut wb = Workbook::new();
703        let result = wb.set_active_sheet("NoSuchSheet");
704        assert!(result.is_err());
705        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
706    }
707
708    #[test]
709    fn test_sheet_management_roundtrip_save_open() {
710        let dir = TempDir::new().unwrap();
711        let path = dir.path().join("sheet_mgmt.xlsx");
712
713        let mut wb = Workbook::new();
714        wb.new_sheet("Data").unwrap();
715        wb.new_sheet("Summary").unwrap();
716        wb.set_sheet_name("Sheet1", "Overview").unwrap();
717        wb.save(&path).unwrap();
718
719        let wb2 = Workbook::open(&path).unwrap();
720        assert_eq!(wb2.sheet_names(), vec!["Overview", "Data", "Summary"]);
721    }
722
723    #[test]
724    fn test_workbook_insert_rows() {
725        let mut wb = Workbook::new();
726        wb.set_cell_value("Sheet1", "A1", "stay").unwrap();
727        wb.set_cell_value("Sheet1", "A2", "shift").unwrap();
728        wb.insert_rows("Sheet1", 2, 1).unwrap();
729
730        assert_eq!(
731            wb.get_cell_value("Sheet1", "A1").unwrap(),
732            CellValue::String("stay".to_string())
733        );
734        assert_eq!(
735            wb.get_cell_value("Sheet1", "A3").unwrap(),
736            CellValue::String("shift".to_string())
737        );
738        assert_eq!(wb.get_cell_value("Sheet1", "A2").unwrap(), CellValue::Empty);
739    }
740
741    #[test]
742    fn test_workbook_insert_rows_updates_formula_and_ranges() {
743        let mut wb = Workbook::new();
744        wb.set_cell_value(
745            "Sheet1",
746            "C1",
747            CellValue::Formula {
748                expr: "SUM(A2:B2)".to_string(),
749                result: None,
750            },
751        )
752        .unwrap();
753        wb.add_data_validation(
754            "Sheet1",
755            &crate::validation::DataValidationConfig::whole_number("A2:A5", 1, 9),
756        )
757        .unwrap();
758        wb.set_auto_filter("Sheet1", "A2:B10").unwrap();
759        wb.merge_cells("Sheet1", "A2", "B3").unwrap();
760
761        wb.insert_rows("Sheet1", 2, 1).unwrap();
762
763        match wb.get_cell_value("Sheet1", "C1").unwrap() {
764            CellValue::Formula { expr, .. } => assert_eq!(expr, "SUM(A3:B3)"),
765            other => panic!("expected formula, got {other:?}"),
766        }
767
768        let validations = wb.get_data_validations("Sheet1").unwrap();
769        assert_eq!(validations.len(), 1);
770        assert_eq!(validations[0].sqref, "A3:A6");
771
772        let merges = wb.get_merge_cells("Sheet1").unwrap();
773        assert_eq!(merges, vec!["A3:B4".to_string()]);
774
775        let ws = wb.worksheet_ref("Sheet1").unwrap();
776        assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A3:B11");
777    }
778
779    #[test]
780    fn test_workbook_insert_rows_sheet_not_found() {
781        let mut wb = Workbook::new();
782        let result = wb.insert_rows("NoSheet", 1, 1);
783        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
784    }
785
786    #[test]
787    fn test_workbook_remove_row() {
788        let mut wb = Workbook::new();
789        wb.set_cell_value("Sheet1", "A1", "first").unwrap();
790        wb.set_cell_value("Sheet1", "A2", "second").unwrap();
791        wb.set_cell_value("Sheet1", "A3", "third").unwrap();
792        wb.remove_row("Sheet1", 2).unwrap();
793
794        assert_eq!(
795            wb.get_cell_value("Sheet1", "A1").unwrap(),
796            CellValue::String("first".to_string())
797        );
798        assert_eq!(
799            wb.get_cell_value("Sheet1", "A2").unwrap(),
800            CellValue::String("third".to_string())
801        );
802    }
803
804    #[test]
805    fn test_workbook_remove_row_sheet_not_found() {
806        let mut wb = Workbook::new();
807        let result = wb.remove_row("NoSheet", 1);
808        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
809    }
810
811    #[test]
812    fn test_workbook_duplicate_row() {
813        let mut wb = Workbook::new();
814        wb.set_cell_value("Sheet1", "A1", "original").unwrap();
815        wb.duplicate_row("Sheet1", 1).unwrap();
816
817        assert_eq!(
818            wb.get_cell_value("Sheet1", "A1").unwrap(),
819            CellValue::String("original".to_string())
820        );
821        // The duplicated row at row 2 has the same SST index.
822        assert_eq!(
823            wb.get_cell_value("Sheet1", "A2").unwrap(),
824            CellValue::String("original".to_string())
825        );
826    }
827
828    #[test]
829    fn test_workbook_set_and_get_row_height() {
830        let mut wb = Workbook::new();
831        wb.set_row_height("Sheet1", 3, 25.0).unwrap();
832        assert_eq!(wb.get_row_height("Sheet1", 3).unwrap(), Some(25.0));
833    }
834
835    #[test]
836    fn test_workbook_get_row_height_sheet_not_found() {
837        let wb = Workbook::new();
838        let result = wb.get_row_height("NoSheet", 1);
839        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
840    }
841
842    #[test]
843    fn test_workbook_set_row_visible() {
844        let mut wb = Workbook::new();
845        wb.set_row_visible("Sheet1", 1, false).unwrap();
846    }
847
848    #[test]
849    fn test_workbook_set_row_visible_sheet_not_found() {
850        let mut wb = Workbook::new();
851        let result = wb.set_row_visible("NoSheet", 1, false);
852        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
853    }
854
855    #[test]
856    fn test_workbook_set_and_get_col_width() {
857        let mut wb = Workbook::new();
858        wb.set_col_width("Sheet1", "A", 18.0).unwrap();
859        assert_eq!(wb.get_col_width("Sheet1", "A").unwrap(), Some(18.0));
860    }
861
862    #[test]
863    fn test_workbook_get_col_width_sheet_not_found() {
864        let wb = Workbook::new();
865        let result = wb.get_col_width("NoSheet", "A");
866        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
867    }
868
869    #[test]
870    fn test_workbook_set_col_visible() {
871        let mut wb = Workbook::new();
872        wb.set_col_visible("Sheet1", "B", false).unwrap();
873    }
874
875    #[test]
876    fn test_workbook_set_col_visible_sheet_not_found() {
877        let mut wb = Workbook::new();
878        let result = wb.set_col_visible("NoSheet", "A", false);
879        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
880    }
881
882    #[test]
883    fn test_workbook_insert_cols() {
884        let mut wb = Workbook::new();
885        wb.set_cell_value("Sheet1", "A1", "a").unwrap();
886        wb.set_cell_value("Sheet1", "B1", "b").unwrap();
887        wb.insert_cols("Sheet1", "B", 1).unwrap();
888
889        assert_eq!(
890            wb.get_cell_value("Sheet1", "A1").unwrap(),
891            CellValue::String("a".to_string())
892        );
893        assert_eq!(wb.get_cell_value("Sheet1", "B1").unwrap(), CellValue::Empty);
894        assert_eq!(
895            wb.get_cell_value("Sheet1", "C1").unwrap(),
896            CellValue::String("b".to_string())
897        );
898    }
899
900    #[test]
901    fn test_workbook_insert_cols_updates_formula_and_ranges() {
902        let mut wb = Workbook::new();
903        wb.set_cell_value(
904            "Sheet1",
905            "D1",
906            CellValue::Formula {
907                expr: "SUM(A1:B1)".to_string(),
908                result: None,
909            },
910        )
911        .unwrap();
912        wb.add_data_validation(
913            "Sheet1",
914            &crate::validation::DataValidationConfig::whole_number("B2:C3", 1, 9),
915        )
916        .unwrap();
917        wb.set_auto_filter("Sheet1", "A1:C10").unwrap();
918        wb.merge_cells("Sheet1", "B3", "C4").unwrap();
919
920        wb.insert_cols("Sheet1", "B", 2).unwrap();
921
922        match wb.get_cell_value("Sheet1", "F1").unwrap() {
923            CellValue::Formula { expr, .. } => assert_eq!(expr, "SUM(A1:D1)"),
924            other => panic!("expected formula, got {other:?}"),
925        }
926
927        let validations = wb.get_data_validations("Sheet1").unwrap();
928        assert_eq!(validations.len(), 1);
929        assert_eq!(validations[0].sqref, "D2:E3");
930
931        let merges = wb.get_merge_cells("Sheet1").unwrap();
932        assert_eq!(merges, vec!["D3:E4".to_string()]);
933
934        let ws = wb.worksheet_ref("Sheet1").unwrap();
935        assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:E10");
936    }
937
938    #[test]
939    fn test_workbook_insert_cols_sheet_not_found() {
940        let mut wb = Workbook::new();
941        let result = wb.insert_cols("NoSheet", "A", 1);
942        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
943    }
944
945    #[test]
946    fn test_workbook_remove_col() {
947        let mut wb = Workbook::new();
948        wb.set_cell_value("Sheet1", "A1", "a").unwrap();
949        wb.set_cell_value("Sheet1", "B1", "b").unwrap();
950        wb.set_cell_value("Sheet1", "C1", "c").unwrap();
951        wb.remove_col("Sheet1", "B").unwrap();
952
953        assert_eq!(
954            wb.get_cell_value("Sheet1", "A1").unwrap(),
955            CellValue::String("a".to_string())
956        );
957        assert_eq!(
958            wb.get_cell_value("Sheet1", "B1").unwrap(),
959            CellValue::String("c".to_string())
960        );
961    }
962
963    #[test]
964    fn test_workbook_remove_col_sheet_not_found() {
965        let mut wb = Workbook::new();
966        let result = wb.remove_col("NoSheet", "A");
967        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
968    }
969
970    #[test]
971    fn test_new_stream_writer_validates_name() {
972        let wb = Workbook::new();
973        let result = wb.new_stream_writer("Bad[Name");
974        assert!(result.is_err());
975        assert!(matches!(result.unwrap_err(), Error::InvalidSheetName(_)));
976    }
977
978    #[test]
979    fn test_new_stream_writer_rejects_duplicate() {
980        let wb = Workbook::new();
981        let result = wb.new_stream_writer("Sheet1");
982        assert!(result.is_err());
983        assert!(matches!(
984            result.unwrap_err(),
985            Error::SheetAlreadyExists { .. }
986        ));
987    }
988
989    #[test]
990    fn test_new_stream_writer_valid_name() {
991        let wb = Workbook::new();
992        let sw = wb.new_stream_writer("StreamSheet").unwrap();
993        assert_eq!(sw.sheet_name(), "StreamSheet");
994    }
995
996    #[test]
997    fn test_apply_stream_writer_adds_sheet() {
998        let mut wb = Workbook::new();
999        let mut sw = wb.new_stream_writer("StreamSheet").unwrap();
1000        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
1001            .unwrap();
1002        let idx = wb.apply_stream_writer(sw).unwrap();
1003        assert_eq!(idx, 1);
1004        assert_eq!(wb.sheet_names(), vec!["Sheet1", "StreamSheet"]);
1005    }
1006
1007    #[test]
1008    fn test_apply_stream_writer_merges_sst() {
1009        let mut wb = Workbook::new();
1010        wb.set_cell_value("Sheet1", "A1", "Existing").unwrap();
1011
1012        let mut sw = wb.new_stream_writer("StreamSheet").unwrap();
1013        sw.write_row(1, &[CellValue::from("New"), CellValue::from("Existing")])
1014            .unwrap();
1015        wb.apply_stream_writer(sw).unwrap();
1016
1017        assert!(wb.sst_runtime.len() >= 2);
1018    }
1019
1020    #[test]
1021    fn test_stream_writer_save_and_reopen() {
1022        let dir = TempDir::new().unwrap();
1023        let path = dir.path().join("stream_test.xlsx");
1024
1025        let mut wb = Workbook::new();
1026        wb.set_cell_value("Sheet1", "A1", "Normal").unwrap();
1027
1028        let mut sw = wb.new_stream_writer("Streamed").unwrap();
1029        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Value")])
1030            .unwrap();
1031        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(100)])
1032            .unwrap();
1033        sw.write_row(3, &[CellValue::from("Bob"), CellValue::from(200)])
1034            .unwrap();
1035        wb.apply_stream_writer(sw).unwrap();
1036
1037        wb.save(&path).unwrap();
1038
1039        let wb2 = Workbook::open(&path).unwrap();
1040        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Streamed"]);
1041        assert_eq!(
1042            wb2.get_cell_value("Sheet1", "A1").unwrap(),
1043            CellValue::String("Normal".to_string())
1044        );
1045        assert_eq!(
1046            wb2.get_cell_value("Streamed", "A1").unwrap(),
1047            CellValue::String("Name".to_string())
1048        );
1049        assert_eq!(
1050            wb2.get_cell_value("Streamed", "B2").unwrap(),
1051            CellValue::Number(100.0)
1052        );
1053        assert_eq!(
1054            wb2.get_cell_value("Streamed", "A3").unwrap(),
1055            CellValue::String("Bob".to_string())
1056        );
1057    }
1058
1059    #[test]
1060    fn test_workbook_get_rows_empty_sheet() {
1061        let wb = Workbook::new();
1062        let rows = wb.get_rows("Sheet1").unwrap();
1063        assert!(rows.is_empty());
1064    }
1065
1066    #[test]
1067    fn test_workbook_get_rows_with_data() {
1068        let mut wb = Workbook::new();
1069        wb.set_cell_value("Sheet1", "A1", "Name").unwrap();
1070        wb.set_cell_value("Sheet1", "B1", 42.0).unwrap();
1071        wb.set_cell_value("Sheet1", "A2", "Alice").unwrap();
1072        wb.set_cell_value("Sheet1", "B2", true).unwrap();
1073
1074        let rows = wb.get_rows("Sheet1").unwrap();
1075        assert_eq!(rows.len(), 2);
1076        assert_eq!(rows[0].0, 1);
1077        assert_eq!(rows[0].1.len(), 2);
1078        assert_eq!(rows[0].1[0].0, "A");
1079        assert_eq!(rows[0].1[0].1, CellValue::String("Name".to_string()));
1080        assert_eq!(rows[0].1[1].0, "B");
1081        assert_eq!(rows[0].1[1].1, CellValue::Number(42.0));
1082        assert_eq!(rows[1].0, 2);
1083        assert_eq!(rows[1].1[0].1, CellValue::String("Alice".to_string()));
1084        assert_eq!(rows[1].1[1].1, CellValue::Bool(true));
1085    }
1086
1087    #[test]
1088    fn test_workbook_get_rows_sheet_not_found() {
1089        let wb = Workbook::new();
1090        assert!(wb.get_rows("NoSheet").is_err());
1091    }
1092
1093    #[test]
1094    fn test_workbook_get_cols_empty_sheet() {
1095        let wb = Workbook::new();
1096        let cols = wb.get_cols("Sheet1").unwrap();
1097        assert!(cols.is_empty());
1098    }
1099
1100    #[test]
1101    fn test_workbook_get_cols_with_data() {
1102        let mut wb = Workbook::new();
1103        wb.set_cell_value("Sheet1", "A1", "Name").unwrap();
1104        wb.set_cell_value("Sheet1", "B1", 42.0).unwrap();
1105        wb.set_cell_value("Sheet1", "A2", "Alice").unwrap();
1106        wb.set_cell_value("Sheet1", "B2", 30.0).unwrap();
1107
1108        let cols = wb.get_cols("Sheet1").unwrap();
1109        assert_eq!(cols.len(), 2);
1110        assert_eq!(cols[0].0, "A");
1111        assert_eq!(cols[0].1.len(), 2);
1112        assert_eq!(cols[0].1[0], (1, CellValue::String("Name".to_string())));
1113        assert_eq!(cols[0].1[1], (2, CellValue::String("Alice".to_string())));
1114        assert_eq!(cols[1].0, "B");
1115        assert_eq!(cols[1].1[0], (1, CellValue::Number(42.0)));
1116        assert_eq!(cols[1].1[1], (2, CellValue::Number(30.0)));
1117    }
1118
1119    #[test]
1120    fn test_workbook_get_cols_sheet_not_found() {
1121        let wb = Workbook::new();
1122        assert!(wb.get_cols("NoSheet").is_err());
1123    }
1124
1125    #[test]
1126    fn test_workbook_get_rows_roundtrip_save_open() {
1127        let mut wb = Workbook::new();
1128        wb.set_cell_value("Sheet1", "A1", "hello").unwrap();
1129        wb.set_cell_value("Sheet1", "B1", 99.0).unwrap();
1130        wb.set_cell_value("Sheet1", "A2", true).unwrap();
1131
1132        let tmp = std::env::temp_dir().join("test_get_rows_roundtrip.xlsx");
1133        wb.save(&tmp).unwrap();
1134
1135        let wb2 = Workbook::open(&tmp).unwrap();
1136        let rows = wb2.get_rows("Sheet1").unwrap();
1137        assert_eq!(rows.len(), 2);
1138        assert_eq!(rows[0].1[0].1, CellValue::String("hello".to_string()));
1139        assert_eq!(rows[0].1[1].1, CellValue::Number(99.0));
1140        assert_eq!(rows[1].1[0].1, CellValue::Bool(true));
1141
1142        let _ = std::fs::remove_file(&tmp);
1143    }
1144}