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