From 363dd12e221223d80563989ec98a322059af2099 Mon Sep 17 00:00:00 2001 From: Artur Sultanov Date: Thu, 25 Jun 2026 11:08:57 +0300 Subject: [PATCH 1/3] [fix] shared formula import with formulas flag --- js/module.js | 4 +- js/worker.js | 6 +- src/lib.rs | 273 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 275 insertions(+), 8 deletions(-) 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/src/lib.rs b/src/lib.rs index b10da26..d914e08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,6 +158,11 @@ struct SheetInfo { use_shared_string_for_next: bool, } +struct SharedFormula { + base_cell: String, + formula: String, +} + impl SheetInfo { pub fn new() -> SheetInfo { SheetInfo { @@ -285,6 +290,9 @@ 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; + let mut shared_formulas: HashMap = HashMap::new(); loop { buf.clear(); @@ -380,6 +388,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(); @@ -430,22 +440,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 +481,30 @@ impl XLSX { } }, Ok(Event::End(ref e)) if e.name().as_ref() == b"f" => { + if current_formula_attrs.get("t").map(|v| v == "shared").unwrap_or(false) { + if let Some(si) = current_formula_attrs.get("si") { + if let Some(formula) = current_formula_text.clone() { + if let Some(cell_name) = current_cell_name.clone() { + shared_formulas.insert(si.clone(), SharedFormula { + base_cell: cell_name, + formula, + }); + } + } else if flags & WITH_FORMULAS > 0 { + if let (Some(shared_formula), Some(cell_name)) = ( + shared_formulas.get(si), + current_cell_name.as_ref(), + ) { + let formula = shift_formula_references( + &shared_formula.formula, + &shared_formula.base_cell, + cell_name, + ); + last_cell.v = Some("=".to_owned() + &formula); + } + } + } + } mode = 0 } Ok(Event::End(ref e)) if e.name().as_ref() == b"v" => { @@ -1072,6 +1120,171 @@ fn xml_reader<'a>(zip: &'a mut ZipArchive>>, path: &str) -> Optio } } +fn shift_formula_references(formula: &str, base_cell: &str, target_cell: &str) -> String { + let (base_col, base_row) = cell_index_to_offsets(base_cell.to_string()); + let (target_col, target_row) = cell_index_to_offsets(target_cell.to_string()); + let delta_col = target_col as i32 - base_col as i32; + let delta_row = target_row as i32 - base_row as i32; + + let chars: Vec = formula.chars().collect(); + let mut out = String::new(); + let mut i = 0; + let mut in_string = false; + + while i < chars.len() { + let ch = chars[i]; + if ch == '"' { + out.push(ch); + if in_string && i + 1 < chars.len() && chars[i + 1] == '"' { + out.push(chars[i + 1]); + i += 2; + continue; + } + in_string = !in_string; + i += 1; + continue; + } + + if !in_string { + if let Some((reference, len)) = parse_a1_reference(&chars, i) { + out.push_str(&shift_a1_reference(&reference, delta_col, delta_row)); + i += len; + continue; + } + } + + out.push(ch); + i += 1; + } + + out +} + +#[derive(Debug, PartialEq)] +struct A1Reference { + col_abs: bool, + col: u32, + row_abs: bool, + row: u32, +} + +fn parse_a1_reference(chars: &[char], start: usize) -> Option<(A1Reference, usize)> { + if start > 0 && is_formula_name_char(chars[start - 1]) { + return None; + } + + let mut i = start; + let col_abs = if chars.get(i) == Some(&'$') { + i += 1; + true + } else { + false + }; + + let col_start = i; + while i < chars.len() && chars[i].is_ascii_alphabetic() { + i += 1; + } + if i == col_start { + return None; + } + + let row_abs = if chars.get(i) == Some(&'$') { + i += 1; + true + } else { + false + }; + + let row_start = i; + while i < chars.len() && chars[i].is_ascii_digit() { + i += 1; + } + if i == row_start { + return None; + } + + if i < chars.len() && (is_formula_name_char(chars[i]) || chars[i] == '(') { + return None; + } + + let col_label: String = chars[col_start..(if row_abs { row_start - 1 } else { row_start })] + .iter() + .collect::() + .to_ascii_uppercase(); + let row_label: String = chars[row_start..i].iter().collect(); + let col = column_label_to_number(&col_label)?; + let row = row_label.parse::().ok()?; + + if col == 0 || col > 16384 || row == 0 || row > 1_048_576 { + return None; + } + + Some(( + A1Reference { + col_abs, + col, + row_abs, + row, + }, + i - start, + )) +} + +fn shift_a1_reference(reference: &A1Reference, delta_col: i32, delta_row: i32) -> String { + let col = if reference.col_abs { + reference.col + } else { + add_delta(reference.col, delta_col) + }; + let row = if reference.row_abs { + reference.row + } else { + add_delta(reference.row, delta_row) + }; + + format!( + "{}{}{}{}", + if reference.col_abs { "$" } else { "" }, + number_to_column_label(col), + if reference.row_abs { "$" } else { "" }, + row, + ) +} + +fn add_delta(value: u32, delta: i32) -> u32 { + if delta < 0 { + value.saturating_sub(delta.unsigned_abs()) + } else { + value.saturating_add(delta as u32) + } +} + +fn column_label_to_number(label: &str) -> Option { + let mut number = 0u32; + for ch in label.chars() { + if !ch.is_ascii_alphabetic() { + return None; + } + number = number * 26 + (ch.to_ascii_uppercase() as u32 - 'A' as u32 + 1); + } + Some(number) +} + +fn number_to_column_label(mut number: u32) -> String { + let mut label = String::new(); + while number > 0 { + number -= 1; + label.insert(0, (b'A' + (number % 26) as u8) as char); + number /= 26; + } + label +} + +fn is_formula_name_char(ch: char) -> bool { + ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' +} + 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 +1409,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 +1429,58 @@ 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 shifts_shared_formula_references() { + assert_eq!( + shift_formula_references("A11/$A$2", "B11", "B12"), + "A12/$A$2" + ); + assert_eq!( + shift_formula_references("$A11+A$11+$A$11", "B11", "C12"), + "$A12+B$11+$A$11" + ); + assert_eq!( + shift_formula_references("A1:B2", "A1", "B2"), + "B2:C3" + ); + assert_eq!( + shift_formula_references("IF(A1=\"A1\",A1,LOG10(A1))", "A1", "A2"), + "IF(A2=\"A1\",A2,LOG10(A2))" + ); + } + + #[test] + fn reads_shared_formula_followers() { + 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(); + + assert_eq!(data.cells[2][4].as_ref().unwrap().v.as_deref(), Some("=SUM(C3:D3)")); + assert_eq!(data.cells[3][4].as_ref().unwrap().v.as_deref(), Some("=SUM(C4:D4)")); + assert_eq!(data.cells[4][4].as_ref().unwrap().v.as_deref(), Some("=SUM(C5:D5)")); + } + + #[test] + fn shared_formula_followers_keep_cached_values_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")); + } } From 8c56b82ea1246e705aa5beb4d810e878bc8bb048 Mon Sep 17 00:00:00 2001 From: Artur Sultanov Date: Thu, 25 Jun 2026 15:14:42 +0300 Subject: [PATCH 2/3] [fix] added cell meta data for formules --- public/worker.html | 3 +- src/lib.rs | 137 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 107 insertions(+), 33 deletions(-) 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 d914e08..e22c810 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 { @@ -272,7 +289,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)) @@ -423,7 +439,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)) @@ -481,17 +497,21 @@ impl XLSX { } }, Ok(Event::End(ref e)) if e.name().as_ref() == b"f" => { - if current_formula_attrs.get("t").map(|v| v == "shared").unwrap_or(false) { - if let Some(si) = current_formula_attrs.get("si") { - if let Some(formula) = current_formula_text.clone() { - if let Some(cell_name) = current_cell_name.clone() { - shared_formulas.insert(si.clone(), SharedFormula { - base_cell: cell_name, - formula, - }); - } - } else if flags & WITH_FORMULAS > 0 { - if let (Some(shared_formula), Some(cell_name)) = ( + if flags & WITH_FORMULAS > 0 { + last_cell.formula = build_cell_formula( + ¤t_formula_attrs, + current_formula_text.clone(), + ); + if current_formula_attrs.get("t").map(|v| v == "shared").unwrap_or(false) { + if let Some(si) = current_formula_attrs.get("si") { + if let Some(formula) = current_formula_text.clone() { + if let Some(cell_name) = current_cell_name.clone() { + shared_formulas.insert(si.clone(), SharedFormula { + base_cell: cell_name, + formula, + }); + } + } else if let (Some(shared_formula), Some(cell_name)) = ( shared_formulas.get(si), current_cell_name.as_ref(), ) { @@ -1120,6 +1140,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 shift_formula_references(formula: &str, base_cell: &str, target_cell: &str) -> String { let (base_col, base_row) = cell_index_to_offsets(base_cell.to_string()); let (target_col, target_row) = cell_index_to_offsets(target_cell.to_string()); @@ -1431,7 +1472,54 @@ mod tests { } #[test] - fn shifts_shared_formula_references() { + 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 shifts_shared_formula_references_for_legacy_value() { assert_eq!( shift_formula_references("A11/$A$2", "B11", "B12"), "A12/$A$2" @@ -1451,24 +1539,7 @@ mod tests { } #[test] - fn reads_shared_formula_followers() { - 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(); - - assert_eq!(data.cells[2][4].as_ref().unwrap().v.as_deref(), Some("=SUM(C3:D3)")); - assert_eq!(data.cells[3][4].as_ref().unwrap().v.as_deref(), Some("=SUM(C4:D4)")); - assert_eq!(data.cells[4][4].as_ref().unwrap().v.as_deref(), Some("=SUM(C5:D5)")); - } - - #[test] - fn shared_formula_followers_keep_cached_values_without_formula_flag() { + fn formulas_are_omitted_without_formula_flag() { use std::io::Read; let mut file = std::fs::File::open("./example/file_example_styles.xlsx").unwrap(); @@ -1482,5 +1553,7 @@ mod tests { 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()); } } From 7b20c9e685d2a6be753f9129477ee567f0066c59 Mon Sep 17 00:00:00 2001 From: Artur Sultanov Date: Thu, 25 Jun 2026 15:37:31 +0300 Subject: [PATCH 3/3] [fix] revert master's cell.v behavior for followed cells with cached v value instead of cell's formula calculation. Cell's field "formula" changed --- README.md | 11 +++ src/lib.rs | 213 ----------------------------------------------------- 2 files changed, 11 insertions(+), 213 deletions(-) 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/src/lib.rs b/src/lib.rs index e22c810..5fcee9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,11 +175,6 @@ struct SheetInfo { use_shared_string_for_next: bool, } -struct SharedFormula { - base_cell: String, - formula: String, -} - impl SheetInfo { pub fn new() -> SheetInfo { SheetInfo { @@ -308,7 +303,6 @@ impl XLSX { let mut current_cell_name: Option = None; let mut current_formula_attrs: HashMap = HashMap::new(); let mut current_formula_text: Option = None; - let mut shared_formulas: HashMap = HashMap::new(); loop { buf.clear(); @@ -502,28 +496,6 @@ impl XLSX { ¤t_formula_attrs, current_formula_text.clone(), ); - if current_formula_attrs.get("t").map(|v| v == "shared").unwrap_or(false) { - if let Some(si) = current_formula_attrs.get("si") { - if let Some(formula) = current_formula_text.clone() { - if let Some(cell_name) = current_cell_name.clone() { - shared_formulas.insert(si.clone(), SharedFormula { - base_cell: cell_name, - formula, - }); - } - } else if let (Some(shared_formula), Some(cell_name)) = ( - shared_formulas.get(si), - current_cell_name.as_ref(), - ) { - let formula = shift_formula_references( - &shared_formula.formula, - &shared_formula.base_cell, - cell_name, - ); - last_cell.v = Some("=".to_owned() + &formula); - } - } - } } mode = 0 } @@ -1161,171 +1133,6 @@ fn build_cell_formula(attrs: &HashMap, text: Option) -> }) } -fn shift_formula_references(formula: &str, base_cell: &str, target_cell: &str) -> String { - let (base_col, base_row) = cell_index_to_offsets(base_cell.to_string()); - let (target_col, target_row) = cell_index_to_offsets(target_cell.to_string()); - let delta_col = target_col as i32 - base_col as i32; - let delta_row = target_row as i32 - base_row as i32; - - let chars: Vec = formula.chars().collect(); - let mut out = String::new(); - let mut i = 0; - let mut in_string = false; - - while i < chars.len() { - let ch = chars[i]; - if ch == '"' { - out.push(ch); - if in_string && i + 1 < chars.len() && chars[i + 1] == '"' { - out.push(chars[i + 1]); - i += 2; - continue; - } - in_string = !in_string; - i += 1; - continue; - } - - if !in_string { - if let Some((reference, len)) = parse_a1_reference(&chars, i) { - out.push_str(&shift_a1_reference(&reference, delta_col, delta_row)); - i += len; - continue; - } - } - - out.push(ch); - i += 1; - } - - out -} - -#[derive(Debug, PartialEq)] -struct A1Reference { - col_abs: bool, - col: u32, - row_abs: bool, - row: u32, -} - -fn parse_a1_reference(chars: &[char], start: usize) -> Option<(A1Reference, usize)> { - if start > 0 && is_formula_name_char(chars[start - 1]) { - return None; - } - - let mut i = start; - let col_abs = if chars.get(i) == Some(&'$') { - i += 1; - true - } else { - false - }; - - let col_start = i; - while i < chars.len() && chars[i].is_ascii_alphabetic() { - i += 1; - } - if i == col_start { - return None; - } - - let row_abs = if chars.get(i) == Some(&'$') { - i += 1; - true - } else { - false - }; - - let row_start = i; - while i < chars.len() && chars[i].is_ascii_digit() { - i += 1; - } - if i == row_start { - return None; - } - - if i < chars.len() && (is_formula_name_char(chars[i]) || chars[i] == '(') { - return None; - } - - let col_label: String = chars[col_start..(if row_abs { row_start - 1 } else { row_start })] - .iter() - .collect::() - .to_ascii_uppercase(); - let row_label: String = chars[row_start..i].iter().collect(); - let col = column_label_to_number(&col_label)?; - let row = row_label.parse::().ok()?; - - if col == 0 || col > 16384 || row == 0 || row > 1_048_576 { - return None; - } - - Some(( - A1Reference { - col_abs, - col, - row_abs, - row, - }, - i - start, - )) -} - -fn shift_a1_reference(reference: &A1Reference, delta_col: i32, delta_row: i32) -> String { - let col = if reference.col_abs { - reference.col - } else { - add_delta(reference.col, delta_col) - }; - let row = if reference.row_abs { - reference.row - } else { - add_delta(reference.row, delta_row) - }; - - format!( - "{}{}{}{}", - if reference.col_abs { "$" } else { "" }, - number_to_column_label(col), - if reference.row_abs { "$" } else { "" }, - row, - ) -} - -fn add_delta(value: u32, delta: i32) -> u32 { - if delta < 0 { - value.saturating_sub(delta.unsigned_abs()) - } else { - value.saturating_add(delta as u32) - } -} - -fn column_label_to_number(label: &str) -> Option { - let mut number = 0u32; - for ch in label.chars() { - if !ch.is_ascii_alphabetic() { - return None; - } - number = number * 26 + (ch.to_ascii_uppercase() as u32 - 'A' as u32 + 1); - } - Some(number) -} - -fn number_to_column_label(mut number: u32) -> String { - let mut label = String::new(); - while number > 0 { - number -= 1; - label.insert(0, (b'A' + (number % 26) as u8) as char); - number /= 26; - } - label -} - -fn is_formula_name_char(ch: char) -> bool { - ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' -} - 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(); @@ -1518,26 +1325,6 @@ mod tests { assert_eq!(cell.formula.as_ref().unwrap().value.as_deref(), Some("A2/B2")); } - #[test] - fn shifts_shared_formula_references_for_legacy_value() { - assert_eq!( - shift_formula_references("A11/$A$2", "B11", "B12"), - "A12/$A$2" - ); - assert_eq!( - shift_formula_references("$A11+A$11+$A$11", "B11", "C12"), - "$A12+B$11+$A$11" - ); - assert_eq!( - shift_formula_references("A1:B2", "A1", "B2"), - "B2:C3" - ); - assert_eq!( - shift_formula_references("IF(A1=\"A1\",A1,LOG10(A1))", "A1", "A2"), - "IF(A2=\"A1\",A2,LOG10(A2))" - ); - } - #[test] fn formulas_are_omitted_without_formula_flag() { use std::io::Read;