1use crate::error::ExcelMcpError;
4
5const MAX_COL: u16 = 16383;
7const MAX_ROW: u32 = 1_048_575;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct CellPos {
13 pub row: u32,
15 pub col: u16,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct CellRange {
22 pub start: CellPos,
23 pub end: CellPos,
24}
25
26pub fn col_letter_to_index(s: &str) -> Result<u16, ExcelMcpError> {
30 if s.is_empty() {
31 return Err(ExcelMcpError::ParseError(
32 "Column letter string is empty".into(),
33 ));
34 }
35
36 let mut index: u32 = 0;
37 for ch in s.chars() {
38 if !ch.is_ascii_alphabetic() {
39 return Err(ExcelMcpError::ParseError(format!(
40 "Invalid character '{}' in column letters '{}'",
41 ch, s
42 )));
43 }
44 let val = ch.to_ascii_uppercase() as u32 - b'A' as u32;
45 index = index * 26 + val + 1;
46 }
47 let index = index - 1;
49
50 if index > MAX_COL as u32 {
51 return Err(ExcelMcpError::InvalidInput(format!(
52 "Column '{}' (index {}) exceeds maximum Excel column XFD ({})",
53 s, index, MAX_COL
54 )));
55 }
56 Ok(index as u16)
57}
58
59pub fn index_to_col_letter(col: u16) -> String {
61 let mut result = String::new();
62 let mut n = col as u32 + 1; while n > 0 {
64 n -= 1;
65 let ch = (b'A' + (n % 26) as u8) as char;
66 result.push(ch);
67 n /= 26;
68 }
69 result.chars().rev().collect()
70}
71
72pub fn cell_pos_to_a1(pos: &CellPos) -> String {
74 format!("{}{}", index_to_col_letter(pos.col), pos.row + 1)
75}
76
77pub fn parse_cell_ref(s: &str) -> Result<CellPos, ExcelMcpError> {
82 let s = s.trim();
83 if s.is_empty() {
84 return Err(ExcelMcpError::ParseError("Cell reference is empty".into()));
85 }
86
87 let split = s.find(|c: char| c.is_ascii_digit()).ok_or_else(|| {
89 ExcelMcpError::ParseError(format!("Cell reference '{}' has no row number", s))
90 })?;
91
92 if split == 0 {
93 return Err(ExcelMcpError::ParseError(format!(
94 "Cell reference '{}' has no column letters",
95 s
96 )));
97 }
98
99 let col_str = &s[..split];
100 let row_str = &s[split..];
101
102 if !row_str.chars().all(|c| c.is_ascii_digit()) {
104 return Err(ExcelMcpError::ParseError(format!(
105 "Cell reference '{}' contains invalid characters in row portion '{}'",
106 s, row_str
107 )));
108 }
109
110 let col = col_letter_to_index(col_str)?;
111
112 let row_1based: u32 = row_str.parse().map_err(|_| {
113 ExcelMcpError::ParseError(format!(
114 "Invalid row number '{}' in cell reference '{}'",
115 row_str, s
116 ))
117 })?;
118
119 if row_1based == 0 {
120 return Err(ExcelMcpError::InvalidInput(format!(
121 "Row number in '{}' must be >= 1",
122 s
123 )));
124 }
125
126 let row = row_1based - 1;
127 if row > MAX_ROW {
128 return Err(ExcelMcpError::InvalidInput(format!(
129 "Row {} in '{}' exceeds maximum Excel row 1048576",
130 row_1based, s
131 )));
132 }
133
134 Ok(CellPos { row, col })
135}
136
137pub fn parse_range_ref(s: &str) -> Result<CellRange, ExcelMcpError> {
139 let s = s.trim();
140 let parts: Vec<&str> = s.split(':').collect();
141 if parts.len() != 2 {
142 return Err(ExcelMcpError::ParseError(format!(
143 "Range reference '{}' must contain exactly one ':'",
144 s
145 )));
146 }
147
148 let start = parse_cell_ref(parts[0])?;
149 let end = parse_cell_ref(parts[1])?;
150
151 Ok(CellRange { start, end })
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use proptest::prelude::*;
158
159 proptest! {
160 #[test]
162 fn roundtrip_cell_ref(row in 0u32..1_048_576, col in 0u16..16384) {
163 let pos = CellPos { row, col };
164 let a1 = cell_pos_to_a1(&pos);
165 let parsed = parse_cell_ref(&a1).unwrap();
166 prop_assert_eq!(parsed.row, pos.row);
167 prop_assert_eq!(parsed.col, pos.col);
168 }
169 }
170
171 #[test]
173 fn test_parse_cell_ref_known() {
174 let cases = [
175 ("A1", 0, 0),
176 ("B1", 0, 1),
177 ("Z1", 0, 25),
178 ("AA1", 0, 26),
179 ("AB1", 0, 27),
180 ("AZ1", 0, 51),
181 ("BA1", 0, 52),
182 ("XFD1", 0, 16383),
183 ("A2", 1, 0),
184 ("A1048576", 1_048_575, 0),
185 ("C10", 9, 2),
186 ];
187 for (input, expected_row, expected_col) in cases {
188 let pos = parse_cell_ref(input)
189 .unwrap_or_else(|e| panic!("Failed to parse '{}': {}", input, e));
190 assert_eq!(pos.row, expected_row, "Row mismatch for '{}'", input);
191 assert_eq!(pos.col, expected_col, "Col mismatch for '{}'", input);
192 }
193 }
194
195 #[test]
197 fn test_parse_cell_ref_invalid() {
198 assert!(parse_cell_ref("").is_err());
200
201 assert!(parse_cell_ref("123").is_err());
203
204 assert!(parse_cell_ref("ABC").is_err());
206
207 assert!(parse_cell_ref("A0").is_err());
209
210 assert!(parse_cell_ref("A1048577").is_err());
212
213 assert!(parse_cell_ref("XFE1").is_err());
215
216 assert!(parse_cell_ref("1A1").is_err());
218 }
219
220 #[test]
222 fn test_parse_range_ref() {
223 let range = parse_range_ref("A1:B2").unwrap();
225 assert_eq!(range.start, CellPos { row: 0, col: 0 });
226 assert_eq!(range.end, CellPos { row: 1, col: 1 });
227
228 let range = parse_range_ref("A1:XFD1048576").unwrap();
230 assert_eq!(range.start, CellPos { row: 0, col: 0 });
231 assert_eq!(
232 range.end,
233 CellPos {
234 row: 1_048_575,
235 col: 16383
236 }
237 );
238
239 let range = parse_range_ref("C3:C3").unwrap();
241 assert_eq!(range.start, CellPos { row: 2, col: 2 });
242 assert_eq!(range.end, CellPos { row: 2, col: 2 });
243
244 assert!(parse_range_ref("A1B2").is_err());
246
247 assert!(parse_range_ref("A1:B2:C3").is_err());
249
250 assert!(parse_range_ref("A0:B2").is_err());
252 }
253
254 #[test]
256 fn test_col_letter_to_index() {
257 assert_eq!(col_letter_to_index("A").unwrap(), 0);
258 assert_eq!(col_letter_to_index("B").unwrap(), 1);
259 assert_eq!(col_letter_to_index("Z").unwrap(), 25);
260 assert_eq!(col_letter_to_index("AA").unwrap(), 26);
261 assert_eq!(col_letter_to_index("AB").unwrap(), 27);
262 assert_eq!(col_letter_to_index("AZ").unwrap(), 51);
263 assert_eq!(col_letter_to_index("BA").unwrap(), 52);
264 assert_eq!(col_letter_to_index("XFD").unwrap(), 16383);
265
266 assert_eq!(col_letter_to_index("a").unwrap(), 0);
268 assert_eq!(col_letter_to_index("aa").unwrap(), 26);
269
270 assert!(col_letter_to_index("").is_err());
272
273 assert!(col_letter_to_index("A1").is_err());
275
276 assert!(col_letter_to_index("XFE").is_err());
278 }
279
280 #[test]
282 fn test_index_to_col_letter() {
283 assert_eq!(index_to_col_letter(0), "A");
284 assert_eq!(index_to_col_letter(1), "B");
285 assert_eq!(index_to_col_letter(25), "Z");
286 assert_eq!(index_to_col_letter(26), "AA");
287 assert_eq!(index_to_col_letter(27), "AB");
288 assert_eq!(index_to_col_letter(51), "AZ");
289 assert_eq!(index_to_col_letter(52), "BA");
290 assert_eq!(index_to_col_letter(16383), "XFD");
291 }
292}