Skip to main content

spreadsheet_formulas/
range.rs

1/// Range parsing and representation.
2/// Supports A1:A10, B:B, $A$1:$C$5, etc.
3
4use crate::cellref::CellRef;
5
6/// A cell range, either two explicit cell references or a full column range.
7#[derive(Debug, Clone, PartialEq)]
8pub enum Range {
9    /// Two explicit cell references: A1:B5
10    CellRange { start: CellRef, end: CellRef },
11    /// Full column range: B:B or B:D
12    ColRange { start_col: usize, end_col: usize },
13}
14
15impl Range {
16    /// Parse a range string.
17    pub fn parse(input: &str) -> Option<Self> {
18        let input = input.trim();
19        let parts: Vec<&str> = input.split(':').collect();
20        if parts.len() != 2 {
21            return None;
22        }
23
24        let left = parts[0].trim();
25        let right = parts[1].trim();
26
27        // Try full column range: both sides are just letters (no digits)
28        let _left_letters: String = left.chars().filter(|c| c.is_ascii_alphabetic()).collect();
29        let _left_digits: String = left.chars().filter(|c| c.is_ascii_digit()).collect();
30        let _right_letters: String = right.chars().filter(|c| c.is_ascii_alphabetic()).collect();
31        let _right_digits: String = right.chars().filter(|c| c.is_ascii_digit()).collect();
32
33        // Strip $ for column-only check
34        let left_clean: String = left.chars().filter(|c| *c != '$').collect();
35        let right_clean: String = right.chars().filter(|c| *c != '$').collect();
36
37        // Column range: B:B or B:D (no digits after stripping $)
38        let left_has_no_digits = left_clean.chars().all(|c| c.is_ascii_alphabetic());
39        let right_has_no_digits = right_clean.chars().all(|c| c.is_ascii_alphabetic());
40
41        if left_has_no_digits && right_has_no_digits {
42            let start_col = crate::cellref::col_letters_to_index(&left_clean)?;
43            let end_col = crate::cellref::col_letters_to_index(&right_clean)?;
44            return Some(Range::ColRange { start_col, end_col });
45        }
46
47        // Cell range: A1:B5
48        let start = CellRef::parse(left)?;
49        let end = CellRef::parse(right)?;
50        Some(Range::CellRange { start, end })
51    }
52
53    /// Iterate over all (row, col) pairs in this range.
54    pub fn iter_cells(&self) -> Box<dyn Iterator<Item = (usize, usize)> + '_> {
55        match self {
56            Range::CellRange { start, end } => {
57                let min_row = start.row.min(end.row);
58                let max_row = start.row.max(end.row);
59                let min_col = start.col.min(end.col);
60                let max_col = start.col.max(end.col);
61                Box::new(
62                    (min_row..=max_row)
63                        .flat_map(move |r| (min_col..=max_col).map(move |c| (r, c))),
64                )
65            }
66            Range::ColRange { start_col, end_col } => {
67                // Column ranges are unbounded; iterate col pairs only
68                let min = *start_col.min(end_col);
69                let max = *start_col.max(end_col);
70                Box::new((min..=max).map(|c| (0usize, c)))
71            }
72        }
73    }
74}
75
76impl std::fmt::Display for Range {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            Range::CellRange { start, end } => write!(f, "{}:{}", start, end),
80            Range::ColRange { start_col, end_col } => write!(
81                f,
82                "{}:{}",
83                crate::cellref::index_to_col_letters(*start_col),
84                crate::cellref::index_to_col_letters(*end_col)
85            ),
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn parse_cell_range() {
96        let r = Range::parse("A1:B5").unwrap();
97        assert!(matches!(r, Range::CellRange { .. }));
98    }
99
100    #[test]
101    fn parse_col_range() {
102        let r = Range::parse("B:B").unwrap();
103        assert!(matches!(r, Range::ColRange { start_col: 1, end_col: 1 }));
104    }
105
106    #[test]
107    fn parse_multi_col_range() {
108        let r = Range::parse("B:D").unwrap();
109        assert!(matches!(r, Range::ColRange { start_col: 1, end_col: 3 }));
110    }
111
112    #[test]
113    fn parse_absolute_range() {
114        let r = Range::parse("$A$1:$C$5").unwrap();
115        if let Range::CellRange { start, end } = r {
116            assert_eq!(start.col, 0);
117            assert_eq!(start.row, 0);
118            assert!(start.abs_col && start.abs_row);
119            assert_eq!(end.col, 2);
120            assert_eq!(end.row, 4);
121        } else {
122            panic!("Expected CellRange");
123        }
124    }
125
126    #[test]
127    fn iter_cells_range() {
128        let r = Range::parse("A1:B2").unwrap();
129        let cells: Vec<_> = r.iter_cells().collect();
130        assert_eq!(cells, vec![(0, 0), (0, 1), (1, 0), (1, 1)]);
131    }
132
133    #[test]
134    fn parse_invalid_no_colon() {
135        assert!(Range::parse("A1").is_none());
136    }
137}