Files
IronCalc/xlsx/src/compare.rs
2025-06-29 11:07:05 +02:00

224 lines
7.4 KiB
Rust

#![allow(clippy::unwrap_used)]
use std::path::Path;
use ironcalc_base::cell::CellValue;
use ironcalc_base::types::*;
use ironcalc_base::{expressions::utils::number_to_column, Model};
use crate::export::save_to_xlsx;
use crate::import::load_from_xlsx;
pub struct CompareError {
message: String,
}
type CompareResult<T> = std::result::Result<T, CompareError>;
pub struct Diff {
pub sheet_name: String,
pub row: i32,
pub column: i32,
pub value1: Cell,
pub value2: Cell,
pub reason: String,
}
// TODO use f64::EPSILON
const EPS: f64 = 5e-8;
// const EPS: f64 = f64::EPSILON;
fn numbers_are_close(x: f64, y: f64, eps: f64) -> bool {
let norm = (x * x + y * y).sqrt();
if norm == 0.0 {
return true;
}
let d = f64::abs(x - y);
if d < eps {
return true;
}
d / norm < eps
}
/// Compares two Models in the internal representation and returns a list of differences
pub fn compare(model1: &Model, model2: &Model) -> CompareResult<Vec<Diff>> {
let ws1 = model1.workbook.get_worksheet_names();
let ws2 = model2.workbook.get_worksheet_names();
if ws1.len() != ws2.len() {
return Err(CompareError {
message: "Different number of sheets".to_string(),
});
}
let eps = if let Ok(CellValue::Number(v)) = model1.get_cell_value_by_ref("METADATA!A1") {
v
} else {
EPS
};
let mut diffs = Vec::new();
let cells = model1.get_all_cells();
for cell in cells {
let sheet = cell.index;
let row = cell.row;
let column = cell.column;
let cell1 = &model1
.workbook
.worksheet(sheet)
.unwrap()
.cell(row, column)
.cloned()
.unwrap_or_default();
let cell2 = &model2
.workbook
.worksheet(sheet)
.unwrap()
.cell(row, column)
.cloned()
.unwrap_or_default();
match (cell1, cell2) {
(Cell::EmptyCell { .. }, Cell::EmptyCell { .. }) => {}
(Cell::NumberCell { .. }, Cell::NumberCell { .. }) => {}
(Cell::BooleanCell { .. }, Cell::BooleanCell { .. }) => {}
(Cell::ErrorCell { .. }, Cell::ErrorCell { .. }) => {}
(Cell::SharedString { .. }, Cell::SharedString { .. }) => {}
(
Cell::CellFormulaNumber { v: value1, .. },
Cell::CellFormulaNumber { v: value2, .. },
) => {
if !numbers_are_close(*value1, *value2, eps) {
diffs.push(Diff {
sheet_name: ws1[cell.index as usize].clone(),
row,
column,
value1: cell1.clone(),
value2: cell2.clone(),
reason: "Numbers are different".to_string(),
});
}
}
(
Cell::CellFormulaString { v: value1, .. },
Cell::CellFormulaString { v: value2, .. },
) => {
// FIXME: We should compare the actual value, not just the index
if value1 != value2 {
diffs.push(Diff {
sheet_name: ws1[cell.index as usize].clone(),
row,
column,
value1: cell1.clone(),
value2: cell2.clone(),
reason: "Strings are different".to_string(),
});
}
}
(
Cell::CellFormulaBoolean { v: value1, .. },
Cell::CellFormulaBoolean { v: value2, .. },
) => {
// FIXME: We should compare the actual value, not just the index
if value1 != value2 {
diffs.push(Diff {
sheet_name: ws1[cell.index as usize].clone(),
row,
column,
value1: cell1.clone(),
value2: cell2.clone(),
reason: "Booleans are different".to_string(),
});
}
}
(
Cell::CellFormulaError { ei: index1, .. },
Cell::CellFormulaError { ei: index2, .. },
) => {
// FIXME: We should compare the actual value, not just the index
if index1 != index2 {
diffs.push(Diff {
sheet_name: ws1[cell.index as usize].clone(),
row,
column,
value1: cell1.clone(),
value2: cell2.clone(),
reason: "Errors are different".to_string(),
});
}
}
(_, _) => {
diffs.push(Diff {
sheet_name: ws1[cell.index as usize].clone(),
row,
column,
value1: cell1.clone(),
value2: cell2.clone(),
reason: "Types are different".to_string(),
});
}
}
}
Ok(diffs)
}
pub(crate) fn compare_models(m1: &Model, m2: &Model) -> Result<(), String> {
match compare(m1, m2) {
Ok(diffs) => {
if diffs.is_empty() {
Ok(())
} else {
let mut message = "".to_string();
for diff in diffs {
message = format!(
"{}\n.Diff: {}!{}{}, value1: {:?}, value2 {:?}\n {}",
message,
diff.sheet_name,
number_to_column(diff.column).unwrap(),
diff.row,
&diff.value1,
&diff.value2,
diff.reason
);
}
Err(format!("Models are different: {message}"))
}
}
Err(r) => Err(format!("Models are different: {}", r.message)),
}
}
/// Tests that file in file_path produces the same results in Excel and in IronCalc.
pub fn test_file(file_path: &str) -> Result<(), String> {
let model1 = load_from_xlsx(file_path, "en", "UTC").unwrap();
let mut model2 = load_from_xlsx(file_path, "en", "UTC").unwrap();
model2.evaluate();
compare_models(&model1, &model2)
}
/// Tests that file in file_path can be converted to xlsx and read again
pub fn test_load_and_saving(file_path: &str, temp_dir_name: &Path) -> Result<(), String> {
let model1 = load_from_xlsx(file_path, "en", "UTC").unwrap();
let base_name = Path::new(file_path).file_name().unwrap().to_str().unwrap();
let temp_path_buff = temp_dir_name.join(base_name);
let temp_file_path = &format!("{}.xlsx", temp_path_buff.to_str().unwrap());
// test can save
save_to_xlsx(&model1, temp_file_path).unwrap();
// test can open
let mut model2 = load_from_xlsx(temp_file_path, "en", "UTC").unwrap();
model2.evaluate();
compare_models(&model1, &model2)
}
#[cfg(test)]
mod tests {
use crate::compare::compare;
use ironcalc_base::Model;
#[test]
fn compare_different_sheets() {
let mut model1 = Model::new_empty("model", "en", "UTC").unwrap();
model1.new_sheet();
let model2 = Model::new_empty("model", "en", "UTC").unwrap();
assert!(compare(&model1, &model2).is_err());
}
}