use quick_xml::events::{BytesStart, Event};
use std::io::{Read, Seek};
use super::{
expand_shared_formula, get_dimension, get_row, get_row_column, read_string_with_bufs,
Dimensions, XlReader,
};
use crate::attrs::RawAttributes;
use crate::{
datatype::DataRef,
formats::{format_excel_f64_ref, CellFormat},
utils::{unescape_entity_to_buffer, unescape_xml},
Cell, XlsxError,
};
#[derive(Clone, Debug)]
struct SharedFormula {
formula: String,
range: Dimensions,
}
struct WorkbookContext<'a> {
strings: &'a [String],
formats: &'a [CellFormat],
is_1904: bool,
}
struct ValueBufs {
xml: Vec<u8>,
value: String,
str_inner: Vec<u8>,
}
impl ValueBufs {
fn new() -> Self {
Self {
xml: Vec::with_capacity(1024),
value: String::with_capacity(64),
str_inner: Vec::with_capacity(1024),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum XlsxFormulaMetadata {
Normal {
formula: String,
},
Shared {
shared_index: usize,
range: Option<Dimensions>,
formula: String,
},
SharedDerived {
shared_index: usize,
},
}
impl XlsxFormulaMetadata {
pub fn shared_index(&self) -> Option<usize> {
match self {
Self::Normal { .. } => None,
Self::Shared { shared_index, .. } | Self::SharedDerived { shared_index } => {
Some(*shared_index)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum FormulaMetadata {
Normal {
formula: String,
},
Shared {
shared_index: usize,
range: Option<Dimensions>,
formula: String,
},
SharedDerived {
shared_index: usize,
translated_formula: Option<String>,
},
}
impl FormulaMetadata {
fn formula_text(&self) -> Option<&str> {
match self {
Self::Normal { formula } | Self::Shared { formula, .. } => Some(formula),
Self::SharedDerived {
translated_formula, ..
} => translated_formula.as_deref(),
}
}
fn into_metadata(self) -> XlsxFormulaMetadata {
match self {
Self::Normal { formula } => XlsxFormulaMetadata::Normal { formula },
Self::Shared {
shared_index,
range,
formula,
} => XlsxFormulaMetadata::Shared {
shared_index,
range,
formula,
},
Self::SharedDerived { shared_index, .. } => {
XlsxFormulaMetadata::SharedDerived { shared_index }
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct XlsxCellFormula<'a> {
pub pos: (u32, u32),
pub value: DataRef<'a>,
pub formula: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct XlsxCellFormulaMetadataRecord<'a> {
pub pos: (u32, u32),
pub value: DataRef<'a>,
pub formula: Option<XlsxFormulaMetadata>,
}
struct XlsxCellFormulaMetadataRecordInternal<'a> {
pos: (u32, u32),
value: DataRef<'a>,
formula: Option<FormulaMetadata>,
}
pub struct XlsxCellReader<'a, RS>
where
RS: Read + Seek,
{
xml: XlReader<'a, RS>,
strings: &'a [String],
formats: &'a [CellFormat],
is_1904: bool,
dimensions: Dimensions,
row_index: u32,
col_index: u32,
buf: Vec<u8>,
cell_buf: Vec<u8>,
value_bufs: ValueBufs,
formulas: Vec<Option<SharedFormula>>,
}
impl<'a, RS> XlsxCellReader<'a, RS>
where
RS: Read + Seek,
{
pub fn new(
mut xml: XlReader<'a, RS>,
strings: &'a [String],
formats: &'a [CellFormat],
is_1904: bool,
) -> Result<Self, XlsxError> {
let mut buf = Vec::with_capacity(1024);
let mut dimensions = Dimensions::default();
let mut sh_type = None;
'xml: loop {
buf.clear();
match xml.read_event_into(&mut buf).map_err(XlsxError::Xml)? {
Event::Start(e) => match e.local_name().as_ref() {
"dimension" => {
if let Some(rdim) = e.raw_attr("ref")? {
dimensions = get_dimension(rdim)?;
continue 'xml;
}
return Err(XlsxError::UnexpectedNode("dimension"));
}
"sheetData" => break,
typ => {
if sh_type.is_none() {
sh_type = Some(typ.to_string());
}
}
},
Event::Eof => {
if let Some(typ) = sh_type {
return Err(XlsxError::NotAWorksheet(typ));
} else {
return Err(XlsxError::XmlEof("worksheet"));
}
}
_ => (),
}
}
Ok(Self {
xml,
strings,
formats,
is_1904,
dimensions,
row_index: 0,
col_index: 0,
buf: Vec::with_capacity(1024),
cell_buf: Vec::with_capacity(1024),
value_bufs: ValueBufs::new(),
formulas: Vec::with_capacity(1024),
})
}
pub fn dimensions(&self) -> Dimensions {
self.dimensions
}
pub fn next_cell(&mut self) -> Result<Option<Cell<DataRef<'a>>>, XlsxError> {
loop {
self.buf.clear();
match self.xml.read_event_into(&mut self.buf) {
Ok(Event::Start(row_element)) if row_element.local_name().as_ref() == "row" => {
if let Some(r) = row_element.raw_attr("r")? {
self.row_index = get_row(r)?;
}
}
Ok(Event::End(row_element)) if row_element.local_name().as_ref() == "row" => {
self.row_index += 1;
self.col_index = 0;
}
Ok(Event::Start(c_element)) if c_element.local_name().as_ref() == "c" => {
let (pos_attr, style_attr, type_attr) =
get_attrs!(c_element, "r" => r, "s" => s, "t" => t)?;
let pos = if let Some(range) = pos_attr {
let (row, col) = get_row_column(range)?;
self.col_index = col;
(row, col)
} else {
(self.row_index, self.col_index)
};
let mut value = DataRef::Empty;
loop {
self.cell_buf.clear();
match self.xml.read_event_into(&mut self.cell_buf) {
Ok(Event::Start(e)) => {
let ctx = WorkbookContext {
strings: self.strings,
formats: self.formats,
is_1904: self.is_1904,
};
value = read_value(
&ctx,
&mut self.xml,
&e,
style_attr,
type_attr,
&mut self.value_bufs,
)?;
}
Ok(Event::End(e)) if e.local_name().as_ref() == "c" => break,
Ok(Event::Eof) => return Err(XlsxError::XmlEof("c")),
Err(e) => return Err(XlsxError::Xml(e)),
_ => (),
}
}
self.col_index += 1;
return Ok(Some(Cell::new(pos, value)));
}
Ok(Event::End(e)) if e.local_name().as_ref() == "sheetData" => {
return Ok(None);
}
Ok(Event::Eof) => return Err(XlsxError::XmlEof("sheetData")),
Err(e) => return Err(XlsxError::Xml(e)),
_ => (),
}
}
}
fn read_formula_record(
xml: &mut XlReader<'_, RS>,
formulas: &mut Vec<Option<SharedFormula>>,
e: &BytesStart<'_>,
pos: (u32, u32),
expand_shared_derived: bool,
) -> Result<Option<FormulaMetadata>, XlsxError> {
let formula = read_formula(xml, e)?;
let (t_attr, si_attr, ref_attr) = get_attrs!(e, "t" => t, "si" => si, "ref" => ref_)?;
if t_attr == Some("shared") {
let shared_index = match si_attr {
Some(res) => match atoi_simd::parse::<usize, true, false>(res.as_bytes()) {
Ok(res) => res,
Err(_) => return Err(XlsxError::Unexpected("si attribute must be a number")),
},
None => {
return Err(XlsxError::Unexpected(
"si attribute is mandatory if it is shared",
));
}
};
return match ref_attr {
Some(res) => {
let range = get_dimension(res)?;
let formula = formula.unwrap_or_default();
if expand_shared_derived {
if formulas.len() <= shared_index {
formulas.resize(shared_index + 1, None);
}
formulas[shared_index] = Some(SharedFormula {
formula: formula.clone(),
range,
});
}
Ok(Some(FormulaMetadata::Shared {
shared_index,
range: Some(range),
formula,
}))
}
None => {
let translated_formula = if expand_shared_derived {
formulas
.get(shared_index)
.and_then(|template| template.as_ref())
.map(|template| {
expand_shared_formula(&template.formula, template.range.start, pos)
})
.transpose()?
} else {
None
};
Ok(Some(FormulaMetadata::SharedDerived {
shared_index,
translated_formula,
}))
}
};
}
Ok(formula.map(|formula| FormulaMetadata::Normal { formula }))
}
pub fn next_cell_with_formula(&mut self) -> Result<Option<XlsxCellFormula<'a>>, XlsxError> {
Ok(self
.next_cell_formula_record_impl(true)?
.map(|record| XlsxCellFormula {
pos: record.pos,
value: record.value,
formula: record
.formula
.and_then(|formula| formula.formula_text().map(str::to_string)),
}))
}
pub fn next_cell_with_formula_metadata(
&mut self,
) -> Result<Option<XlsxCellFormulaMetadataRecord<'a>>, XlsxError> {
Ok(self
.next_cell_formula_record_impl(false)?
.map(|record| XlsxCellFormulaMetadataRecord {
pos: record.pos,
value: record.value,
formula: record.formula.map(FormulaMetadata::into_metadata),
}))
}
fn next_cell_formula_record_impl(
&mut self,
expand_shared_derived: bool,
) -> Result<Option<XlsxCellFormulaMetadataRecordInternal<'a>>, XlsxError> {
loop {
self.buf.clear();
match self.xml.read_event_into(&mut self.buf) {
Ok(Event::Start(row_element)) if row_element.local_name().as_ref() == "row" => {
if let Some(r) = row_element.raw_attr("r")? {
self.row_index = get_row(r)?;
}
}
Ok(Event::End(row_element)) if row_element.local_name().as_ref() == "row" => {
self.row_index += 1;
self.col_index = 0;
}
Ok(Event::Start(c_element)) if c_element.local_name().as_ref() == "c" => {
let (pos_attr, style_attr, type_attr) =
get_attrs!(c_element, "r" => r, "s" => s, "t" => t)?;
let pos = if let Some(range) = pos_attr {
let (row, col) = get_row_column(range)?;
self.col_index = col;
(row, col)
} else {
(self.row_index, self.col_index)
};
let mut value = DataRef::Empty;
let mut formula = None;
loop {
self.cell_buf.clear();
match self.xml.read_event_into(&mut self.cell_buf) {
Ok(Event::Start(e)) if e.local_name().as_ref() == "f" => {
formula = Self::read_formula_record(
&mut self.xml,
&mut self.formulas,
&e,
pos,
expand_shared_derived,
)?;
}
Ok(Event::Start(e)) => {
let ctx = WorkbookContext {
strings: self.strings,
formats: self.formats,
is_1904: self.is_1904,
};
value = read_value(
&ctx,
&mut self.xml,
&e,
style_attr,
type_attr,
&mut self.value_bufs,
)?;
}
Ok(Event::End(e)) if e.local_name().as_ref() == "c" => break,
Ok(Event::Eof) => return Err(XlsxError::XmlEof("c")),
Err(e) => return Err(XlsxError::Xml(e)),
_ => (),
}
}
self.col_index += 1;
return Ok(Some(XlsxCellFormulaMetadataRecordInternal {
pos,
value,
formula,
}));
}
Ok(Event::End(e)) if e.local_name().as_ref() == "sheetData" => {
return Ok(None);
}
Ok(Event::Eof) => return Err(XlsxError::XmlEof("sheetData")),
Err(e) => return Err(XlsxError::Xml(e)),
_ => (),
}
}
}
pub fn next_formula(&mut self) -> Result<Option<Cell<String>>, XlsxError> {
loop {
self.buf.clear();
match self.xml.read_event_into(&mut self.buf) {
Ok(Event::Start(row_element)) if row_element.local_name().as_ref() == "row" => {
if let Some(r) = row_element.raw_attr("r")? {
self.row_index = get_row(r)?;
}
}
Ok(Event::End(row_element)) if row_element.local_name().as_ref() == "row" => {
self.row_index += 1;
self.col_index = 0;
}
Ok(Event::Start(c_element)) if c_element.local_name().as_ref() == "c" => {
let pos = if let Some(r) = c_element.raw_attr("r")? {
let (row, col) = get_row_column(r)?;
self.col_index = col;
(row, col)
} else {
(self.row_index, self.col_index)
};
let mut value = None;
loop {
self.cell_buf.clear();
match self.xml.read_event_into(&mut self.cell_buf) {
Ok(Event::Start(e)) => {
let formula_record = Self::read_formula_record(
&mut self.xml,
&mut self.formulas,
&e,
pos,
true,
)?;
if let Some(formula_text) = formula_record
.and_then(|record| record.formula_text().map(str::to_string))
{
value = Some(formula_text);
}
}
Ok(Event::End(e)) if e.local_name().as_ref() == "c" => break,
Ok(Event::Eof) => return Err(XlsxError::XmlEof("c")),
Err(e) => return Err(XlsxError::Xml(e)),
_ => (),
}
}
self.col_index += 1;
return Ok(Some(Cell::new(pos, value.unwrap_or_default())));
}
Ok(Event::End(e)) if e.local_name().as_ref() == "sheetData" => {
return Ok(None);
}
Ok(Event::Eof) => return Err(XlsxError::XmlEof("sheetData")),
Err(e) => return Err(XlsxError::Xml(e)),
_ => (),
}
}
}
}
fn read_value<'s, RS>(
ctx: &WorkbookContext<'s>,
xml: &mut XlReader<'_, RS>,
e: &BytesStart<'_>,
style_attr: Option<&str>,
type_attr: Option<&str>,
bufs: &mut ValueBufs,
) -> Result<DataRef<'s>, XlsxError>
where
RS: Read + Seek,
{
Ok(match e.local_name().as_ref() {
"is" => {
read_string_with_bufs(xml, e.name(), &mut bufs.xml, &mut bufs.str_inner)?
.map_or(DataRef::Empty, DataRef::String)
}
"v" if matches!(type_attr, Some("inlineStr") | Some("is")) => {
bufs.xml.clear();
xml.read_to_end_into(e.name(), &mut bufs.xml)?;
DataRef::Empty
}
"v" => match type_attr {
Some("n") | Some("s") | Some("b") | Some("e") | None => {
bufs.xml.clear();
let val = match xml.read_event_into(&mut bufs.xml)? {
Event::Text(t) => read_v(ctx, t.as_ref(), style_attr, type_attr)?,
Event::End(end) if end.name() == e.name() => return Ok(DataRef::Empty),
Event::Eof => return Err(XlsxError::XmlEof("v")),
_ => DataRef::Empty,
};
bufs.xml.clear();
xml.read_to_end_into(e.name(), &mut bufs.xml)?;
val
}
_ => {
bufs.value.clear();
loop {
bufs.xml.clear();
match xml.read_event_into(&mut bufs.xml)? {
Event::Text(t) => bufs.value.push_str(&t.xml10_content()),
Event::GeneralRef(e) => unescape_entity_to_buffer(&e, &mut bufs.value)?,
Event::End(end) if end.name() == e.name() => break,
Event::Eof => return Err(XlsxError::XmlEof("v")),
_ => (),
}
}
read_v(ctx, &bufs.value, style_attr, type_attr)?
}
},
"f" => {
bufs.xml.clear();
xml.read_to_end_into(e.name(), &mut bufs.xml)?;
DataRef::Empty
}
_n => return Err(XlsxError::UnexpectedNode("v, f, or is")),
})
}
fn read_v<'s>(
ctx: &WorkbookContext<'s>,
v: &str,
style_attr: Option<&str>,
type_attr: Option<&str>,
) -> Result<DataRef<'s>, XlsxError> {
let cell_format = match style_attr {
Some(style) => {
let id = atoi_simd::parse::<usize, true, false>(style.as_bytes()).unwrap_or(0);
ctx.formats.get(id)
}
None => Some(&CellFormat::Other),
};
match type_attr {
Some("s") => {
if v.is_empty() {
return Ok(DataRef::Empty);
}
let idx = atoi_simd::parse::<usize, true, false>(v.as_bytes()).unwrap_or(0);
ctx.strings
.get(idx)
.map(|s| DataRef::SharedString(s))
.ok_or(XlsxError::Unexpected(
"Cell string index not found in shared strings table",
))
}
Some("b") => read_bool(v),
Some("d") => Ok(DataRef::DateTimeIso(v.to_string())),
Some("e") => Ok(DataRef::Error(v.parse()?)),
Some("str") => Ok(DataRef::String(unescape_xml(v).into_owned())),
Some("n") | None => {
if v.is_empty() {
return Ok(DataRef::Empty);
}
fast_float2::parse::<f64, _>(v.as_bytes())
.map(|n| format_excel_f64_ref(n, cell_format, ctx.is_1904))
.or_else(|_| {
if type_attr.is_none() {
Ok(DataRef::String(v.to_string()))
} else {
Err(XlsxError::ParseFloat(v.parse::<f64>().unwrap_err()))
}
})
}
Some(t) => Err(XlsxError::CellTAttribute(t.to_string())),
}
}
fn read_formula<RS>(xml: &mut XlReader<RS>, e: &BytesStart) -> Result<Option<String>, XlsxError>
where
RS: Read + Seek,
{
match e.local_name().as_ref() {
"is" | "v" => {
xml.read_to_end_into(e.name(), &mut Vec::new())?;
Ok(None)
}
"f" => {
let mut f_buf = Vec::with_capacity(512);
let mut f = String::new();
loop {
match xml.read_event_into(&mut f_buf)? {
Event::Text(t) => f.push_str(&t.xml10_content()),
Event::GeneralRef(e) => unescape_entity_to_buffer(&e, &mut f)?,
Event::End(end) if end.name() == e.name() => break,
Event::Eof => return Err(XlsxError::XmlEof("f")),
_ => (),
}
f_buf.clear();
}
Ok(Some(f))
}
_ => Err(XlsxError::UnexpectedNode("v, f, or is")),
}
}
fn read_bool<'s>(v: &str) -> Result<DataRef<'s>, XlsxError> {
let value = v.trim();
if value.is_empty() {
Ok(DataRef::Empty)
} else if value == "0" || value.eq_ignore_ascii_case("false") {
Ok(DataRef::Bool(false))
} else if value == "1" || value.eq_ignore_ascii_case("true") {
Ok(DataRef::Bool(true))
} else {
Err(XlsxError::Unrecognized {
typ: "boolean cell value",
val: v.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_bool_cell_values() {
for value in ["0", "false", "FALSE", "False", " 0 "] {
assert_eq!(read_bool(value).unwrap(), DataRef::Bool(false), "{value:?}");
}
for value in ["1", "true", "TRUE", "True", " 1 "] {
assert_eq!(read_bool(value).unwrap(), DataRef::Bool(true), "{value:?}");
}
assert_eq!(read_bool("").unwrap(), DataRef::Empty);
assert_eq!(read_bool(" ").unwrap(), DataRef::Empty);
for value in ["00", "0.0", "-1", "2", "banana", "#REF!"] {
assert!(
matches!(
read_bool(value),
Err(XlsxError::Unrecognized { typ: "boolean cell value", ref val }) if val == value
),
"{value:?}"
);
}
}
}