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