Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add shared formula logic #418

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ zip = { version = "0.6", default-features = false, features = ["deflate"] }
chrono = { version = "0.4", features = [
"serde",
], optional = true, default-features = false }
regex = "1.10"
ling7334 marked this conversation as resolved.
Show resolved Hide resolved

[dev-dependencies]
glob = "0.3"
Expand Down
113 changes: 111 additions & 2 deletions src/xlsx/cells_reader.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::collections::HashMap;

use quick_xml::{
events::{attributes::Attribute, BytesStart, Event},
name::QName,
};
use regex::Regex;

use super::{
get_attribute, get_dimension, get_row, get_row_column, read_string, Dimensions, XlReader,
get_attribute, get_dimension, get_row, get_row_column, position_to_title, read_string,
Dimensions, XlReader,
};
use crate::{
datatype::DataRef,
Expand All @@ -23,6 +27,7 @@ pub struct XlsxCellReader<'a> {
col_index: u32,
buf: Vec<u8>,
cell_buf: Vec<u8>,
formulas: Vec<Option<(String, HashMap<String, (i32, i32)>)>>,
}

impl<'a> XlsxCellReader<'a> {
Expand Down Expand Up @@ -68,6 +73,7 @@ impl<'a> XlsxCellReader<'a> {
col_index: 0,
buf: Vec::with_capacity(1024),
cell_buf: Vec::with_capacity(1024),
formulas: Vec::with_capacity(1024),
})
}

Expand Down Expand Up @@ -165,8 +171,111 @@ impl<'a> XlsxCellReader<'a> {
self.cell_buf.clear();
match self.xml.read_event_into(&mut self.cell_buf) {
Ok(Event::Start(ref e)) => {
let mut offset_map: HashMap<String, (i32, i32)> = HashMap::new();
let mut shared_index = None;
let mut shared_ref = None;
let shared =
get_attribute(e.attributes(), QName(b"t")).unwrap_or(None);
match shared {
Some(b"shared") => {
shared_index = Some(
String::from_utf8(
get_attribute(e.attributes(), QName(b"si"))?
.unwrap()
.to_vec(),
)
.unwrap()
.parse::<u32>()?,
);
match get_attribute(e.attributes(), QName(b"ref"))? {
Some(res) => {
let reference = get_dimension(res)?;
if reference.start.0 != reference.end.0 {
for i in
0..=(reference.end.0 - reference.start.0)
{
offset_map.insert(
position_to_title((
reference.start.0 + i,
reference.start.1,
))?,
(
(reference.start.0 as i64
- pos.0 as i64
+ i as i64)
as i32,
0,
),
);
}
} else if reference.start.1 != reference.end.1 {
for i in
0..=(reference.end.1 - reference.start.1)
{
offset_map.insert(
position_to_title((
reference.start.0,
reference.start.1 + i,
))?,
(
0,
(reference.start.1 as i64
- pos.1 as i64
+ i as i64)
as i32,
),
);
}
}
shared_ref = Some(reference);
}
None => {}
}
}
_ => {}
}
if let Some(f) = read_formula(&mut self.xml, e)? {
value = Some(f);
value = Some(f.clone());
if shared_index.is_some() && shared_ref.is_some() {
// original shared formula
while self.formulas.len() < shared_index.unwrap() as usize {
self.formulas.push(None);
}
self.formulas.push(Some((f, offset_map)));
}
}
if shared_index.is_some() && shared_ref.is_none() {
// shared formula
let cell_regex = Regex::new(r"[A-Z]+[0-9]+").unwrap();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't build regex in a loop.

if let Some((f, offset)) =
self.formulas[shared_index.unwrap() as usize].clone()
{
let cells = cell_regex
.find_iter(f.as_str())
.map(|x| get_row_column(x.as_str().as_bytes()));
let mut template = cell_regex
.replace_all(f.as_str(), r"\uffff")
.into_owned();
let ffff_regex = Regex::new(r"\\uffff").unwrap();
let (row, col) =
offset.get(&position_to_title(pos)?).unwrap();
for res in cells {
match res {
Ok(cell) => {
// calculate new formula cell pos
let name = position_to_title((
(cell.0 as i64 + *col as i64) as u32,
(cell.1 as i64 + *row as i64 + 1) as u32,
))?;
template = ffff_regex
.replace(&template, name.as_str())
.into_owned();
}
Err(_) => {}
};
}
value = Some(template.clone());
};
}
}
Ok(Event::End(ref e)) if e.local_name().as_ref() == b"c" => break,
Expand Down
45 changes: 45 additions & 0 deletions src/xlsx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,31 @@ fn check_for_password_protected<RS: Read + Seek>(reader: &mut RS) -> Result<(),
Ok(())
}

/// Convert the integer to Excelsheet column title.
/// If the column number not in 1~16384, an Error is returned.
pub(crate) fn column_number_to_name(num: u32) -> Result<String, XlsxError> {
if num < 1 || num > MAX_COLUMNS {
return Err(XlsxError::Unexpected("column number overflow"));
}
let mut col: Vec<u8> = Vec::new();
let mut num = num;
while num > 0 {
let integer: u8 = (num as u8 - 1) % 26 + 65;
col.push(integer);
num = (num - 1) / 26;
}
col.reverse();
match String::from_utf8(col) {
Ok(s) => Ok(s),
Err(_) => Err(XlsxError::NumericColumn(num as u8)),
}
}

pub(crate) fn position_to_title(cell: (u32, u32)) -> Result<String, XlsxError> {
let col = column_number_to_name(cell.0)?;
Ok(format!("{col}{}", cell.1 + 1).to_owned())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1174,4 +1199,24 @@ mod tests {
CellErrorType::Value
);
}

#[test]
fn test_column_number_to_name() {
assert_eq!(column_number_to_name(1).unwrap(), String::from("A"));
assert_eq!(column_number_to_name(37).unwrap(), String::from("AK"));
assert_eq!(
column_number_to_name(MAX_COLUMNS - 1).unwrap(),
String::from("XNU")
);
}

#[test]
fn test_position_to_title() {
assert_eq!(position_to_title((1, 1)).unwrap(), String::from("A1"));
assert_eq!(position_to_title((37, 1)).unwrap(), String::from("AK1"));
assert_eq!(
position_to_title((MAX_COLUMNS - 1, 1)).unwrap(),
String::from("XNU1")
);
}
}
Binary file added tests/issue_391.xlsx
Binary file not shown.
23 changes: 21 additions & 2 deletions tests/test.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use calamine::Data::{Bool, DateTime, DateTimeIso, DurationIso, Empty, Error, Float, String};
use calamine::{
open_workbook, open_workbook_auto, DataType, ExcelDateTime, ExcelDateTimeType, Ods, Reader,
Sheet, SheetType, SheetVisible, Xls, Xlsb, Xlsx,
open_workbook, open_workbook_auto, DataType, ExcelDateTime, ExcelDateTimeType, Ods, Range,
Reader, Sheet, SheetType, SheetVisible, Xls, Xlsb, Xlsx,
};
use calamine::{CellErrorType::*, Data};
use std::collections::BTreeSet;
Expand Down Expand Up @@ -1878,3 +1878,22 @@ fn issue_401_empty_tables() {
let tables = excel.table_names();
assert!(tables.is_empty());
}

#[test]
fn issue_391_shared_formula() {
setup();

let path = format!("{}/tests/issue_391.xlsx", env!("CARGO_MANIFEST_DIR"));
let mut excel: Xlsx<_> = open_workbook(&path).unwrap();
let mut expect = Range::<std::string::String>::new((1, 0), (6, 0));
for (i, cell) in vec!["A1+1", "A2+1", "A3+1", "A4+1", "A5+1", "A6+1"]
.iter()
.enumerate()
{
expect.set_value((1 + i as u32, 0), cell.to_string());
}
let res = excel.worksheet_formula("Sheet1").unwrap();
assert_eq!(expect.start(), res.start());
assert_eq!(expect.end(), res.end());
assert!(expect.cells().eq(res.cells()));
}
Loading