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