UPDATE: Dump of initial files

This commit is contained in:
Nicolás Hatcher
2023-11-18 21:26:18 +01:00
commit c5b8efd83d
279 changed files with 42654 additions and 0 deletions

6
xlsx/src/export/_rels.rs Normal file
View File

@@ -0,0 +1,6 @@
use ironcalc_base::types::Workbook;
pub(crate) fn get_dot_rels(_: &Workbook) -> String {
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#.to_owned()
}

View File

@@ -0,0 +1,69 @@
use chrono::NaiveDateTime;
use ironcalc_base::{
new_empty::{APPLICATION, APP_VERSION, IRONCALC_USER},
types::Workbook,
};
use crate::error::XlsxError;
// Application-Defined File Properties part
pub(crate) fn get_app_xml(_: &Workbook) -> String {
// contains application name and version
// The next few are not needed:
// security. It is password protected (not implemented)
// Scale
// Titles of parts
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\" \
xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\
<Application>{}</Application>\
<AppVersion>{}</AppVersion>\
</Properties>",
APPLICATION, APP_VERSION
)
}
// Core File Properties part
pub(crate) fn get_core_xml(workbook: &Workbook, milliseconds: i64) -> Result<String, XlsxError> {
// contains the name of the creator, last modified and date
let metadata = &workbook.metadata;
let creator = metadata.creator.to_string();
let last_modified_by = IRONCALC_USER.to_string();
let created = metadata.created.to_string();
// FIXME add now
let seconds = milliseconds / 1000;
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
Some(s) => s,
None => {
return Err(XlsxError::Xml(format!(
"Invalid timestamp: {}",
milliseconds
)))
}
};
let last_modified = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
Ok(format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
<cp:coreProperties \
xmlns:cp=\"http://schemas.openxmlformats.org/package/2006/metadata/core-properties\" \
xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" \
xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\" \
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> \
<dc:title></dc:title><dc:subject></dc:subject>\
<dc:creator>{}</dc:creator>\
<cp:keywords></cp:keywords>\
<dc:description></dc:description>\
<cp:lastModifiedBy>{}</cp:lastModifiedBy>\
<cp:revision></cp:revision>\
<dcterms:created xsi:type=\"dcterms:W3CDTF\">{}</dcterms:created>\
<dcterms:modified xsi:type=\"dcterms:W3CDTF\">{}</dcterms:modified>\
<cp:category></cp:category>\
<cp:contentStatus></cp:contentStatus>\
</cp:coreProperties>",
creator, last_modified_by, created, last_modified
))
}

99
xlsx/src/export/escape.rs Normal file
View File

@@ -0,0 +1,99 @@
// Taken from :
// https://docs.rs/xml-rs/latest/src/xml/escape.rs.html#1-125
//! Contains functions for performing XML special characters escaping.
use std::borrow::Cow;
enum Value {
Char(char),
Str(&'static str),
}
fn escape_char(c: char) -> Value {
match c {
'<' => Value::Str("&lt;"),
'>' => Value::Str("&gt;"),
'"' => Value::Str("&quot;"),
'\'' => Value::Str("&apos;"),
'&' => Value::Str("&amp;"),
'\n' => Value::Str("&#xA;"),
'\r' => Value::Str("&#xD;"),
_ => Value::Char(c),
}
}
enum Process<'a> {
Borrowed(&'a str),
Owned(String),
}
impl<'a> Process<'a> {
fn process(&mut self, (i, next): (usize, Value)) {
match next {
Value::Str(s) => match *self {
Process::Owned(ref mut o) => o.push_str(s),
Process::Borrowed(b) => {
let mut r = String::with_capacity(b.len() + s.len());
r.push_str(&b[..i]);
r.push_str(s);
*self = Process::Owned(r);
}
},
Value::Char(c) => match *self {
Process::Borrowed(_) => {}
Process::Owned(ref mut o) => o.push(c),
},
}
}
fn into_result(self) -> Cow<'a, str> {
match self {
Process::Borrowed(b) => Cow::Borrowed(b),
Process::Owned(o) => Cow::Owned(o),
}
}
}
impl<'a> Extend<(usize, Value)> for Process<'a> {
fn extend<I: IntoIterator<Item = (usize, Value)>>(&mut self, it: I) {
for v in it.into_iter() {
self.process(v);
}
}
}
/// Performs escaping of common XML characters inside an attribute value.
///
/// This function replaces several important markup characters with their
/// entity equivalents:
///
/// * `<` → `&lt;`
/// * `>` → `&gt;`
/// * `"` → `&quot;`
/// * `'` → `&apos;`
/// * `&` → `&amp;`
///
/// The resulting string is safe to use inside XML attribute values.
///
/// Does not perform allocations if the given string does not contain escapable characters.
pub fn escape_xml(s: &str) -> Cow<str> {
let mut p = Process::Borrowed(s);
p.extend(s.char_indices().map(|(ind, c)| (ind, escape_char(c))));
p.into_result()
}
// A simpler function that allocates memory for each replacement
// fn escape_xml(value: &str) -> String {
// value
// .replace('&', "&amp")
// .replace('<', "&lt;")
// .replace('>', "&gt;")
// .replace('"', "&quot;")
// .replace('\'', "&apos;")
// }
// See also:
// https://docs.rs/shell-escape/0.1.5/src/shell_escape/lib.rs.html#17-23
// https://aaronerhardt.github.io/docs/relm4/src/quick_xml/escapei.rs.html#69-106

138
xlsx/src/export/mod.rs Normal file
View File

@@ -0,0 +1,138 @@
mod _rels;
mod doc_props;
mod escape;
mod shared_strings;
mod styles;
mod workbook;
mod workbook_xml_rels;
mod worksheets;
mod xml_constants;
use std::io::BufWriter;
use std::{
fs,
io::{Seek, Write},
};
use ironcalc_base::expressions::utils::number_to_column;
use ironcalc_base::model::{get_milliseconds_since_epoch, Model};
use ironcalc_base::types::Workbook;
use self::xml_constants::XML_DECLARATION;
use crate::error::XlsxError;
#[cfg(test)]
mod test;
fn get_content_types_xml(workbook: &Workbook) -> String {
// A list of all files in the zip
let mut content = vec![
r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">"#.to_string(),
r#"<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>"#.to_string(),
r#"<Default Extension="xml" ContentType="application/xml"/>"#.to_string(),
r#"<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>"#.to_string(),
];
for worksheet in 0..workbook.worksheets.len() {
let sheet = format!(
r#"<Override PartName="/xl/worksheets/sheet{}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>"#,
worksheet + 1
);
content.push(sheet);
}
// we skip the theme and calcChain
// r#"<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>"#,
// r#"<Override PartName="/xl/calcChain.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"/>"#,
content.extend([
r#"<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>"#.to_string(),
r#"<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>"#.to_string(),
r#"<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>"#.to_string(),
r#"<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>"#.to_string(),
r#"</Types>"#.to_string(),
]);
format!("{XML_DECLARATION}\n{}", content.join(""))
}
/// Exports a model to an xlsx file
pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> {
let file_path = std::path::Path::new(&file_name);
if file_path.exists() {
return Err(XlsxError::IO(format!("file {} already exists", file_name)));
}
let file = fs::File::create(file_path).unwrap();
let writer = BufWriter::new(file);
save_xlsx_to_writer(model, writer)?;
Ok(())
}
pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<W, XlsxError> {
let workbook = &model.workbook;
let mut zip = zip::ZipWriter::new(writer);
let options =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
// root folder
zip.start_file("[Content_Types].xml", options)?;
zip.write_all(get_content_types_xml(workbook).as_bytes())?;
zip.add_directory("docProps", options)?;
zip.start_file("docProps/app.xml", options)?;
zip.write_all(doc_props::get_app_xml(workbook).as_bytes())?;
zip.start_file("docProps/core.xml", options)?;
let milliseconds = get_milliseconds_since_epoch();
zip.write_all(doc_props::get_core_xml(workbook, milliseconds)?.as_bytes())?;
// Package-relationship item
zip.add_directory("_rels", options)?;
zip.start_file("_rels/.rels", options)?;
zip.write_all(_rels::get_dot_rels(workbook).as_bytes())?;
zip.add_directory("xl", options)?;
zip.start_file("xl/sharedStrings.xml", options)?;
zip.write_all(shared_strings::get_shared_strings_xml(workbook).as_bytes())?;
zip.start_file("xl/styles.xml", options)?;
zip.write_all(styles::get_styles_xml(workbook).as_bytes())?;
zip.start_file("xl/workbook.xml", options)?;
zip.write_all(workbook::get_workbook_xml(workbook).as_bytes())?;
zip.add_directory("xl/_rels", options)?;
zip.start_file("xl/_rels/workbook.xml.rels", options)?;
zip.write_all(workbook_xml_rels::get_workbook_xml_rels(workbook).as_bytes())?;
zip.add_directory("xl/worksheets", options)?;
for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() {
let id = sheet_index + 1;
zip.start_file(&format!("xl/worksheets/sheet{id}.xml"), options)?;
let dimension = model
.workbook
.worksheet(sheet_index as u32)
.unwrap()
.dimension();
let column_min_str = number_to_column(dimension.min_column).unwrap();
let column_max_str = number_to_column(dimension.max_column).unwrap();
let min_row = dimension.min_row;
let max_row = dimension.max_row;
let sheet_dimension_str = &format!("{column_min_str}{min_row}:{column_max_str}{max_row}");
zip.write_all(
worksheets::get_worksheet_xml(
worksheet,
&model.parsed_formulas[sheet_index],
sheet_dimension_str,
)
.as_bytes(),
)?;
}
let writer = zip.finish()?;
Ok(writer)
}
/// Exports an internal representation of a workbook into an equivalent IronCalc json format
pub fn save_to_json(workbook: Workbook, output: &str) {
let s = serde_json::to_string(&workbook).unwrap();
let file_path = std::path::Path::new(output);
let mut file = fs::File::create(file_path).unwrap();
file.write_all(s.as_bytes()).unwrap();
}

View File

@@ -0,0 +1,16 @@
use ironcalc_base::types::Workbook;
use super::{escape::escape_xml, xml_constants::XML_DECLARATION};
pub(crate) fn get_shared_strings_xml(model: &Workbook) -> String {
let mut shared_strings: Vec<String> = vec![];
let count = &model.shared_strings.len();
let unique_count = &model.shared_strings.len();
for shared_string in &model.shared_strings {
shared_strings.push(format!("<si><t>{}</t></si>", escape_xml(shared_string)));
}
format!("{}\n\
<sst xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" count=\"{count}\" uniqueCount=\"{unique_count}\">\
{}\
</sst>", XML_DECLARATION, shared_strings.join(""))
}

282
xlsx/src/export/styles.rs Normal file
View File

@@ -0,0 +1,282 @@
use ironcalc_base::types::{
Alignment, BorderItem, HorizontalAlignment, Styles, VerticalAlignment, Workbook,
};
use super::{escape::escape_xml, xml_constants::XML_DECLARATION};
fn get_fonts_xml(styles: &Styles) -> String {
let fonts = &styles.fonts;
let mut fonts_str: Vec<String> = vec![];
for font in fonts {
let size = format!("<sz val=\"{}\"/>", font.sz);
let color = if let Some(some_color) = &font.color {
format!("<color rgb=\"FF{}\"/>", some_color.trim_start_matches('#'))
} else {
"".to_string()
};
let name = format!("<name val=\"{}\"/>", escape_xml(&font.name));
let bold = if font.b { "<b/>" } else { "" };
let italic = if font.i { "<i/>" } else { "" };
let underline = if font.u { "<u/>" } else { "" };
let strike = if font.strike { "<strike/>" } else { "" };
let family = format!("<family val=\"{}\"/>", font.family);
let scheme = format!("<scheme val=\"{}\"/>", font.scheme);
fonts_str.push(format!(
"<font>\
{size}\
{color}\
{name}\
{bold}\
{italic}\
{underline}\
{strike}\
{family}\
{scheme}\
</font>"
));
}
let font_count = fonts.len();
format!(
"<fonts count=\"{font_count}\">{}</fonts>",
fonts_str.join("")
)
}
fn get_color_xml(color: &Option<String>, name: &str) -> String {
// We blindly append FF at the beginning of these RGB color to make it ARGB
if let Some(some_color) = color {
format!("<{name} rgb=\"FF{}\"/>", some_color.trim_start_matches('#'))
} else {
"".to_string()
}
}
fn get_fills_xml(styles: &Styles) -> String {
let fills = &styles.fills;
let mut fills_str: Vec<String> = vec![];
for fill in fills {
let pattern_type = &fill.pattern_type;
let fg_color = get_color_xml(&fill.fg_color, "fgColor");
let bg_color = get_color_xml(&fill.bg_color, "bgColor");
fills_str.push(format!(
"<fill><patternFill patternType=\"{pattern_type}\">{fg_color}{bg_color}</patternFill></fill>"
));
}
let fill_count = fills.len();
format!(
"<fills count=\"{fill_count}\">{}</fills>",
fills_str.join("")
)
}
fn get_border_xml(border: &Option<BorderItem>, name: &str) -> String {
if let Some(border_item) = border {
let color = get_color_xml(&border_item.color, "color");
return format!("<{name} style=\"{}\">{color}</{name}>", border_item.style);
}
format!("<{name}/>")
}
fn get_borders_xml(styles: &Styles) -> String {
let borders = &styles.borders;
let mut borders_str: Vec<String> = vec![];
let border_count = borders.len();
for border in borders {
// TODO: diagonal_up/diagonal_down?
let border_left = get_border_xml(&border.left, "left");
let border_right = get_border_xml(&border.right, "right");
let border_top = get_border_xml(&border.top, "top");
let border_bottom = get_border_xml(&border.bottom, "bottom");
let border_diagonal = get_border_xml(&border.diagonal, "diagonal");
borders_str.push(format!(
"<border>{border_left}{border_right}{border_top}{border_bottom}{border_diagonal}</border>"
));
}
format!(
"<borders count=\"{border_count}\">{}</borders>",
borders_str.join("")
)
}
// <numFmts count="1">
// <numFmt numFmtId="164" formatCode="##,#00;[Blue]\-\-#,##0"/>
// </numFmts>
fn get_cell_number_formats_xml(styles: &Styles) -> String {
let num_fmts = &styles.num_fmts;
let mut num_fmts_str: Vec<String> = vec![];
let num_fmt_count = num_fmts.len();
for num_fmt in num_fmts {
let num_fmt_id = num_fmt.num_fmt_id;
let format_code = &num_fmt.format_code;
let format_code = escape_xml(format_code);
num_fmts_str.push(format!(
"<numFmt numFmtId=\"{num_fmt_id}\" formatCode=\"{format_code}\"/>"
));
}
if num_fmt_count == 0 {
return "".to_string();
}
format!(
"<numFmts count=\"{num_fmt_count}\">{}</numFmts>",
num_fmts_str.join("")
)
}
fn get_alignment(alignment: &Alignment) -> String {
let wrap_text = if alignment.wrap_text {
" wrapText=\"1\""
} else {
""
};
let horizontal = if alignment.horizontal != HorizontalAlignment::default() {
format!(" horizontal=\"{}\"", alignment.horizontal)
} else {
"".to_string()
};
let vertical = if alignment.vertical != VerticalAlignment::default() {
format!(" vertical=\"{}\"", alignment.vertical)
} else {
"".to_string()
};
format!("<alignment{wrap_text}{horizontal}{vertical}/>")
}
fn get_cell_style_xfs_xml(styles: &Styles) -> String {
let cell_style_xfs = &styles.cell_style_xfs;
let mut cell_style_str: Vec<String> = vec![];
for cell_style_xf in cell_style_xfs {
let border_id = cell_style_xf.border_id;
let fill_id = cell_style_xf.fill_id;
let font_id = cell_style_xf.font_id;
let num_fmt_id = cell_style_xf.num_fmt_id;
let apply_alignment_str = if cell_style_xf.apply_alignment {
r#" applyAlignment="1""#
} else {
""
};
let apply_font_str = if cell_style_xf.apply_font {
r#" applyFont="1""#
} else {
""
};
let apply_fill_str = if cell_style_xf.apply_fill {
r#" applyFill="1""#
} else {
""
};
cell_style_str.push(format!(
"<xf \
borderId=\"{border_id}\" \
fillId=\"{fill_id}\" \
fontId=\"{font_id}\" \
numFmtId=\"{num_fmt_id}\"\
{apply_alignment_str}\
{apply_font_str}\
{apply_fill_str}/>"
));
}
let style_count = cell_style_xfs.len();
format!(
"<cellStyleXfs count=\"{style_count}\">{}</cellStyleXfs>",
cell_style_str.join("")
)
}
fn get_cell_xfs_xml(styles: &Styles) -> String {
let cell_xfs = &styles.cell_xfs;
let mut cell_xfs_str: Vec<String> = vec![];
for cell_xf in cell_xfs {
let xf_id = cell_xf.xf_id;
let border_id = cell_xf.border_id;
let fill_id = cell_xf.fill_id;
let font_id = cell_xf.font_id;
let num_fmt_id = cell_xf.num_fmt_id;
let quote_prefix_str = if cell_xf.quote_prefix {
r#" quotePrefix="1""#
} else {
""
};
let apply_alignment_str = if cell_xf.apply_alignment {
r#" applyAlignment="1""#
} else {
""
};
let apply_font_str = if cell_xf.apply_font {
r#" applyFont="1""#
} else {
""
};
let apply_fill_str = if cell_xf.apply_fill {
r#" applyFill="1""#
} else {
""
};
let properties = format!(
"xfId=\"{xf_id}\" \
borderId=\"{border_id}\" \
fillId=\"{fill_id}\" \
fontId=\"{font_id}\" \
numFmtId=\"{num_fmt_id}\"\
{quote_prefix_str}\
{apply_alignment_str}\
{apply_font_str}\
{apply_fill_str}"
);
if let Some(alignment) = &cell_xf.alignment {
let alignment = get_alignment(alignment);
cell_xfs_str.push(format!("<xf {properties}>{alignment}</xf>"));
} else {
cell_xfs_str.push(format!("<xf {properties}/>"));
}
}
let style_count = cell_xfs.len();
format!(
"<cellXfs count=\"{style_count}\">{}</cellXfs>",
cell_xfs_str.join("")
)
}
// <cellStyle xfId="0" name="Normal" builtinId="0"/>
fn get_cell_styles_xml(styles: &Styles) -> String {
let cell_styles = &styles.cell_styles;
let mut cell_styles_str: Vec<String> = vec![];
for cell_style in cell_styles {
let xf_id = cell_style.xf_id;
let name = &cell_style.name;
let name = escape_xml(name);
let builtin_id = cell_style.builtin_id;
cell_styles_str.push(format!(
"<cellStyle xfId=\"{xf_id}\" name=\"{name}\" builtinId=\"{builtin_id}\"/>"
));
}
let style_count = cell_styles.len();
format!(
"<cellStyles count=\"{style_count}\">{}</cellStyles>",
cell_styles_str.join("")
)
}
pub(crate) fn get_styles_xml(model: &Workbook) -> String {
let styles = &model.styles;
let fonts = get_fonts_xml(styles);
let fills = get_fills_xml(styles);
let borders = get_borders_xml(styles);
let number_formats = get_cell_number_formats_xml(styles);
let cell_style_xfs = get_cell_style_xfs_xml(styles);
let cell_xfs = get_cell_xfs_xml(styles);
let cell_styles = get_cell_styles_xml(styles);
format!(
"{XML_DECLARATION}
<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\
{number_formats}\
{fonts}\
{fills}\
{borders}\
{cell_style_xfs}\
{cell_xfs}\
{cell_styles}\
<dxfs count=\"0\"/>\
</styleSheet>"
)
}

View File

@@ -0,0 +1,2 @@
mod test_escape;
mod test_export;

View File

@@ -0,0 +1,25 @@
use crate::export::escape::escape_xml;
#[test]
fn test_escape_xml() {
assert_eq!(escape_xml("all good"), "all good");
assert_eq!(escape_xml("3 < 4"), "3 &lt; 4");
assert_eq!(escape_xml("3 > 4"), "3 &gt; 4");
assert_eq!(escape_xml("3 & 4"), "3 &amp; 4");
assert_eq!(escape_xml("3 && 4"), "3 &amp;&amp; 4");
assert_eq!(escape_xml("3 \"literal\" 4"), "3 &quot;literal&quot; 4");
assert_eq!(
escape_xml("I don't 'know'"),
"I don&apos;t &apos;know&apos;"
);
assert_eq!(
escape_xml("This is <>&\"' say"),
"This is &lt;&gt;&amp;&quot;&apos; say"
);
}
// '&' => "&amp;"
// '<' "&lt;")
// '>' => "&gt;"
// '"' => "&quot;"
// '\'' => "&apos;"

View File

@@ -0,0 +1,134 @@
use std::fs;
use ironcalc_base::model::Model;
use crate::error::XlsxError;
use crate::{export::save_to_xlsx, import::load_model_from_xlsx};
pub fn new_empty_model() -> Model {
Model::new_empty("model", "en", "UTC").unwrap()
}
#[test]
fn test_values() {
let mut model = new_empty_model();
// numbers
model.set_user_input(0, 1, 1, "123.456".to_string());
// strings
model.set_user_input(0, 2, 1, "Hello world!".to_string());
model.set_user_input(0, 3, 1, "Hello world!".to_string());
model.set_user_input(0, 4, 1, "你好世界!".to_string());
// booleans
model.set_user_input(0, 5, 1, "TRUE".to_string());
model.set_user_input(0, 6, 1, "FALSE".to_string());
// errors
model.set_user_input(0, 7, 1, "#VALUE!".to_string());
// noop
model.evaluate();
let temp_file_name = "temp_file_test_values.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap();
assert_eq!(model.formatted_cell_value(0, 1, 1).unwrap(), "123.456");
assert_eq!(model.formatted_cell_value(0, 2, 1).unwrap(), "Hello world!");
assert_eq!(model.formatted_cell_value(0, 3, 1).unwrap(), "Hello world!");
assert_eq!(model.formatted_cell_value(0, 4, 1).unwrap(), "你好世界!");
assert_eq!(model.formatted_cell_value(0, 5, 1).unwrap(), "TRUE");
assert_eq!(model.formatted_cell_value(0, 6, 1).unwrap(), "FALSE");
assert_eq!(model.formatted_cell_value(0, 7, 1).unwrap(), "#VALUE!");
fs::remove_file(temp_file_name).unwrap();
}
#[test]
fn test_formulas() {
let mut model = new_empty_model();
model.set_user_input(0, 1, 1, "5.5".to_string());
model.set_user_input(0, 2, 1, "6.5".to_string());
model.set_user_input(0, 3, 1, "7.5".to_string());
model.set_user_input(0, 1, 2, "=A1*2".to_string());
model.set_user_input(0, 2, 2, "=A2*2".to_string());
model.set_user_input(0, 3, 2, "=A3*2".to_string());
model.set_user_input(0, 4, 2, "=SUM(A1:B3)".to_string());
model.evaluate();
let temp_file_name = "temp_file_test_formulas.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap();
assert_eq!(model.formatted_cell_value(0, 1, 2).unwrap(), "11");
assert_eq!(model.formatted_cell_value(0, 2, 2).unwrap(), "13");
assert_eq!(model.formatted_cell_value(0, 3, 2).unwrap(), "15");
assert_eq!(model.formatted_cell_value(0, 4, 2).unwrap(), "58.5");
fs::remove_file(temp_file_name).unwrap();
}
#[test]
fn test_sheets() {
let mut model = new_empty_model();
model.add_sheet("With space").unwrap();
// xml escaped
model.add_sheet("Tango & Cash").unwrap();
model.add_sheet("你好世界").unwrap();
// noop
model.evaluate();
let temp_file_name = "temp_file_test_sheets.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap();
assert_eq!(
model.workbook.get_worksheet_names(),
vec!["Sheet1", "With space", "Tango & Cash", "你好世界"]
);
fs::remove_file(temp_file_name).unwrap();
}
#[test]
fn test_named_styles() {
let mut model = new_empty_model();
model.set_user_input(0, 1, 1, "5.5".to_string());
let mut style = model.get_style_for_cell(0, 1, 1);
style.font.b = true;
style.font.i = true;
assert!(model.set_cell_style(0, 1, 1, &style).is_ok());
let bold_style_index = model.get_cell_style_index(0, 1, 1);
let e = model
.workbook
.styles
.add_named_cell_style("bold & italics", bold_style_index);
assert!(e.is_ok());
// noop
model.evaluate();
let temp_file_name = "temp_file_test_named_styles.xlsx";
save_to_xlsx(&model, temp_file_name).unwrap();
let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap();
assert!(model
.workbook
.styles
.get_style_index_by_name("bold & italics")
.is_ok());
fs::remove_file(temp_file_name).unwrap();
}
#[test]
fn test_existing_file() {
let file_name = "existing_file.xlsx";
fs::File::create(file_name).unwrap();
assert_eq!(
save_to_xlsx(&new_empty_model(), file_name),
Err(XlsxError::IO(
"file existing_file.xlsx already exists".to_string()
)),
);
fs::remove_file(file_name).unwrap();
}

View File

@@ -0,0 +1,91 @@
//! <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
//! A workbook is composed of workbook-level properties and a collection of 1 or more sheets.
//! The workbook part and corresponding properties comprise data
//! used to set application and workbook-level operational state. The workbook also serves to bind all the sheets
//! and child elements into an organized single file. The workbook XML attributes and elements include information
//! about what application last saved the file, where and how the windows of the workbook were positioned, and
//! an enumeration of the worksheets in the workbook.
//! This is the XML for the smallest possible (blank) workbook:
//!
//! <workbook>
//! <sheets>
//! <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
//! </sheets>
//! </workbook>
//!
//! Note that this workbook has a single sheet, named Sheet1. An Id for the sheet is required, and a relationship Id
//! pointing to the location of the sheet definition is also required.
//!
//!
//!
//! The most important objet of this part is a collection of all the sheets and all the defined names
//! of the workbook.
//!
//! It also may hold state properties like the selected tab
//! # bookViews
//!
use std::collections::HashMap;
use ironcalc_base::types::{SheetState, Workbook};
use super::escape::escape_xml;
use super::xml_constants::XML_DECLARATION;
pub(crate) fn get_workbook_xml(workbook: &Workbook) -> String {
// sheets
// <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
let mut sheets_str: Vec<String> = vec![];
let mut sheet_id_to_sheet_index: HashMap<u32, u32> = HashMap::new();
for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() {
let name = &worksheet.name;
let name = escape_xml(name);
let sheet_id = worksheet.sheet_id;
let state_str = match &worksheet.state {
SheetState::Visible => "",
SheetState::Hidden => " state=\"hidden\"",
SheetState::VeryHidden => " state=\"veryHidden\"",
};
sheets_str.push(format!(
"<sheet name=\"{name}\" sheetId=\"{sheet_id}\" r:id=\"rId{}\"{state_str}/>",
sheet_index + 1
));
sheet_id_to_sheet_index.insert(sheet_id, sheet_index as u32);
}
// defined names
// <definedName localSheetId="4" name="answer">shared!$G$5</definedName>
// <definedName name="numbers">Sheet1!$A$16:$A$18</definedName>
let mut defined_names_str: Vec<String> = vec![];
for defined_name in &workbook.defined_names {
let name = &defined_name.name;
let name = escape_xml(name);
let local_sheet_id = if let Some(sheet_id) = defined_name.sheet_id {
// In Excel the localSheetId is actually the index of the sheet.
let excel_local_sheet_id = sheet_id_to_sheet_index.get(&sheet_id).unwrap();
format!(" localSheetId=\"{excel_local_sheet_id}\"")
} else {
"".to_string()
};
let formula = escape_xml(&defined_name.formula);
defined_names_str.push(format!(
"<definedName name=\"{name}\"{local_sheet_id}>{formula}</definedName>"
))
}
let sheets = sheets_str.join("");
let defined_names = defined_names_str.join("");
format!("{XML_DECLARATION}\n\
<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
<sheets>\
{sheets}\
</sheets>\
<definedNames>\
{defined_names}\
</definedNames>\
<calcPr/>\
</workbook>")
}

View File

@@ -0,0 +1,25 @@
use ironcalc_base::types::Workbook;
use super::xml_constants::{XML_DECLARATION, XML_WORKSHEET};
pub(crate) fn get_workbook_xml_rels(workbook: &Workbook) -> String {
let mut relationships_str: Vec<String> = vec![];
let worksheet_count = workbook.worksheets.len() + 1;
for id in 1..worksheet_count {
relationships_str.push(format!(
"<Relationship Id=\"rId{id}\" Type=\"{XML_WORKSHEET}\" Target=\"worksheets/sheet{id}.xml\"/>"
));
}
let mut id = worksheet_count;
relationships_str.push(
format!("<Relationship Id=\"rId{id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>")
);
id += 1;
relationships_str.push(
format!("<Relationship Id=\"rId{id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\" Target=\"sharedStrings.xml\"/>")
);
format!(
"{XML_DECLARATION}\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">{}</Relationships>",
relationships_str.join("")
)
}

View File

@@ -0,0 +1,267 @@
//! # 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::{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],
dimension: &str,
) -> String {
let mut sheet_data_str: Vec<String> = vec![];
let mut cols_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::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::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::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],
);
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],
);
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],
);
let style = get_cell_style_attribute(*s);
row_data_str.push(format!(
"<c r=\"{cell_name}\" t=\"str\"{style}><f>{formula}</f><v>{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],
);
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>"
));
}
}
}
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("");
let cols = cols_str.join("");
let cols = if cols.is_empty() {
"".to_string()
} else {
format!("<cols>{cols}</cols>")
};
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\">\
<selection activeCell=\"A1\" sqref=\"A1\"/>\
</sheetView>\
</sheetViews>\
{cols}\
<sheetData>\
{sheet_data}\
</sheetData>\
</worksheet>"
)
}

View File

@@ -0,0 +1,5 @@
pub(crate) const XML_DECLARATION: &str =
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#;
pub(crate) const XML_WORKSHEET: &str =
r#"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"#;