use crate::error::{Error, Result};
use regex::Regex;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CellRef {
pub row: u32,
pub col: u32,
}
impl CellRef {
pub fn new(row: u32, col: u32) -> Result<Self> {
if row == 0 || col == 0 {
return Err(Error::InvalidReference(format!(
"row and col must be greater than zero, got ({row}, {col})"
)));
}
Ok(Self { row, col })
}
pub fn parse<S: AsRef<str>>(s: S) -> Result<Self> {
parse_a1(s.as_ref())
}
pub fn to_a1(&self) -> String {
format!("{}{}", col_to_letters(self.col), self.row)
}
}
impl std::fmt::Display for CellRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_a1())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RangeRef {
pub start: CellRef,
pub end: CellRef,
}
impl RangeRef {
pub fn new(start: CellRef, end: CellRef) -> Self {
Self { start, end }
}
pub fn parse<S: AsRef<str>>(s: S) -> Result<Self> {
parse_range_a1(s.as_ref())
}
pub fn normalized(&self) -> Self {
Self {
start: CellRef {
row: self.start.row.min(self.end.row),
col: self.start.col.min(self.end.col),
},
end: CellRef {
row: self.start.row.max(self.end.row),
col: self.start.col.max(self.end.col),
},
}
}
pub fn to_a1(&self) -> String {
format!("{}:{}", self.start.to_a1(), self.end.to_a1())
}
}
impl std::fmt::Display for RangeRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_a1())
}
}
fn parse_a1(s: &str) -> Result<CellRef> {
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| Regex::new(r"^\$?([A-Za-z]+)\$?(\d+)$").unwrap());
let caps = re
.captures(s)
.ok_or_else(|| Error::InvalidReference(s.to_string()))?;
let letters = caps.get(1).unwrap().as_str();
let row: u32 = caps
.get(2)
.unwrap()
.as_str()
.parse()
.map_err(|_| Error::InvalidReference(s.to_string()))?;
let col = letters_to_col(letters)?;
CellRef::new(row, col)
}
fn parse_range_a1(s: &str) -> Result<RangeRef> {
let parts: Vec<&str> = s.split(':').collect();
match parts.len() {
1 => {
let cell = parse_a1(parts[0])?;
Ok(RangeRef::new(cell, cell))
}
2 => {
let start = parse_a1(parts[0])?;
let end = parse_a1(parts[1])?;
Ok(RangeRef::new(start, end).normalized())
}
_ => Err(Error::InvalidReference(s.to_string())),
}
}
pub fn letters_to_col(letters: &str) -> Result<u32> {
let mut col: u32 = 0;
for ch in letters.chars() {
if !ch.is_ascii_alphabetic() {
return Err(Error::InvalidReference(format!(
"invalid column letters: {letters}"
)));
}
let val = (ch.to_ascii_uppercase() as u32) - ('A' as u32) + 1;
col = col
.checked_mul(26)
.and_then(|c| c.checked_add(val))
.ok_or_else(|| Error::InvalidReference(format!("column out of range: {letters}")))?;
}
if col == 0 {
return Err(Error::InvalidReference(format!(
"invalid column letters: {letters}"
)));
}
Ok(col)
}
fn col_to_letters(col: u32) -> String {
let mut col = col;
let mut letters = String::new();
while col > 0 {
let rem = (col - 1) % 26;
letters.push((b'A' + rem as u8) as char);
col = (col - 1) / 26;
}
letters.chars().rev().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic() {
assert_eq!(CellRef::parse("A1").unwrap(), CellRef::new(1, 1).unwrap());
assert_eq!(CellRef::parse("B2").unwrap(), CellRef::new(2, 2).unwrap());
assert_eq!(
CellRef::parse("AB12").unwrap(),
CellRef::new(12, 28).unwrap()
);
assert_eq!(
CellRef::parse("$B$2").unwrap(),
CellRef::new(2, 2).unwrap()
);
}
#[test]
fn round_trip_columns() {
for col in [1, 26, 27, 28, 256, 16384] {
let letters = col_to_letters(col);
assert_eq!(letters_to_col(&letters).unwrap(), col);
}
}
#[test]
fn range_parse() {
let r = RangeRef::parse("B2:A1").unwrap();
assert_eq!(r.start, CellRef::new(1, 1).unwrap());
assert_eq!(r.end, CellRef::new(2, 2).unwrap());
}
}