Skip to main content

sheetkit_core/
row.rs

1//! Row operations for worksheet manipulation.
2//!
3//! All functions operate directly on a [`WorksheetXml`] structure, keeping the
4//! business logic decoupled from the [`Workbook`](crate::workbook::Workbook)
5//! wrapper.
6
7use sheetkit_xml::worksheet::{cell_types, Row, WorksheetXml};
8
9use crate::cell::CellValue;
10use crate::error::{Error, Result};
11use crate::sst::SharedStringTable;
12use crate::utils::cell_ref::{
13    cell_name_to_coordinates, column_number_to_name, coordinates_to_cell_name,
14};
15use crate::utils::constants::{MAX_ROWS, MAX_ROW_HEIGHT};
16
17/// Get all rows with their data from a worksheet.
18///
19/// Returns a Vec of `(row_number, Vec<(column_name, CellValue)>)` tuples.
20/// Only rows that contain at least one cell are included (sparse).
21/// The SST is used to resolve shared string references.
22#[allow(clippy::type_complexity)]
23pub fn get_rows(
24    ws: &WorksheetXml,
25    sst: &SharedStringTable,
26) -> Result<Vec<(u32, Vec<(String, CellValue)>)>> {
27    let mut result = Vec::new();
28
29    for row in &ws.sheet_data.rows {
30        if row.cells.is_empty() {
31            continue;
32        }
33
34        let mut cells = Vec::new();
35        for cell in &row.cells {
36            let (col_num, _) = cell_name_to_coordinates(&cell.r)?;
37            let col_name = column_number_to_name(col_num)?;
38            let value = resolve_cell_value(cell, sst);
39            cells.push((col_name, value));
40        }
41
42        result.push((row.r, cells));
43    }
44
45    Ok(result)
46}
47
48/// Resolve the value of an XML cell to a [`CellValue`], using the SST for
49/// shared string lookups.
50fn resolve_cell_value(cell: &sheetkit_xml::worksheet::Cell, sst: &SharedStringTable) -> CellValue {
51    // Check for formula first.
52    if let Some(ref formula) = cell.f {
53        let expr = formula.value.clone().unwrap_or_default();
54        let cached = match (cell.t.as_deref(), &cell.v) {
55            (Some("b"), Some(v)) => Some(Box::new(CellValue::Bool(v == "1"))),
56            (Some("e"), Some(v)) => Some(Box::new(CellValue::Error(v.clone()))),
57            (_, Some(v)) => v
58                .parse::<f64>()
59                .ok()
60                .map(|n| Box::new(CellValue::Number(n))),
61            _ => None,
62        };
63        return CellValue::Formula {
64            expr,
65            result: cached,
66        };
67    }
68
69    let cell_type = cell.t.as_deref();
70    let cell_value = cell.v.as_deref();
71
72    match (cell_type, cell_value) {
73        (Some(cell_types::SHARED_STRING), Some(v)) => {
74            let idx: usize = match v.parse() {
75                Ok(i) => i,
76                Err(_) => return CellValue::Empty,
77            };
78            let s = sst.get(idx).unwrap_or("").to_string();
79            CellValue::String(s)
80        }
81        (Some(cell_types::BOOLEAN), Some(v)) => CellValue::Bool(v == "1"),
82        (Some(cell_types::ERROR), Some(v)) => CellValue::Error(v.to_string()),
83        (Some(cell_types::INLINE_STRING), _) => {
84            let s = cell
85                .is
86                .as_ref()
87                .and_then(|is| is.t.clone())
88                .unwrap_or_default();
89            CellValue::String(s)
90        }
91        (Some(cell_types::FORMULA_STRING), Some(v)) => CellValue::String(v.to_string()),
92        (None | Some(cell_types::NUMBER), Some(v)) => match v.parse::<f64>() {
93            Ok(n) => CellValue::Number(n),
94            Err(_) => CellValue::Empty,
95        },
96        _ => CellValue::Empty,
97    }
98}
99
100/// Insert `count` empty rows starting at `start_row`, shifting existing rows
101/// at and below `start_row` downward.
102///
103/// Cell references inside shifted rows are updated so that e.g. "B5" becomes
104/// "B8" when 3 rows are inserted at row 5.
105pub fn insert_rows(ws: &mut WorksheetXml, start_row: u32, count: u32) -> Result<()> {
106    if start_row == 0 {
107        return Err(Error::InvalidRowNumber(0));
108    }
109    if count == 0 {
110        return Ok(());
111    }
112    // Validate that shifting won't exceed MAX_ROWS.
113    let max_existing = ws.sheet_data.rows.iter().map(|r| r.r).max().unwrap_or(0);
114    let furthest = max_existing.max(start_row);
115    if furthest.checked_add(count).is_none_or(|v| v > MAX_ROWS) {
116        return Err(Error::InvalidRowNumber(furthest + count));
117    }
118
119    // Shift rows that are >= start_row downward by `count`.
120    // Iterate in reverse to avoid overwriting.
121    for row in ws.sheet_data.rows.iter_mut().rev() {
122        if row.r >= start_row {
123            let new_row_num = row.r + count;
124            shift_row_cells(row, new_row_num)?;
125            row.r = new_row_num;
126        }
127    }
128
129    Ok(())
130}
131
132/// Remove a single row, shifting rows below it upward by one.
133pub fn remove_row(ws: &mut WorksheetXml, row: u32) -> Result<()> {
134    if row == 0 {
135        return Err(Error::InvalidRowNumber(0));
136    }
137
138    // Remove the target row.
139    ws.sheet_data.rows.retain(|r| r.r != row);
140
141    // Shift rows above `row` upward.
142    for r in ws.sheet_data.rows.iter_mut() {
143        if r.r > row {
144            let new_row_num = r.r - 1;
145            shift_row_cells(r, new_row_num)?;
146            r.r = new_row_num;
147        }
148    }
149
150    Ok(())
151}
152
153/// Duplicate a row, inserting the copy directly below the source row.
154pub fn duplicate_row(ws: &mut WorksheetXml, row: u32) -> Result<()> {
155    duplicate_row_to(ws, row, row + 1)
156}
157
158/// Duplicate a row to a specific target row number. Existing rows at and
159/// below `target` are shifted down to make room.
160pub fn duplicate_row_to(ws: &mut WorksheetXml, row: u32, target: u32) -> Result<()> {
161    if row == 0 {
162        return Err(Error::InvalidRowNumber(0));
163    }
164    if target == 0 {
165        return Err(Error::InvalidRowNumber(0));
166    }
167    if target > MAX_ROWS {
168        return Err(Error::InvalidRowNumber(target));
169    }
170
171    // Find and clone the source row.
172    let source = ws
173        .sheet_data
174        .rows
175        .iter()
176        .find(|r| r.r == row)
177        .cloned()
178        .ok_or(Error::InvalidRowNumber(row))?;
179
180    // Shift existing rows at target downward.
181    insert_rows(ws, target, 1)?;
182
183    // Build the duplicated row with updated cell references.
184    let mut new_row = source;
185    shift_row_cells(&mut new_row, target)?;
186    new_row.r = target;
187
188    // Insert the new row in sorted position.
189    let pos = ws
190        .sheet_data
191        .rows
192        .iter()
193        .position(|r| r.r > target)
194        .unwrap_or(ws.sheet_data.rows.len());
195    // Check if there's already a row at target (shouldn't be, but be safe).
196    if let Some(existing) = ws.sheet_data.rows.iter().position(|r| r.r == target) {
197        ws.sheet_data.rows[existing] = new_row;
198    } else {
199        ws.sheet_data.rows.insert(pos, new_row);
200    }
201
202    Ok(())
203}
204
205/// Set the height of a row in points. Creates the row if it does not exist.
206///
207/// Valid range: `0.0 ..= 409.0`.
208pub fn set_row_height(ws: &mut WorksheetXml, row: u32, height: f64) -> Result<()> {
209    if row == 0 || row > MAX_ROWS {
210        return Err(Error::InvalidRowNumber(row));
211    }
212    if !(0.0..=MAX_ROW_HEIGHT).contains(&height) {
213        return Err(Error::RowHeightExceeded {
214            height,
215            max: MAX_ROW_HEIGHT,
216        });
217    }
218
219    let r = find_or_create_row(ws, row);
220    r.ht = Some(height);
221    r.custom_height = Some(true);
222    Ok(())
223}
224
225/// Get the height of a row. Returns `None` if the row does not exist or has
226/// no explicit height set.
227pub fn get_row_height(ws: &WorksheetXml, row: u32) -> Option<f64> {
228    ws.sheet_data
229        .rows
230        .iter()
231        .find(|r| r.r == row)
232        .and_then(|r| r.ht)
233}
234
235/// Set the visibility of a row. Creates the row if it does not exist.
236pub fn set_row_visible(ws: &mut WorksheetXml, row: u32, visible: bool) -> Result<()> {
237    if row == 0 || row > MAX_ROWS {
238        return Err(Error::InvalidRowNumber(row));
239    }
240
241    let r = find_or_create_row(ws, row);
242    r.hidden = if visible { None } else { Some(true) };
243    Ok(())
244}
245
246/// Get the visibility of a row. Returns true if visible (not hidden).
247///
248/// Rows are visible by default, so this returns true if the row does not
249/// exist or has no explicit `hidden` attribute.
250pub fn get_row_visible(ws: &WorksheetXml, row: u32) -> bool {
251    ws.sheet_data
252        .rows
253        .iter()
254        .find(|r| r.r == row)
255        .and_then(|r| r.hidden)
256        .map(|h| !h)
257        .unwrap_or(true)
258}
259
260/// Get the outline (grouping) level of a row. Returns 0 if not set.
261pub fn get_row_outline_level(ws: &WorksheetXml, row: u32) -> u8 {
262    ws.sheet_data
263        .rows
264        .iter()
265        .find(|r| r.r == row)
266        .and_then(|r| r.outline_level)
267        .unwrap_or(0)
268}
269
270/// Set the outline (grouping) level of a row.
271///
272/// Valid range: `0..=7` (Excel supports up to 7 outline levels).
273pub fn set_row_outline_level(ws: &mut WorksheetXml, row: u32, level: u8) -> Result<()> {
274    if row == 0 || row > MAX_ROWS {
275        return Err(Error::InvalidRowNumber(row));
276    }
277    if level > 7 {
278        return Err(Error::OutlineLevelExceeded { level, max: 7 });
279    }
280
281    let r = find_or_create_row(ws, row);
282    r.outline_level = if level == 0 { None } else { Some(level) };
283    Ok(())
284}
285
286/// Set the style for an entire row. The `style_id` is the ID returned by
287/// `add_style()`. Setting a row style applies the `s` attribute on the row
288/// element and marks `customFormat` so Excel honours it.  Existing cells in
289/// the row also have their individual style updated.
290pub fn set_row_style(ws: &mut WorksheetXml, row: u32, style_id: u32) -> Result<()> {
291    if row == 0 || row > MAX_ROWS {
292        return Err(Error::InvalidRowNumber(row));
293    }
294
295    let r = find_or_create_row(ws, row);
296    r.s = Some(style_id);
297    r.custom_format = if style_id == 0 { None } else { Some(true) };
298
299    // Apply to all existing cells in the row.
300    for cell in r.cells.iter_mut() {
301        cell.s = Some(style_id);
302    }
303    Ok(())
304}
305
306/// Get the style ID for a row. Returns 0 (default) if the row does not
307/// exist or has no explicit style.
308pub fn get_row_style(ws: &WorksheetXml, row: u32) -> u32 {
309    ws.sheet_data
310        .rows
311        .iter()
312        .find(|r| r.r == row)
313        .and_then(|r| r.s)
314        .unwrap_or(0)
315}
316
317/// Update all cell references in a row to point to `new_row_num`.
318fn shift_row_cells(row: &mut Row, new_row_num: u32) -> Result<()> {
319    for cell in row.cells.iter_mut() {
320        let (col, _) = cell_name_to_coordinates(&cell.r)?;
321        cell.r = coordinates_to_cell_name(col, new_row_num)?;
322    }
323    Ok(())
324}
325
326/// Find an existing row or create a new empty one, keeping rows sorted.
327fn find_or_create_row(ws: &mut WorksheetXml, row: u32) -> &mut Row {
328    // Check if row exists already.
329    let exists = ws.sheet_data.rows.iter().position(|r| r.r == row);
330    if let Some(idx) = exists {
331        return &mut ws.sheet_data.rows[idx];
332    }
333
334    // Insert in sorted order.
335    let pos = ws
336        .sheet_data
337        .rows
338        .iter()
339        .position(|r| r.r > row)
340        .unwrap_or(ws.sheet_data.rows.len());
341    ws.sheet_data.rows.insert(
342        pos,
343        Row {
344            r: row,
345            spans: None,
346            s: None,
347            custom_format: None,
348            ht: None,
349            hidden: None,
350            custom_height: None,
351            outline_level: None,
352            cells: vec![],
353        },
354    );
355    &mut ws.sheet_data.rows[pos]
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use sheetkit_xml::worksheet::{Cell, SheetData};
362
363    /// Helper: build a minimal worksheet with some pre-populated rows.
364    fn sample_ws() -> WorksheetXml {
365        let mut ws = WorksheetXml::default();
366        ws.sheet_data = SheetData {
367            rows: vec![
368                Row {
369                    r: 1,
370                    spans: None,
371                    s: None,
372                    custom_format: None,
373                    ht: None,
374                    hidden: None,
375                    custom_height: None,
376                    outline_level: None,
377                    cells: vec![
378                        Cell {
379                            r: "A1".to_string(),
380                            s: None,
381                            t: None,
382                            v: Some("10".to_string()),
383                            f: None,
384                            is: None,
385                        },
386                        Cell {
387                            r: "B1".to_string(),
388                            s: None,
389                            t: None,
390                            v: Some("20".to_string()),
391                            f: None,
392                            is: None,
393                        },
394                    ],
395                },
396                Row {
397                    r: 2,
398                    spans: None,
399                    s: None,
400                    custom_format: None,
401                    ht: None,
402                    hidden: None,
403                    custom_height: None,
404                    outline_level: None,
405                    cells: vec![Cell {
406                        r: "A2".to_string(),
407                        s: None,
408                        t: None,
409                        v: Some("30".to_string()),
410                        f: None,
411                        is: None,
412                    }],
413                },
414                Row {
415                    r: 5,
416                    spans: None,
417                    s: None,
418                    custom_format: None,
419                    ht: None,
420                    hidden: None,
421                    custom_height: None,
422                    outline_level: None,
423                    cells: vec![Cell {
424                        r: "C5".to_string(),
425                        s: None,
426                        t: None,
427                        v: Some("50".to_string()),
428                        f: None,
429                        is: None,
430                    }],
431                },
432            ],
433        };
434        ws
435    }
436
437    #[test]
438    fn test_insert_rows_shifts_cells_down() {
439        let mut ws = sample_ws();
440        insert_rows(&mut ws, 2, 3).unwrap();
441
442        // Row 1 should be untouched.
443        assert_eq!(ws.sheet_data.rows[0].r, 1);
444        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
445
446        // Row 2 -> 5 (shifted by 3).
447        assert_eq!(ws.sheet_data.rows[1].r, 5);
448        assert_eq!(ws.sheet_data.rows[1].cells[0].r, "A5");
449
450        // Row 5 -> 8 (shifted by 3).
451        assert_eq!(ws.sheet_data.rows[2].r, 8);
452        assert_eq!(ws.sheet_data.rows[2].cells[0].r, "C8");
453    }
454
455    #[test]
456    fn test_insert_rows_at_row_1() {
457        let mut ws = sample_ws();
458        insert_rows(&mut ws, 1, 2).unwrap();
459
460        // All rows shift by 2.
461        assert_eq!(ws.sheet_data.rows[0].r, 3);
462        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A3");
463        assert_eq!(ws.sheet_data.rows[1].r, 4);
464        assert_eq!(ws.sheet_data.rows[2].r, 7);
465    }
466
467    #[test]
468    fn test_insert_rows_count_zero_is_noop() {
469        let mut ws = sample_ws();
470        insert_rows(&mut ws, 1, 0).unwrap();
471        assert_eq!(ws.sheet_data.rows[0].r, 1);
472        assert_eq!(ws.sheet_data.rows[1].r, 2);
473        assert_eq!(ws.sheet_data.rows[2].r, 5);
474    }
475
476    #[test]
477    fn test_insert_rows_row_zero_returns_error() {
478        let mut ws = sample_ws();
479        let result = insert_rows(&mut ws, 0, 1);
480        assert!(result.is_err());
481    }
482
483    #[test]
484    fn test_insert_rows_beyond_max_returns_error() {
485        let mut ws = WorksheetXml::default();
486        ws.sheet_data.rows.push(Row {
487            r: MAX_ROWS,
488            spans: None,
489            s: None,
490            custom_format: None,
491            ht: None,
492            hidden: None,
493            custom_height: None,
494            outline_level: None,
495            cells: vec![],
496        });
497        let result = insert_rows(&mut ws, 1, 1);
498        assert!(result.is_err());
499    }
500
501    #[test]
502    fn test_insert_rows_on_empty_sheet() {
503        let mut ws = WorksheetXml::default();
504        insert_rows(&mut ws, 1, 5).unwrap();
505        assert!(ws.sheet_data.rows.is_empty());
506    }
507
508    #[test]
509    fn test_remove_row_shifts_up() {
510        let mut ws = sample_ws();
511        remove_row(&mut ws, 2).unwrap();
512
513        // Row 1 untouched.
514        assert_eq!(ws.sheet_data.rows[0].r, 1);
515        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
516
517        // Original row 2 is gone; row 5 shifted to 4.
518        assert_eq!(ws.sheet_data.rows.len(), 2);
519        assert_eq!(ws.sheet_data.rows[1].r, 4);
520        assert_eq!(ws.sheet_data.rows[1].cells[0].r, "C4");
521    }
522
523    #[test]
524    fn test_remove_first_row() {
525        let mut ws = sample_ws();
526        remove_row(&mut ws, 1).unwrap();
527
528        // Remaining rows shift up.
529        assert_eq!(ws.sheet_data.rows[0].r, 1);
530        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
531        assert_eq!(ws.sheet_data.rows[1].r, 4);
532    }
533
534    #[test]
535    fn test_remove_nonexistent_row_still_shifts() {
536        let mut ws = sample_ws();
537        // Row 3 doesn't exist, but rows below should shift.
538        remove_row(&mut ws, 3).unwrap();
539        assert_eq!(ws.sheet_data.rows.len(), 3); // no row removed
540        assert_eq!(ws.sheet_data.rows[2].r, 4); // row 5 -> 4
541    }
542
543    #[test]
544    fn test_remove_row_zero_returns_error() {
545        let mut ws = sample_ws();
546        let result = remove_row(&mut ws, 0);
547        assert!(result.is_err());
548    }
549
550    #[test]
551    fn test_duplicate_row_inserts_copy_below() {
552        let mut ws = sample_ws();
553        duplicate_row(&mut ws, 1).unwrap();
554
555        // Row 1 stays.
556        assert_eq!(ws.sheet_data.rows[0].r, 1);
557        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
558        assert_eq!(ws.sheet_data.rows[0].cells[0].v, Some("10".to_string()));
559
560        // Row 2 is the duplicate with updated refs.
561        assert_eq!(ws.sheet_data.rows[1].r, 2);
562        assert_eq!(ws.sheet_data.rows[1].cells[0].r, "A2");
563        assert_eq!(ws.sheet_data.rows[1].cells[0].v, Some("10".to_string()));
564        assert_eq!(ws.sheet_data.rows[1].cells.len(), 2);
565
566        // Original row 2 shifted to 3.
567        assert_eq!(ws.sheet_data.rows[2].r, 3);
568        assert_eq!(ws.sheet_data.rows[2].cells[0].r, "A3");
569    }
570
571    #[test]
572    fn test_duplicate_row_to_specific_target() {
573        let mut ws = sample_ws();
574        duplicate_row_to(&mut ws, 1, 5).unwrap();
575
576        // Row 1 unchanged.
577        assert_eq!(ws.sheet_data.rows[0].r, 1);
578
579        // Target row 5 is the copy.
580        let row5 = ws.sheet_data.rows.iter().find(|r| r.r == 5).unwrap();
581        assert_eq!(row5.cells[0].r, "A5");
582        assert_eq!(row5.cells[0].v, Some("10".to_string()));
583
584        // Original row 5 shifted to 6.
585        let row6 = ws.sheet_data.rows.iter().find(|r| r.r == 6).unwrap();
586        assert_eq!(row6.cells[0].r, "C6");
587    }
588
589    #[test]
590    fn test_duplicate_nonexistent_row_returns_error() {
591        let mut ws = sample_ws();
592        let result = duplicate_row(&mut ws, 99);
593        assert!(result.is_err());
594    }
595
596    #[test]
597    fn test_set_and_get_row_height() {
598        let mut ws = sample_ws();
599        set_row_height(&mut ws, 1, 25.5).unwrap();
600
601        assert_eq!(get_row_height(&ws, 1), Some(25.5));
602        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
603        assert_eq!(row.custom_height, Some(true));
604    }
605
606    #[test]
607    fn test_set_row_height_creates_row_if_missing() {
608        let mut ws = WorksheetXml::default();
609        set_row_height(&mut ws, 10, 30.0).unwrap();
610
611        assert_eq!(get_row_height(&ws, 10), Some(30.0));
612        assert_eq!(ws.sheet_data.rows.len(), 1);
613        assert_eq!(ws.sheet_data.rows[0].r, 10);
614    }
615
616    #[test]
617    fn test_set_row_height_zero_is_valid() {
618        let mut ws = WorksheetXml::default();
619        set_row_height(&mut ws, 1, 0.0).unwrap();
620        assert_eq!(get_row_height(&ws, 1), Some(0.0));
621    }
622
623    #[test]
624    fn test_set_row_height_max_is_valid() {
625        let mut ws = WorksheetXml::default();
626        set_row_height(&mut ws, 1, 409.0).unwrap();
627        assert_eq!(get_row_height(&ws, 1), Some(409.0));
628    }
629
630    #[test]
631    fn test_set_row_height_exceeds_max_returns_error() {
632        let mut ws = WorksheetXml::default();
633        let result = set_row_height(&mut ws, 1, 410.0);
634        assert!(result.is_err());
635        assert!(matches!(
636            result.unwrap_err(),
637            Error::RowHeightExceeded { .. }
638        ));
639    }
640
641    #[test]
642    fn test_set_row_height_negative_returns_error() {
643        let mut ws = WorksheetXml::default();
644        let result = set_row_height(&mut ws, 1, -1.0);
645        assert!(result.is_err());
646    }
647
648    #[test]
649    fn test_set_row_height_row_zero_returns_error() {
650        let mut ws = WorksheetXml::default();
651        let result = set_row_height(&mut ws, 0, 15.0);
652        assert!(result.is_err());
653    }
654
655    #[test]
656    fn test_get_row_height_nonexistent_returns_none() {
657        let ws = WorksheetXml::default();
658        assert_eq!(get_row_height(&ws, 99), None);
659    }
660
661    #[test]
662    fn test_set_row_hidden() {
663        let mut ws = sample_ws();
664        set_row_visible(&mut ws, 1, false).unwrap();
665
666        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
667        assert_eq!(row.hidden, Some(true));
668    }
669
670    #[test]
671    fn test_set_row_visible_clears_hidden() {
672        let mut ws = sample_ws();
673        set_row_visible(&mut ws, 1, false).unwrap();
674        set_row_visible(&mut ws, 1, true).unwrap();
675
676        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
677        assert_eq!(row.hidden, None);
678    }
679
680    #[test]
681    fn test_set_row_visible_creates_row_if_missing() {
682        let mut ws = WorksheetXml::default();
683        set_row_visible(&mut ws, 3, false).unwrap();
684        assert_eq!(ws.sheet_data.rows.len(), 1);
685        assert_eq!(ws.sheet_data.rows[0].r, 3);
686        assert_eq!(ws.sheet_data.rows[0].hidden, Some(true));
687    }
688
689    #[test]
690    fn test_set_row_visible_row_zero_returns_error() {
691        let mut ws = WorksheetXml::default();
692        let result = set_row_visible(&mut ws, 0, true);
693        assert!(result.is_err());
694    }
695
696    #[test]
697    fn test_set_row_outline_level() {
698        let mut ws = sample_ws();
699        set_row_outline_level(&mut ws, 1, 3).unwrap();
700
701        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
702        assert_eq!(row.outline_level, Some(3));
703    }
704
705    #[test]
706    fn test_set_row_outline_level_zero_clears() {
707        let mut ws = sample_ws();
708        set_row_outline_level(&mut ws, 1, 3).unwrap();
709        set_row_outline_level(&mut ws, 1, 0).unwrap();
710
711        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
712        assert_eq!(row.outline_level, None);
713    }
714
715    #[test]
716    fn test_set_row_outline_level_exceeds_max_returns_error() {
717        let mut ws = sample_ws();
718        let result = set_row_outline_level(&mut ws, 1, 8);
719        assert!(result.is_err());
720    }
721
722    #[test]
723    fn test_set_row_outline_level_row_zero_returns_error() {
724        let mut ws = WorksheetXml::default();
725        let result = set_row_outline_level(&mut ws, 0, 1);
726        assert!(result.is_err());
727    }
728
729    #[test]
730    fn test_get_row_visible_default_is_true() {
731        let ws = sample_ws();
732        assert!(get_row_visible(&ws, 1));
733    }
734
735    #[test]
736    fn test_get_row_visible_nonexistent_row_is_true() {
737        let ws = WorksheetXml::default();
738        assert!(get_row_visible(&ws, 99));
739    }
740
741    #[test]
742    fn test_get_row_visible_after_hide() {
743        let mut ws = sample_ws();
744        set_row_visible(&mut ws, 1, false).unwrap();
745        assert!(!get_row_visible(&ws, 1));
746    }
747
748    #[test]
749    fn test_get_row_visible_after_hide_then_show() {
750        let mut ws = sample_ws();
751        set_row_visible(&mut ws, 1, false).unwrap();
752        set_row_visible(&mut ws, 1, true).unwrap();
753        assert!(get_row_visible(&ws, 1));
754    }
755
756    #[test]
757    fn test_get_row_outline_level_default_is_zero() {
758        let ws = sample_ws();
759        assert_eq!(get_row_outline_level(&ws, 1), 0);
760    }
761
762    #[test]
763    fn test_get_row_outline_level_nonexistent_row() {
764        let ws = WorksheetXml::default();
765        assert_eq!(get_row_outline_level(&ws, 99), 0);
766    }
767
768    #[test]
769    fn test_get_row_outline_level_after_set() {
770        let mut ws = sample_ws();
771        set_row_outline_level(&mut ws, 1, 5).unwrap();
772        assert_eq!(get_row_outline_level(&ws, 1), 5);
773    }
774
775    #[test]
776    fn test_get_row_outline_level_after_clear() {
777        let mut ws = sample_ws();
778        set_row_outline_level(&mut ws, 1, 3).unwrap();
779        set_row_outline_level(&mut ws, 1, 0).unwrap();
780        assert_eq!(get_row_outline_level(&ws, 1), 0);
781    }
782
783    // -- get_rows tests --
784
785    #[test]
786    fn test_get_rows_empty_sheet() {
787        let ws = WorksheetXml::default();
788        let sst = SharedStringTable::new();
789        let rows = get_rows(&ws, &sst).unwrap();
790        assert!(rows.is_empty());
791    }
792
793    #[test]
794    fn test_get_rows_returns_numeric_values() {
795        let ws = sample_ws();
796        let sst = SharedStringTable::new();
797        let rows = get_rows(&ws, &sst).unwrap();
798
799        assert_eq!(rows.len(), 3);
800
801        // Row 1: A1=10, B1=20
802        assert_eq!(rows[0].0, 1);
803        assert_eq!(rows[0].1.len(), 2);
804        assert_eq!(rows[0].1[0].0, "A");
805        assert_eq!(rows[0].1[0].1, CellValue::Number(10.0));
806        assert_eq!(rows[0].1[1].0, "B");
807        assert_eq!(rows[0].1[1].1, CellValue::Number(20.0));
808
809        // Row 2: A2=30
810        assert_eq!(rows[1].0, 2);
811        assert_eq!(rows[1].1.len(), 1);
812        assert_eq!(rows[1].1[0].0, "A");
813        assert_eq!(rows[1].1[0].1, CellValue::Number(30.0));
814
815        // Row 5: C5=50 (sparse -- rows 3 and 4 skipped)
816        assert_eq!(rows[2].0, 5);
817        assert_eq!(rows[2].1.len(), 1);
818        assert_eq!(rows[2].1[0].0, "C");
819        assert_eq!(rows[2].1[0].1, CellValue::Number(50.0));
820    }
821
822    #[test]
823    fn test_get_rows_shared_strings() {
824        let mut sst = SharedStringTable::new();
825        sst.add("hello");
826        sst.add("world");
827
828        let mut ws = WorksheetXml::default();
829        ws.sheet_data = SheetData {
830            rows: vec![Row {
831                r: 1,
832                spans: None,
833                s: None,
834                custom_format: None,
835                ht: None,
836                hidden: None,
837                custom_height: None,
838                outline_level: None,
839                cells: vec![
840                    Cell {
841                        r: "A1".to_string(),
842                        s: None,
843                        t: Some("s".to_string()),
844                        v: Some("0".to_string()),
845                        f: None,
846                        is: None,
847                    },
848                    Cell {
849                        r: "B1".to_string(),
850                        s: None,
851                        t: Some("s".to_string()),
852                        v: Some("1".to_string()),
853                        f: None,
854                        is: None,
855                    },
856                ],
857            }],
858        };
859
860        let rows = get_rows(&ws, &sst).unwrap();
861        assert_eq!(rows.len(), 1);
862        assert_eq!(rows[0].1[0].1, CellValue::String("hello".to_string()));
863        assert_eq!(rows[0].1[1].1, CellValue::String("world".to_string()));
864    }
865
866    #[test]
867    fn test_get_rows_mixed_types() {
868        let mut sst = SharedStringTable::new();
869        sst.add("text");
870
871        let mut ws = WorksheetXml::default();
872        ws.sheet_data = SheetData {
873            rows: vec![Row {
874                r: 1,
875                spans: None,
876                s: None,
877                custom_format: None,
878                ht: None,
879                hidden: None,
880                custom_height: None,
881                outline_level: None,
882                cells: vec![
883                    Cell {
884                        r: "A1".to_string(),
885                        s: None,
886                        t: Some("s".to_string()),
887                        v: Some("0".to_string()),
888                        f: None,
889                        is: None,
890                    },
891                    Cell {
892                        r: "B1".to_string(),
893                        s: None,
894                        t: None,
895                        v: Some("42.5".to_string()),
896                        f: None,
897                        is: None,
898                    },
899                    Cell {
900                        r: "C1".to_string(),
901                        s: None,
902                        t: Some("b".to_string()),
903                        v: Some("1".to_string()),
904                        f: None,
905                        is: None,
906                    },
907                    Cell {
908                        r: "D1".to_string(),
909                        s: None,
910                        t: Some("e".to_string()),
911                        v: Some("#DIV/0!".to_string()),
912                        f: None,
913                        is: None,
914                    },
915                ],
916            }],
917        };
918
919        let rows = get_rows(&ws, &sst).unwrap();
920        assert_eq!(rows.len(), 1);
921        assert_eq!(rows[0].1[0].1, CellValue::String("text".to_string()));
922        assert_eq!(rows[0].1[1].1, CellValue::Number(42.5));
923        assert_eq!(rows[0].1[2].1, CellValue::Bool(true));
924        assert_eq!(rows[0].1[3].1, CellValue::Error("#DIV/0!".to_string()));
925    }
926
927    #[test]
928    fn test_get_rows_skips_rows_with_no_cells() {
929        let mut ws = WorksheetXml::default();
930        ws.sheet_data = SheetData {
931            rows: vec![
932                Row {
933                    r: 1,
934                    spans: None,
935                    s: None,
936                    custom_format: None,
937                    ht: None,
938                    hidden: None,
939                    custom_height: None,
940                    outline_level: None,
941                    cells: vec![Cell {
942                        r: "A1".to_string(),
943                        s: None,
944                        t: None,
945                        v: Some("1".to_string()),
946                        f: None,
947                        is: None,
948                    }],
949                },
950                // Row 2 exists but has no cells (e.g., only has row height set).
951                Row {
952                    r: 2,
953                    spans: None,
954                    s: None,
955                    custom_format: None,
956                    ht: Some(30.0),
957                    hidden: None,
958                    custom_height: Some(true),
959                    outline_level: None,
960                    cells: vec![],
961                },
962                Row {
963                    r: 3,
964                    spans: None,
965                    s: None,
966                    custom_format: None,
967                    ht: None,
968                    hidden: None,
969                    custom_height: None,
970                    outline_level: None,
971                    cells: vec![Cell {
972                        r: "A3".to_string(),
973                        s: None,
974                        t: None,
975                        v: Some("3".to_string()),
976                        f: None,
977                        is: None,
978                    }],
979                },
980            ],
981        };
982
983        let sst = SharedStringTable::new();
984        let rows = get_rows(&ws, &sst).unwrap();
985        // Only rows 1 and 3 should be returned (row 2 has no cells).
986        assert_eq!(rows.len(), 2);
987        assert_eq!(rows[0].0, 1);
988        assert_eq!(rows[1].0, 3);
989    }
990
991    #[test]
992    fn test_get_rows_with_formula() {
993        let mut ws = WorksheetXml::default();
994        ws.sheet_data = SheetData {
995            rows: vec![Row {
996                r: 1,
997                spans: None,
998                s: None,
999                custom_format: None,
1000                ht: None,
1001                hidden: None,
1002                custom_height: None,
1003                outline_level: None,
1004                cells: vec![Cell {
1005                    r: "A1".to_string(),
1006                    s: None,
1007                    t: None,
1008                    v: Some("42".to_string()),
1009                    f: Some(sheetkit_xml::worksheet::CellFormula {
1010                        t: None,
1011                        reference: None,
1012                        si: None,
1013                        value: Some("B1+C1".to_string()),
1014                    }),
1015                    is: None,
1016                }],
1017            }],
1018        };
1019
1020        let sst = SharedStringTable::new();
1021        let rows = get_rows(&ws, &sst).unwrap();
1022        assert_eq!(rows.len(), 1);
1023        match &rows[0].1[0].1 {
1024            CellValue::Formula { expr, result } => {
1025                assert_eq!(expr, "B1+C1");
1026                assert_eq!(*result, Some(Box::new(CellValue::Number(42.0))));
1027            }
1028            _ => panic!("expected Formula"),
1029        }
1030    }
1031
1032    #[test]
1033    fn test_get_rows_with_inline_string() {
1034        let mut ws = WorksheetXml::default();
1035        ws.sheet_data = SheetData {
1036            rows: vec![Row {
1037                r: 1,
1038                spans: None,
1039                s: None,
1040                custom_format: None,
1041                ht: None,
1042                hidden: None,
1043                custom_height: None,
1044                outline_level: None,
1045                cells: vec![Cell {
1046                    r: "A1".to_string(),
1047                    s: None,
1048                    t: Some("inlineStr".to_string()),
1049                    v: None,
1050                    f: None,
1051                    is: Some(sheetkit_xml::worksheet::InlineString {
1052                        t: Some("inline text".to_string()),
1053                    }),
1054                }],
1055            }],
1056        };
1057
1058        let sst = SharedStringTable::new();
1059        let rows = get_rows(&ws, &sst).unwrap();
1060        assert_eq!(rows.len(), 1);
1061        assert_eq!(rows[0].1[0].1, CellValue::String("inline text".to_string()));
1062    }
1063
1064    // -- set_row_style / get_row_style tests --
1065
1066    #[test]
1067    fn test_get_row_style_default_is_zero() {
1068        let ws = WorksheetXml::default();
1069        assert_eq!(get_row_style(&ws, 1), 0);
1070    }
1071
1072    #[test]
1073    fn test_get_row_style_nonexistent_row_is_zero() {
1074        let ws = sample_ws();
1075        assert_eq!(get_row_style(&ws, 99), 0);
1076    }
1077
1078    #[test]
1079    fn test_set_row_style_applies_style() {
1080        let mut ws = sample_ws();
1081        set_row_style(&mut ws, 1, 5).unwrap();
1082
1083        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
1084        assert_eq!(row.s, Some(5));
1085        assert_eq!(row.custom_format, Some(true));
1086    }
1087
1088    #[test]
1089    fn test_set_row_style_applies_to_existing_cells() {
1090        let mut ws = sample_ws();
1091        set_row_style(&mut ws, 1, 3).unwrap();
1092
1093        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
1094        for cell in &row.cells {
1095            assert_eq!(cell.s, Some(3));
1096        }
1097    }
1098
1099    #[test]
1100    fn test_get_row_style_after_set() {
1101        let mut ws = sample_ws();
1102        set_row_style(&mut ws, 2, 7).unwrap();
1103        assert_eq!(get_row_style(&ws, 2), 7);
1104    }
1105
1106    #[test]
1107    fn test_set_row_style_creates_row_if_missing() {
1108        let mut ws = WorksheetXml::default();
1109        set_row_style(&mut ws, 5, 2).unwrap();
1110
1111        assert_eq!(ws.sheet_data.rows.len(), 1);
1112        assert_eq!(ws.sheet_data.rows[0].r, 5);
1113        assert_eq!(ws.sheet_data.rows[0].s, Some(2));
1114    }
1115
1116    #[test]
1117    fn test_set_row_style_zero_clears_custom_format() {
1118        let mut ws = sample_ws();
1119        set_row_style(&mut ws, 1, 5).unwrap();
1120        set_row_style(&mut ws, 1, 0).unwrap();
1121
1122        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
1123        assert_eq!(row.s, Some(0));
1124        assert_eq!(row.custom_format, None);
1125    }
1126
1127    #[test]
1128    fn test_set_row_style_row_zero_returns_error() {
1129        let mut ws = WorksheetXml::default();
1130        let result = set_row_style(&mut ws, 0, 1);
1131        assert!(result.is_err());
1132    }
1133}