use crate::error::ExcelMcpError;
const MAX_COL: u16 = 16383;
const MAX_ROW: u32 = 1_048_575;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellPos {
pub row: u32,
pub col: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellRange {
pub start: CellPos,
pub end: CellPos,
}
pub fn col_letter_to_index(s: &str) -> Result<u16, ExcelMcpError> {
if s.is_empty() {
return Err(ExcelMcpError::ParseError(
"Column letter string is empty".into(),
));
}
let mut index: u32 = 0;
for ch in s.chars() {
if !ch.is_ascii_alphabetic() {
return Err(ExcelMcpError::ParseError(format!(
"Invalid character '{}' in column letters '{}'",
ch, s
)));
}
let val = ch.to_ascii_uppercase() as u32 - b'A' as u32;
index = index * 26 + val + 1;
}
let index = index - 1;
if index > MAX_COL as u32 {
return Err(ExcelMcpError::InvalidInput(format!(
"Column '{}' (index {}) exceeds maximum Excel column XFD ({})",
s, index, MAX_COL
)));
}
Ok(index as u16)
}
pub fn index_to_col_letter(col: u16) -> String {
let mut result = String::new();
let mut n = col as u32 + 1; while n > 0 {
n -= 1;
let ch = (b'A' + (n % 26) as u8) as char;
result.push(ch);
n /= 26;
}
result.chars().rev().collect()
}
pub fn cell_pos_to_a1(pos: &CellPos) -> String {
format!("{}{}", index_to_col_letter(pos.col), pos.row + 1)
}
pub fn parse_cell_ref(s: &str) -> Result<CellPos, ExcelMcpError> {
let s = s.trim();
if s.is_empty() {
return Err(ExcelMcpError::ParseError("Cell reference is empty".into()));
}
let split = s.find(|c: char| c.is_ascii_digit()).ok_or_else(|| {
ExcelMcpError::ParseError(format!("Cell reference '{}' has no row number", s))
})?;
if split == 0 {
return Err(ExcelMcpError::ParseError(format!(
"Cell reference '{}' has no column letters",
s
)));
}
let col_str = &s[..split];
let row_str = &s[split..];
if !row_str.chars().all(|c| c.is_ascii_digit()) {
return Err(ExcelMcpError::ParseError(format!(
"Cell reference '{}' contains invalid characters in row portion '{}'",
s, row_str
)));
}
let col = col_letter_to_index(col_str)?;
let row_1based: u32 = row_str.parse().map_err(|_| {
ExcelMcpError::ParseError(format!(
"Invalid row number '{}' in cell reference '{}'",
row_str, s
))
})?;
if row_1based == 0 {
return Err(ExcelMcpError::InvalidInput(format!(
"Row number in '{}' must be >= 1",
s
)));
}
let row = row_1based - 1;
if row > MAX_ROW {
return Err(ExcelMcpError::InvalidInput(format!(
"Row {} in '{}' exceeds maximum Excel row 1048576",
row_1based, s
)));
}
Ok(CellPos { row, col })
}
pub fn parse_range_ref(s: &str) -> Result<CellRange, ExcelMcpError> {
let s = s.trim();
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return Err(ExcelMcpError::ParseError(format!(
"Range reference '{}' must contain exactly one ':'",
s
)));
}
let start = parse_cell_ref(parts[0])?;
let end = parse_cell_ref(parts[1])?;
Ok(CellRange { start, end })
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn roundtrip_cell_ref(row in 0u32..1_048_576, col in 0u16..16384) {
let pos = CellPos { row, col };
let a1 = cell_pos_to_a1(&pos);
let parsed = parse_cell_ref(&a1).unwrap();
prop_assert_eq!(parsed.row, pos.row);
prop_assert_eq!(parsed.col, pos.col);
}
}
#[test]
fn test_parse_cell_ref_known() {
let cases = [
("A1", 0, 0),
("B1", 0, 1),
("Z1", 0, 25),
("AA1", 0, 26),
("AB1", 0, 27),
("AZ1", 0, 51),
("BA1", 0, 52),
("XFD1", 0, 16383),
("A2", 1, 0),
("A1048576", 1_048_575, 0),
("C10", 9, 2),
];
for (input, expected_row, expected_col) in cases {
let pos = parse_cell_ref(input)
.unwrap_or_else(|e| panic!("Failed to parse '{}': {}", input, e));
assert_eq!(pos.row, expected_row, "Row mismatch for '{}'", input);
assert_eq!(pos.col, expected_col, "Col mismatch for '{}'", input);
}
}
#[test]
fn test_parse_cell_ref_invalid() {
assert!(parse_cell_ref("").is_err());
assert!(parse_cell_ref("123").is_err());
assert!(parse_cell_ref("ABC").is_err());
assert!(parse_cell_ref("A0").is_err());
assert!(parse_cell_ref("A1048577").is_err());
assert!(parse_cell_ref("XFE1").is_err());
assert!(parse_cell_ref("1A1").is_err());
}
#[test]
fn test_parse_range_ref() {
let range = parse_range_ref("A1:B2").unwrap();
assert_eq!(range.start, CellPos { row: 0, col: 0 });
assert_eq!(range.end, CellPos { row: 1, col: 1 });
let range = parse_range_ref("A1:XFD1048576").unwrap();
assert_eq!(range.start, CellPos { row: 0, col: 0 });
assert_eq!(
range.end,
CellPos {
row: 1_048_575,
col: 16383
}
);
let range = parse_range_ref("C3:C3").unwrap();
assert_eq!(range.start, CellPos { row: 2, col: 2 });
assert_eq!(range.end, CellPos { row: 2, col: 2 });
assert!(parse_range_ref("A1B2").is_err());
assert!(parse_range_ref("A1:B2:C3").is_err());
assert!(parse_range_ref("A0:B2").is_err());
}
#[test]
fn test_col_letter_to_index() {
assert_eq!(col_letter_to_index("A").unwrap(), 0);
assert_eq!(col_letter_to_index("B").unwrap(), 1);
assert_eq!(col_letter_to_index("Z").unwrap(), 25);
assert_eq!(col_letter_to_index("AA").unwrap(), 26);
assert_eq!(col_letter_to_index("AB").unwrap(), 27);
assert_eq!(col_letter_to_index("AZ").unwrap(), 51);
assert_eq!(col_letter_to_index("BA").unwrap(), 52);
assert_eq!(col_letter_to_index("XFD").unwrap(), 16383);
assert_eq!(col_letter_to_index("a").unwrap(), 0);
assert_eq!(col_letter_to_index("aa").unwrap(), 26);
assert!(col_letter_to_index("").is_err());
assert!(col_letter_to_index("A1").is_err());
assert!(col_letter_to_index("XFE").is_err());
}
#[test]
fn test_index_to_col_letter() {
assert_eq!(index_to_col_letter(0), "A");
assert_eq!(index_to_col_letter(1), "B");
assert_eq!(index_to_col_letter(25), "Z");
assert_eq!(index_to_col_letter(26), "AA");
assert_eq!(index_to_col_letter(27), "AB");
assert_eq!(index_to_col_letter(51), "AZ");
assert_eq!(index_to_col_letter(52), "BA");
assert_eq!(index_to_col_letter(16383), "XFD");
}
}