1#![allow(clippy::unwrap_used, clippy::panic)]
2
3use std::borrow::Cow;
4use std::path::Path;
5
6use ironcalc_base::cell::CellValue;
7use ironcalc_base::types::*;
8use ironcalc_base::{expressions::utils::number_to_column, Model};
9
10use crate::export::save_to_xlsx;
11use crate::import::load_from_xlsx;
12
13use super::util::get_workbook_metadata;
14
15#[cfg(feature = "mock_time")]
16use crate::mock_time::set_mock_time_from_metadata;
17
18pub struct CompareError {
19 message: String,
20}
21
22type CompareResult<T> = std::result::Result<T, CompareError>;
23
24pub struct Diff {
25 pub sheet_name: String,
26 pub row: i32,
27 pub column: i32,
28 pub value1: Cell,
29 pub value2: Cell,
30 pub reason: String,
31}
32
33const EPS: f64 = 5e-8;
35fn numbers_are_close(x: f64, y: f64, eps: f64) -> bool {
38 let norm = (x * x + y * y).sqrt();
39 if norm == 0.0 {
40 return true;
41 }
42 let d = f64::abs(x - y);
43 if d < eps {
44 return true;
45 }
46 d / norm < eps
47}
48pub fn compare(model1: &Model, model2: &Model) -> CompareResult<Vec<Diff>> {
50 let ws1 = model1.workbook.get_worksheet_names();
51 let ws2 = model2.workbook.get_worksheet_names();
52 if ws1.len() != ws2.len() {
53 return Err(CompareError {
54 message: "Different number of sheets".to_string(),
55 });
56 }
57 let eps = if let Ok(CellValue::Number(v)) = model1.get_cell_value_by_ref("METADATA!A1") {
58 v
59 } else {
60 EPS
61 };
62 let mut diffs = Vec::new();
63 let cells = model1.get_all_cells();
64 for cell in cells {
65 let sheet = cell.index;
66 let row = cell.row;
67 let column = cell.column;
68 let cell1 = &model1
69 .workbook
70 .worksheet(sheet)
71 .unwrap()
72 .cell(row, column)
73 .map(Cow::into_owned)
74 .unwrap_or_default();
75 let cell2 = &model2
76 .workbook
77 .worksheet(sheet)
78 .unwrap()
79 .cell(row, column)
80 .map(Cow::into_owned)
81 .unwrap_or_default();
82 match (cell1, cell2) {
83 (Cell::EmptyCell { .. }, Cell::EmptyCell { .. }) => {}
84
85 (Cell::NumberCell { v: value1, .. }, Cell::NumberCell { v: value2, .. }) => {
86 if !numbers_are_close(*value1, *value2, eps) {
87 diffs.push(Diff {
88 sheet_name: ws1[cell.index as usize].clone(),
89 row,
90 column,
91 value1: cell1.clone(),
92 value2: cell2.clone(),
93 reason: "Numbers are different".to_string(),
94 });
95 }
96 }
97
98 (Cell::BooleanCell { v: value1, .. }, Cell::BooleanCell { v: value2, .. }) => {
99 if value1 != value2 {
100 diffs.push(Diff {
101 sheet_name: ws1[cell.index as usize].clone(),
102 row,
103 column,
104 value1: cell1.clone(),
105 value2: cell2.clone(),
106 reason: "Booleans are different".to_string(),
107 });
108 }
109 }
110
111 (Cell::ErrorCell { ei: value1, .. }, Cell::ErrorCell { ei: value2, .. }) => {
112 if value1 != value2 {
113 diffs.push(Diff {
114 sheet_name: ws1[cell.index as usize].clone(),
115 row,
116 column,
117 value1: cell1.clone(),
118 value2: cell2.clone(),
119 reason: "Errors are different".to_string(),
120 });
121 }
122 }
123
124 (Cell::SharedString { si: value1, .. }, Cell::SharedString { si: value2, .. }) => {
125 if value1 != value2 {
128 diffs.push(Diff {
129 sheet_name: ws1[cell.index as usize].clone(),
130 row,
131 column,
132 value1: cell1.clone(),
133 value2: cell2.clone(),
134 reason: "Strings are different".to_string(),
135 });
136 }
137 }
138
139 (
140 Cell::CellFormula { v: v1, .. } | Cell::ArrayFormula { v: v1, .. },
141 Cell::CellFormula { v: v2, .. } | Cell::ArrayFormula { v: v2, .. },
142 ) => {
143 let mismatch = match (v1, v2) {
144 (FormulaValue::Unevaluated, FormulaValue::Unevaluated) => false,
145 (FormulaValue::Boolean(a), FormulaValue::Boolean(b)) => a != b,
146 (FormulaValue::Number(a), FormulaValue::Number(b)) => {
147 !numbers_are_close(*a, *b, eps)
148 }
149 (FormulaValue::Text(a), FormulaValue::Text(b)) => a != b,
150 (FormulaValue::Error { ei: e1, .. }, FormulaValue::Error { ei: e2, .. }) => {
151 e1 != e2
152 }
153 (FormulaValue::Text(s), FormulaValue::Error { ei, .. })
156 | (FormulaValue::Error { ei, .. }, FormulaValue::Text(s)) => {
157 s != &format!("{ei}")
158 }
159 _ => true,
160 };
161 if mismatch {
162 diffs.push(Diff {
163 sheet_name: ws1[cell.index as usize].clone(),
164 row,
165 column,
166 value1: cell1.clone(),
167 value2: cell2.clone(),
168 reason: "Formula values are different".to_string(),
169 });
170 }
171 }
172
173 (Cell::SpillCell { v: v1, .. }, Cell::SpillCell { v: v2, .. }) => {
174 let mismatch = match (v1, v2) {
175 (SpillValue::Boolean(a), SpillValue::Boolean(b)) => a != b,
176 (SpillValue::Number(a), SpillValue::Number(b)) => {
177 !numbers_are_close(*a, *b, eps)
178 }
179 (SpillValue::Text(a), SpillValue::Text(b)) => a != b,
180 (SpillValue::Error(a), SpillValue::Error(b)) => a != b,
181 _ => true,
182 };
183 if mismatch {
184 diffs.push(Diff {
185 sheet_name: ws1[cell.index as usize].clone(),
186 row,
187 column,
188 value1: cell1.clone(),
189 value2: cell2.clone(),
190 reason: "Spill values are different".to_string(),
191 });
192 }
193 }
194
195 (_, _) => {
196 diffs.push(Diff {
197 sheet_name: ws1[cell.index as usize].clone(),
198 row,
199 column,
200 value1: cell1.clone(),
201 value2: cell2.clone(),
202 reason: "Types are different".to_string(),
203 });
204 }
205 }
206 }
207 Ok(diffs)
208}
209
210fn cell_display(cell: &Cell) -> String {
211 match cell {
212 Cell::EmptyCell { .. } => "(empty)".to_string(),
213 Cell::NumberCell { v, .. } => format!("{v}"),
214 Cell::BooleanCell { v, .. } => format!("{v}"),
215 Cell::ErrorCell { ei, .. } => format!("{ei} (error)"),
216 Cell::SharedString { si, .. } => format!("shared_string[{si}]"),
217 Cell::CellFormula {
218 v: FormulaValue::Unevaluated,
219 ..
220 } => "(unevaluated formula)".to_string(),
221 Cell::CellFormula {
222 v: FormulaValue::Boolean(v),
223 ..
224 }
225 | Cell::ArrayFormula {
226 v: FormulaValue::Boolean(v),
227 ..
228 } => format!("{v} (bool)"),
229 Cell::CellFormula {
230 v: FormulaValue::Number(v),
231 ..
232 }
233 | Cell::ArrayFormula {
234 v: FormulaValue::Number(v),
235 ..
236 } => format!("{v} (number)"),
237 Cell::CellFormula {
238 v: FormulaValue::Text(v),
239 ..
240 }
241 | Cell::ArrayFormula {
242 v: FormulaValue::Text(v),
243 ..
244 } => format!("\"{v}\" (string)"),
245 Cell::CellFormula {
246 v: FormulaValue::Error { ei, .. },
247 ..
248 }
249 | Cell::ArrayFormula {
250 v: FormulaValue::Error { ei, .. },
251 ..
252 } => format!("{ei} (error)"),
253 Cell::ArrayFormula {
254 v: FormulaValue::Unevaluated,
255 s,
256 r,
257 kind,
258 ..
259 } => {
260 format!("(unevaluated {kind:?} formula, size={s}, range={r:?})")
261 }
262 Cell::SpillCell {
263 v: SpillValue::Number(v),
264 s,
265 a,
266 } => {
267 format!("{v} (spill, size={s}, area={a:?})")
268 }
269 Cell::SpillCell {
270 v: SpillValue::Boolean(v),
271 s,
272 a,
273 } => {
274 format!("{v} (spill, size={s}, area={a:?})")
275 }
276 Cell::SpillCell {
277 v: SpillValue::Error(ei),
278 s,
279 a,
280 } => {
281 format!("{ei} (spill, size={s}, area={a:?})")
282 }
283 Cell::SpillCell {
284 v: SpillValue::Text(v),
285 s,
286 a,
287 } => {
288 format!("\"{v}\" (spill, size={s}, area={a:?})")
289 }
290 }
291}
292
293pub(crate) fn compare_models(m1: &Model, m2: &Model) -> Result<(), String> {
294 match compare(m1, m2) {
295 Ok(diffs) => {
296 if diffs.is_empty() {
297 Ok(())
298 } else {
299 let count = diffs.len();
300 let mut lines = format!(
301 "Models are different ({count} diff{}):\n",
302 if count == 1 { "" } else { "s" }
303 );
304 for diff in diffs {
305 let col = number_to_column(diff.column).unwrap();
306 let cell_ref = format!("{}!{}{}", diff.sheet_name, col, diff.row);
307 let excel = cell_display(&diff.value1);
308 let ironcalc = cell_display(&diff.value2);
309 lines.push_str(&format!(
310 "\n {cell_ref:<16} Excel: {excel:<30} IronCalc: {ironcalc:<30} [{}]",
311 diff.reason
312 ));
313 }
314 Err(lines)
315 }
316 }
317 Err(r) => Err(format!("Models are different: {}", r.message)),
318 }
319}
320
321pub fn test_file(file_path: &str) -> Result<(), String> {
323 let model1 = load_from_xlsx(file_path, "en", "UTC", "en").unwrap();
325 let locale = get_workbook_metadata(&model1);
326 #[cfg(feature = "mock_time")]
329 set_mock_time_from_metadata(&model1);
330 let model1 = load_from_xlsx(file_path, &locale, "UTC", "en").unwrap();
331 let mut model2 = load_from_xlsx(file_path, &locale, "UTC", "en").unwrap();
332 model2.evaluate();
333 compare_models(&model1, &model2)
334}
335
336pub fn test_load_and_saving(file_path: &str, temp_dir_name: &Path) -> Result<(), String> {
338 let model1 = load_from_xlsx(file_path, "en", "UTC", "en").unwrap();
340 let locale = get_workbook_metadata(&model1);
341 #[cfg(feature = "mock_time")]
344 set_mock_time_from_metadata(&model1);
345
346 let model1 = load_from_xlsx(file_path, &locale, "UTC", "en").unwrap();
347
348 let base_name = Path::new(file_path).file_name().unwrap().to_str().unwrap();
349
350 let temp_path_buff = temp_dir_name.join(base_name);
351 let temp_file_path = &format!("{}.xlsx", temp_path_buff.to_str().unwrap());
352 save_to_xlsx(&model1, temp_file_path).unwrap();
354 let mut model2 = load_from_xlsx(temp_file_path, &locale, "UTC", "en").unwrap();
356 model2.evaluate();
357 compare_models(&model1, &model2)
358}
359
360#[cfg(test)]
361mod tests {
362 use crate::compare::compare;
363 use ironcalc_base::Model;
364
365 #[test]
366 fn compare_different_sheets() {
367 let mut model1 = Model::new_empty("model", "en", "UTC", "en").unwrap();
368 model1.new_sheet();
369 let model2 = Model::new_empty("model", "en", "UTC", "en").unwrap();
370
371 assert!(compare(&model1, &model2).is_err());
372 }
373}