use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellAddr {
pub col: u32,
pub row: u32,
pub col_abs: bool,
pub row_abs: bool,
}
impl CellAddr {
pub const fn new(col: u32, row: u32) -> Self {
Self { col, row, col_abs: false, row_abs: false }
}
pub const fn with_col_abs(mut self, abs: bool) -> Self {
self.col_abs = abs;
self
}
pub const fn with_row_abs(mut self, abs: bool) -> Self {
self.row_abs = abs;
self
}
pub fn parse(text: &str) -> Option<Self> {
let bytes = text.as_bytes();
let mut idx = 0;
let col_abs = bytes.first() == Some(&b'$');
if col_abs {
idx += 1;
}
let col_start = idx;
let col_end =
col_start + bytes[col_start..].iter().take_while(|b| b.is_ascii_alphabetic()).count();
if col_end == col_start {
return None;
}
idx = col_end;
let row_abs = bytes.get(idx) == Some(&b'$');
if row_abs {
idx += 1;
}
let row_str = &text[idx..];
if row_str.is_empty() || !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_start..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, col_abs, row_abs })
}
}
impl fmt::Display for CellAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.col_abs {
write!(f, "$")?;
}
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)?;
}
if self.row_abs {
write!(f, "$")?;
}
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())
}
pub fn relative_display(&self) -> String {
match self {
Ref::Cell { sheet, addr } => Ref::Cell {
sheet: sheet.clone(),
addr: CellAddr::new(addr.col, addr.row),
}
.to_string(),
Ref::Range { sheet, start, end } => Ref::Range {
sheet: sheet.clone(),
start: CellAddr::new(start.col, start.row),
end: CellAddr::new(end.col, end.row),
}
.to_string(),
Ref::Name(_) => self.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")
}
pub(crate) fn write_sheet(f: &mut dyn fmt::Write, 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),
}
}
}
#[cfg(test)]
mod tests;