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_deref(),
116        cell.is.as_deref(),
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)]
368#[allow(clippy::field_reassign_with_default)]
369mod tests {
370    use super::*;
371    use sheetkit_xml::worksheet::{Cell, CellTypeTag, SheetData};
372
373    /// Helper: build a minimal worksheet with some pre-populated rows.
374    fn sample_ws() -> WorksheetXml {
375        let mut ws = WorksheetXml::default();
376        ws.sheet_data = SheetData {
377            rows: vec![
378                Row {
379                    r: 1,
380                    spans: None,
381                    s: None,
382                    custom_format: None,
383                    ht: None,
384                    hidden: None,
385                    custom_height: None,
386                    outline_level: None,
387                    cells: vec![
388                        Cell {
389                            r: "A1".into(),
390                            col: 1,
391                            s: None,
392                            t: CellTypeTag::None,
393                            v: Some("10".to_string()),
394                            f: None,
395                            is: None,
396                        },
397                        Cell {
398                            r: "B1".into(),
399                            col: 2,
400                            s: None,
401                            t: CellTypeTag::None,
402                            v: Some("20".to_string()),
403                            f: None,
404                            is: None,
405                        },
406                    ],
407                },
408                Row {
409                    r: 2,
410                    spans: None,
411                    s: None,
412                    custom_format: None,
413                    ht: None,
414                    hidden: None,
415                    custom_height: None,
416                    outline_level: None,
417                    cells: vec![Cell {
418                        r: "A2".into(),
419                        col: 1,
420                        s: None,
421                        t: CellTypeTag::None,
422                        v: Some("30".to_string()),
423                        f: None,
424                        is: None,
425                    }],
426                },
427                Row {
428                    r: 5,
429                    spans: None,
430                    s: None,
431                    custom_format: None,
432                    ht: None,
433                    hidden: None,
434                    custom_height: None,
435                    outline_level: None,
436                    cells: vec![Cell {
437                        r: "C5".into(),
438                        col: 3,
439                        s: None,
440                        t: CellTypeTag::None,
441                        v: Some("50".to_string()),
442                        f: None,
443                        is: None,
444                    }],
445                },
446            ],
447        };
448        ws
449    }
450
451    #[test]
452    fn test_insert_rows_shifts_cells_down() {
453        let mut ws = sample_ws();
454        insert_rows(&mut ws, 2, 3).unwrap();
455
456        // Row 1 should be untouched.
457        assert_eq!(ws.sheet_data.rows[0].r, 1);
458        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
459
460        // Row 2 -> 5 (shifted by 3).
461        assert_eq!(ws.sheet_data.rows[1].r, 5);
462        assert_eq!(ws.sheet_data.rows[1].cells[0].r, "A5");
463
464        // Row 5 -> 8 (shifted by 3).
465        assert_eq!(ws.sheet_data.rows[2].r, 8);
466        assert_eq!(ws.sheet_data.rows[2].cells[0].r, "C8");
467    }
468
469    #[test]
470    fn test_insert_rows_at_row_1() {
471        let mut ws = sample_ws();
472        insert_rows(&mut ws, 1, 2).unwrap();
473
474        // All rows shift by 2.
475        assert_eq!(ws.sheet_data.rows[0].r, 3);
476        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A3");
477        assert_eq!(ws.sheet_data.rows[1].r, 4);
478        assert_eq!(ws.sheet_data.rows[2].r, 7);
479    }
480
481    #[test]
482    fn test_insert_rows_count_zero_is_noop() {
483        let mut ws = sample_ws();
484        insert_rows(&mut ws, 1, 0).unwrap();
485        assert_eq!(ws.sheet_data.rows[0].r, 1);
486        assert_eq!(ws.sheet_data.rows[1].r, 2);
487        assert_eq!(ws.sheet_data.rows[2].r, 5);
488    }
489
490    #[test]
491    fn test_insert_rows_row_zero_returns_error() {
492        let mut ws = sample_ws();
493        let result = insert_rows(&mut ws, 0, 1);
494        assert!(result.is_err());
495    }
496
497    #[test]
498    fn test_insert_rows_beyond_max_returns_error() {
499        let mut ws = WorksheetXml::default();
500        ws.sheet_data.rows.push(Row {
501            r: MAX_ROWS,
502            spans: None,
503            s: None,
504            custom_format: None,
505            ht: None,
506            hidden: None,
507            custom_height: None,
508            outline_level: None,
509            cells: vec![],
510        });
511        let result = insert_rows(&mut ws, 1, 1);
512        assert!(result.is_err());
513    }
514
515    #[test]
516    fn test_insert_rows_on_empty_sheet() {
517        let mut ws = WorksheetXml::default();
518        insert_rows(&mut ws, 1, 5).unwrap();
519        assert!(ws.sheet_data.rows.is_empty());
520    }
521
522    #[test]
523    fn test_remove_row_shifts_up() {
524        let mut ws = sample_ws();
525        remove_row(&mut ws, 2).unwrap();
526
527        // Row 1 untouched.
528        assert_eq!(ws.sheet_data.rows[0].r, 1);
529        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
530
531        // Original row 2 is gone; row 5 shifted to 4.
532        assert_eq!(ws.sheet_data.rows.len(), 2);
533        assert_eq!(ws.sheet_data.rows[1].r, 4);
534        assert_eq!(ws.sheet_data.rows[1].cells[0].r, "C4");
535    }
536
537    #[test]
538    fn test_remove_first_row() {
539        let mut ws = sample_ws();
540        remove_row(&mut ws, 1).unwrap();
541
542        // Remaining rows shift up.
543        assert_eq!(ws.sheet_data.rows[0].r, 1);
544        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
545        assert_eq!(ws.sheet_data.rows[1].r, 4);
546    }
547
548    #[test]
549    fn test_remove_nonexistent_row_still_shifts() {
550        let mut ws = sample_ws();
551        // Row 3 doesn't exist, but rows below should shift.
552        remove_row(&mut ws, 3).unwrap();
553        assert_eq!(ws.sheet_data.rows.len(), 3); // no row removed
554        assert_eq!(ws.sheet_data.rows[2].r, 4); // row 5 -> 4
555    }
556
557    #[test]
558    fn test_remove_row_zero_returns_error() {
559        let mut ws = sample_ws();
560        let result = remove_row(&mut ws, 0);
561        assert!(result.is_err());
562    }
563
564    #[test]
565    fn test_duplicate_row_inserts_copy_below() {
566        let mut ws = sample_ws();
567        duplicate_row(&mut ws, 1).unwrap();
568
569        // Row 1 stays.
570        assert_eq!(ws.sheet_data.rows[0].r, 1);
571        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
572        assert_eq!(ws.sheet_data.rows[0].cells[0].v, Some("10".to_string()));
573
574        // Row 2 is the duplicate with updated refs.
575        assert_eq!(ws.sheet_data.rows[1].r, 2);
576        assert_eq!(ws.sheet_data.rows[1].cells[0].r, "A2");
577        assert_eq!(ws.sheet_data.rows[1].cells[0].v, Some("10".to_string()));
578        assert_eq!(ws.sheet_data.rows[1].cells.len(), 2);
579
580        // Original row 2 shifted to 3.
581        assert_eq!(ws.sheet_data.rows[2].r, 3);
582        assert_eq!(ws.sheet_data.rows[2].cells[0].r, "A3");
583    }
584
585    #[test]
586    fn test_duplicate_row_to_specific_target() {
587        let mut ws = sample_ws();
588        duplicate_row_to(&mut ws, 1, 5).unwrap();
589
590        // Row 1 unchanged.
591        assert_eq!(ws.sheet_data.rows[0].r, 1);
592
593        // Target row 5 is the copy.
594        let row5 = ws.sheet_data.rows.iter().find(|r| r.r == 5).unwrap();
595        assert_eq!(row5.cells[0].r, "A5");
596        assert_eq!(row5.cells[0].v, Some("10".to_string()));
597
598        // Original row 5 shifted to 6.
599        let row6 = ws.sheet_data.rows.iter().find(|r| r.r == 6).unwrap();
600        assert_eq!(row6.cells[0].r, "C6");
601    }
602
603    #[test]
604    fn test_duplicate_nonexistent_row_returns_error() {
605        let mut ws = sample_ws();
606        let result = duplicate_row(&mut ws, 99);
607        assert!(result.is_err());
608    }
609
610    #[test]
611    fn test_set_and_get_row_height() {
612        let mut ws = sample_ws();
613        set_row_height(&mut ws, 1, 25.5).unwrap();
614
615        assert_eq!(get_row_height(&ws, 1), Some(25.5));
616        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
617        assert_eq!(row.custom_height, Some(true));
618    }
619
620    #[test]
621    fn test_set_row_height_creates_row_if_missing() {
622        let mut ws = WorksheetXml::default();
623        set_row_height(&mut ws, 10, 30.0).unwrap();
624
625        assert_eq!(get_row_height(&ws, 10), Some(30.0));
626        assert_eq!(ws.sheet_data.rows.len(), 1);
627        assert_eq!(ws.sheet_data.rows[0].r, 10);
628    }
629
630    #[test]
631    fn test_set_row_height_zero_is_valid() {
632        let mut ws = WorksheetXml::default();
633        set_row_height(&mut ws, 1, 0.0).unwrap();
634        assert_eq!(get_row_height(&ws, 1), Some(0.0));
635    }
636
637    #[test]
638    fn test_set_row_height_max_is_valid() {
639        let mut ws = WorksheetXml::default();
640        set_row_height(&mut ws, 1, 409.0).unwrap();
641        assert_eq!(get_row_height(&ws, 1), Some(409.0));
642    }
643
644    #[test]
645    fn test_set_row_height_exceeds_max_returns_error() {
646        let mut ws = WorksheetXml::default();
647        let result = set_row_height(&mut ws, 1, 410.0);
648        assert!(result.is_err());
649        assert!(matches!(
650            result.unwrap_err(),
651            Error::RowHeightExceeded { .. }
652        ));
653    }
654
655    #[test]
656    fn test_set_row_height_negative_returns_error() {
657        let mut ws = WorksheetXml::default();
658        let result = set_row_height(&mut ws, 1, -1.0);
659        assert!(result.is_err());
660    }
661
662    #[test]
663    fn test_set_row_height_row_zero_returns_error() {
664        let mut ws = WorksheetXml::default();
665        let result = set_row_height(&mut ws, 0, 15.0);
666        assert!(result.is_err());
667    }
668
669    #[test]
670    fn test_get_row_height_nonexistent_returns_none() {
671        let ws = WorksheetXml::default();
672        assert_eq!(get_row_height(&ws, 99), None);
673    }
674
675    #[test]
676    fn test_set_row_hidden() {
677        let mut ws = sample_ws();
678        set_row_visible(&mut ws, 1, false).unwrap();
679
680        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
681        assert_eq!(row.hidden, Some(true));
682    }
683
684    #[test]
685    fn test_set_row_visible_clears_hidden() {
686        let mut ws = sample_ws();
687        set_row_visible(&mut ws, 1, false).unwrap();
688        set_row_visible(&mut ws, 1, true).unwrap();
689
690        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
691        assert_eq!(row.hidden, None);
692    }
693
694    #[test]
695    fn test_set_row_visible_creates_row_if_missing() {
696        let mut ws = WorksheetXml::default();
697        set_row_visible(&mut ws, 3, false).unwrap();
698        assert_eq!(ws.sheet_data.rows.len(), 1);
699        assert_eq!(ws.sheet_data.rows[0].r, 3);
700        assert_eq!(ws.sheet_data.rows[0].hidden, Some(true));
701    }
702
703    #[test]
704    fn test_set_row_visible_row_zero_returns_error() {
705        let mut ws = WorksheetXml::default();
706        let result = set_row_visible(&mut ws, 0, true);
707        assert!(result.is_err());
708    }
709
710    #[test]
711    fn test_set_row_outline_level() {
712        let mut ws = sample_ws();
713        set_row_outline_level(&mut ws, 1, 3).unwrap();
714
715        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
716        assert_eq!(row.outline_level, Some(3));
717    }
718
719    #[test]
720    fn test_set_row_outline_level_zero_clears() {
721        let mut ws = sample_ws();
722        set_row_outline_level(&mut ws, 1, 3).unwrap();
723        set_row_outline_level(&mut ws, 1, 0).unwrap();
724
725        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
726        assert_eq!(row.outline_level, None);
727    }
728
729    #[test]
730    fn test_set_row_outline_level_exceeds_max_returns_error() {
731        let mut ws = sample_ws();
732        let result = set_row_outline_level(&mut ws, 1, 8);
733        assert!(result.is_err());
734    }
735
736    #[test]
737    fn test_set_row_outline_level_row_zero_returns_error() {
738        let mut ws = WorksheetXml::default();
739        let result = set_row_outline_level(&mut ws, 0, 1);
740        assert!(result.is_err());
741    }
742
743    #[test]
744    fn test_get_row_visible_default_is_true() {
745        let ws = sample_ws();
746        assert!(get_row_visible(&ws, 1));
747    }
748
749    #[test]
750    fn test_get_row_visible_nonexistent_row_is_true() {
751        let ws = WorksheetXml::default();
752        assert!(get_row_visible(&ws, 99));
753    }
754
755    #[test]
756    fn test_get_row_visible_after_hide() {
757        let mut ws = sample_ws();
758        set_row_visible(&mut ws, 1, false).unwrap();
759        assert!(!get_row_visible(&ws, 1));
760    }
761
762    #[test]
763    fn test_get_row_visible_after_hide_then_show() {
764        let mut ws = sample_ws();
765        set_row_visible(&mut ws, 1, false).unwrap();
766        set_row_visible(&mut ws, 1, true).unwrap();
767        assert!(get_row_visible(&ws, 1));
768    }
769
770    #[test]
771    fn test_get_row_outline_level_default_is_zero() {
772        let ws = sample_ws();
773        assert_eq!(get_row_outline_level(&ws, 1), 0);
774    }
775
776    #[test]
777    fn test_get_row_outline_level_nonexistent_row() {
778        let ws = WorksheetXml::default();
779        assert_eq!(get_row_outline_level(&ws, 99), 0);
780    }
781
782    #[test]
783    fn test_get_row_outline_level_after_set() {
784        let mut ws = sample_ws();
785        set_row_outline_level(&mut ws, 1, 5).unwrap();
786        assert_eq!(get_row_outline_level(&ws, 1), 5);
787    }
788
789    #[test]
790    fn test_get_row_outline_level_after_clear() {
791        let mut ws = sample_ws();
792        set_row_outline_level(&mut ws, 1, 3).unwrap();
793        set_row_outline_level(&mut ws, 1, 0).unwrap();
794        assert_eq!(get_row_outline_level(&ws, 1), 0);
795    }
796
797    // -- get_rows tests --
798
799    #[test]
800    fn test_get_rows_empty_sheet() {
801        let ws = WorksheetXml::default();
802        let sst = SharedStringTable::new();
803        let rows = get_rows(&ws, &sst).unwrap();
804        assert!(rows.is_empty());
805    }
806
807    #[test]
808    fn test_get_rows_returns_numeric_values() {
809        let ws = sample_ws();
810        let sst = SharedStringTable::new();
811        let rows = get_rows(&ws, &sst).unwrap();
812
813        assert_eq!(rows.len(), 3);
814
815        // Row 1: A1=10, B1=20
816        assert_eq!(rows[0].0, 1);
817        assert_eq!(rows[0].1.len(), 2);
818        assert_eq!(rows[0].1[0].0, 1);
819        assert_eq!(rows[0].1[0].1, CellValue::Number(10.0));
820        assert_eq!(rows[0].1[1].0, 2);
821        assert_eq!(rows[0].1[1].1, CellValue::Number(20.0));
822
823        // Row 2: A2=30
824        assert_eq!(rows[1].0, 2);
825        assert_eq!(rows[1].1.len(), 1);
826        assert_eq!(rows[1].1[0].0, 1);
827        assert_eq!(rows[1].1[0].1, CellValue::Number(30.0));
828
829        // Row 5: C5=50 (sparse -- rows 3 and 4 skipped)
830        assert_eq!(rows[2].0, 5);
831        assert_eq!(rows[2].1.len(), 1);
832        assert_eq!(rows[2].1[0].0, 3);
833        assert_eq!(rows[2].1[0].1, CellValue::Number(50.0));
834    }
835
836    #[test]
837    fn test_get_rows_shared_strings() {
838        let mut sst = SharedStringTable::new();
839        sst.add("hello");
840        sst.add("world");
841
842        let mut ws = WorksheetXml::default();
843        ws.sheet_data = SheetData {
844            rows: vec![Row {
845                r: 1,
846                spans: None,
847                s: None,
848                custom_format: None,
849                ht: None,
850                hidden: None,
851                custom_height: None,
852                outline_level: None,
853                cells: vec![
854                    Cell {
855                        r: "A1".into(),
856                        col: 1,
857                        s: None,
858                        t: CellTypeTag::SharedString,
859                        v: Some("0".to_string()),
860                        f: None,
861                        is: None,
862                    },
863                    Cell {
864                        r: "B1".into(),
865                        col: 2,
866                        s: None,
867                        t: CellTypeTag::SharedString,
868                        v: Some("1".to_string()),
869                        f: None,
870                        is: None,
871                    },
872                ],
873            }],
874        };
875
876        let rows = get_rows(&ws, &sst).unwrap();
877        assert_eq!(rows.len(), 1);
878        assert_eq!(rows[0].1[0].1, CellValue::String("hello".to_string()));
879        assert_eq!(rows[0].1[1].1, CellValue::String("world".to_string()));
880    }
881
882    #[test]
883    fn test_get_rows_mixed_types() {
884        let mut sst = SharedStringTable::new();
885        sst.add("text");
886
887        let mut ws = WorksheetXml::default();
888        ws.sheet_data = SheetData {
889            rows: vec![Row {
890                r: 1,
891                spans: None,
892                s: None,
893                custom_format: None,
894                ht: None,
895                hidden: None,
896                custom_height: None,
897                outline_level: None,
898                cells: vec![
899                    Cell {
900                        r: "A1".into(),
901                        col: 1,
902                        s: None,
903                        t: CellTypeTag::SharedString,
904                        v: Some("0".to_string()),
905                        f: None,
906                        is: None,
907                    },
908                    Cell {
909                        r: "B1".into(),
910                        col: 2,
911                        s: None,
912                        t: CellTypeTag::None,
913                        v: Some("42.5".to_string()),
914                        f: None,
915                        is: None,
916                    },
917                    Cell {
918                        r: "C1".into(),
919                        col: 3,
920                        s: None,
921                        t: CellTypeTag::Boolean,
922                        v: Some("1".to_string()),
923                        f: None,
924                        is: None,
925                    },
926                    Cell {
927                        r: "D1".into(),
928                        col: 4,
929                        s: None,
930                        t: CellTypeTag::Error,
931                        v: Some("#DIV/0!".to_string()),
932                        f: None,
933                        is: None,
934                    },
935                ],
936            }],
937        };
938
939        let rows = get_rows(&ws, &sst).unwrap();
940        assert_eq!(rows.len(), 1);
941        assert_eq!(rows[0].1[0].1, CellValue::String("text".to_string()));
942        assert_eq!(rows[0].1[1].1, CellValue::Number(42.5));
943        assert_eq!(rows[0].1[2].1, CellValue::Bool(true));
944        assert_eq!(rows[0].1[3].1, CellValue::Error("#DIV/0!".to_string()));
945    }
946
947    #[test]
948    fn test_get_rows_skips_rows_with_no_cells() {
949        let mut ws = WorksheetXml::default();
950        ws.sheet_data = SheetData {
951            rows: vec![
952                Row {
953                    r: 1,
954                    spans: None,
955                    s: None,
956                    custom_format: None,
957                    ht: None,
958                    hidden: None,
959                    custom_height: None,
960                    outline_level: None,
961                    cells: vec![Cell {
962                        r: "A1".into(),
963                        col: 1,
964                        s: None,
965                        t: CellTypeTag::None,
966                        v: Some("1".to_string()),
967                        f: None,
968                        is: None,
969                    }],
970                },
971                // Row 2 exists but has no cells (e.g., only has row height set).
972                Row {
973                    r: 2,
974                    spans: None,
975                    s: None,
976                    custom_format: None,
977                    ht: Some(30.0),
978                    hidden: None,
979                    custom_height: Some(true),
980                    outline_level: None,
981                    cells: vec![],
982                },
983                Row {
984                    r: 3,
985                    spans: None,
986                    s: None,
987                    custom_format: None,
988                    ht: None,
989                    hidden: None,
990                    custom_height: None,
991                    outline_level: None,
992                    cells: vec![Cell {
993                        r: "A3".into(),
994                        col: 1,
995                        s: None,
996                        t: CellTypeTag::None,
997                        v: Some("3".to_string()),
998                        f: None,
999                        is: None,
1000                    }],
1001                },
1002            ],
1003        };
1004
1005        let sst = SharedStringTable::new();
1006        let rows = get_rows(&ws, &sst).unwrap();
1007        // Only rows 1 and 3 should be returned (row 2 has no cells).
1008        assert_eq!(rows.len(), 2);
1009        assert_eq!(rows[0].0, 1);
1010        assert_eq!(rows[1].0, 3);
1011    }
1012
1013    #[test]
1014    fn test_get_rows_with_formula() {
1015        let mut ws = WorksheetXml::default();
1016        ws.sheet_data = SheetData {
1017            rows: vec![Row {
1018                r: 1,
1019                spans: None,
1020                s: None,
1021                custom_format: None,
1022                ht: None,
1023                hidden: None,
1024                custom_height: None,
1025                outline_level: None,
1026                cells: vec![Cell {
1027                    r: "A1".into(),
1028                    col: 1,
1029                    s: None,
1030                    t: CellTypeTag::None,
1031                    v: Some("42".to_string()),
1032                    f: Some(Box::new(sheetkit_xml::worksheet::CellFormula {
1033                        t: None,
1034                        reference: None,
1035                        si: None,
1036                        value: Some("B1+C1".to_string()),
1037                    })),
1038                    is: None,
1039                }],
1040            }],
1041        };
1042
1043        let sst = SharedStringTable::new();
1044        let rows = get_rows(&ws, &sst).unwrap();
1045        assert_eq!(rows.len(), 1);
1046        match &rows[0].1[0].1 {
1047            CellValue::Formula { expr, result } => {
1048                assert_eq!(expr, "B1+C1");
1049                assert_eq!(*result, Some(Box::new(CellValue::Number(42.0))));
1050            }
1051            _ => panic!("expected Formula"),
1052        }
1053    }
1054
1055    #[test]
1056    fn test_get_rows_with_inline_string() {
1057        let mut ws = WorksheetXml::default();
1058        ws.sheet_data = SheetData {
1059            rows: vec![Row {
1060                r: 1,
1061                spans: None,
1062                s: None,
1063                custom_format: None,
1064                ht: None,
1065                hidden: None,
1066                custom_height: None,
1067                outline_level: None,
1068                cells: vec![Cell {
1069                    r: "A1".into(),
1070                    col: 1,
1071                    s: None,
1072                    t: CellTypeTag::InlineString,
1073                    v: None,
1074                    f: None,
1075                    is: Some(Box::new(sheetkit_xml::worksheet::InlineString {
1076                        t: Some("inline text".to_string()),
1077                    })),
1078                }],
1079            }],
1080        };
1081
1082        let sst = SharedStringTable::new();
1083        let rows = get_rows(&ws, &sst).unwrap();
1084        assert_eq!(rows.len(), 1);
1085        assert_eq!(rows[0].1[0].1, CellValue::String("inline text".to_string()));
1086    }
1087
1088    // -- set_row_style / get_row_style tests --
1089
1090    #[test]
1091    fn test_get_row_style_default_is_zero() {
1092        let ws = WorksheetXml::default();
1093        assert_eq!(get_row_style(&ws, 1), 0);
1094    }
1095
1096    #[test]
1097    fn test_get_row_style_nonexistent_row_is_zero() {
1098        let ws = sample_ws();
1099        assert_eq!(get_row_style(&ws, 99), 0);
1100    }
1101
1102    #[test]
1103    fn test_set_row_style_applies_style() {
1104        let mut ws = sample_ws();
1105        set_row_style(&mut ws, 1, 5).unwrap();
1106
1107        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
1108        assert_eq!(row.s, Some(5));
1109        assert_eq!(row.custom_format, Some(true));
1110    }
1111
1112    #[test]
1113    fn test_set_row_style_applies_to_existing_cells() {
1114        let mut ws = sample_ws();
1115        set_row_style(&mut ws, 1, 3).unwrap();
1116
1117        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
1118        for cell in &row.cells {
1119            assert_eq!(cell.s, Some(3));
1120        }
1121    }
1122
1123    #[test]
1124    fn test_get_row_style_after_set() {
1125        let mut ws = sample_ws();
1126        set_row_style(&mut ws, 2, 7).unwrap();
1127        assert_eq!(get_row_style(&ws, 2), 7);
1128    }
1129
1130    #[test]
1131    fn test_set_row_style_creates_row_if_missing() {
1132        let mut ws = WorksheetXml::default();
1133        set_row_style(&mut ws, 5, 2).unwrap();
1134
1135        assert_eq!(ws.sheet_data.rows.len(), 1);
1136        assert_eq!(ws.sheet_data.rows[0].r, 5);
1137        assert_eq!(ws.sheet_data.rows[0].s, Some(2));
1138    }
1139
1140    #[test]
1141    fn test_set_row_style_zero_clears_custom_format() {
1142        let mut ws = sample_ws();
1143        set_row_style(&mut ws, 1, 5).unwrap();
1144        set_row_style(&mut ws, 1, 0).unwrap();
1145
1146        let row = ws.sheet_data.rows.iter().find(|r| r.r == 1).unwrap();
1147        assert_eq!(row.s, Some(0));
1148        assert_eq!(row.custom_format, None);
1149    }
1150
1151    #[test]
1152    fn test_set_row_style_row_zero_returns_error() {
1153        let mut ws = WorksheetXml::default();
1154        let result = set_row_style(&mut ws, 0, 1);
1155        assert!(result.is_err());
1156    }
1157}