Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions js/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
6 changes: 3 additions & 3 deletions js/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand All @@ -45,4 +45,4 @@ async function doConvert(input, config) {
});
}

postMessage({ type:"init" });
postMessage({ type:"init" });
3 changes: 2 additions & 1 deletion public/worker.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ <h3>Select an XLSX file for conversion</h3>
worker.then(x => {
x.postMessage({
type: "convert",
data: this.files[0]
data: this.files[0],
formulas: true,
});
});
});
Expand Down
137 changes: 132 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ pub struct Cell {
pub s: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub hyperlink: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub formula: Option<CellFormula>,
}

impl Cell {
Expand All @@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub si: Option<String>,
#[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
pub reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
}

#[derive(Debug, Serialize)]
#[serde(tag = "type", content = "value")]
pub enum DataValidationSource {
Expand Down Expand Up @@ -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))
Expand All @@ -285,6 +301,8 @@ impl XLSX {

let mut hyperlinks = HashMap::<String, String>::new();
let mut current_cell_name: Option<String> = None;
let mut current_formula_attrs: HashMap<String, String> = HashMap::new();
let mut current_formula_text: Option<String> = None;

loop {
buf.clear();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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))
Expand All @@ -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 {
Expand All @@ -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(
&current_formula_attrs,
current_formula_text.clone(),
);
}
mode = 0
}
Ok(Event::End(ref e)) if e.name().as_ref() == b"v" => {
Expand Down Expand Up @@ -1072,6 +1112,27 @@ fn xml_reader<'a>(zip: &'a mut ZipArchive<Cursor<Vec<u8>>>, path: &str) -> Optio
}
}

fn build_cell_formula(attrs: &HashMap<String, String>, text: Option<String>) -> Option<CellFormula> {
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();
Expand Down Expand Up @@ -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);
Expand All @@ -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());
}
}