use std::collections::BTreeMap;
use crate::cell::{parse_a1, to_a1, Cell, CellType, CellValue};
use crate::error::{Error, Result};
use crate::shared_strings::SharedStringsTable;
const NS_SPREADSHEETML: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
#[derive(Debug, Clone)]
pub struct Worksheet {
name: String,
rows: BTreeMap<u32, BTreeMap<u32, Cell>>,
}
impl Worksheet {
pub fn new(name: impl Into<String>) -> Self {
Worksheet {
name: name.into(),
rows: BTreeMap::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, name: impl Into<String>) {
self.name = name.into();
}
pub fn set_cell(&mut self, reference: &str, value: CellValue) -> Result<()> {
let (row, col) = parse_a1(reference)?;
self.set_cell_rc(row, col, value);
Ok(())
}
pub fn set_cell_rc(&mut self, row: u32, col: u32, value: CellValue) {
if value.is_empty() {
if let Some(row_map) = self.rows.get_mut(&row) {
row_map.remove(&col);
if row_map.is_empty() {
self.rows.remove(&row);
}
}
return;
}
let reference = to_a1(row, col);
let cell = Cell::new(reference, value);
self.rows.entry(row).or_default().insert(col, cell);
}
pub fn get_cell(&self, reference: &str) -> Result<Option<&Cell>> {
let (row, col) = parse_a1(reference)?;
Ok(self.get_cell_rc(row, col))
}
pub fn get_cell_rc(&self, row: u32, col: u32) -> Option<&Cell> {
self.rows.get(&row).and_then(|r| r.get(&col))
}
pub fn get_cell_mut(&mut self, reference: &str) -> Result<Option<&mut Cell>> {
let (row, col) = parse_a1(reference)?;
Ok(self.rows.get_mut(&row).and_then(|r| r.get_mut(&col)))
}
pub fn iter_cells(&self) -> impl Iterator<Item = &Cell> {
self.rows.values().flat_map(|r| r.values())
}
pub fn iter_rows(&self) -> impl Iterator<Item = (u32, &BTreeMap<u32, Cell>)> {
self.rows.iter().map(|(r, m)| (*r, m))
}
pub fn cell_count(&self) -> usize {
self.rows.values().map(|r| r.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn max_row(&self) -> Option<u32> {
self.rows.keys().copied().max()
}
pub fn max_col(&self) -> Option<u32> {
self.rows.values().flat_map(|r| r.keys().copied()).max()
}
pub fn dimension(&self) -> String {
match (self.max_row(), self.max_col()) {
(None, _) | (_, None) => "A1".to_string(),
(Some(1), Some(1)) => "A1".to_string(),
(Some(max_r), Some(max_c)) => format!("A1:{}", to_a1(max_r, max_c)),
}
}
pub fn clear(&mut self) {
self.rows.clear();
}
pub fn to_xml(&self, sst: &mut SharedStringsTable) -> String {
let mut s = String::with_capacity(256);
s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
s.push_str(&format!("<worksheet xmlns=\"{}\">", NS_SPREADSHEETML));
s.push_str(&format!("<dimension ref=\"{}\"/>", self.dimension()));
s.push_str("<sheetData>");
for (row_idx, row_map) in &self.rows {
s.push_str(&format!("<row r=\"{}\">", row_idx));
for (_col_idx, cell) in row_map {
let cell_to_write = match &cell.value {
CellValue::String(text) => {
let idx = sst.add(text);
let mut c2 = cell.clone();
c2.value = CellValue::SharedString(idx);
c2
}
_ => cell.clone(),
};
s.push_str(&cell_to_write.to_xml());
}
s.push_str("</row>");
}
s.push_str("</sheetData>");
s.push_str("</worksheet>");
s
}
pub fn from_xml(xml: &str, sst: &SharedStringsTable) -> Result<Self> {
use quick_xml::events::Event;
use quick_xml::reader::Reader;
let mut ws = Worksheet::new("");
let mut rd = Reader::from_str(xml);
let mut buf = Vec::new();
let mut in_sheetdata = false;
let mut in_row = false;
let mut in_c = false;
let mut in_v = false;
let mut in_is = false;
let mut in_t = false;
let mut cur_ref: Option<String> = None;
let mut cur_type: Option<CellType> = None;
let mut cur_text = String::new();
loop {
match rd.read_event_into(&mut buf) {
Ok(Event::Start(e)) => match e.name().as_ref() {
b"sheetData" => in_sheetdata = true,
b"row" if in_sheetdata => {
in_row = true;
}
b"c" if in_row => {
in_c = true;
cur_ref = None;
cur_type = None;
cur_text.clear();
for attr in e.attributes().flatten() {
match attr.key.as_ref() {
b"r" => {
cur_ref = 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());
cur_type = Some(CellType::from_str(v.as_deref())?);
}
_ => {}
}
}
}
b"v" if in_c => in_v = true,
b"is" if in_c => in_is = true,
b"t" if in_is => in_t = true,
_ => {}
},
Ok(Event::Empty(e)) if in_row && e.name().as_ref() == b"c" => {
continue;
}
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" => {
if let Some(reference) = cur_ref.take() {
let value = build_cell_value(
cur_type.unwrap_or(CellType::Number),
&cur_text,
sst,
)?;
if !value.is_empty() {
let (row, col) = parse_a1(&reference)?;
ws.rows
.entry(row)
.or_default()
.insert(col, Cell::new(reference, value));
}
}
in_c = false;
cur_text.clear();
cur_type = None;
}
b"row" => in_row = false,
b"sheetData" => in_sheetdata = false,
_ => {}
},
Ok(Event::Text(t)) if in_v || in_t => {
let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
cur_text.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!("worksheet char ref: {e}")))?
{
cur_text.push(ch);
} else {
let name = r
.decode()
.map_err(|e| Error::Xml(format!("worksheet entity decode: {e}")))?;
let ch = match name.as_ref() {
"lt" => '<',
"gt" => '>',
"amp" => '&',
"quot" => '"',
"apos" => '\'',
other => {
return Err(Error::Xml(format!(
"worksheet unknown entity: &{other};"
)))
}
};
cur_text.push(ch);
}
}
Ok(Event::CData(t)) if in_v || in_t => {
if let Ok(s) = std::str::from_utf8(&t) {
cur_text.push_str(s);
}
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(e) => return Err(Error::Xml(format!("worksheet parse: {e}"))),
}
buf.clear();
}
Ok(ws)
}
}
fn build_cell_value(
cell_type: CellType,
text: &str,
sst: &SharedStringsTable,
) -> Result<CellValue> {
match cell_type {
CellType::Number => {
if text.is_empty() {
Ok(CellValue::Empty)
} else {
let n = text
.trim()
.parse::<f64>()
.map_err(|e| Error::Schema(format!("cell number parse: {e}")))?;
Ok(CellValue::Number(n))
}
}
CellType::Boolean => {
let b = match text.trim() {
"1" | "true" => true,
"0" | "false" => false,
other => return Err(Error::Schema(format!("cell boolean parse: '{}'", other))),
};
Ok(CellValue::Boolean(b))
}
CellType::Error => Ok(CellValue::Error(text.to_string())),
CellType::SharedString => {
let i = text
.trim()
.parse::<usize>()
.map_err(|e| Error::Schema(format!("cell shared index parse: {e}")))?;
sst.get(i)
.map(|s| CellValue::String(s.to_string()))
.ok_or_else(|| Error::Schema(format!("shared string index out of range: {}", i)))
}
CellType::FormulaString => Ok(CellValue::String(text.to_string())),
CellType::InlineString => Ok(CellValue::String(text.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_sheet() -> Worksheet {
let mut ws = Worksheet::new("Sheet1");
ws.set_cell("A1", CellValue::Number(1.0)).unwrap();
ws.set_cell("B1", CellValue::Number(2.0)).unwrap();
ws.set_cell("A2", CellValue::String("Hello".to_string()))
.unwrap();
ws.set_cell("B2", CellValue::Boolean(true)).unwrap();
ws
}
#[test]
fn set_and_get_cell() {
let ws = make_sheet();
assert_eq!(
ws.get_cell("A1").unwrap().unwrap().value(),
&CellValue::Number(1.0)
);
assert_eq!(
ws.get_cell("A2").unwrap().unwrap().value(),
&CellValue::String("Hello".to_string())
);
assert_eq!(
ws.get_cell("B2").unwrap().unwrap().value(),
&CellValue::Boolean(true)
);
assert!(ws.get_cell("C5").unwrap().is_none());
}
#[test]
fn set_cell_rc_skips_a1_parse() {
let mut ws = Worksheet::new("Sheet1");
ws.set_cell_rc(3, 5, CellValue::Number(42.0));
assert_eq!(
ws.get_cell_rc(3, 5).unwrap().value(),
&CellValue::Number(42.0)
);
assert_eq!(ws.get_cell_rc(3, 5).unwrap().reference(), "E3");
}
#[test]
fn empty_value_removes_cell() {
let mut ws = Worksheet::new("Sheet1");
ws.set_cell("A1", CellValue::Number(1.0)).unwrap();
assert!(ws.get_cell("A1").unwrap().is_some());
ws.set_cell("A1", CellValue::Empty).unwrap();
assert!(ws.get_cell("A1").unwrap().is_none());
assert!(ws.is_empty());
}
#[test]
fn invalid_reference_errors() {
let mut ws = Worksheet::new("Sheet1");
assert!(ws.set_cell("XYZ", CellValue::Number(1.0)).is_err());
assert!(ws.get_cell("XYZ").is_err());
}
#[test]
fn dimension_calculation() {
let ws = make_sheet();
assert_eq!(ws.dimension(), "A1:B2");
}
#[test]
fn dimension_empty() {
let ws = Worksheet::new("Sheet1");
assert_eq!(ws.dimension(), "A1");
}
#[test]
fn dimension_single_cell() {
let mut ws = Worksheet::new("Sheet1");
ws.set_cell("A1", CellValue::Number(1.0)).unwrap();
assert_eq!(ws.dimension(), "A1");
}
#[test]
fn iter_cells_order() {
let ws = make_sheet();
let cells: Vec<&Cell> = ws.iter_cells().collect();
assert_eq!(cells.len(), 4);
assert_eq!(cells[0].reference(), "A1");
assert_eq!(cells[1].reference(), "B1");
assert_eq!(cells[2].reference(), "A2");
assert_eq!(cells[3].reference(), "B2");
}
#[test]
fn cell_count() {
let ws = make_sheet();
assert_eq!(ws.cell_count(), 4);
}
#[test]
fn max_row_col() {
let ws = make_sheet();
assert_eq!(ws.max_row(), Some(2));
assert_eq!(ws.max_col(), Some(2));
}
#[test]
fn to_xml_basic_structure() {
let ws = make_sheet();
let mut sst = SharedStringsTable::new();
let xml = ws.to_xml(&mut sst);
assert!(xml.contains("<?xml version=\"1.0\""));
assert!(xml.contains("<worksheet xmlns=\""));
assert!(xml.contains("<dimension ref=\"A1:B2\"/>"));
assert!(xml.contains("<sheetData>"));
assert!(xml.contains("<row r=\"1\">"));
assert!(xml.contains("<row r=\"2\">"));
assert!(xml.contains("<c r=\"A1\"><v>1</v></c>"));
assert!(xml.contains("<c r=\"A2\" t=\"s\"><v>0</v></c>"));
assert!(xml.contains("<c r=\"B2\" t=\"b\"><v>1</v></c>"));
}
#[test]
fn to_xml_registers_strings_in_sst() {
let ws = make_sheet();
let mut sst = SharedStringsTable::new();
let _xml = ws.to_xml(&mut sst);
assert_eq!(sst.unique_count(), 1); assert_eq!(sst.get(0), Some("Hello"));
}
#[test]
fn from_xml_round_trip() {
let ws = make_sheet();
let mut sst = SharedStringsTable::new();
let xml = ws.to_xml(&mut sst);
let ws2 = Worksheet::from_xml(&xml, &sst).unwrap();
assert_eq!(ws2.cell_count(), 4);
assert_eq!(
ws2.get_cell("A1").unwrap().unwrap().value(),
&CellValue::Number(1.0)
);
assert_eq!(
ws2.get_cell("A2").unwrap().unwrap().value(),
&CellValue::String("Hello".to_string())
);
assert_eq!(
ws2.get_cell("B2").unwrap().unwrap().value(),
&CellValue::Boolean(true)
);
}
#[test]
fn from_xml_empty_sheet() {
let xml = "<?xml version=\"1.0\"?>\
<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\
<dimension ref=\"A1\"/>\
<sheetData/>\
</worksheet>";
let sst = SharedStringsTable::new();
let ws = Worksheet::from_xml(xml, &sst).unwrap();
assert!(ws.is_empty());
}
#[test]
fn from_xml_shared_string_resolves() {
let xml = "<?xml version=\"1.0\"?>\
<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\
<sheetData>\
<row r=\"1\"><c r=\"A1\" t=\"s\"><v>0</v></c></row>\
</sheetData>\
</worksheet>";
let mut sst = SharedStringsTable::new();
sst.add("World");
let ws = Worksheet::from_xml(xml, &sst).unwrap();
assert_eq!(
ws.get_cell("A1").unwrap().unwrap().value(),
&CellValue::String("World".to_string())
);
}
#[test]
fn from_xml_shared_string_out_of_range_errors() {
let xml = "<?xml version=\"1.0\"?>\
<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\
<sheetData>\
<row r=\"1\"><c r=\"A1\" t=\"s\"><v>99</v></c></row>\
</sheetData>\
</worksheet>";
let sst = SharedStringsTable::new();
assert!(Worksheet::from_xml(xml, &sst).is_err());
}
#[test]
fn round_trip_preserves_special_chars() {
let mut ws = Worksheet::new("Sheet1");
ws.set_cell("A1", CellValue::String("a<b>&c".to_string()))
.unwrap();
let mut sst = SharedStringsTable::new();
let xml = ws.to_xml(&mut sst);
let ws2 = Worksheet::from_xml(&xml, &sst).unwrap();
assert_eq!(
ws2.get_cell("A1").unwrap().unwrap().value(),
&CellValue::String("a<b>&c".to_string())
);
}
#[test]
fn rename_sheet() {
let mut ws = Worksheet::new("Old");
ws.set_name("New");
assert_eq!(ws.name(), "New");
}
#[test]
fn clear_empties_sheet() {
let mut ws = make_sheet();
assert_eq!(ws.cell_count(), 4);
ws.clear();
assert!(ws.is_empty());
}
}