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