Skip to main content

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)]
304mod tests {
305    use super::*;
306    use sheetkit_xml::worksheet::{Cell, CellTypeTag, Row, SheetData};
307
308    /// Helper: build a worksheet with some cells for column tests.
309    fn sample_ws() -> WorksheetXml {
310        let mut ws = WorksheetXml::default();
311        ws.sheet_data = SheetData {
312            rows: vec![
313                Row {
314                    r: 1,
315                    spans: None,
316                    s: None,
317                    custom_format: None,
318                    ht: None,
319                    hidden: None,
320                    custom_height: None,
321                    outline_level: None,
322                    cells: vec![
323                        Cell {
324                            r: "A1".into(),
325                            col: 1,
326                            s: None,
327                            t: CellTypeTag::None,
328                            v: Some("10".to_string()),
329                            f: None,
330                            is: None,
331                        },
332                        Cell {
333                            r: "B1".into(),
334                            col: 2,
335                            s: None,
336                            t: CellTypeTag::None,
337                            v: Some("20".to_string()),
338                            f: None,
339                            is: None,
340                        },
341                        Cell {
342                            r: "D1".into(),
343                            col: 4,
344                            s: None,
345                            t: CellTypeTag::None,
346                            v: Some("40".to_string()),
347                            f: None,
348                            is: None,
349                        },
350                    ],
351                },
352                Row {
353                    r: 2,
354                    spans: None,
355                    s: None,
356                    custom_format: None,
357                    ht: None,
358                    hidden: None,
359                    custom_height: None,
360                    outline_level: None,
361                    cells: vec![
362                        Cell {
363                            r: "A2".into(),
364                            col: 1,
365                            s: None,
366                            t: CellTypeTag::None,
367                            v: Some("100".to_string()),
368                            f: None,
369                            is: None,
370                        },
371                        Cell {
372                            r: "C2".into(),
373                            col: 3,
374                            s: None,
375                            t: CellTypeTag::None,
376                            v: Some("300".to_string()),
377                            f: None,
378                            is: None,
379                        },
380                    ],
381                },
382            ],
383        };
384        ws
385    }
386
387    #[test]
388    fn test_set_and_get_col_width() {
389        let mut ws = WorksheetXml::default();
390        set_col_width(&mut ws, "A", 15.0).unwrap();
391        assert_eq!(get_col_width(&ws, "A"), Some(15.0));
392    }
393
394    #[test]
395    fn test_set_col_width_creates_cols_container() {
396        let mut ws = WorksheetXml::default();
397        assert!(ws.cols.is_none());
398        set_col_width(&mut ws, "B", 20.0).unwrap();
399        assert!(ws.cols.is_some());
400        let cols = ws.cols.as_ref().unwrap();
401        assert_eq!(cols.cols.len(), 1);
402        assert_eq!(cols.cols[0].min, 2);
403        assert_eq!(cols.cols[0].max, 2);
404        assert_eq!(cols.cols[0].custom_width, Some(true));
405    }
406
407    #[test]
408    fn test_set_col_width_zero_is_valid() {
409        let mut ws = WorksheetXml::default();
410        set_col_width(&mut ws, "A", 0.0).unwrap();
411        assert_eq!(get_col_width(&ws, "A"), Some(0.0));
412    }
413
414    #[test]
415    fn test_set_col_width_max_is_valid() {
416        let mut ws = WorksheetXml::default();
417        set_col_width(&mut ws, "A", 255.0).unwrap();
418        assert_eq!(get_col_width(&ws, "A"), Some(255.0));
419    }
420
421    #[test]
422    fn test_set_col_width_exceeds_max_returns_error() {
423        let mut ws = WorksheetXml::default();
424        let result = set_col_width(&mut ws, "A", 256.0);
425        assert!(result.is_err());
426        assert!(matches!(
427            result.unwrap_err(),
428            Error::ColumnWidthExceeded { .. }
429        ));
430    }
431
432    #[test]
433    fn test_set_col_width_negative_returns_error() {
434        let mut ws = WorksheetXml::default();
435        let result = set_col_width(&mut ws, "A", -1.0);
436        assert!(result.is_err());
437    }
438
439    #[test]
440    fn test_get_col_width_nonexistent_returns_none() {
441        let ws = WorksheetXml::default();
442        assert_eq!(get_col_width(&ws, "Z"), None);
443    }
444
445    #[test]
446    fn test_set_col_width_invalid_column_returns_error() {
447        let mut ws = WorksheetXml::default();
448        let result = set_col_width(&mut ws, "XFE", 10.0);
449        assert!(result.is_err());
450    }
451
452    #[test]
453    fn test_set_col_hidden() {
454        let mut ws = WorksheetXml::default();
455        set_col_visible(&mut ws, "A", false).unwrap();
456
457        let col = &ws.cols.as_ref().unwrap().cols[0];
458        assert_eq!(col.hidden, Some(true));
459    }
460
461    #[test]
462    fn test_set_col_visible_clears_hidden() {
463        let mut ws = WorksheetXml::default();
464        set_col_visible(&mut ws, "A", false).unwrap();
465        set_col_visible(&mut ws, "A", true).unwrap();
466
467        let col = &ws.cols.as_ref().unwrap().cols[0];
468        assert_eq!(col.hidden, None);
469    }
470
471    #[test]
472    fn test_insert_cols_shifts_cells_right() {
473        let mut ws = sample_ws();
474        insert_cols(&mut ws, "B", 2).unwrap();
475
476        // Row 1: A1 stays, B1->D1, D1->F1.
477        let r1 = &ws.sheet_data.rows[0];
478        assert_eq!(r1.cells[0].r, "A1");
479        assert_eq!(r1.cells[1].r, "D1"); // B shifted by 2
480        assert_eq!(r1.cells[2].r, "F1"); // D shifted by 2
481
482        // Row 2: A2 stays, C2->E2.
483        let r2 = &ws.sheet_data.rows[1];
484        assert_eq!(r2.cells[0].r, "A2");
485        assert_eq!(r2.cells[1].r, "E2"); // C shifted by 2
486    }
487
488    #[test]
489    fn test_insert_cols_at_column_a() {
490        let mut ws = sample_ws();
491        insert_cols(&mut ws, "A", 1).unwrap();
492
493        // All cells shift right by 1.
494        let r1 = &ws.sheet_data.rows[0];
495        assert_eq!(r1.cells[0].r, "B1"); // A->B
496        assert_eq!(r1.cells[1].r, "C1"); // B->C
497        assert_eq!(r1.cells[2].r, "E1"); // D->E
498    }
499
500    #[test]
501    fn test_insert_cols_count_zero_is_noop() {
502        let mut ws = sample_ws();
503        insert_cols(&mut ws, "B", 0).unwrap();
504        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
505        assert_eq!(ws.sheet_data.rows[0].cells[1].r, "B1");
506    }
507
508    #[test]
509    fn test_insert_cols_on_empty_sheet() {
510        let mut ws = WorksheetXml::default();
511        insert_cols(&mut ws, "A", 5).unwrap();
512        assert!(ws.sheet_data.rows.is_empty());
513    }
514
515    #[test]
516    fn test_insert_cols_shifts_col_definitions() {
517        let mut ws = WorksheetXml::default();
518        set_col_width(&mut ws, "C", 20.0).unwrap();
519        insert_cols(&mut ws, "B", 2).unwrap();
520
521        // Col C (3) should now be at col 5 (E).
522        let col = &ws.cols.as_ref().unwrap().cols[0];
523        assert_eq!(col.min, 5);
524        assert_eq!(col.max, 5);
525    }
526
527    #[test]
528    fn test_remove_col_shifts_cells_left() {
529        let mut ws = sample_ws();
530        remove_col(&mut ws, "B").unwrap();
531
532        // Row 1: A1 stays, B1 removed, D1->C1.
533        let r1 = &ws.sheet_data.rows[0];
534        assert_eq!(r1.cells.len(), 2);
535        assert_eq!(r1.cells[0].r, "A1");
536        assert_eq!(r1.cells[1].r, "C1"); // D shifted left
537        assert_eq!(r1.cells[1].v, Some("40".to_string()));
538
539        // Row 2: A2 stays, C2->B2.
540        let r2 = &ws.sheet_data.rows[1];
541        assert_eq!(r2.cells[0].r, "A2");
542        assert_eq!(r2.cells[1].r, "B2"); // C shifted left
543    }
544
545    #[test]
546    fn test_remove_first_col() {
547        let mut ws = sample_ws();
548        remove_col(&mut ws, "A").unwrap();
549
550        // Row 1: A1 removed, B1->A1, D1->C1.
551        let r1 = &ws.sheet_data.rows[0];
552        assert_eq!(r1.cells.len(), 2);
553        assert_eq!(r1.cells[0].r, "A1");
554        assert_eq!(r1.cells[0].v, Some("20".to_string())); // was B1
555        assert_eq!(r1.cells[1].r, "C1");
556        assert_eq!(r1.cells[1].v, Some("40".to_string())); // was D1
557    }
558
559    #[test]
560    fn test_remove_col_with_col_definitions() {
561        let mut ws = WorksheetXml::default();
562        set_col_width(&mut ws, "B", 20.0).unwrap();
563        remove_col(&mut ws, "B").unwrap();
564
565        // The Col entry for B should be removed.
566        assert!(ws.cols.is_none());
567    }
568
569    #[test]
570    fn test_remove_col_shrinks_range_ending_at_removed_column() {
571        // Range B:C (min=2, max=3); removing column C should shrink to B:B (min=2, max=2).
572        let mut ws = WorksheetXml::default();
573        set_col_width(&mut ws, "B", 15.0).unwrap();
574        // Manually extend the col entry to span B:C.
575        ws.cols.as_mut().unwrap().cols[0].max = 3;
576        remove_col(&mut ws, "C").unwrap();
577        let col = &ws.cols.as_ref().unwrap().cols[0];
578        assert_eq!(col.min, 2);
579        assert_eq!(col.max, 2);
580    }
581
582    #[test]
583    fn test_remove_col_shrinks_range_spanning_removed_column() {
584        // Range B:E (min=2, max=5); removing column C should shrink to B:D (min=2, max=4).
585        let mut ws = WorksheetXml::default();
586        set_col_width(&mut ws, "B", 15.0).unwrap();
587        ws.cols.as_mut().unwrap().cols[0].max = 5;
588        remove_col(&mut ws, "C").unwrap();
589        let col = &ws.cols.as_ref().unwrap().cols[0];
590        assert_eq!(col.min, 2);
591        assert_eq!(col.max, 4);
592    }
593
594    #[test]
595    fn test_remove_col_invalid_column_returns_error() {
596        let mut ws = WorksheetXml::default();
597        let result = remove_col(&mut ws, "XFE");
598        assert!(result.is_err());
599    }
600
601    #[test]
602    fn test_remove_col_invalid_cell_reference_returns_error() {
603        let mut ws = sample_ws();
604        ws.sheet_data.rows[0].cells.push(Cell {
605            r: "INVALID".into(),
606            col: 0,
607            s: None,
608            t: CellTypeTag::None,
609            v: Some("1".to_string()),
610            f: None,
611            is: None,
612        });
613        let result = remove_col(&mut ws, "A");
614        assert!(result.is_err());
615    }
616
617    #[test]
618    fn test_set_multiple_col_widths() {
619        let mut ws = WorksheetXml::default();
620        set_col_width(&mut ws, "A", 10.0).unwrap();
621        set_col_width(&mut ws, "C", 30.0).unwrap();
622
623        assert_eq!(get_col_width(&ws, "A"), Some(10.0));
624        assert_eq!(get_col_width(&ws, "B"), None);
625        assert_eq!(get_col_width(&ws, "C"), Some(30.0));
626    }
627
628    #[test]
629    fn test_overwrite_col_width() {
630        let mut ws = WorksheetXml::default();
631        set_col_width(&mut ws, "A", 10.0).unwrap();
632        set_col_width(&mut ws, "A", 25.0).unwrap();
633
634        assert_eq!(get_col_width(&ws, "A"), Some(25.0));
635    }
636
637    #[test]
638    fn test_get_col_visible_default_is_true() {
639        let ws = WorksheetXml::default();
640        assert!(get_col_visible(&ws, "A").unwrap());
641    }
642
643    #[test]
644    fn test_get_col_visible_after_hide() {
645        let mut ws = WorksheetXml::default();
646        set_col_visible(&mut ws, "B", false).unwrap();
647        assert!(!get_col_visible(&ws, "B").unwrap());
648    }
649
650    #[test]
651    fn test_get_col_visible_after_hide_then_show() {
652        let mut ws = WorksheetXml::default();
653        set_col_visible(&mut ws, "A", false).unwrap();
654        set_col_visible(&mut ws, "A", true).unwrap();
655        assert!(get_col_visible(&ws, "A").unwrap());
656    }
657
658    #[test]
659    fn test_get_col_visible_invalid_column_returns_error() {
660        let ws = WorksheetXml::default();
661        let result = get_col_visible(&ws, "XFE");
662        assert!(result.is_err());
663    }
664
665    #[test]
666    fn test_set_col_outline_level() {
667        let mut ws = WorksheetXml::default();
668        set_col_outline_level(&mut ws, "A", 3).unwrap();
669
670        let col = &ws.cols.as_ref().unwrap().cols[0];
671        assert_eq!(col.outline_level, Some(3));
672    }
673
674    #[test]
675    fn test_set_col_outline_level_zero_clears() {
676        let mut ws = WorksheetXml::default();
677        set_col_outline_level(&mut ws, "A", 3).unwrap();
678        set_col_outline_level(&mut ws, "A", 0).unwrap();
679
680        let col = &ws.cols.as_ref().unwrap().cols[0];
681        assert_eq!(col.outline_level, None);
682    }
683
684    #[test]
685    fn test_set_col_outline_level_exceeds_max_returns_error() {
686        let mut ws = WorksheetXml::default();
687        let result = set_col_outline_level(&mut ws, "A", 8);
688        assert!(result.is_err());
689    }
690
691    #[test]
692    fn test_set_col_outline_level_max_valid() {
693        let mut ws = WorksheetXml::default();
694        set_col_outline_level(&mut ws, "A", 7).unwrap();
695
696        let col = &ws.cols.as_ref().unwrap().cols[0];
697        assert_eq!(col.outline_level, Some(7));
698    }
699
700    #[test]
701    fn test_get_col_outline_level_default_is_zero() {
702        let ws = WorksheetXml::default();
703        assert_eq!(get_col_outline_level(&ws, "A").unwrap(), 0);
704    }
705
706    #[test]
707    fn test_get_col_outline_level_after_set() {
708        let mut ws = WorksheetXml::default();
709        set_col_outline_level(&mut ws, "B", 5).unwrap();
710        assert_eq!(get_col_outline_level(&ws, "B").unwrap(), 5);
711    }
712
713    #[test]
714    fn test_get_col_outline_level_after_clear() {
715        let mut ws = WorksheetXml::default();
716        set_col_outline_level(&mut ws, "C", 4).unwrap();
717        set_col_outline_level(&mut ws, "C", 0).unwrap();
718        assert_eq!(get_col_outline_level(&ws, "C").unwrap(), 0);
719    }
720
721    #[test]
722    fn test_get_col_outline_level_invalid_column_returns_error() {
723        let ws = WorksheetXml::default();
724        let result = get_col_outline_level(&ws, "XFE");
725        assert!(result.is_err());
726    }
727
728    // -- get_cols tests --
729
730    #[test]
731    fn test_get_cols_empty_sheet() {
732        let ws = WorksheetXml::default();
733        let sst = SharedStringTable::new();
734        let cols = get_cols(&ws, &sst).unwrap();
735        assert!(cols.is_empty());
736    }
737
738    #[test]
739    fn test_get_cols_transposes_row_data() {
740        let ws = sample_ws();
741        let sst = SharedStringTable::new();
742        let cols = get_cols(&ws, &sst).unwrap();
743
744        // sample_ws has:
745        //   Row 1: A1=10, B1=20, D1=40
746        //   Row 2: A2=100, C2=300
747        // Columns should be: A, B, C, D
748
749        assert_eq!(cols.len(), 4);
750
751        // Column A: (1, 10.0), (2, 100.0)
752        assert_eq!(cols[0].0, "A");
753        assert_eq!(cols[0].1.len(), 2);
754        assert_eq!(cols[0].1[0], (1, CellValue::Number(10.0)));
755        assert_eq!(cols[0].1[1], (2, CellValue::Number(100.0)));
756
757        // Column B: (1, 20.0)
758        assert_eq!(cols[1].0, "B");
759        assert_eq!(cols[1].1.len(), 1);
760        assert_eq!(cols[1].1[0], (1, CellValue::Number(20.0)));
761
762        // Column C: (2, 300.0)
763        assert_eq!(cols[2].0, "C");
764        assert_eq!(cols[2].1.len(), 1);
765        assert_eq!(cols[2].1[0], (2, CellValue::Number(300.0)));
766
767        // Column D: (1, 40.0)
768        assert_eq!(cols[3].0, "D");
769        assert_eq!(cols[3].1.len(), 1);
770        assert_eq!(cols[3].1[0], (1, CellValue::Number(40.0)));
771    }
772
773    #[test]
774    fn test_get_cols_with_shared_strings() {
775        let mut sst = SharedStringTable::new();
776        sst.add("Name");
777        sst.add("Age");
778        sst.add("Alice");
779
780        let mut ws = WorksheetXml::default();
781        ws.sheet_data = SheetData {
782            rows: vec![
783                Row {
784                    r: 1,
785                    spans: None,
786                    s: None,
787                    custom_format: None,
788                    ht: None,
789                    hidden: None,
790                    custom_height: None,
791                    outline_level: None,
792                    cells: vec![
793                        Cell {
794                            r: "A1".into(),
795                            col: 1,
796                            s: None,
797                            t: CellTypeTag::SharedString,
798                            v: Some("0".to_string()),
799                            f: None,
800                            is: None,
801                        },
802                        Cell {
803                            r: "B1".into(),
804                            col: 2,
805                            s: None,
806                            t: CellTypeTag::SharedString,
807                            v: Some("1".to_string()),
808                            f: None,
809                            is: None,
810                        },
811                    ],
812                },
813                Row {
814                    r: 2,
815                    spans: None,
816                    s: None,
817                    custom_format: None,
818                    ht: None,
819                    hidden: None,
820                    custom_height: None,
821                    outline_level: None,
822                    cells: vec![
823                        Cell {
824                            r: "A2".into(),
825                            col: 1,
826                            s: None,
827                            t: CellTypeTag::SharedString,
828                            v: Some("2".to_string()),
829                            f: None,
830                            is: None,
831                        },
832                        Cell {
833                            r: "B2".into(),
834                            col: 2,
835                            s: None,
836                            t: CellTypeTag::None,
837                            v: Some("30".to_string()),
838                            f: None,
839                            is: None,
840                        },
841                    ],
842                },
843            ],
844        };
845
846        let cols = get_cols(&ws, &sst).unwrap();
847        assert_eq!(cols.len(), 2);
848
849        // Column A: "Name", "Alice"
850        assert_eq!(cols[0].0, "A");
851        assert_eq!(cols[0].1[0].1, CellValue::String("Name".to_string()));
852        assert_eq!(cols[0].1[1].1, CellValue::String("Alice".to_string()));
853
854        // Column B: "Age", 30
855        assert_eq!(cols[1].0, "B");
856        assert_eq!(cols[1].1[0].1, CellValue::String("Age".to_string()));
857        assert_eq!(cols[1].1[1].1, CellValue::Number(30.0));
858    }
859
860    #[test]
861    fn test_get_cols_sorted_correctly() {
862        // Verify that columns are sorted by length then alphabetically:
863        // A, B, ..., Z, AA, AB, ...
864        let mut ws = WorksheetXml::default();
865        ws.sheet_data = SheetData {
866            rows: vec![Row {
867                r: 1,
868                spans: None,
869                s: None,
870                custom_format: None,
871                ht: None,
872                hidden: None,
873                custom_height: None,
874                outline_level: None,
875                cells: vec![
876                    Cell {
877                        r: "AA1".into(),
878                        col: 27,
879                        s: None,
880                        t: CellTypeTag::None,
881                        v: Some("1".to_string()),
882                        f: None,
883                        is: None,
884                    },
885                    Cell {
886                        r: "B1".into(),
887                        col: 2,
888                        s: None,
889                        t: CellTypeTag::None,
890                        v: Some("2".to_string()),
891                        f: None,
892                        is: None,
893                    },
894                    Cell {
895                        r: "A1".into(),
896                        col: 1,
897                        s: None,
898                        t: CellTypeTag::None,
899                        v: Some("3".to_string()),
900                        f: None,
901                        is: None,
902                    },
903                ],
904            }],
905        };
906
907        let sst = SharedStringTable::new();
908        let cols = get_cols(&ws, &sst).unwrap();
909
910        assert_eq!(cols.len(), 3);
911        assert_eq!(cols[0].0, "A");
912        assert_eq!(cols[1].0, "B");
913        assert_eq!(cols[2].0, "AA");
914    }
915
916    // -- set_col_style / get_col_style tests --
917
918    #[test]
919    fn test_get_col_style_default_is_zero() {
920        let ws = WorksheetXml::default();
921        assert_eq!(get_col_style(&ws, "A").unwrap(), 0);
922    }
923
924    #[test]
925    fn test_set_col_style_applies_style() {
926        let mut ws = WorksheetXml::default();
927        set_col_style(&mut ws, "B", 4).unwrap();
928
929        let col = &ws.cols.as_ref().unwrap().cols[0];
930        assert_eq!(col.style, Some(4));
931    }
932
933    #[test]
934    fn test_get_col_style_after_set() {
935        let mut ws = WorksheetXml::default();
936        set_col_style(&mut ws, "C", 10).unwrap();
937        assert_eq!(get_col_style(&ws, "C").unwrap(), 10);
938    }
939
940    #[test]
941    fn test_set_col_style_creates_cols_container() {
942        let mut ws = WorksheetXml::default();
943        assert!(ws.cols.is_none());
944        set_col_style(&mut ws, "A", 2).unwrap();
945        assert!(ws.cols.is_some());
946    }
947
948    #[test]
949    fn test_set_col_style_overwrite() {
950        let mut ws = WorksheetXml::default();
951        set_col_style(&mut ws, "A", 3).unwrap();
952        set_col_style(&mut ws, "A", 7).unwrap();
953        assert_eq!(get_col_style(&ws, "A").unwrap(), 7);
954    }
955
956    #[test]
957    fn test_get_col_style_invalid_column_returns_error() {
958        let ws = WorksheetXml::default();
959        let result = get_col_style(&ws, "XFE");
960        assert!(result.is_err());
961    }
962
963    #[test]
964    fn test_set_col_style_invalid_column_returns_error() {
965        let mut ws = WorksheetXml::default();
966        let result = set_col_style(&mut ws, "XFE", 1);
967        assert!(result.is_err());
968    }
969}