use crate::error::Error;
use crate::model::style::ResolvedStyle;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub struct DateTimeValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CellRef {
pub row: u32,
pub col: u32,
}
impl CellRef {
pub const MAX_ROW: u32 = 1_048_576;
pub const MAX_COL: u32 = 16_384;
pub fn from_a1(s: &str) -> Result<Self, Error> {
let invalid = || Error::InvalidCellRef(s.to_string());
let split_at = s.find(|c: char| c.is_ascii_digit()).ok_or_else(invalid)?;
let (letters, digits) = s.split_at(split_at);
if letters.is_empty() || !letters.chars().all(|c| c.is_ascii_uppercase()) {
return Err(invalid());
}
if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
return Err(invalid());
}
let col = column_letters_to_number(letters).ok_or_else(invalid)?;
let row: u32 = digits.parse().map_err(|_| invalid())?;
if row == 0 || row > Self::MAX_ROW || col > Self::MAX_COL {
return Err(invalid());
}
Ok(CellRef { row, col })
}
pub fn to_a1(&self) -> String {
format!("{}{}", column_number_to_letters(self.col), self.row)
}
}
fn column_number_to_letters(mut n: u32) -> String {
let mut buf = Vec::new();
while n > 0 {
let rem = ((n - 1) % 26) as u8;
buf.push(b'A' + rem);
n = (n - 1) / 26;
}
buf.reverse();
String::from_utf8(buf).expect("column letters are always ASCII")
}
fn column_letters_to_number(letters: &str) -> Option<u32> {
let mut n: u32 = 0;
for c in letters.chars() {
let digit = (c as u32) - ('A' as u32) + 1;
n = n.checked_mul(26)?.checked_add(digit)?;
}
Some(n)
}
#[derive(Debug, Clone, PartialEq)]
pub enum CellValue {
Number(f64),
DateTime(DateTimeValue),
Text(Arc<str>),
Boolean(bool),
Error(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
pub value: Option<CellValue>,
pub style: Option<Arc<ResolvedStyle>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_a1_to_a1_round_trip() {
let cases = [
("A1", CellRef { row: 1, col: 1 }),
("Z1", CellRef { row: 1, col: 26 }),
("AA1", CellRef { row: 1, col: 27 }),
(
"XFD1048576",
CellRef {
row: 1_048_576,
col: 16_384,
},
),
];
for (a1, expected) in cases {
let parsed = CellRef::from_a1(a1).unwrap();
assert_eq!(parsed, expected, "parsing {a1}");
assert_eq!(parsed.to_a1(), a1, "round-trip of {a1}");
}
}
#[test]
fn from_a1_rejects_invalid_strings() {
let invalid = [
"a1", "A#1", "A1B2", "A", "123", "", "A0", "A10000000000000", "A1048577", "XFE1", ];
for s in invalid {
assert!(CellRef::from_a1(s).is_err(), "expected {s:?} to be invalid");
}
}
#[test]
fn from_a1_rejects_row_or_col_far_beyond_excels_real_maximum() {
let err = CellRef::from_a1("ZZZZZZ4294967294").unwrap_err();
assert!(matches!(err, Error::InvalidCellRef(_)));
}
#[test]
fn cell_value_equality() {
assert_eq!(CellValue::Number(1.5), CellValue::Number(1.5));
assert_ne!(CellValue::Number(1.5), CellValue::Number(2.5));
assert_eq!(
CellValue::DateTime(DateTimeValue),
CellValue::DateTime(DateTimeValue)
);
assert_eq!(
CellValue::Text(Arc::from("hello")),
CellValue::Text(Arc::from("hello"))
);
assert_ne!(
CellValue::Text(Arc::from("hello")),
CellValue::Text(Arc::from("world"))
);
assert_eq!(CellValue::Boolean(true), CellValue::Boolean(true));
assert_ne!(CellValue::Boolean(true), CellValue::Boolean(false));
assert_eq!(
CellValue::Error("#DIV/0!".into()),
CellValue::Error("#DIV/0!".into())
);
assert_ne!(CellValue::Number(1.0), CellValue::Boolean(true));
}
#[test]
fn cell_with_formatting_only_has_no_value() {
let style = Arc::new(ResolvedStyle {
is_date_time: false,
});
let cell = Cell {
value: None,
style: Some(style.clone()),
};
assert_eq!(cell.value, None);
assert_eq!(cell.style, Some(style));
}
#[test]
fn arc_sharing_avoids_duplication() {
let style = Arc::new(ResolvedStyle { is_date_time: true });
let a = Cell {
value: None,
style: Some(style.clone()),
};
let b = Cell {
value: None,
style: Some(style.clone()),
};
assert!(Arc::ptr_eq(
a.style.as_ref().unwrap(),
b.style.as_ref().unwrap()
));
let text: Arc<str> = Arc::from("shared");
let a = Cell {
value: Some(CellValue::Text(text.clone())),
style: None,
};
let b = Cell {
value: Some(CellValue::Text(text.clone())),
style: None,
};
let (Some(CellValue::Text(a_text)), Some(CellValue::Text(b_text))) = (&a.value, &b.value)
else {
unreachable!()
};
assert!(Arc::ptr_eq(a_text, b_text));
}
}