diff --git a/README.md b/README.md
index 5723942..6115257 100644
--- a/README.md
+++ b/README.md
@@ -168,6 +168,17 @@ interface IRowData {
interface IDataCell{
v: string;
s: number:
+ formula?: IFormula;
+}
+
+// present when formulas:true; v keeps the original legacy behavior:
+// cells with formula text in XLSX use "=...", shared formula followers keep cached values
+interface IFormula {
+ type: "normal" | "shared";
+ role?: "master" | "follower";
+ si?: string;
+ ref?: string;
+ value?: string;
}
interface IStyle {
diff --git a/js/module.js b/js/module.js
index ef928fa..506b3a2 100644
--- a/js/module.js
+++ b/js/module.js
@@ -17,13 +17,13 @@ export async function convertArray(jsonData, config = {}) {
const getStyles = config.styles === undefined ? true : config.styles;
const xlsx = XLSX.new(jsonData);
const styles = getStyles ? xlsx.get_styles() : null;
+ const mode = 0 | (config.formulas ? XLSX.with_formulas() : 0);
let data;
if (config.sheet) {
- data = [xlsx.get_sheet_data(config.sheet)];
+ data = [xlsx.get_sheet_data(config.sheet, mode)];
} else {
const sheets = xlsx.get_sheets();
- const mode = 0 | (config.formulas ? XLSX.with_formulas() : 0);
data = sheets.map(name => xlsx.get_sheet_data(name, mode));
}
diff --git a/js/worker.js b/js/worker.js
index da81b33..ac60612 100644
--- a/js/worker.js
+++ b/js/worker.js
@@ -26,14 +26,14 @@ async function doConvert(input, config) {
const xlsx = XLSX.new(input);
const styles = getStyles ? xlsx.get_styles() : null;
+ const mode = 0 | (config.formulas ? XLSX.with_formulas() : 0);
let sheetsData;
if (config.sheet) {
- const data = xlsx.get_sheet_data(config.sheet);
+ const data = xlsx.get_sheet_data(config.sheet, mode);
sheetsData = [data];
} else {
const sheets = xlsx.get_sheets();
- const mode = 0 | (config.formulas ? XLSX.with_formulas() : 0);
sheetsData = sheets.map(name => xlsx.get_sheet_data(name, mode));
}
@@ -45,4 +45,4 @@ async function doConvert(input, config) {
});
}
-postMessage({ type:"init" });
\ No newline at end of file
+postMessage({ type:"init" });
diff --git a/public/worker.html b/public/worker.html
index 2d4db7d..393d050 100644
--- a/public/worker.html
+++ b/public/worker.html
@@ -51,7 +51,8 @@
Select an XLSX file for conversion
worker.then(x => {
x.postMessage({
type: "convert",
- data: this.files[0]
+ data: this.files[0],
+ formulas: true,
});
});
});
diff --git a/src/lib.rs b/src/lib.rs
index b10da26..5fcee9d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -95,6 +95,8 @@ pub struct Cell {
pub s: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub hyperlink: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub formula: Option,
}
impl Cell {
@@ -103,10 +105,25 @@ impl Cell {
v: None,
s: 0,
hyperlink: None,
+ formula: None,
}
}
}
+#[derive(Debug, Serialize, PartialEq)]
+pub struct CellFormula {
+ #[serde(rename = "type")]
+ pub formula_type: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub role: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub si: Option,
+ #[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
+ pub reference: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub value: Option,
+}
+
#[derive(Debug, Serialize)]
#[serde(tag = "type", content = "value")]
pub enum DataValidationSource {
@@ -267,7 +284,6 @@ impl XLSX {
.trim_end_matches(".xml");
let hyperlinks_map = Self::read_sheet_relationships(&mut self.zip, sheet_rel_path)?;
- dbg!(&hyperlinks_map);
let mut xml = match xml_reader(&mut self.zip, &path) {
None => {
return Err(XlsxError::FileNotFound(path))
@@ -285,6 +301,8 @@ impl XLSX {
let mut hyperlinks = HashMap::::new();
let mut current_cell_name: Option = None;
+ let mut current_formula_attrs: HashMap = HashMap::new();
+ let mut current_formula_text: Option = None;
loop {
buf.clear();
@@ -380,6 +398,8 @@ impl XLSX {
},
Ok(Event::Start(ref e)) if e.name().as_ref() == b"c" => {
info.use_shared_string_for_next = false;
+ current_formula_attrs.clear();
+ current_formula_text = None;
for a in e.attributes() {
let att = a.unwrap();
@@ -413,7 +433,7 @@ impl XLSX {
}
},
Ok(Event::End(ref e)) if e.name().as_ref() == b"c" => {
- let has_value = last_cell.v.is_some() || last_cell.s > 0;
+ let has_value = last_cell.v.is_some() || last_cell.s > 0 || last_cell.formula.is_some();
let has_hyperlink = current_cell_name
.as_ref()
.map(|name| hyperlinks.contains_key(name))
@@ -430,22 +450,36 @@ impl XLSX {
}
last_cell = Cell::new();
current_cell_name = None;
+ current_formula_attrs.clear();
+ current_formula_text = None;
},
Ok(Event::Start(ref e)) if e.name().as_ref() == b"f" => {
+ current_formula_attrs.clear();
+ current_formula_text = None;
+
+ for a in e.attributes().flatten() {
+ let key = String::from_utf8_lossy(a.key.as_ref()).into_owned();
+ let value = a.decode_and_unescape_value(&xml).unwrap().into_owned();
+ current_formula_attrs.insert(key, value);
+ }
+
mode = 1
}
Ok(Event::Start(ref e)) if e.name().as_ref() == b"v" => {
mode = 2
},
Ok(Event::Text(ref e)) if mode == 1 => {
+ let value = e.unescape().unwrap().to_string();
+ current_formula_text = Some(value.clone());
+
if flags & WITH_FORMULAS > 0 {
- let value = e.unescape().unwrap().to_string();
last_cell.v = Some("=".to_owned() + &value);
}
}
Ok(Event::Text(ref e)) if mode == 2 => {
+ let value = e.unescape().unwrap().to_string();
+
if last_cell.v.is_none(){
- let value = e.unescape().unwrap().to_string();
if info.use_shared_string_for_next {
let index: usize = value.parse().unwrap();
if self.shared_strings[index].len() > 0 {
@@ -457,6 +491,12 @@ impl XLSX {
}
},
Ok(Event::End(ref e)) if e.name().as_ref() == b"f" => {
+ if flags & WITH_FORMULAS > 0 {
+ last_cell.formula = build_cell_formula(
+ ¤t_formula_attrs,
+ current_formula_text.clone(),
+ );
+ }
mode = 0
}
Ok(Event::End(ref e)) if e.name().as_ref() == b"v" => {
@@ -1072,6 +1112,27 @@ fn xml_reader<'a>(zip: &'a mut ZipArchive>>, path: &str) -> Optio
}
}
+fn build_cell_formula(attrs: &HashMap, text: Option) -> Option {
+ if attrs.get("t").map(|v| v == "shared").unwrap_or(false) {
+ let role = if text.is_some() { "master" } else { "follower" };
+ return Some(CellFormula {
+ formula_type: String::from("shared"),
+ role: Some(String::from(role)),
+ si: attrs.get("si").cloned(),
+ reference: attrs.get("ref").cloned(),
+ value: text,
+ });
+ }
+
+ text.map(|value| CellFormula {
+ formula_type: String::from("normal"),
+ role: None,
+ si: None,
+ reference: None,
+ value: Some(value),
+ })
+}
+
fn get_xlsx_rgb(argb: String) -> String {
let raw_a = u8::from_str_radix(&argb[..2], 16).unwrap();
let a = (raw_a as f32 / 255f32).to_string();
@@ -1196,7 +1257,7 @@ mod tests {
let now = std::time::Instant::now();
{
- let mut file = std::fs::File::open("./example/Test.xlsx").unwrap();
+ let mut file = std::fs::File::open("./example/file_example_styles.xlsx").unwrap();
let mut buf = vec!();
file.read_to_end(&mut buf).unwrap();
let mut xlsx = XLSX::new(buf);
@@ -1216,4 +1277,70 @@ mod tests {
assert_eq!(cell_index_to_offsets(String::from("AB1")), (27, 0));
assert_eq!(cell_index_to_offsets(String::from("ZZ100")), (701, 99));
}
+
+ #[test]
+ fn reads_shared_formula_metadata() {
+ use std::io::Read;
+
+ let mut file = std::fs::File::open("./example/file_example_styles.xlsx").unwrap();
+ let mut buf = vec!();
+ file.read_to_end(&mut buf).unwrap();
+
+ let mut xlsx = XLSX::new(buf);
+ let (name, path) = xlsx.sheets[0].clone();
+ let data = xlsx.read_sheet(path, name, WITH_FORMULAS).unwrap();
+
+ let master = data.cells[2][4].as_ref().unwrap();
+ assert_eq!(master.v.as_deref(), Some("=SUM(C3:D3)"));
+ assert_eq!(master.formula.as_ref().unwrap().formula_type, "shared");
+ assert_eq!(master.formula.as_ref().unwrap().role.as_deref(), Some("master"));
+ assert_eq!(master.formula.as_ref().unwrap().si.as_deref(), Some("0"));
+ assert_eq!(master.formula.as_ref().unwrap().reference.as_deref(), Some("E3:E7"));
+ assert_eq!(master.formula.as_ref().unwrap().value.as_deref(), Some("SUM(C3:D3)"));
+
+ let follower = data.cells[3][4].as_ref().unwrap();
+ assert_eq!(follower.v.as_deref(), Some("=SUM(C4:D4)"));
+ assert_eq!(follower.formula.as_ref().unwrap().formula_type, "shared");
+ assert_eq!(follower.formula.as_ref().unwrap().role.as_deref(), Some("follower"));
+ assert_eq!(follower.formula.as_ref().unwrap().si.as_deref(), Some("0"));
+ assert_eq!(follower.formula.as_ref().unwrap().reference, None);
+ assert_eq!(follower.formula.as_ref().unwrap().value, None);
+ }
+
+ #[test]
+ fn reads_normal_formula_metadata() {
+ use std::io::Read;
+
+ let mut file = std::fs::File::open("./example/formats.xlsx").unwrap();
+ let mut buf = vec!();
+ file.read_to_end(&mut buf).unwrap();
+
+ let mut xlsx = XLSX::new(buf);
+ let (name, path) = xlsx.sheets[0].clone();
+ let data = xlsx.read_sheet(path, name, WITH_FORMULAS).unwrap();
+
+ let cell = data.cells[1][2].as_ref().unwrap();
+ assert_eq!(cell.v.as_deref(), Some("=A2/B2"));
+ assert_eq!(cell.formula.as_ref().unwrap().formula_type, "normal");
+ assert_eq!(cell.formula.as_ref().unwrap().value.as_deref(), Some("A2/B2"));
+ }
+
+ #[test]
+ fn formulas_are_omitted_without_formula_flag() {
+ use std::io::Read;
+
+ let mut file = std::fs::File::open("./example/file_example_styles.xlsx").unwrap();
+ let mut buf = vec!();
+ file.read_to_end(&mut buf).unwrap();
+
+ let mut xlsx = XLSX::new(buf);
+ let (name, path) = xlsx.sheets[0].clone();
+ let data = xlsx.read_sheet(path, name, 0).unwrap();
+
+ assert_eq!(data.cells[2][4].as_ref().unwrap().v.as_deref(), Some("240400"));
+ assert_eq!(data.cells[3][4].as_ref().unwrap().v.as_deref(), Some("204200"));
+ assert_eq!(data.cells[4][4].as_ref().unwrap().v.as_deref(), Some("76900"));
+ assert!(data.cells[2][4].as_ref().unwrap().formula.is_none());
+ assert!(data.cells[3][4].as_ref().unwrap().formula.is_none());
+ }
}