#![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,
}
const EPS: f64 = 5e-8;
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
}
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, .. },
) => {
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, .. },
) => {
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, .. },
) => {
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)),
}
}
fn get_workbook_metadata(model: &Model) -> String {
let mut metadata_sheet_index = None;
for (index, ws) in model.workbook.worksheets.iter().enumerate() {
if ws.name.eq_ignore_ascii_case("METADATA") {
metadata_sheet_index = Some(index as u32);
break;
}
}
let default_locale = "en".to_string();
if let Some(sheet_index) = metadata_sheet_index {
if let Ok(a1) = model.get_formatted_cell_value(sheet_index, 1, 1) {
if a1 == "Locale" {
match model.get_formatted_cell_value(sheet_index, 1, 2) {
Ok(v) if v == "en-GB" => {
return "en-GB".to_string();
}
_ => return default_locale,
}
}
}
}
default_locale
}
pub fn test_file(file_path: &str) -> Result<(), String> {
let model1 = load_from_xlsx(file_path, "en", "UTC", "en").unwrap();
let locale = get_workbook_metadata(&model1);
let model1 = load_from_xlsx(file_path, &locale, "UTC", "en").unwrap();
let mut model2 = load_from_xlsx(file_path, &locale, "UTC", "en").unwrap();
model2.evaluate();
compare_models(&model1, &model2)
}
pub fn test_load_and_saving(file_path: &str, temp_dir_name: &Path) -> Result<(), String> {
let model1 = load_from_xlsx(file_path, "en", "UTC", "en").unwrap();
let locale = get_workbook_metadata(&model1);
let model1 = load_from_xlsx(file_path, &locale, "UTC", "en").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());
save_to_xlsx(&model1, temp_file_path).unwrap();
let mut model2 = load_from_xlsx(temp_file_path, &locale, "UTC", "en").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", "en").unwrap();
model1.new_sheet();
let model2 = Model::new_empty("model", "en", "UTC", "en").unwrap();
assert!(compare(&model1, &model2).is_err());
}
}