use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellAddr {
pub col: u32,
pub row: u32,
}
impl CellAddr {
pub fn parse(text: &str) -> Option<Self> {
let bytes = text.as_bytes();
let col_end = bytes.iter().take_while(|b| b.is_ascii_alphabetic()).count();
if col_end == 0 || col_end == bytes.len() {
return None;
}
let row_str = &text[col_end..];
if !row_str.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let row: u32 = row_str.parse().ok()?;
if row == 0 {
return None;
}
let mut col: u32 = 0;
for b in &bytes[..col_end] {
let v = (b.to_ascii_uppercase() - b'A' + 1) as u32;
col = col.checked_mul(26)?.checked_add(v)?;
}
Some(Self { col, row })
}
}
impl fmt::Display for CellAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut col = self.col;
let mut letters = [0u8; 8];
let mut n = 0;
while col > 0 {
let rem = ((col - 1) % 26) as u8;
letters[n] = b'A' + rem;
n += 1;
col = (col - 1) / 26;
}
for b in letters[..n].iter().rev() {
write!(f, "{}", *b as char)?;
}
write!(f, "{}", self.row)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Ref {
Cell {
sheet: Option<String>,
addr: CellAddr,
},
Range {
sheet: Option<String>,
start: CellAddr,
end: CellAddr,
},
Name(String),
}
impl Ref {
pub fn classify(text: &str) -> Ref {
if let Some(addr) = CellAddr::parse(text) {
return Ref::Cell { sheet: None, addr };
}
if let Some((s, e)) = text.split_once(':') {
if let (Some(start), Some(end)) = (CellAddr::parse(s), CellAddr::parse(e)) {
return Ref::Range { sheet: None, start, end };
}
}
Ref::Name(text.to_string())
}
}
fn looks_like_cell_addr(name: &str) -> bool {
let letters = name.bytes().take_while(|b| b.is_ascii_alphabetic()).count();
(1..=3).contains(&letters) && CellAddr::parse(name).is_some()
}
fn sheet_needs_quoting(name: &str) -> bool {
let mut chars = name.chars();
let bare = match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
_ => false,
};
!bare
|| looks_like_cell_addr(name)
|| name.eq_ignore_ascii_case("TRUE")
|| name.eq_ignore_ascii_case("FALSE")
}
fn write_sheet(f: &mut fmt::Formatter<'_>, sheet: &Option<String>) -> fmt::Result {
match sheet {
None => Ok(()),
Some(name) => {
if sheet_needs_quoting(name) {
write!(f, "'{}'!", name.replace('\'', "''"))
} else {
write!(f, "{}!", name)
}
}
}
}
impl fmt::Display for Ref {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Ref::Cell { sheet, addr } => {
write_sheet(f, sheet)?;
write!(f, "{}", addr)
}
Ref::Range { sheet, start, end } => {
write_sheet(f, sheet)?;
write!(f, "{}:{}", start, end)
}
Ref::Name(name) => write!(f, "{}", name),
}
}
}