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