use crate::error::{Error, Result};
const NS_SPREADSHEETML: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
#[derive(Debug, Clone, PartialEq)]
pub enum CellValue {
Number(f64),
String(String),
Boolean(bool),
Error(String),
SharedString(usize),
Empty,
}
impl Default for CellValue {
fn default() -> Self {
CellValue::Empty
}
}
impl CellValue {
pub fn cell_type(&self) -> CellType {
match self {
CellValue::Number(_) => CellType::Number,
CellValue::String(_) => CellType::InlineString,
CellValue::Boolean(_) => CellType::Boolean,
CellValue::Error(_) => CellType::Error,
CellValue::SharedString(_) => CellType::SharedString,
CellValue::Empty => CellType::Number,
}
}
pub fn is_empty(&self) -> bool {
matches!(self, CellValue::Empty)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellType {
Boolean,
Number,
Error,
SharedString,
FormulaString,
InlineString,
}
impl CellType {
pub fn as_str(&self) -> &'static str {
match self {
CellType::Boolean => "b",
CellType::Number => "n",
CellType::Error => "e",
CellType::SharedString => "s",
CellType::FormulaString => "str",
CellType::InlineString => "inlineStr",
}
}
pub fn from_str(s: Option<&str>) -> Result<Self> {
match s {
None | Some("n") => Ok(CellType::Number),
Some("b") => Ok(CellType::Boolean),
Some("e") => Ok(CellType::Error),
Some("s") => Ok(CellType::SharedString),
Some("str") => Ok(CellType::FormulaString),
Some("inlineStr") => Ok(CellType::InlineString),
Some(other) => Err(Error::Schema(format!("unknown cell type '{}'", other))),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
pub reference: String,
pub value: CellValue,
}
impl Cell {
pub fn new(reference: impl Into<String>, value: CellValue) -> Self {
Cell {
reference: reference.into(),
value,
}
}
pub fn empty(reference: impl Into<String>) -> Self {
Cell {
reference: reference.into(),
value: CellValue::Empty,
}
}
pub fn reference(&self) -> &str {
&self.reference
}
pub fn value(&self) -> &CellValue {
&self.value
}
pub fn to_xml(&self) -> String {
let mut s = String::with_capacity(64);
s.push_str("<c r=\"");
s.push_str(&xml_escape_attr(&self.reference));
s.push('"');
let t = self.value.cell_type();
if t != CellType::Number {
s.push_str(" t=\"");
s.push_str(t.as_str());
s.push('"');
}
match &self.value {
CellValue::Empty => {
s.push_str("/>");
}
CellValue::Number(n) => {
s.push_str("><v>");
s.push_str(&format_number(*n));
s.push_str("</v></c>");
}
CellValue::SharedString(i) => {
s.push_str("><v>");
s.push_str(&i.to_string());
s.push_str("</v></c>");
}
CellValue::Boolean(b) => {
s.push_str("><v>");
s.push_str(if *b { "1" } else { "0" });
s.push_str("</v></c>");
}
CellValue::Error(e) => {
s.push_str("><v>");
s.push_str(&xml_escape_text(e));
s.push_str("</v></c>");
}
CellValue::String(text) => {
s.push_str("><is><t");
if text.starts_with(' ') || text.ends_with(' ') {
s.push_str(" xml:space=\"preserve\"");
}
s.push('>');
s.push_str(&xml_escape_text(text));
s.push_str("</t></is></c>");
}
}
s
}
pub fn from_xml(xml: &str) -> Result<Self> {
use quick_xml::events::Event;
use quick_xml::reader::Reader;
let mut rd = Reader::from_str(xml);
let mut buf = Vec::new();
let mut reference: Option<String> = None;
let mut cell_type: Option<CellType> = None;
let mut in_v = false;
let mut in_is = false;
let mut in_t = false;
let mut text_buf = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"c" => {
for attr in e.attributes().flatten() {
match attr.key.as_ref() {
b"r" => {
reference = Some(
attr.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()
.map(|v| v.to_string())
.unwrap_or_default(),
);
}
b"t" => {
let v = attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()
.map(|v| v.to_string());
cell_type = Some(CellType::from_str(v.as_deref())?);
}
_ => {}
}
}
}
Ok(Event::Empty(e)) if e.name().as_ref() == b"c" => {
for attr in e.attributes().flatten() {
if attr.key.as_ref() == b"r" {
reference = Some(
attr.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()
.map(|v| v.to_string())
.unwrap_or_default(),
);
}
}
return Ok(Cell::new(reference.unwrap_or_default(), CellValue::Empty));
}
Ok(Event::Start(e)) => match e.name().as_ref() {
b"v" => in_v = true,
b"is" => in_is = true,
b"t" if in_is => in_t = true,
_ => {}
},
Ok(Event::End(e)) => match e.name().as_ref() {
b"v" => in_v = false,
b"is" => in_is = false,
b"t" => in_t = false,
b"c" => {
let value = match cell_type.unwrap_or(CellType::Number) {
CellType::Number => {
if text_buf.is_empty() {
CellValue::Empty
} else {
let n = text_buf.trim().parse::<f64>().map_err(|e| {
Error::Schema(format!("cell number parse: {e}"))
})?;
CellValue::Number(n)
}
}
CellType::Boolean => {
let b = match text_buf.trim() {
"1" | "true" => true,
"0" | "false" => false,
other => {
return Err(Error::Schema(format!(
"cell boolean parse: '{}'",
other
)))
}
};
CellValue::Boolean(b)
}
CellType::Error => CellValue::Error(text_buf.clone()),
CellType::SharedString => {
let i = text_buf.trim().parse::<usize>().map_err(|e| {
Error::Schema(format!("cell shared index parse: {e}"))
})?;
CellValue::SharedString(i)
}
CellType::FormulaString => CellValue::String(text_buf.clone()),
CellType::InlineString => CellValue::String(text_buf.clone()),
};
return Ok(Cell::new(reference.unwrap_or_default(), value));
}
_ => {}
},
Ok(Event::Text(t)) if in_v || in_t => {
let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
text_buf.push_str(text_str);
}
Ok(Event::GeneralRef(r)) if in_v || in_t => {
if let Some(ch) = r
.resolve_char_ref()
.map_err(|e| Error::Xml(format!("cell char ref: {e}")))?
{
text_buf.push(ch);
} else {
let name = r
.decode()
.map_err(|e| Error::Xml(format!("cell entity decode: {e}")))?;
let ch = match name.as_ref() {
"lt" => '<',
"gt" => '>',
"amp" => '&',
"quot" => '"',
"apos" => '\'',
other => {
return Err(Error::Xml(format!("cell unknown entity: &{other};")))
}
};
text_buf.push(ch);
}
}
Ok(Event::CData(t)) if in_v || in_t => {
if let Ok(s) = std::str::from_utf8(&t) {
text_buf.push_str(s);
}
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(e) => return Err(Error::Xml(format!("cell parse: {e}"))),
}
buf.clear();
}
Err(Error::Schema("cell parse: unexpected EOF".to_string()))
}
}
pub fn parse_a1(reference: &str) -> Result<(u32, u32)> {
if reference.is_empty() {
return Err(Error::InvalidCellRef("empty reference".to_string()));
}
let mut chars = reference.chars();
let mut col: u32 = 0;
while let Some(c) = chars.clone().next() {
if c.is_ascii_alphabetic() {
chars.next();
col = col * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
if col > 16384 {
return Err(Error::InvalidCellRef(format!(
"column overflow in '{}': {} > 16384",
reference, col
)));
}
} else {
break;
}
}
if col == 0 {
return Err(Error::InvalidCellRef(format!(
"missing column letters in '{}'",
reference
)));
}
let row_str: String = chars.collect();
if row_str.is_empty() {
return Err(Error::InvalidCellRef(format!(
"missing row number in '{}'",
reference
)));
}
let row: u32 = row_str
.parse()
.map_err(|_| Error::InvalidCellRef(format!("invalid row number in '{}'", reference)))?;
if row == 0 {
return Err(Error::InvalidCellRef(format!(
"zero row in '{}'",
reference
)));
}
if row > 1048576 {
return Err(Error::InvalidCellRef(format!(
"row overflow in '{}': {} > 1048576",
reference, row
)));
}
Ok((row, col))
}
pub fn to_a1(row: u32, col: u32) -> String {
debug_assert!(row >= 1 && col >= 1, "row/col must be 1-indexed");
let mut col = col;
let mut letters = Vec::new();
while col > 0 {
col -= 1;
let rem = col % 26;
letters.push((b'A' + rem as u8) as char);
col /= 26;
}
letters.reverse();
let mut s = String::with_capacity(letters.len() + 5);
for c in letters {
s.push(c);
}
s.push_str(&row.to_string());
s
}
fn format_number(n: f64) -> String {
if n.is_finite() && n.fract() == 0.0 && n.abs() < 1e16 {
format!("{}", n as i64)
} else {
format!("{}", n)
}
}
fn xml_escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
_ => out.push(c),
}
}
out
}
fn xml_escape_attr(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(c),
}
}
out
}
pub fn spreadsheetml_ns() -> &'static str {
NS_SPREADSHEETML
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_a1_basic() {
assert_eq!(parse_a1("A1").unwrap(), (1, 1));
assert_eq!(parse_a1("B2").unwrap(), (2, 2));
assert_eq!(parse_a1("Z1").unwrap(), (1, 26));
assert_eq!(parse_a1("AA1").unwrap(), (1, 27));
assert_eq!(parse_a1("AB12").unwrap(), (12, 28));
assert_eq!(parse_a1("XFD1048576").unwrap(), (1048576, 16384));
}
#[test]
fn parse_a1_case_insensitive() {
assert_eq!(parse_a1("a1").unwrap(), (1, 1));
assert_eq!(parse_a1("Ab12").unwrap(), (12, 28));
}
#[test]
fn parse_a1_errors() {
assert!(parse_a1("").is_err());
assert!(parse_a1("1A").is_err()); assert!(parse_a1("A").is_err()); assert!(parse_a1("1").is_err()); assert!(parse_a1("A0").is_err()); assert!(parse_a1("A1X").is_err()); assert!(parse_a1("XFD1048577").is_err()); assert!(parse_a1("XFE1").is_err()); }
#[test]
fn to_a1_basic() {
assert_eq!(to_a1(1, 1), "A1");
assert_eq!(to_a1(10, 26), "Z10");
assert_eq!(to_a1(1, 27), "AA1");
assert_eq!(to_a1(12, 28), "AB12");
assert_eq!(to_a1(1048576, 16384), "XFD1048576");
}
#[test]
fn a1_round_trip() {
for (row, col) in [
(1, 1),
(5, 10),
(100, 26),
(1, 27),
(9999, 702),
(1048576, 16384),
] {
let s = to_a1(row, col);
let (r2, c2) = parse_a1(&s).unwrap();
assert_eq!(
(row, col),
(r2, c2),
"round trip failed for ({},{})",
row,
col
);
}
}
#[test]
fn cell_to_xml_number() {
let c = Cell::new("A1", CellValue::Number(123.0));
assert_eq!(c.to_xml(), "<c r=\"A1\"><v>123</v></c>");
}
#[test]
fn cell_to_xml_float() {
let c = Cell::new("B2", CellValue::Number(3.14));
assert_eq!(c.to_xml(), "<c r=\"B2\"><v>3.14</v></c>");
}
#[test]
fn cell_to_xml_shared_string() {
let c = Cell::new("A1", CellValue::SharedString(0));
assert_eq!(c.to_xml(), "<c r=\"A1\" t=\"s\"><v>0</v></c>");
}
#[test]
fn cell_to_xml_boolean() {
let c1 = Cell::new("A1", CellValue::Boolean(true));
assert_eq!(c1.to_xml(), "<c r=\"A1\" t=\"b\"><v>1</v></c>");
let c2 = Cell::new("A2", CellValue::Boolean(false));
assert_eq!(c2.to_xml(), "<c r=\"A2\" t=\"b\"><v>0</v></c>");
}
#[test]
fn cell_to_xml_error() {
let c = Cell::new("A1", CellValue::Error("#REF!".to_string()));
assert_eq!(c.to_xml(), "<c r=\"A1\" t=\"e\"><v>#REF!</v></c>");
}
#[test]
fn cell_to_xml_inline_string() {
let c = Cell::new("A1", CellValue::String("Hello".to_string()));
assert_eq!(
c.to_xml(),
"<c r=\"A1\" t=\"inlineStr\"><is><t>Hello</t></is></c>"
);
}
#[test]
fn cell_to_xml_inline_string_preserve_space() {
let c = Cell::new("A1", CellValue::String(" Hi ".to_string()));
assert!(c.to_xml().contains("xml:space=\"preserve\""));
assert!(c.to_xml().contains(" Hi "));
}
#[test]
fn cell_to_xml_empty() {
let c = Cell::new("A1", CellValue::Empty);
assert_eq!(c.to_xml(), "<c r=\"A1\"/>");
}
#[test]
fn cell_to_xml_escape_special_chars() {
let c = Cell::new("A1", CellValue::String("a<b>&c\"d".to_string()));
let xml = c.to_xml();
assert!(xml.contains("a<b>&c\"d"));
}
#[test]
fn cell_from_xml_round_trip_number() {
let c = Cell::new("A1", CellValue::Number(123.0));
let xml = c.to_xml();
let c2 = Cell::from_xml(&xml).unwrap();
assert_eq!(c, c2);
}
#[test]
fn cell_from_xml_round_trip_shared() {
let c = Cell::new("B2", CellValue::SharedString(5));
let xml = c.to_xml();
let c2 = Cell::from_xml(&xml).unwrap();
assert_eq!(c, c2);
}
#[test]
fn cell_from_xml_round_trip_boolean() {
let c = Cell::new("A1", CellValue::Boolean(true));
let xml = c.to_xml();
let c2 = Cell::from_xml(&xml).unwrap();
assert_eq!(c, c2);
}
#[test]
fn cell_from_xml_round_trip_inline_string() {
let c = Cell::new("A1", CellValue::String("Hello".to_string()));
let xml = c.to_xml();
let c2 = Cell::from_xml(&xml).unwrap();
assert_eq!(c, c2);
}
#[test]
fn cell_from_xml_round_trip_empty() {
let c = Cell::new("A1", CellValue::Empty);
let xml = c.to_xml();
let c2 = Cell::from_xml(&xml).unwrap();
assert_eq!(c, c2);
}
#[test]
fn cell_type_from_str_defaults_to_number() {
assert_eq!(CellType::from_str(None).unwrap(), CellType::Number);
assert_eq!(CellType::from_str(Some("n")).unwrap(), CellType::Number);
}
#[test]
fn cell_type_from_str_unknown() {
assert!(CellType::from_str(Some("xyz")).is_err());
}
}