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()); + } }