use super::Document;
use crate::media_type;
use crate::value::Length;
use crate::xml::{Element, Ns};
use crate::{Error, Family, Properties};
pub struct SheetDocument {
pub document: Document,
sheets: Vec<Sheet>,
}
pub struct Sheet {
pub name: String,
pub columns: Vec<Column>,
pub used_rows: usize,
pub used_columns: usize,
pub visible: bool,
rows: Vec<RowRange>,
table: usize,
}
pub struct Column {
pub width: Option<Length>,
pub default_cell_style: Option<String>,
pub visible: bool,
}
struct RowRange {
first: usize,
count: usize,
path: RowPath,
}
enum RowPath {
Direct(usize),
Nested(Box<[usize]>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Empty,
Number(f64),
Percentage(f64),
Currency(f64, Option<String>),
Date(String),
Time(String),
Boolean(bool),
Text(String),
}
impl Value {
pub fn is_numeric(&self) -> bool {
matches!(
self,
Self::Number(_)
| Self::Percentage(_)
| Self::Currency(..)
| Self::Date(_)
| Self::Time(_)
)
}
}
pub struct Cell<'a> {
pub element: &'a Element,
pub covered: bool,
}
impl Cell<'_> {
pub fn value(&self) -> Value {
let e = self.element;
match e.attr(&Ns::Office, "value-type") {
Some("float") => e
.attr(&Ns::Office, "value")
.and_then(|v| v.parse().ok())
.map_or(Value::Empty, Value::Number),
Some("percentage") => e
.attr(&Ns::Office, "value")
.and_then(|v| v.parse().ok())
.map_or(Value::Empty, Value::Percentage),
Some("currency") => e
.attr(&Ns::Office, "value")
.and_then(|v| v.parse().ok())
.map_or(Value::Empty, |amount| {
Value::Currency(
amount,
e.attr(&Ns::Office, "currency").map(ToOwned::to_owned),
)
}),
Some("date") => e
.attr(&Ns::Office, "date-value")
.map_or(Value::Empty, |v| Value::Date(v.to_owned())),
Some("time") => e
.attr(&Ns::Office, "time-value")
.map_or(Value::Empty, |v| Value::Time(v.to_owned())),
Some("boolean") => e
.attr(&Ns::Office, "boolean-value")
.and_then(crate::value::boolean)
.map_or(Value::Empty, Value::Boolean),
Some("string") => match e.attr(&Ns::Office, "string-value") {
Some(text) => Value::Text(text.to_owned()),
None => Value::Text(self.text()),
},
_ => {
let text = self.text();
if text.is_empty() {
Value::Empty
} else {
Value::Text(text)
}
}
}
}
pub fn text(&self) -> String {
let mut out = String::new();
for paragraph in self.element.elements() {
if paragraph.is(&Ns::Text, "p") {
if !out.is_empty() {
out.push('\n');
}
out.push_str(¶graph.plain_text());
}
}
out
}
pub fn formula(&self) -> Option<&str> {
let formula = self.element.attr(&Ns::Table, "formula")?;
Some(match formula.split_once(":=") {
Some((_, expression)) => expression,
None => formula,
})
}
pub fn style_name(&self) -> Option<&str> {
self.element.attr(&Ns::Table, "style-name")
}
pub fn columns_spanned(&self) -> usize {
self.element
.attr_usize(&Ns::Table, "number-columns-spanned")
.unwrap_or(1)
.max(1)
}
pub fn rows_spanned(&self) -> usize {
self.element
.attr_usize(&Ns::Table, "number-rows-spanned")
.unwrap_or(1)
.max(1)
}
fn occupied(&self) -> bool {
self.element.attr(&Ns::Office, "value-type").is_some()
|| self.element.attr(&Ns::Table, "formula").is_some()
|| self.element.elements().any(|e| e.is(&Ns::Text, "p"))
}
}
impl SheetDocument {
pub fn read(bytes: &[u8]) -> Result<Self, Error> {
let document = Document::read(bytes, media_type::SPREADSHEET_ANY)?;
let sheets = index_sheets(&document);
Ok(Self { document, sheets })
}
pub fn sheets(&self) -> &[Sheet] {
&self.sheets
}
fn table(&self, sheet: &Sheet) -> Option<&Element> {
let body = self.document.body_of("spreadsheet")?;
body.children.get(sheet.table).and_then(|node| match node {
crate::xml::Node::Element(e) => Some(e),
_ => None,
})
}
pub fn cell(&self, sheet: &Sheet, row: usize, column: usize) -> Option<Cell<'_>> {
cell_in_row(self.row_element(sheet, row)?, column)
}
pub fn row_element(&self, sheet: &Sheet, row: usize) -> Option<&Element> {
let table = self.table(sheet)?;
let range = sheet.row_range(row)?;
let mut element = table;
for step in range.path.steps() {
let crate::xml::Node::Element(child) = element.children.get(*step)? else {
return None;
};
element = child;
}
Some(element)
}
pub fn row_height(&self, sheet: &Sheet, row: usize) -> Option<Length> {
let name = self
.row_element(sheet, row)?
.attr(&Ns::Table, "style-name")?;
self.document
.styles
.resolve(&Family::TableRow, name)
.row_height
}
pub fn cell_style(
&self,
sheet: &Sheet,
cell: Option<&Cell<'_>>,
column: usize,
) -> std::rc::Rc<Properties> {
let named = cell.and_then(Cell::style_name);
let from_column = sheet
.columns
.get(column)
.and_then(|c| c.default_cell_style.as_deref());
let name = named.or(from_column).unwrap_or("Default");
self.document.styles.resolve(&Family::TableCell, name)
}
}
impl Sheet {
fn row_range(&self, row: usize) -> Option<&RowRange> {
let found = self
.rows
.binary_search_by(|range| {
if row < range.first {
std::cmp::Ordering::Greater
} else if row >= range.first + range.count {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Equal
}
})
.ok()?;
self.rows.get(found)
}
pub fn column_width(&self, column: usize) -> Option<Length> {
self.columns.get(column).and_then(|c| c.width)
}
}
fn cell_in_row(row: &Element, column: usize) -> Option<Cell<'_>> {
let mut at = 0usize;
for child in row.elements() {
let covered = child.is(&Ns::Table, "covered-table-cell");
if !covered && !child.is(&Ns::Table, "table-cell") {
continue;
}
let repeat = child
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.max(1);
if column < at + repeat {
return Some(Cell {
element: child,
covered,
});
}
at += repeat;
}
None
}
fn index_sheets(document: &Document) -> Vec<Sheet> {
let Some(body) = document.body_of("spreadsheet") else {
return Vec::new();
};
let mut sheets = Vec::new();
for (position, node) in body.children.iter().enumerate() {
let crate::xml::Node::Element(table) = node else {
continue;
};
if !table.is(&Ns::Table, "table") {
continue;
}
sheets.push(index_sheet(document, table, position));
}
sheets
}
fn index_sheet(document: &Document, table: &Element, position: usize) -> Sheet {
let mut index = Index {
document,
rows: Vec::new(),
columns: Vec::new(),
at_row: 0,
used_rows: 0,
used_columns: 0,
};
index.walk(table, &mut Vec::new());
Sheet {
name: table
.attr(&Ns::Table, "name")
.unwrap_or_default()
.to_owned(),
columns: index.columns,
used_rows: index.used_rows,
used_columns: index.used_columns,
visible: table.attr(&Ns::Table, "display").unwrap_or("true") != "false",
rows: index.rows,
table: position,
}
}
struct Index<'a> {
document: &'a Document,
rows: Vec<RowRange>,
columns: Vec<Column>,
at_row: usize,
used_rows: usize,
used_columns: usize,
}
impl Index<'_> {
fn walk(&mut self, parent: &Element, path: &mut Vec<usize>) {
for (child_index, child) in parent.children.iter().enumerate() {
let crate::xml::Node::Element(element) = child else {
continue;
};
if element.is(&Ns::Table, "table-column") {
self.column(element);
} else if element.is(&Ns::Table, "table-row") {
path.push(child_index);
self.row(element, path);
path.pop();
} else if is_row_container(element) || is_column_container(element) {
path.push(child_index);
self.walk(element, path);
path.pop();
}
}
}
fn column(&mut self, element: &Element) {
let repeat = element
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.max(1);
let width = element
.attr(&Ns::Table, "style-name")
.map(|name| self.document.styles.resolve(&Family::TableColumn, name))
.and_then(|p| p.column_width);
let default_cell_style = element
.attr(&Ns::Table, "default-cell-style-name")
.map(ToOwned::to_owned);
let visible = element.attr(&Ns::Table, "visibility").unwrap_or("visible") == "visible";
let keep = repeat.min(MAX_COLUMNS.saturating_sub(self.columns.len()));
for _ in 0..keep {
self.columns.push(Column {
width,
default_cell_style: default_cell_style.clone(),
visible,
});
}
}
fn row(&mut self, element: &Element, path: &[usize]) {
let repeat = element
.attr_usize(&Ns::Table, "number-rows-repeated")
.unwrap_or(1)
.max(1);
if let Some(last) = last_occupied_column(element) {
self.used_rows = self.at_row + repeat;
self.used_columns = self.used_columns.max(last + 1);
}
self.rows.push(RowRange {
first: self.at_row,
count: repeat,
path: RowPath::of(path),
});
self.at_row += repeat;
}
}
impl RowPath {
fn of(path: &[usize]) -> Self {
match path {
[only] => Self::Direct(*only),
nested => Self::Nested(nested.into()),
}
}
fn steps(&self) -> &[usize] {
match self {
Self::Direct(only) => std::slice::from_ref(only),
Self::Nested(path) => path,
}
}
}
fn is_row_container(element: &Element) -> bool {
element.is(&Ns::Table, "table-rows")
|| element.is(&Ns::Table, "table-header-rows")
|| element.is(&Ns::Table, "table-row-group")
}
fn is_column_container(element: &Element) -> bool {
element.is(&Ns::Table, "table-columns")
|| element.is(&Ns::Table, "table-header-columns")
|| element.is(&Ns::Table, "table-column-group")
}
const MAX_COLUMNS: usize = 16_384;
fn last_occupied_column(row: &Element) -> Option<usize> {
let mut at = 0usize;
let mut last = None;
for child in row.elements() {
let covered = child.is(&Ns::Table, "covered-table-cell");
if !covered && !child.is(&Ns::Table, "table-cell") {
continue;
}
let repeat = child
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.max(1);
let cell = Cell {
element: child,
covered,
};
if cell.occupied() {
last = Some(at + repeat - 1);
}
at += repeat;
}
last
}