445 lines
16 KiB
Rust
445 lines
16 KiB
Rust
#![allow(clippy::unwrap_used)]
|
|
#![allow(clippy::panic)]
|
|
|
|
//! # A note on shared formulas
|
|
//! Although both Excel and IronCalc uses shared formulas they are used in a slightly different way that cannot be mapped 1-1
|
|
//! In IronCalc _all_ formulas are shared and there is a list of shared formulas much like there is a list of shared strings.
|
|
//! In Excel the situation in more nuanced. A shared formula is shared amongst a rage of cells.
|
|
//! The top left cell would be the "mother" cell that would contain the shared formula:
|
|
//! <c r="F4" t="str">
|
|
//! <f t="shared" ref="F4:F8" si="42">A4+C4</f>
|
|
//! <v>123</v>
|
|
//! </c>
|
|
//! Cells in the range F4:F8 will then link to that formula like so:
|
|
//! <c r="F6">
|
|
//! <f t="shared" si="42"/>
|
|
//! <v>1</v>
|
|
//! </c>
|
|
//! Formula in F6 would then be 'A6+C6'
|
|
use std::collections::HashMap;
|
|
|
|
use itertools::Itertools;
|
|
|
|
use ironcalc_base::{
|
|
expressions::{
|
|
parser::{static_analysis::StaticResult, stringify::to_excel_string, Node},
|
|
types::CellReferenceRC,
|
|
utils::number_to_column,
|
|
},
|
|
types::{Cell, Worksheet},
|
|
};
|
|
|
|
use super::{escape::escape_xml, xml_constants::XML_DECLARATION};
|
|
|
|
fn get_cell_style_attribute(s: i32) -> String {
|
|
if s == 0 {
|
|
"".to_string()
|
|
} else {
|
|
format!(" s=\"{}\"", s)
|
|
}
|
|
}
|
|
|
|
fn get_formula_attribute(
|
|
sheet_name: String,
|
|
row: i32,
|
|
column: i32,
|
|
parsed_formula: &Node,
|
|
) -> String {
|
|
let cell_ref = CellReferenceRC {
|
|
sheet: sheet_name,
|
|
row,
|
|
column,
|
|
};
|
|
let formula = &to_excel_string(parsed_formula, &cell_ref);
|
|
escape_xml(formula).to_string()
|
|
}
|
|
|
|
pub(crate) fn get_worksheet_xml(
|
|
worksheet: &Worksheet,
|
|
parsed_formulas: &[(Node, StaticResult)],
|
|
dimension: &str,
|
|
is_sheet_selected: bool,
|
|
) -> String {
|
|
let mut sheet_data_str: Vec<String> = vec![];
|
|
let mut cols_str: Vec<String> = vec![];
|
|
let mut merged_cells_str: Vec<String> = vec![];
|
|
|
|
for col in &worksheet.cols {
|
|
// <col min="4" max="4" width="12" customWidth="1"/>
|
|
let min = col.min;
|
|
let max = col.max;
|
|
let width = col.width;
|
|
let custom_width = i32::from(col.custom_width);
|
|
let column_style = match col.style {
|
|
Some(s) => format!(" style=\"{s}\""),
|
|
None => "".to_string(),
|
|
};
|
|
cols_str.push(format!(
|
|
"<col min=\"{min}\" max=\"{max}\" width=\"{width}\" customWidth=\"{custom_width}\"{column_style}/>"
|
|
));
|
|
}
|
|
|
|
// this is a bit of an overkill. A dictionary of the row styles by row_index
|
|
let mut row_style_dict = HashMap::new();
|
|
for row in &worksheet.rows {
|
|
// {
|
|
// "height": 13,
|
|
// "r": 7,
|
|
// "custom_format": false,
|
|
// "custom_height": true,
|
|
// "s": 0
|
|
// "hidden": false,
|
|
// },
|
|
row_style_dict.insert(row.r, row.clone());
|
|
}
|
|
|
|
for (row_index, row_data) in worksheet.sheet_data.iter().sorted_by_key(|x| x.0) {
|
|
let mut row_data_str: Vec<String> = vec![];
|
|
for (column_index, cell) in row_data.iter().sorted_by_key(|x| x.0) {
|
|
let column_name = number_to_column(*column_index).unwrap();
|
|
let cell_name = format!("{column_name}{row_index}");
|
|
match cell {
|
|
Cell::EmptyCell { s } => {
|
|
// they only hold the style
|
|
let style = get_cell_style_attribute(*s);
|
|
row_data_str.push(format!("<c r=\"{cell_name}\"{style}/>"));
|
|
}
|
|
Cell::SpillBooleanCell { v, s, .. } | Cell::BooleanCell { v, s } => {
|
|
// <c r="A8" t="b" s="1">
|
|
// <v>1</v>
|
|
// </c>
|
|
let b = i32::from(*v);
|
|
let style = get_cell_style_attribute(*s);
|
|
row_data_str.push(format!(
|
|
"<c r=\"{cell_name}\" t=\"b\"{style}><v>{b}</v></c>"
|
|
));
|
|
}
|
|
Cell::SpillNumberCell { v, s, .. } | Cell::NumberCell { v, s } => {
|
|
// Normally the type number is left out. Example:
|
|
// <c r="C6" s="1">
|
|
// <v>3</v>
|
|
// </c>
|
|
let style = get_cell_style_attribute(*s);
|
|
row_data_str.push(format!("<c r=\"{cell_name}\"{style}><v>{v}</v></c>"));
|
|
}
|
|
Cell::SpillErrorCell { ei, s, .. } | Cell::ErrorCell { ei, s } => {
|
|
let style = get_cell_style_attribute(*s);
|
|
row_data_str.push(format!(
|
|
"<c r=\"{cell_name}\" t=\"e\"{style}><v>{ei}</v></c>"
|
|
));
|
|
}
|
|
Cell::SharedString { si, s } => {
|
|
// Example:
|
|
// <c r="A1" s="1" t="s">
|
|
// <v>5</v>
|
|
// </c>
|
|
// Cell on A1 contains a string (t="s") of style="1". The string is the 6th in the list of shared strings
|
|
let style = get_cell_style_attribute(*s);
|
|
row_data_str.push(format!(
|
|
"<c r=\"{cell_name}\" t=\"s\"{style}><v>{si}</v></c>"
|
|
));
|
|
}
|
|
Cell::CellFormula { f: _, s: _ } => {
|
|
panic!("Model needs to be evaluated before saving!");
|
|
}
|
|
Cell::CellFormulaBoolean { f, v, s } => {
|
|
// <c r="A4" t="b" s="3">
|
|
// <f>ISTEXT(A5)</f>
|
|
// <v>1</v>
|
|
// </c>
|
|
let style = get_cell_style_attribute(*s);
|
|
|
|
let formula = get_formula_attribute(
|
|
worksheet.get_name(),
|
|
*row_index,
|
|
*column_index,
|
|
&parsed_formulas[*f as usize].0,
|
|
);
|
|
|
|
let b = i32::from(*v);
|
|
row_data_str.push(format!(
|
|
"<c r=\"{cell_name}\" t=\"b\"{style}><f>{formula}</f><v>{b}</v></c>"
|
|
));
|
|
}
|
|
Cell::CellFormulaNumber { f, v, s } => {
|
|
// Note again type is skipped
|
|
// <c r="C4" s="3">
|
|
// <f>A5+C3</f>
|
|
// <v>123</v>
|
|
// </c>
|
|
|
|
let formula = get_formula_attribute(
|
|
worksheet.get_name(),
|
|
*row_index,
|
|
*column_index,
|
|
&parsed_formulas[*f as usize].0,
|
|
);
|
|
let style = get_cell_style_attribute(*s);
|
|
|
|
row_data_str.push(format!(
|
|
"<c r=\"{cell_name}\"{style}><f>{formula}</f><v>{v}</v></c>"
|
|
));
|
|
}
|
|
Cell::CellFormulaString { f, v, s } => {
|
|
// <c r="C6" t="str" s="5">
|
|
// <f>CONCATENATE(A1, A2)</f>
|
|
// <v>Hello world!</v>
|
|
// </c>
|
|
let formula = get_formula_attribute(
|
|
worksheet.get_name(),
|
|
*row_index,
|
|
*column_index,
|
|
&parsed_formulas[*f as usize].0,
|
|
);
|
|
let style = get_cell_style_attribute(*s);
|
|
let escaped_v = escape_xml(v);
|
|
|
|
row_data_str.push(format!(
|
|
"<c r=\"{cell_name}\" t=\"str\"{style}><f>{formula}</f><v>{escaped_v}</v></c>"
|
|
));
|
|
}
|
|
Cell::CellFormulaError {
|
|
f,
|
|
ei,
|
|
s,
|
|
o: _,
|
|
m: _,
|
|
} => {
|
|
// <c r="C6" t="e" s="4">
|
|
// <f>A1/A3<f/>
|
|
// <v>#DIV/0!</v>
|
|
// </c>
|
|
let formula = get_formula_attribute(
|
|
worksheet.get_name(),
|
|
*row_index,
|
|
*column_index,
|
|
&parsed_formulas[*f as usize].0,
|
|
);
|
|
let style = get_cell_style_attribute(*s);
|
|
row_data_str.push(format!(
|
|
"<c r=\"{cell_name}\" t=\"e\"{style}><f>{formula}</f><v>{ei}</v></c>"
|
|
));
|
|
}
|
|
Cell::SpillStringCell { v, s, .. } => {
|
|
// inline string
|
|
// <c r="A1" t="str">
|
|
let style = get_cell_style_attribute(*s);
|
|
let escaped_v = escape_xml(v);
|
|
row_data_str.push(format!(
|
|
"<c r=\"{cell_name}\" t=\"str\"{style}><v>{escaped_v}</v></c>"
|
|
));
|
|
}
|
|
Cell::DynamicCellFormula { .. } => {
|
|
panic!("Model needs to be evaluated before saving!");
|
|
}
|
|
Cell::DynamicCellFormulaBoolean { f, v, s, r, a: _ } => {
|
|
// <c r="A1" s="3" cm="1">
|
|
// <f t="array" ref="A1:A10">A1:A10</f>
|
|
// <v>1</v>
|
|
// </c>
|
|
let style = get_cell_style_attribute(*s);
|
|
let range = format!(
|
|
"{}{}:{}{}",
|
|
column_name,
|
|
row_index,
|
|
number_to_column(r.0 + column_index).unwrap(),
|
|
r.1 + row_index
|
|
);
|
|
|
|
let formula = get_formula_attribute(
|
|
worksheet.get_name(),
|
|
*row_index,
|
|
*column_index,
|
|
&parsed_formulas[*f as usize].0,
|
|
);
|
|
|
|
let b = i32::from(*v);
|
|
row_data_str.push(format!(
|
|
r#"<c r="{cell_name}" t="b" s="{style}" cm="1"><f t="array" ref="{range}">{formula}</f><v>{b}</v></c>"#
|
|
));
|
|
}
|
|
Cell::DynamicCellFormulaNumber { f, v, s, r, a: _ } => {
|
|
// <c r="C4" s="3" cm="1">
|
|
// <f t="array" ref="C4:C10">C4:C10</f>
|
|
// <v>123</v>
|
|
// </c>
|
|
let style = get_cell_style_attribute(*s);
|
|
let range = format!(
|
|
"{}{}:{}{}",
|
|
column_name,
|
|
row_index,
|
|
number_to_column(r.0 + column_index).unwrap(),
|
|
r.1 + row_index
|
|
);
|
|
|
|
let formula = get_formula_attribute(
|
|
worksheet.get_name(),
|
|
*row_index,
|
|
*column_index,
|
|
&parsed_formulas[*f as usize].0,
|
|
);
|
|
|
|
row_data_str.push(format!(
|
|
r#"<c r="{cell_name}" s="{style}" cm="1"><f t="array" ref="{range}">{formula}</f><v>{v}</v></c>"#
|
|
));
|
|
}
|
|
Cell::DynamicCellFormulaString { f, v, s, r, a: _ } => {
|
|
// <c r="C6" t="str" s="5" cm="1">
|
|
// <f t="array" ref="C6:C10">C6:C10</f>
|
|
// <v>Hello world!</v>
|
|
// </c>
|
|
let style = get_cell_style_attribute(*s);
|
|
let range = format!(
|
|
"{}{}:{}{}",
|
|
column_name,
|
|
row_index,
|
|
number_to_column(r.0 + column_index).unwrap(),
|
|
r.1 + row_index
|
|
);
|
|
|
|
let formula = get_formula_attribute(
|
|
worksheet.get_name(),
|
|
*row_index,
|
|
*column_index,
|
|
&parsed_formulas[*f as usize].0,
|
|
);
|
|
let escaped_v = escape_xml(v);
|
|
|
|
row_data_str.push(format!(
|
|
r#"<c r="{cell_name}" t="str" s="{style}" cm="1"><f t="array" ref="{range}">{formula}</f><v>{escaped_v}</v></c>"#
|
|
));
|
|
}
|
|
Cell::DynamicCellFormulaError {
|
|
f,
|
|
ei,
|
|
s,
|
|
o: _,
|
|
m: _,
|
|
r,
|
|
a: _,
|
|
} => {
|
|
// <c r="C6" t="e" s="4" cm="1">
|
|
// <f t="array" ref="C6:C10">C6:C10</f>
|
|
// <v>#DIV/0!</v>
|
|
// </c>
|
|
let style = get_cell_style_attribute(*s);
|
|
let range = format!(
|
|
"{}{}:{}{}",
|
|
column_name,
|
|
row_index,
|
|
number_to_column(r.0 + column_index).unwrap(),
|
|
r.1 + row_index
|
|
);
|
|
|
|
let formula = get_formula_attribute(
|
|
worksheet.get_name(),
|
|
*row_index,
|
|
*column_index,
|
|
&parsed_formulas[*f as usize].0,
|
|
);
|
|
|
|
row_data_str.push(format!(
|
|
r#"<c r="{cell_name}" t="e" s="{style}" cm="1"><f t="array" ref="{range}">{formula}</f><v>{ei}</v></c>"#
|
|
));
|
|
}
|
|
}
|
|
}
|
|
let row_style_str = match row_style_dict.get(row_index) {
|
|
Some(row_style) => {
|
|
let hidden_str = if row_style.hidden {
|
|
r#" hidden="1""#
|
|
} else {
|
|
""
|
|
};
|
|
format!(
|
|
r#" s="{}" ht="{}" customHeight="{}" customFormat="{}"{}"#,
|
|
row_style.s,
|
|
row_style.height,
|
|
i32::from(row_style.custom_height),
|
|
i32::from(row_style.custom_format),
|
|
hidden_str,
|
|
)
|
|
}
|
|
None => "".to_string(),
|
|
};
|
|
sheet_data_str.push(format!(
|
|
"<row r=\"{row_index}\"{row_style_str}>{}</row>",
|
|
row_data_str.join("")
|
|
))
|
|
}
|
|
let sheet_data = sheet_data_str.join("");
|
|
|
|
for merge_cell_ref in &worksheet.merge_cells {
|
|
merged_cells_str.push(format!("<mergeCell ref=\"{merge_cell_ref}\"/>"))
|
|
}
|
|
let merged_cells_count = merged_cells_str.len();
|
|
|
|
let cols = cols_str.join("");
|
|
let cols = if cols.is_empty() {
|
|
"".to_string()
|
|
} else {
|
|
format!("<cols>{cols}</cols>")
|
|
};
|
|
|
|
let tab_selected = if is_sheet_selected {
|
|
" tabSelected=\"1\""
|
|
} else {
|
|
""
|
|
};
|
|
|
|
let show_grid_lines = if !worksheet.show_grid_lines {
|
|
" showGridLines=\"0\""
|
|
} else {
|
|
""
|
|
};
|
|
|
|
let mut active_cell = "A1".to_string();
|
|
let mut sqref = "A1".to_string();
|
|
|
|
let views = &worksheet.views;
|
|
if let Some(view) = views.get(&0) {
|
|
let range = view.range;
|
|
let row = view.row;
|
|
let column = view.column;
|
|
let column_name = number_to_column(column).unwrap_or("A".to_string());
|
|
active_cell = format!("{column_name}{row}");
|
|
|
|
let column_start = number_to_column(range[1]).unwrap_or("A".to_string());
|
|
let column_end = number_to_column(range[3]).unwrap_or("A".to_string());
|
|
if range[0] == range[2] && range[1] == range[3] {
|
|
sqref = format!("{column_start}{}", range[0]);
|
|
} else {
|
|
sqref = format!("{}{}:{}{}", column_start, range[0], column_end, range[2]);
|
|
}
|
|
}
|
|
|
|
let merge_cells_section = if merged_cells_count > 0 {
|
|
format!(
|
|
"<mergeCells count=\"{}\">{}</mergeCells>",
|
|
merged_cells_count,
|
|
merged_cells_str.join("")
|
|
)
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
format!(
|
|
"{XML_DECLARATION}
|
|
<worksheet \
|
|
xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" \
|
|
xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
|
|
<dimension ref=\"{dimension}\"/>\
|
|
<sheetViews>\
|
|
<sheetView workbookViewId=\"0\"{show_grid_lines}{tab_selected}>\
|
|
<selection activeCell=\"{active_cell}\" sqref=\"{sqref}\"/>\
|
|
</sheetView>\
|
|
</sheetViews>\
|
|
{cols}\
|
|
<sheetData>\
|
|
{sheet_data}\
|
|
</sheetData>\
|
|
{merge_cells_section}\
|
|
</worksheet>"
|
|
)
|
|
}
|