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}