sheetkit_core/utils/
cell_ref.rs1use crate::error::{Error, Result};
8use crate::utils::constants::{MAX_COLUMNS, MAX_ROWS};
9
10pub fn column_name_to_number(name: &str) -> Result<u32> {
24 if name.is_empty() {
25 return Err(Error::InvalidCellReference("empty column name".to_string()));
26 }
27
28 let mut result: u32 = 0;
29
30 for c in name.chars() {
31 if !c.is_ascii_alphabetic() {
32 return Err(Error::InvalidCellReference(format!(
33 "non-alphabetic character in column name: '{c}'"
34 )));
35 }
36
37 let digit = (c.to_ascii_uppercase() as u32) - ('A' as u32) + 1;
38
39 result = result
40 .checked_mul(26)
41 .and_then(|r| r.checked_add(digit))
42 .ok_or(Error::InvalidColumnNumber(0))?;
43 }
44
45 if result > MAX_COLUMNS {
46 return Err(Error::InvalidColumnNumber(result));
47 }
48
49 Ok(result)
50}
51
52pub fn column_number_to_name(num: u32) -> Result<String> {
65 if !(1..=MAX_COLUMNS).contains(&num) {
66 return Err(Error::InvalidColumnNumber(num));
67 }
68
69 let mut col = num;
70 let mut result = String::with_capacity(3);
71
72 while col > 0 {
73 col -= 1; let remainder = (col % 26) as u8;
75 result.insert(0, (b'A' + remainder) as char);
76 col /= 26;
77 }
78
79 Ok(result)
80}
81
82pub fn cell_name_to_coordinates(cell: &str) -> Result<(u32, u32)> {
97 let cell = cell.replace('$', "");
99
100 if cell.is_empty() {
101 return Err(Error::InvalidCellReference(
102 "empty cell reference".to_string(),
103 ));
104 }
105
106 let mut col_end = 0;
108 for (i, c) in cell.char_indices() {
109 if c.is_ascii_alphabetic() {
110 col_end = i + c.len_utf8();
111 } else {
112 break;
113 }
114 }
115
116 if col_end == 0 {
117 return Err(Error::InvalidCellReference(format!(
118 "no column letters in '{cell}'"
119 )));
120 }
121
122 let col_str = &cell[..col_end];
123 let row_str = &cell[col_end..];
124
125 if row_str.is_empty() {
126 return Err(Error::InvalidCellReference(format!(
127 "no row number in '{cell}'"
128 )));
129 }
130
131 let col = column_name_to_number(col_str)?;
132
133 let row: u32 = row_str
134 .parse()
135 .map_err(|_| Error::InvalidCellReference(format!("invalid row number in '{cell}'")))?;
136
137 if !(1..=MAX_ROWS).contains(&row) {
138 return Err(Error::InvalidRowNumber(row));
139 }
140
141 Ok((col, row))
142}
143
144pub fn coordinates_to_cell_name(col: u32, row: u32) -> Result<String> {
155 if !(1..=MAX_COLUMNS).contains(&col) {
156 return Err(Error::InvalidColumnNumber(col));
157 }
158 if !(1..=MAX_ROWS).contains(&row) {
159 return Err(Error::InvalidRowNumber(row));
160 }
161
162 let col_name = column_number_to_name(col)?;
163 Ok(format!("{col_name}{row}"))
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_column_name_a() {
172 assert_eq!(column_name_to_number("A").unwrap(), 1);
173 }
174
175 #[test]
176 fn test_column_name_z() {
177 assert_eq!(column_name_to_number("Z").unwrap(), 26);
178 }
179
180 #[test]
181 fn test_column_name_aa() {
182 assert_eq!(column_name_to_number("AA").unwrap(), 27);
183 }
184
185 #[test]
186 fn test_column_name_az() {
187 assert_eq!(column_name_to_number("AZ").unwrap(), 52);
188 }
189
190 #[test]
191 fn test_column_name_ba() {
192 assert_eq!(column_name_to_number("BA").unwrap(), 53);
193 }
194
195 #[test]
196 fn test_column_name_xfd() {
197 assert_eq!(column_name_to_number("XFD").unwrap(), 16384);
198 }
199
200 #[test]
201 fn test_column_name_lowercase() {
202 assert_eq!(column_name_to_number("a").unwrap(), 1);
203 assert_eq!(column_name_to_number("xfd").unwrap(), 16384);
204 }
205
206 #[test]
207 fn test_column_name_xfe_out_of_range() {
208 assert!(column_name_to_number("XFE").is_err());
209 }
210
211 #[test]
212 fn test_column_name_empty() {
213 assert!(column_name_to_number("").is_err());
214 }
215
216 #[test]
217 fn test_column_name_with_digit() {
218 assert!(column_name_to_number("A1").is_err());
219 }
220
221 #[test]
222 fn test_column_number_1() {
223 assert_eq!(column_number_to_name(1).unwrap(), "A");
224 }
225
226 #[test]
227 fn test_column_number_26() {
228 assert_eq!(column_number_to_name(26).unwrap(), "Z");
229 }
230
231 #[test]
232 fn test_column_number_27() {
233 assert_eq!(column_number_to_name(27).unwrap(), "AA");
234 }
235
236 #[test]
237 fn test_column_number_52() {
238 assert_eq!(column_number_to_name(52).unwrap(), "AZ");
239 }
240
241 #[test]
242 fn test_column_number_53() {
243 assert_eq!(column_number_to_name(53).unwrap(), "BA");
244 }
245
246 #[test]
247 fn test_column_number_16384() {
248 assert_eq!(column_number_to_name(16384).unwrap(), "XFD");
249 }
250
251 #[test]
252 fn test_column_number_0_err() {
253 assert!(column_number_to_name(0).is_err());
254 }
255
256 #[test]
257 fn test_column_number_16385_err() {
258 assert!(column_number_to_name(16385).is_err());
259 }
260
261 #[test]
262 fn test_column_roundtrip_all() {
263 for n in 1..=MAX_COLUMNS {
264 let name = column_number_to_name(n).unwrap();
265 let back = column_name_to_number(&name).unwrap();
266 assert_eq!(n, back, "roundtrip failed for column {n} (name={name})");
267 }
268 }
269
270 #[test]
271 fn test_cell_a1() {
272 assert_eq!(cell_name_to_coordinates("A1").unwrap(), (1, 1));
273 }
274
275 #[test]
276 fn test_cell_z10() {
277 assert_eq!(cell_name_to_coordinates("Z10").unwrap(), (26, 10));
278 }
279
280 #[test]
281 fn test_cell_aa1() {
282 assert_eq!(cell_name_to_coordinates("AA1").unwrap(), (27, 1));
283 }
284
285 #[test]
286 fn test_cell_absolute_a1() {
287 assert_eq!(cell_name_to_coordinates("$A$1").unwrap(), (1, 1));
288 }
289
290 #[test]
291 fn test_cell_absolute_ab100() {
292 assert_eq!(cell_name_to_coordinates("$AB$100").unwrap(), (28, 100));
293 }
294
295 #[test]
296 fn test_cell_mixed_absolute() {
297 assert_eq!(cell_name_to_coordinates("$A1").unwrap(), (1, 1));
298 assert_eq!(cell_name_to_coordinates("A$1").unwrap(), (1, 1));
299 }
300
301 #[test]
302 fn test_cell_max() {
303 assert_eq!(
304 cell_name_to_coordinates("XFD1048576").unwrap(),
305 (16384, 1_048_576)
306 );
307 }
308
309 #[test]
310 fn test_cell_empty_err() {
311 assert!(cell_name_to_coordinates("").is_err());
312 }
313
314 #[test]
315 fn test_cell_only_letters_err() {
316 assert!(cell_name_to_coordinates("ABC").is_err());
317 }
318
319 #[test]
320 fn test_cell_only_digits_err() {
321 assert!(cell_name_to_coordinates("123").is_err());
322 }
323
324 #[test]
325 fn test_cell_row_zero_err() {
326 assert!(cell_name_to_coordinates("A0").is_err());
327 }
328
329 #[test]
330 fn test_cell_row_too_large_err() {
331 assert!(cell_name_to_coordinates("A1048577").is_err());
332 }
333
334 #[test]
335 fn test_cell_col_too_large_err() {
336 assert!(cell_name_to_coordinates("XFE1").is_err());
337 }
338
339 #[test]
340 fn test_coords_1_1() {
341 assert_eq!(coordinates_to_cell_name(1, 1).unwrap(), "A1");
342 }
343
344 #[test]
345 fn test_coords_27_100() {
346 assert_eq!(coordinates_to_cell_name(27, 100).unwrap(), "AA100");
347 }
348
349 #[test]
350 fn test_coords_max() {
351 assert_eq!(
352 coordinates_to_cell_name(16384, 1_048_576).unwrap(),
353 "XFD1048576"
354 );
355 }
356
357 #[test]
358 fn test_coords_col_0_err() {
359 assert!(coordinates_to_cell_name(0, 1).is_err());
360 }
361
362 #[test]
363 fn test_coords_row_0_err() {
364 assert!(coordinates_to_cell_name(1, 0).is_err());
365 }
366
367 #[test]
368 fn test_coords_col_too_large_err() {
369 assert!(coordinates_to_cell_name(16385, 1).is_err());
370 }
371
372 #[test]
373 fn test_coords_row_too_large_err() {
374 assert!(coordinates_to_cell_name(1, 1_048_577).is_err());
375 }
376
377 #[test]
378 fn test_cell_roundtrip() {
379 let cases = vec![
380 (1, 1, "A1"),
381 (26, 1, "Z1"),
382 (27, 1, "AA1"),
383 (52, 10, "AZ10"),
384 (16384, 1_048_576, "XFD1048576"),
385 ];
386
387 for (col, row, expected_name) in cases {
388 let name = coordinates_to_cell_name(col, row).unwrap();
389 assert_eq!(name, expected_name);
390
391 let (c, r) = cell_name_to_coordinates(&name).unwrap();
392 assert_eq!((c, r), (col, row));
393 }
394 }
395}