sheetkit_core/
col.rs

1//! Column 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 std::collections::BTreeMap;
8
9use sheetkit_xml::worksheet::{Col, Cols, WorksheetXml};
10
11use crate::cell::CellValue;
12use crate::error::{Error, Result};
13use crate::row::get_rows;
14use crate::sst::SharedStringTable;
15use crate::utils::cell_ref::{
16    cell_name_to_coordinates, column_name_to_number, column_number_to_name,
17    coordinates_to_cell_name,
18};
19use crate::utils::constants::{MAX_COLUMNS, MAX_COLUMN_WIDTH};
20
21/// Get all columns with their data from a worksheet.
22///
23/// Returns a Vec of `(column_name, Vec<(row_number, CellValue)>)` tuples.
24/// Only columns that have data are included (sparse). The columns are sorted
25/// by column order (A, B, ..., Z, AA, AB, ...).
26#[allow(clippy::type_complexity)]
27pub fn get_cols(
28    ws: &WorksheetXml,
29    sst: &SharedStringTable,
30) -> Result<Vec<(String, Vec<(u32, CellValue)>)>> {
31    let rows = get_rows(ws, sst)?;
32
33    // Transpose row-based data into column-based data using a BTreeMap
34    // keyed by column number so columns are naturally sorted.
35    let mut col_map: BTreeMap<u32, Vec<(u32, CellValue)>> = BTreeMap::new();
36
37    for (row_num, cells) in rows {
38        for (col_num, value) in cells {
39            col_map.entry(col_num).or_default().push((row_num, value));
40        }
41    }
42
43    // Convert column numbers to names for the public API.
44    let mut result = Vec::with_capacity(col_map.len());
45    for (col_num, cells) in col_map {
46        let col_name = column_number_to_name(col_num)?;
47        result.push((col_name, cells));
48    }
49
50    Ok(result)
51}
52
53/// Set the width of a column. Creates the `Cols` container and/or a `Col`
54/// entry if they do not yet exist.
55///
56/// Valid range: `0.0 ..= 255.0`.
57pub fn set_col_width(ws: &mut WorksheetXml, col: &str, width: f64) -> Result<()> {
58    let col_num = column_name_to_number(col)?;
59    if !(0.0..=MAX_COLUMN_WIDTH).contains(&width) {
60        return Err(Error::ColumnWidthExceeded {
61            width,
62            max: MAX_COLUMN_WIDTH,
63        });
64    }
65
66    let col_entry = find_or_create_col(ws, col_num);
67    col_entry.width = Some(width);
68    col_entry.custom_width = Some(true);
69    Ok(())
70}
71
72/// Get the width of a column. Returns `None` when there is no explicit width
73/// defined for the column.
74pub fn get_col_width(ws: &WorksheetXml, col: &str) -> Option<f64> {
75    let col_num = column_name_to_number(col).ok()?;
76    ws.cols
77        .as_ref()
78        .and_then(|cols| {
79            cols.cols
80                .iter()
81                .find(|c| col_num >= c.min && col_num <= c.max)
82        })
83        .and_then(|c| c.width)
84}
85
86/// Set the visibility of a column.
87pub fn set_col_visible(ws: &mut WorksheetXml, col: &str, visible: bool) -> Result<()> {
88    let col_num = column_name_to_number(col)?;
89    let col_entry = find_or_create_col(ws, col_num);
90    col_entry.hidden = if visible { None } else { Some(true) };
91    Ok(())
92}
93
94/// Get the visibility of a column. Returns true if visible (not hidden).
95///
96/// Columns are visible by default, so this returns true if no `Col` entry
97/// exists or if it has no `hidden` attribute set.
98pub fn get_col_visible(ws: &WorksheetXml, col: &str) -> Result<bool> {
99    let col_num = column_name_to_number(col)?;
100    let hidden = ws
101        .cols
102        .as_ref()
103        .and_then(|cols| {
104            cols.cols
105                .iter()
106                .find(|c| col_num >= c.min && col_num <= c.max)
107        })
108        .and_then(|c| c.hidden)
109        .unwrap_or(false);
110    Ok(!hidden)
111}
112
113/// Set the outline (grouping) level of a column.
114///
115/// Valid range: `0..=7` (Excel supports up to 7 outline levels).
116pub fn set_col_outline_level(ws: &mut WorksheetXml, col: &str, level: u8) -> Result<()> {
117    let col_num = column_name_to_number(col)?;
118    if level > 7 {
119        return Err(Error::OutlineLevelExceeded { level, max: 7 });
120    }
121
122    let col_entry = find_or_create_col(ws, col_num);
123    col_entry.outline_level = if level == 0 { None } else { Some(level) };
124    Ok(())
125}
126
127/// Get the outline (grouping) level of a column. Returns 0 if not set.
128pub fn get_col_outline_level(ws: &WorksheetXml, col: &str) -> Result<u8> {
129    let col_num = column_name_to_number(col)?;
130    let level = ws
131        .cols
132        .as_ref()
133        .and_then(|cols| {
134            cols.cols
135                .iter()
136                .find(|c| col_num >= c.min && col_num <= c.max)
137        })
138        .and_then(|c| c.outline_level)
139        .unwrap_or(0);
140    Ok(level)
141}
142
143/// Insert `count` columns starting at `col`, shifting existing columns at
144/// and to the right of `col` further right.
145///
146/// Cell references are updated: e.g. when inserting 2 columns at "B", a cell
147/// "C3" becomes "E3".
148pub fn insert_cols(ws: &mut WorksheetXml, col: &str, count: u32) -> Result<()> {
149    let start_col = column_name_to_number(col)?;
150    if count == 0 {
151        return Ok(());
152    }
153
154    // Validate we won't exceed max columns.
155    let max_existing = ws
156        .sheet_data
157        .rows
158        .iter()
159        .flat_map(|r| r.cells.iter())
160        .filter_map(|c| cell_name_to_coordinates(c.r.as_str()).ok())
161        .map(|(col_n, _)| col_n)
162        .max()
163        .unwrap_or(0);
164    let furthest = max_existing.max(start_col);
165    if furthest.checked_add(count).is_none_or(|v| v > MAX_COLUMNS) {
166        return Err(Error::InvalidColumnNumber(furthest + count));
167    }
168
169    // Shift cell references.
170    for row in ws.sheet_data.rows.iter_mut() {
171        for cell in row.cells.iter_mut() {
172            let (c, r) = cell_name_to_coordinates(cell.r.as_str())?;
173            if c >= start_col {
174                let new_col = c + count;
175                cell.r = coordinates_to_cell_name(new_col, r)?.into();
176                cell.col = new_col;
177            }
178        }
179    }
180
181    // Shift Col definitions.
182    if let Some(ref mut cols) = ws.cols {
183        for c in cols.cols.iter_mut() {
184            if c.min >= start_col {
185                c.min += count;
186            }
187            if c.max >= start_col {
188                c.max += count;
189            }
190        }
191    }
192
193    Ok(())
194}
195
196/// Remove a single column, shifting columns to its right leftward by one.
197pub fn remove_col(ws: &mut WorksheetXml, col: &str) -> Result<()> {
198    let col_num = column_name_to_number(col)?;
199
200    // Remove cells in the target column and shift cells to the right.
201    for row in ws.sheet_data.rows.iter_mut() {
202        // Remove cells at the target column.
203        row.cells.retain(|cell| {
204            cell_name_to_coordinates(cell.r.as_str())
205                .map(|(c, _)| c != col_num)
206                .unwrap_or(true)
207        });
208
209        // Shift cells that are to the right of the removed column.
210        for cell in row.cells.iter_mut() {
211            let (c, r) = cell_name_to_coordinates(cell.r.as_str())?;
212            if c > col_num {
213                let new_col = c - 1;
214                cell.r = coordinates_to_cell_name(new_col, r)?.into();
215                cell.col = new_col;
216            }
217        }
218    }
219
220    // Shift Col definitions.
221    if let Some(ref mut cols) = ws.cols {
222        // Remove col entries that exactly span the removed column only.
223        cols.cols
224            .retain(|c| !(c.min == col_num && c.max == col_num));
225
226        for c in cols.cols.iter_mut() {
227            if c.min > col_num {
228                c.min -= 1;
229            }
230            if c.max >= col_num {
231                c.max -= 1;
232            }
233        }
234
235        // Remove the Cols wrapper if it's now empty.
236        if cols.cols.is_empty() {
237            ws.cols = None;
238        }
239    }
240
241    Ok(())
242}
243
244/// Set the style for an entire column. The `style_id` is the ID returned by
245/// `add_style()`.
246pub fn set_col_style(ws: &mut WorksheetXml, col: &str, style_id: u32) -> Result<()> {
247    let col_num = column_name_to_number(col)?;
248    let col_entry = find_or_create_col(ws, col_num);
249    col_entry.style = Some(style_id);
250    Ok(())
251}
252
253/// Get the style ID for a column. Returns 0 (default) if the column has no
254/// explicit style set.
255pub fn get_col_style(ws: &WorksheetXml, col: &str) -> Result<u32> {
256    let col_num = column_name_to_number(col)?;
257    let style = ws
258        .cols
259        .as_ref()
260        .and_then(|cols| {
261            cols.cols
262                .iter()
263                .find(|c| col_num >= c.min && col_num <= c.max)
264        })
265        .and_then(|c| c.style)
266        .unwrap_or(0);
267    Ok(style)
268}
269
270/// Find an existing Col entry that covers exactly `col_num`, or create a
271/// new single-column entry for it.
272fn find_or_create_col(ws: &mut WorksheetXml, col_num: u32) -> &mut Col {
273    // Ensure the Cols container exists.
274    if ws.cols.is_none() {
275        ws.cols = Some(Cols { cols: vec![] });
276    }
277    let cols = ws.cols.as_mut().unwrap();
278
279    // Look for an existing entry that spans exactly this column.
280    let existing = cols
281        .cols
282        .iter()
283        .position(|c| c.min == col_num && c.max == col_num);
284
285    if let Some(idx) = existing {
286        return &mut cols.cols[idx];
287    }
288
289    // Create a new single-column entry.
290    cols.cols.push(Col {
291        min: col_num,
292        max: col_num,
293        width: None,
294        style: None,
295        hidden: None,
296        custom_width: None,
297        outline_level: None,
298    });
299    let last = cols.cols.len() - 1;
300    &mut cols.cols[last]
301}
302
303#[cfg(test)]
304#[allow(clippy::field_reassign_with_default)]
305mod tests {
306    use super::*;
307    use sheetkit_xml::worksheet::{Cell, CellTypeTag, Row, SheetData};
308
309    /// Helper: build a worksheet with some cells for column tests.
310    fn sample_ws() -> WorksheetXml {
311        let mut ws = WorksheetXml::default();
312        ws.sheet_data = SheetData {
313            rows: vec![
314                Row {
315                    r: 1,
316                    spans: None,
317                    s: None,
318                    custom_format: None,
319                    ht: None,
320                    hidden: None,
321                    custom_height: None,
322                    outline_level: None,
323                    cells: vec![
324                        Cell {
325                            r: "A1".into(),
326                            col: 1,
327                            s: None,
328                            t: CellTypeTag::None,
329                            v: Some("10".to_string()),
330                            f: None,
331                            is: None,
332                        },
333                        Cell {
334                            r: "B1".into(),
335                            col: 2,
336                            s: None,
337                            t: CellTypeTag::None,
338                            v: Some("20".to_string()),
339                            f: None,
340                            is: None,
341                        },
342                        Cell {
343                            r: "D1".into(),
344                            col: 4,
345                            s: None,
346                            t: CellTypeTag::None,
347                            v: Some("40".to_string()),
348                            f: None,
349                            is: None,
350                        },
351                    ],
352                },
353                Row {
354                    r: 2,
355                    spans: None,
356                    s: None,
357                    custom_format: None,
358                    ht: None,
359                    hidden: None,
360                    custom_height: None,
361                    outline_level: None,
362                    cells: vec![
363                        Cell {
364                            r: "A2".into(),
365                            col: 1,
366                            s: None,
367                            t: CellTypeTag::None,
368                            v: Some("100".to_string()),
369                            f: None,
370                            is: None,
371                        },
372                        Cell {
373                            r: "C2".into(),
374                            col: 3,
375                            s: None,
376                            t: CellTypeTag::None,
377                            v: Some("300".to_string()),
378                            f: None,
379                            is: None,
380                        },
381                    ],
382                },
383            ],
384        };
385        ws
386    }
387
388    #[test]
389    fn test_set_and_get_col_width() {
390        let mut ws = WorksheetXml::default();
391        set_col_width(&mut ws, "A", 15.0).unwrap();
392        assert_eq!(get_col_width(&ws, "A"), Some(15.0));
393    }
394
395    #[test]
396    fn test_set_col_width_creates_cols_container() {
397        let mut ws = WorksheetXml::default();
398        assert!(ws.cols.is_none());
399        set_col_width(&mut ws, "B", 20.0).unwrap();
400        assert!(ws.cols.is_some());
401        let cols = ws.cols.as_ref().unwrap();
402        assert_eq!(cols.cols.len(), 1);
403        assert_eq!(cols.cols[0].min, 2);
404        assert_eq!(cols.cols[0].max, 2);
405        assert_eq!(cols.cols[0].custom_width, Some(true));
406    }
407
408    #[test]
409    fn test_set_col_width_zero_is_valid() {
410        let mut ws = WorksheetXml::default();
411        set_col_width(&mut ws, "A", 0.0).unwrap();
412        assert_eq!(get_col_width(&ws, "A"), Some(0.0));
413    }
414
415    #[test]
416    fn test_set_col_width_max_is_valid() {
417        let mut ws = WorksheetXml::default();
418        set_col_width(&mut ws, "A", 255.0).unwrap();
419        assert_eq!(get_col_width(&ws, "A"), Some(255.0));
420    }
421
422    #[test]
423    fn test_set_col_width_exceeds_max_returns_error() {
424        let mut ws = WorksheetXml::default();
425        let result = set_col_width(&mut ws, "A", 256.0);
426        assert!(result.is_err());
427        assert!(matches!(
428            result.unwrap_err(),
429            Error::ColumnWidthExceeded { .. }
430        ));
431    }
432
433    #[test]
434    fn test_set_col_width_negative_returns_error() {
435        let mut ws = WorksheetXml::default();
436        let result = set_col_width(&mut ws, "A", -1.0);
437        assert!(result.is_err());
438    }
439
440    #[test]
441    fn test_get_col_width_nonexistent_returns_none() {
442        let ws = WorksheetXml::default();
443        assert_eq!(get_col_width(&ws, "Z"), None);
444    }
445
446    #[test]
447    fn test_set_col_width_invalid_column_returns_error() {
448        let mut ws = WorksheetXml::default();
449        let result = set_col_width(&mut ws, "XFE", 10.0);
450        assert!(result.is_err());
451    }
452
453    #[test]
454    fn test_set_col_hidden() {
455        let mut ws = WorksheetXml::default();
456        set_col_visible(&mut ws, "A", false).unwrap();
457
458        let col = &ws.cols.as_ref().unwrap().cols[0];
459        assert_eq!(col.hidden, Some(true));
460    }
461
462    #[test]
463    fn test_set_col_visible_clears_hidden() {
464        let mut ws = WorksheetXml::default();
465        set_col_visible(&mut ws, "A", false).unwrap();
466        set_col_visible(&mut ws, "A", true).unwrap();
467
468        let col = &ws.cols.as_ref().unwrap().cols[0];
469        assert_eq!(col.hidden, None);
470    }
471
472    #[test]
473    fn test_insert_cols_shifts_cells_right() {
474        let mut ws = sample_ws();
475        insert_cols(&mut ws, "B", 2).unwrap();
476
477        // Row 1: A1 stays, B1->D1, D1->F1.
478        let r1 = &ws.sheet_data.rows[0];
479        assert_eq!(r1.cells[0].r, "A1");
480        assert_eq!(r1.cells[1].r, "D1"); // B shifted by 2
481        assert_eq!(r1.cells[2].r, "F1"); // D shifted by 2
482
483        // Row 2: A2 stays, C2->E2.
484        let r2 = &ws.sheet_data.rows[1];
485        assert_eq!(r2.cells[0].r, "A2");
486        assert_eq!(r2.cells[1].r, "E2"); // C shifted by 2
487    }
488
489    #[test]
490    fn test_insert_cols_at_column_a() {
491        let mut ws = sample_ws();
492        insert_cols(&mut ws, "A", 1).unwrap();
493
494        // All cells shift right by 1.
495        let r1 = &ws.sheet_data.rows[0];
496        assert_eq!(r1.cells[0].r, "B1"); // A->B
497        assert_eq!(r1.cells[1].r, "C1"); // B->C
498        assert_eq!(r1.cells[2].r, "E1"); // D->E
499    }
500
501    #[test]
502    fn test_insert_cols_count_zero_is_noop() {
503        let mut ws = sample_ws();
504        insert_cols(&mut ws, "B", 0).unwrap();
505        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
506        assert_eq!(ws.sheet_data.rows[0].cells[1].r, "B1");
507    }
508
509    #[test]
510    fn test_insert_cols_on_empty_sheet() {
511        let mut ws = WorksheetXml::default();
512        insert_cols(&mut ws, "A", 5).unwrap();
513        assert!(ws.sheet_data.rows.is_empty());
514    }
515
516    #[test]
517    fn test_insert_cols_shifts_col_definitions() {
518        let mut ws = WorksheetXml::default();
519        set_col_width(&mut ws, "C", 20.0).unwrap();
520        insert_cols(&mut ws, "B", 2).unwrap();
521
522        // Col C (3) should now be at col 5 (E).
523        let col = &ws.cols.as_ref().unwrap().cols[0];
524        assert_eq!(col.min, 5);
525        assert_eq!(col.max, 5);
526    }
527
528    #[test]
529    fn test_remove_col_shifts_cells_left() {
530        let mut ws = sample_ws();
531        remove_col(&mut ws, "B").unwrap();
532
533        // Row 1: A1 stays, B1 removed, D1->C1.
534        let r1 = &ws.sheet_data.rows[0];
535        assert_eq!(r1.cells.len(), 2);
536        assert_eq!(r1.cells[0].r, "A1");
537        assert_eq!(r1.cells[1].r, "C1"); // D shifted left
538        assert_eq!(r1.cells[1].v, Some("40".to_string()));
539
540        // Row 2: A2 stays, C2->B2.
541        let r2 = &ws.sheet_data.rows[1];
542        assert_eq!(r2.cells[0].r, "A2");
543        assert_eq!(r2.cells[1].r, "B2"); // C shifted left
544    }
545
546    #[test]
547    fn test_remove_first_col() {
548        let mut ws = sample_ws();
549        remove_col(&mut ws, "A").unwrap();
550
551        // Row 1: A1 removed, B1->A1, D1->C1.
552        let r1 = &ws.sheet_data.rows[0];
553        assert_eq!(r1.cells.len(), 2);
554        assert_eq!(r1.cells[0].r, "A1");
555        assert_eq!(r1.cells[0].v, Some("20".to_string())); // was B1
556        assert_eq!(r1.cells[1].r, "C1");
557        assert_eq!(r1.cells[1].v, Some("40".to_string())); // was D1
558    }
559
560    #[test]
561    fn test_remove_col_with_col_definitions() {
562        let mut ws = WorksheetXml::default();
563        set_col_width(&mut ws, "B", 20.0).unwrap();
564        remove_col(&mut ws, "B").unwrap();
565
566        // The Col entry for B should be removed.
567        assert!(ws.cols.is_none());
568    }
569
570    #[test]
571    fn test_remove_col_shrinks_range_ending_at_removed_column() {
572        // Range B:C (min=2, max=3); removing column C should shrink to B:B (min=2, max=2).
573        let mut ws = WorksheetXml::default();
574        set_col_width(&mut ws, "B", 15.0).unwrap();
575        // Manually extend the col entry to span B:C.
576        ws.cols.as_mut().unwrap().cols[0].max = 3;
577        remove_col(&mut ws, "C").unwrap();
578        let col = &ws.cols.as_ref().unwrap().cols[0];
579        assert_eq!(col.min, 2);
580        assert_eq!(col.max, 2);
581    }
582
583    #[test]
584    fn test_remove_col_shrinks_range_spanning_removed_column() {
585        // Range B:E (min=2, max=5); removing column C should shrink to B:D (min=2, max=4).
586        let mut ws = WorksheetXml::default();
587        set_col_width(&mut ws, "B", 15.0).unwrap();
588        ws.cols.as_mut().unwrap().cols[0].max = 5;
589        remove_col(&mut ws, "C").unwrap();
590        let col = &ws.cols.as_ref().unwrap().cols[0];
591        assert_eq!(col.min, 2);
592        assert_eq!(col.max, 4);
593    }
594
595    #[test]
596    fn test_remove_col_invalid_column_returns_error() {
597        let mut ws = WorksheetXml::default();
598        let result = remove_col(&mut ws, "XFE");
599        assert!(result.is_err());
600    }
601
602    #[test]
603    fn test_remove_col_invalid_cell_reference_returns_error() {
604        let mut ws = sample_ws();
605        ws.sheet_data.rows[0].cells.push(Cell {
606            r: "INVALID".into(),
607            col: 0,
608            s: None,
609            t: CellTypeTag::None,
610            v: Some("1".to_string()),
611            f: None,
612            is: None,
613        });
614        let result = remove_col(&mut ws, "A");
615        assert!(result.is_err());
616    }
617
618    #[test]
619    fn test_set_multiple_col_widths() {
620        let mut ws = WorksheetXml::default();
621        set_col_width(&mut ws, "A", 10.0).unwrap();
622        set_col_width(&mut ws, "C", 30.0).unwrap();
623
624        assert_eq!(get_col_width(&ws, "A"), Some(10.0));
625        assert_eq!(get_col_width(&ws, "B"), None);
626        assert_eq!(get_col_width(&ws, "C"), Some(30.0));
627    }
628
629    #[test]
630    fn test_overwrite_col_width() {
631        let mut ws = WorksheetXml::default();
632        set_col_width(&mut ws, "A", 10.0).unwrap();
633        set_col_width(&mut ws, "A", 25.0).unwrap();
634
635        assert_eq!(get_col_width(&ws, "A"), Some(25.0));
636    }
637
638    #[test]
639    fn test_get_col_visible_default_is_true() {
640        let ws = WorksheetXml::default();
641        assert!(get_col_visible(&ws, "A").unwrap());
642    }
643
644    #[test]
645    fn test_get_col_visible_after_hide() {
646        let mut ws = WorksheetXml::default();
647        set_col_visible(&mut ws, "B", false).unwrap();
648        assert!(!get_col_visible(&ws, "B").unwrap());
649    }
650
651    #[test]
652    fn test_get_col_visible_after_hide_then_show() {
653        let mut ws = WorksheetXml::default();
654        set_col_visible(&mut ws, "A", false).unwrap();
655        set_col_visible(&mut ws, "A", true).unwrap();
656        assert!(get_col_visible(&ws, "A").unwrap());
657    }
658
659    #[test]
660    fn test_get_col_visible_invalid_column_returns_error() {
661        let ws = WorksheetXml::default();
662        let result = get_col_visible(&ws, "XFE");
663        assert!(result.is_err());
664    }
665
666    #[test]
667    fn test_set_col_outline_level() {
668        let mut ws = WorksheetXml::default();
669        set_col_outline_level(&mut ws, "A", 3).unwrap();
670
671        let col = &ws.cols.as_ref().unwrap().cols[0];
672        assert_eq!(col.outline_level, Some(3));
673    }
674
675    #[test]
676    fn test_set_col_outline_level_zero_clears() {
677        let mut ws = WorksheetXml::default();
678        set_col_outline_level(&mut ws, "A", 3).unwrap();
679        set_col_outline_level(&mut ws, "A", 0).unwrap();
680
681        let col = &ws.cols.as_ref().unwrap().cols[0];
682        assert_eq!(col.outline_level, None);
683    }
684
685    #[test]
686    fn test_set_col_outline_level_exceeds_max_returns_error() {
687        let mut ws = WorksheetXml::default();
688        let result = set_col_outline_level(&mut ws, "A", 8);
689        assert!(result.is_err());
690    }
691
692    #[test]
693    fn test_set_col_outline_level_max_valid() {
694        let mut ws = WorksheetXml::default();
695        set_col_outline_level(&mut ws, "A", 7).unwrap();
696
697        let col = &ws.cols.as_ref().unwrap().cols[0];
698        assert_eq!(col.outline_level, Some(7));
699    }
700
701    #[test]
702    fn test_get_col_outline_level_default_is_zero() {
703        let ws = WorksheetXml::default();
704        assert_eq!(get_col_outline_level(&ws, "A").unwrap(), 0);
705    }
706
707    #[test]
708    fn test_get_col_outline_level_after_set() {
709        let mut ws = WorksheetXml::default();
710        set_col_outline_level(&mut ws, "B", 5).unwrap();
711        assert_eq!(get_col_outline_level(&ws, "B").unwrap(), 5);
712    }
713
714    #[test]
715    fn test_get_col_outline_level_after_clear() {
716        let mut ws = WorksheetXml::default();
717        set_col_outline_level(&mut ws, "C", 4).unwrap();
718        set_col_outline_level(&mut ws, "C", 0).unwrap();
719        assert_eq!(get_col_outline_level(&ws, "C").unwrap(), 0);
720    }
721
722    #[test]
723    fn test_get_col_outline_level_invalid_column_returns_error() {
724        let ws = WorksheetXml::default();
725        let result = get_col_outline_level(&ws, "XFE");
726        assert!(result.is_err());
727    }
728
729    // -- get_cols tests --
730
731    #[test]
732    fn test_get_cols_empty_sheet() {
733        let ws = WorksheetXml::default();
734        let sst = SharedStringTable::new();
735        let cols = get_cols(&ws, &sst).unwrap();
736        assert!(cols.is_empty());
737    }
738
739    #[test]
740    fn test_get_cols_transposes_row_data() {
741        let ws = sample_ws();
742        let sst = SharedStringTable::new();
743        let cols = get_cols(&ws, &sst).unwrap();
744
745        // sample_ws has:
746        //   Row 1: A1=10, B1=20, D1=40
747        //   Row 2: A2=100, C2=300
748        // Columns should be: A, B, C, D
749
750        assert_eq!(cols.len(), 4);
751
752        // Column A: (1, 10.0), (2, 100.0)
753        assert_eq!(cols[0].0, "A");
754        assert_eq!(cols[0].1.len(), 2);
755        assert_eq!(cols[0].1[0], (1, CellValue::Number(10.0)));
756        assert_eq!(cols[0].1[1], (2, CellValue::Number(100.0)));
757
758        // Column B: (1, 20.0)
759        assert_eq!(cols[1].0, "B");
760        assert_eq!(cols[1].1.len(), 1);
761        assert_eq!(cols[1].1[0], (1, CellValue::Number(20.0)));
762
763        // Column C: (2, 300.0)
764        assert_eq!(cols[2].0, "C");
765        assert_eq!(cols[2].1.len(), 1);
766        assert_eq!(cols[2].1[0], (2, CellValue::Number(300.0)));
767
768        // Column D: (1, 40.0)
769        assert_eq!(cols[3].0, "D");
770        assert_eq!(cols[3].1.len(), 1);
771        assert_eq!(cols[3].1[0], (1, CellValue::Number(40.0)));
772    }
773
774    #[test]
775    fn test_get_cols_with_shared_strings() {
776        let mut sst = SharedStringTable::new();
777        sst.add("Name");
778        sst.add("Age");
779        sst.add("Alice");
780
781        let mut ws = WorksheetXml::default();
782        ws.sheet_data = SheetData {
783            rows: vec![
784                Row {
785                    r: 1,
786                    spans: None,
787                    s: None,
788                    custom_format: None,
789                    ht: None,
790                    hidden: None,
791                    custom_height: None,
792                    outline_level: None,
793                    cells: vec![
794                        Cell {
795                            r: "A1".into(),
796                            col: 1,
797                            s: None,
798                            t: CellTypeTag::SharedString,
799                            v: Some("0".to_string()),
800                            f: None,
801                            is: None,
802                        },
803                        Cell {
804                            r: "B1".into(),
805                            col: 2,
806                            s: None,
807                            t: CellTypeTag::SharedString,
808                            v: Some("1".to_string()),
809                            f: None,
810                            is: None,
811                        },
812                    ],
813                },
814                Row {
815                    r: 2,
816                    spans: None,
817                    s: None,
818                    custom_format: None,
819                    ht: None,
820                    hidden: None,
821                    custom_height: None,
822                    outline_level: None,
823                    cells: vec![
824                        Cell {
825                            r: "A2".into(),
826                            col: 1,
827                            s: None,
828                            t: CellTypeTag::SharedString,
829                            v: Some("2".to_string()),
830                            f: None,
831                            is: None,
832                        },
833                        Cell {
834                            r: "B2".into(),
835                            col: 2,
836                            s: None,
837                            t: CellTypeTag::None,
838                            v: Some("30".to_string()),
839                            f: None,
840                            is: None,
841                        },
842                    ],
843                },
844            ],
845        };
846
847        let cols = get_cols(&ws, &sst).unwrap();
848        assert_eq!(cols.len(), 2);
849
850        // Column A: "Name", "Alice"
851        assert_eq!(cols[0].0, "A");
852        assert_eq!(cols[0].1[0].1, CellValue::String("Name".to_string()));
853        assert_eq!(cols[0].1[1].1, CellValue::String("Alice".to_string()));
854
855        // Column B: "Age", 30
856        assert_eq!(cols[1].0, "B");
857        assert_eq!(cols[1].1[0].1, CellValue::String("Age".to_string()));
858        assert_eq!(cols[1].1[1].1, CellValue::Number(30.0));
859    }
860
861    #[test]
862    fn test_get_cols_sorted_correctly() {
863        // Verify that columns are sorted by length then alphabetically:
864        // A, B, ..., Z, AA, AB, ...
865        let mut ws = WorksheetXml::default();
866        ws.sheet_data = SheetData {
867            rows: vec![Row {
868                r: 1,
869                spans: None,
870                s: None,
871                custom_format: None,
872                ht: None,
873                hidden: None,
874                custom_height: None,
875                outline_level: None,
876                cells: vec![
877                    Cell {
878                        r: "AA1".into(),
879                        col: 27,
880                        s: None,
881                        t: CellTypeTag::None,
882                        v: Some("1".to_string()),
883                        f: None,
884                        is: None,
885                    },
886                    Cell {
887                        r: "B1".into(),
888                        col: 2,
889                        s: None,
890                        t: CellTypeTag::None,
891                        v: Some("2".to_string()),
892                        f: None,
893                        is: None,
894                    },
895                    Cell {
896                        r: "A1".into(),
897                        col: 1,
898                        s: None,
899                        t: CellTypeTag::None,
900                        v: Some("3".to_string()),
901                        f: None,
902                        is: None,
903                    },
904                ],
905            }],
906        };
907
908        let sst = SharedStringTable::new();
909        let cols = get_cols(&ws, &sst).unwrap();
910
911        assert_eq!(cols.len(), 3);
912        assert_eq!(cols[0].0, "A");
913        assert_eq!(cols[1].0, "B");
914        assert_eq!(cols[2].0, "AA");
915    }
916
917    // -- set_col_style / get_col_style tests --
918
919    #[test]
920    fn test_get_col_style_default_is_zero() {
921        let ws = WorksheetXml::default();
922        assert_eq!(get_col_style(&ws, "A").unwrap(), 0);
923    }
924
925    #[test]
926    fn test_set_col_style_applies_style() {
927        let mut ws = WorksheetXml::default();
928        set_col_style(&mut ws, "B", 4).unwrap();
929
930        let col = &ws.cols.as_ref().unwrap().cols[0];
931        assert_eq!(col.style, Some(4));
932    }
933
934    #[test]
935    fn test_get_col_style_after_set() {
936        let mut ws = WorksheetXml::default();
937        set_col_style(&mut ws, "C", 10).unwrap();
938        assert_eq!(get_col_style(&ws, "C").unwrap(), 10);
939    }
940
941    #[test]
942    fn test_set_col_style_creates_cols_container() {
943        let mut ws = WorksheetXml::default();
944        assert!(ws.cols.is_none());
945        set_col_style(&mut ws, "A", 2).unwrap();
946        assert!(ws.cols.is_some());
947    }
948
949    #[test]
950    fn test_set_col_style_overwrite() {
951        let mut ws = WorksheetXml::default();
952        set_col_style(&mut ws, "A", 3).unwrap();
953        set_col_style(&mut ws, "A", 7).unwrap();
954        assert_eq!(get_col_style(&ws, "A").unwrap(), 7);
955    }
956
957    #[test]
958    fn test_get_col_style_invalid_column_returns_error() {
959        let ws = WorksheetXml::default();
960        let result = get_col_style(&ws, "XFE");
961        assert!(result.is_err());
962    }
963
964    #[test]
965    fn test_set_col_style_invalid_column_returns_error() {
966        let mut ws = WorksheetXml::default();
967        let result = set_col_style(&mut ws, "XFE", 1);
968        assert!(result.is_err());
969    }
970}