Skip to main content

zellij_sheets/
address.rs

1use thiserror::Error;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub struct CellAddress {
5    pub row: usize,
6    pub col: usize,
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum AddressCommand {
11    Cell(CellAddress),
12    Range {
13        start: CellAddress,
14        end: CellAddress,
15    },
16    Write {
17        target: CellAddress,
18        value: String,
19    },
20}
21
22#[derive(Debug, Error, PartialEq, Eq)]
23pub enum AddressError {
24    #[error("invalid address: {0}")]
25    InvalidAddress(String),
26}
27
28pub type Result<T> = std::result::Result<T, AddressError>;
29
30pub fn parse_address_command(input: &str) -> Result<AddressCommand> {
31    if let Some((lhs, rhs)) = input.split_once('=') {
32        let target = parse_cell_address(lhs.trim())?;
33        return Ok(AddressCommand::Write {
34            target,
35            value: rhs.to_string(),
36        });
37    }
38
39    if let Some((lhs, rhs)) = input.split_once(':') {
40        let start = parse_cell_address(lhs.trim())?;
41        let end = parse_cell_address(rhs.trim())?;
42        return Ok(AddressCommand::Range {
43            start: normalize_cell_range(start, end).0,
44            end: normalize_cell_range(start, end).1,
45        });
46    }
47
48    Ok(AddressCommand::Cell(parse_cell_address(input.trim())?))
49}
50
51pub fn parse_cell_address(input: &str) -> Result<CellAddress> {
52    let trimmed = input.trim();
53    if trimmed.is_empty() {
54        return Err(AddressError::InvalidAddress("empty address".to_string()));
55    }
56
57    let split_at = trimmed
58        .find(|ch: char| ch.is_ascii_digit())
59        .ok_or_else(|| AddressError::InvalidAddress(trimmed.to_string()))?;
60    let (col_part, row_part) = trimmed.split_at(split_at);
61
62    if col_part.is_empty()
63        || row_part.is_empty()
64        || !col_part.chars().all(|ch| ch.is_ascii_alphabetic())
65        || !row_part.chars().all(|ch| ch.is_ascii_digit())
66    {
67        return Err(AddressError::InvalidAddress(trimmed.to_string()));
68    }
69
70    let col = col_letter_to_index(col_part)?;
71    let row_number = row_part
72        .parse::<usize>()
73        .map_err(|_| AddressError::InvalidAddress(trimmed.to_string()))?;
74    let row = row_number
75        .checked_sub(1)
76        .ok_or_else(|| AddressError::InvalidAddress(trimmed.to_string()))?;
77
78    Ok(CellAddress { row, col })
79}
80
81pub fn col_letter_to_index(input: &str) -> Result<usize> {
82    let trimmed = input.trim();
83    if trimmed.is_empty() || !trimmed.chars().all(|ch| ch.is_ascii_alphabetic()) {
84        return Err(AddressError::InvalidAddress(input.to_string()));
85    }
86
87    let mut value = 0usize;
88    for ch in trimmed.chars() {
89        let letter = ch.to_ascii_uppercase();
90        let digit = (letter as u8 - b'A' + 1) as usize;
91        value = value
92            .checked_mul(26)
93            .and_then(|current| current.checked_add(digit))
94            .ok_or_else(|| AddressError::InvalidAddress(input.to_string()))?;
95    }
96
97    value
98        .checked_sub(1)
99        .ok_or_else(|| AddressError::InvalidAddress(input.to_string()))
100}
101
102pub fn index_to_col_letters(index: usize) -> String {
103    let mut n = index + 1;
104    let mut out = Vec::new();
105
106    while n > 0 {
107        let rem = (n - 1) % 26;
108        out.push((b'A' + rem as u8) as char);
109        n = (n - 1) / 26;
110    }
111
112    out.iter().rev().collect()
113}
114
115fn normalize_cell_range(a: CellAddress, b: CellAddress) -> (CellAddress, CellAddress) {
116    (
117        CellAddress {
118            row: a.row.min(b.row),
119            col: a.col.min(b.col),
120        },
121        CellAddress {
122            row: a.row.max(b.row),
123            col: a.col.max(b.col),
124        },
125    )
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_address_parse_single_cell() {
134        assert_eq!(
135            parse_address_command("B9").unwrap(),
136            AddressCommand::Cell(CellAddress { row: 8, col: 1 })
137        );
138    }
139
140    #[test]
141    fn test_address_parse_range() {
142        assert_eq!(
143            parse_address_command("B1:B3").unwrap(),
144            AddressCommand::Range {
145                start: CellAddress { row: 0, col: 1 },
146                end: CellAddress { row: 2, col: 1 },
147            }
148        );
149    }
150
151    #[test]
152    fn test_address_parse_write() {
153        assert_eq!(
154            parse_address_command("B7=10").unwrap(),
155            AddressCommand::Write {
156                target: CellAddress { row: 6, col: 1 },
157                value: "10".to_string(),
158            }
159        );
160    }
161
162    #[test]
163    fn test_address_parse_aa_column() {
164        assert_eq!(
165            parse_cell_address("AA1").unwrap(),
166            CellAddress { row: 0, col: 26 }
167        );
168    }
169
170    #[test]
171    fn test_address_rejects_invalid_row() {
172        assert!(parse_cell_address("A0").is_err());
173    }
174
175    #[test]
176    fn test_address_rejects_invalid_format() {
177        assert!(parse_address_command("9B").is_err());
178    }
179
180    #[test]
181    fn test_address_index_to_col_letters() {
182        assert_eq!(index_to_col_letters(0), "A");
183        assert_eq!(index_to_col_letters(25), "Z");
184        assert_eq!(index_to_col_letters(26), "AA");
185    }
186}