1use std::fmt;
2use std::io::{Read, Seek};
3use std::path::Path;
4
5use calamine::{Data, Reader, Xlsx, open_workbook};
6#[cfg(feature = "serde_derive")]
7use serde::{Deserialize, Serialize};
8
9use super::error::{SheetsDiffError, WorkbookSide};
10use super::utils::{cell_pos_to_address, diff_range, filter_same_name_sheets};
11
12#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
18#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
19pub enum CellDiffKind {
20 Value,
21 Formula,
22}
23
24impl fmt::Display for CellDiffKind {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 CellDiffKind::Value => write!(f, "value"),
28 CellDiffKind::Formula => write!(f, "formula"),
29 }
30 }
31}
32
33#[derive(Clone, Debug)]
35#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
36pub struct Diff {
37 pub old_filepath: String,
38 pub new_filepath: String,
39 pub sheet_diff: Vec<SheetDiff>,
40 pub cell_diffs: Vec<SheetCellDiff>,
41}
42
43#[derive(Clone, Debug)]
45#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
46pub struct SheetDiff {
47 pub old: Option<String>,
48 pub new: Option<String>,
49}
50
51#[derive(Clone, Debug)]
53#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
54pub struct SheetCellDiff {
55 pub sheet: String,
56 pub cells: Vec<CellDiff>,
57}
58
59#[derive(Clone, Debug)]
61#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
62pub struct CellDiff {
63 pub row: usize,
65 pub col: usize,
67 pub addr: String,
69 pub kind: CellDiffKind,
70 pub old: Option<String>,
71 pub new: Option<String>,
72}
73
74impl Diff {
79 pub fn new(old_filepath: &str, new_filepath: &str) -> Self {
90 match Self::try_new(old_filepath, new_filepath) {
91 Ok(diff) => diff,
92 Err(err) => panic!("failed to diff workbooks: {err}"),
93 }
94 }
95
96 pub fn try_new(
102 old_filepath: impl AsRef<Path>,
103 new_filepath: impl AsRef<Path>,
104 ) -> Result<Self, SheetsDiffError> {
105 let old_path = old_filepath.as_ref();
106 let new_path = new_filepath.as_ref();
107
108 let mut old_workbook: Xlsx<_> =
109 open_workbook(old_path).map_err(|source| SheetsDiffError::OpenWorkbook {
110 side: WorkbookSide::Old,
111 path: old_path.to_path_buf(),
112 source,
113 })?;
114
115 let mut new_workbook: Xlsx<_> =
116 open_workbook(new_path).map_err(|source| SheetsDiffError::OpenWorkbook {
117 side: WorkbookSide::New,
118 path: new_path.to_path_buf(),
119 source,
120 })?;
121
122 let old_label = old_path.to_string_lossy().into_owned();
123 let new_label = new_path.to_string_lossy().into_owned();
124
125 Self::try_from_workbooks(old_label, new_label, &mut old_workbook, &mut new_workbook)
126 }
127
128 pub fn try_from_named_readers<R1, R2>(
138 old_name: impl Into<String>,
139 old_reader: R1,
140 new_name: impl Into<String>,
141 new_reader: R2,
142 ) -> Result<Self, SheetsDiffError>
143 where
144 R1: Read + Seek,
145 R2: Read + Seek,
146 {
147 let mut old_workbook = Xlsx::new(old_reader).map_err(|source| {
148 SheetsDiffError::OpenReader {
149 side: WorkbookSide::Old,
150 source,
151 }
152 })?;
153
154 let mut new_workbook = Xlsx::new(new_reader).map_err(|source| {
155 SheetsDiffError::OpenReader {
156 side: WorkbookSide::New,
157 source,
158 }
159 })?;
160
161 Self::try_from_workbooks(
162 old_name.into(),
163 new_name.into(),
164 &mut old_workbook,
165 &mut new_workbook,
166 )
167 }
168
169 pub fn diff(&mut self) -> Diff {
171 self.clone()
172 }
173}
174
175impl Diff {
180 fn empty(old_filepath: String, new_filepath: String) -> Self {
182 Diff {
183 old_filepath,
184 new_filepath,
185 sheet_diff: vec![],
186 cell_diffs: vec![],
187 }
188 }
189
190 fn try_from_workbooks<R1, R2>(
192 old_label: String,
193 new_label: String,
194 old_workbook: &mut Xlsx<R1>,
195 new_workbook: &mut Xlsx<R2>,
196 ) -> Result<Self, SheetsDiffError>
197 where
198 R1: Read + Seek,
199 R2: Read + Seek,
200 {
201 let mut diff = Self::empty(old_label, new_label);
202 diff.collect_diff_from_workbooks(old_workbook, new_workbook)?;
203 diff.normalize_cell_diffs();
204 Ok(diff)
205 }
206
207 fn collect_diff_from_workbooks<R1, R2>(
209 &mut self,
210 old_workbook: &mut Xlsx<R1>,
211 new_workbook: &mut Xlsx<R2>,
212 ) -> Result<(), SheetsDiffError>
213 where
214 R1: Read + Seek,
215 R2: Read + Seek,
216 {
217 let old_sheets = old_workbook.sheet_names().to_owned();
218 let new_sheets = new_workbook.sheet_names().to_owned();
219
220 self.collect_sheet_diff(&old_sheets, &new_sheets);
221
222 let same_name_sheets = filter_same_name_sheets(&old_sheets, &new_sheets);
223 self.collect_cell_value_diff(old_workbook, new_workbook, &same_name_sheets)?;
224 self.collect_cell_formula_diff(old_workbook, new_workbook, &same_name_sheets)?;
225
226 Ok(())
227 }
228
229 fn collect_sheet_diff(&mut self, old_sheets: &[String], new_sheets: &[String]) {
231 if old_sheets == new_sheets {
232 return;
233 }
234
235 for sheet in old_sheets {
236 if !new_sheets.contains(sheet) {
237 self.sheet_diff.push(SheetDiff {
238 old: Some(sheet.clone()),
239 new: None,
240 });
241 }
242 }
243 for sheet in new_sheets {
244 if !old_sheets.contains(sheet) {
245 self.sheet_diff.push(SheetDiff {
246 old: None,
247 new: Some(sheet.clone()),
248 });
249 }
250 }
251 }
252
253 fn collect_cell_value_diff<R1, R2>(
255 &mut self,
256 old_workbook: &mut Xlsx<R1>,
257 new_workbook: &mut Xlsx<R2>,
258 same_name_sheets: &[String],
259 ) -> Result<(), SheetsDiffError>
260 where
261 R1: Read + Seek,
262 R2: Read + Seek,
263 {
264 for sheet in same_name_sheets {
265 let old_range =
266 old_workbook
267 .worksheet_range(sheet)
268 .map_err(|source| SheetsDiffError::ReadSheetValues {
269 side: WorkbookSide::Old,
270 sheet: sheet.clone(),
271 source,
272 })?;
273
274 let new_range =
275 new_workbook
276 .worksheet_range(sheet)
277 .map_err(|source| SheetsDiffError::ReadSheetValues {
278 side: WorkbookSide::New,
279 sheet: sheet.clone(),
280 source,
281 })?;
282
283 let mut cell_diffs: Vec<CellDiff> = vec![];
284
285 let (start_row, start_col, end_row, end_col) = diff_range(
286 old_range.start(),
287 new_range.start(),
288 old_range.end(),
289 new_range.end(),
290 );
291
292 for row in start_row..end_row {
293 for col in start_col..end_col {
294 let old_cell = old_range.get_value((row, col)).unwrap_or(&Data::Empty);
295 let new_cell = new_range.get_value((row, col)).unwrap_or(&Data::Empty);
296
297 if old_cell != new_cell {
298 let row1 = (row + 1) as usize;
299 let col1 = (col + 1) as usize;
300 cell_diffs.push(CellDiff {
301 row: row1,
302 col: col1,
303 addr: cell_pos_to_address(row1, col1),
304 kind: CellDiffKind::Value,
305 old: if old_cell != &Data::Empty {
306 Some(old_cell.to_string())
307 } else {
308 None
309 },
310 new: if new_cell != &Data::Empty {
311 Some(new_cell.to_string())
312 } else {
313 None
314 },
315 });
316 }
317 }
318 }
319
320 if !cell_diffs.is_empty() {
321 self.cell_diffs.push(SheetCellDiff {
322 sheet: sheet.clone(),
323 cells: cell_diffs,
324 });
325 }
326 }
327
328 Ok(())
329 }
330
331 fn collect_cell_formula_diff<R1, R2>(
333 &mut self,
334 old_workbook: &mut Xlsx<R1>,
335 new_workbook: &mut Xlsx<R2>,
336 same_name_sheets: &[String],
337 ) -> Result<(), SheetsDiffError>
338 where
339 R1: Read + Seek,
340 R2: Read + Seek,
341 {
342 for sheet in same_name_sheets {
343 let old_range = old_workbook
344 .worksheet_formula(sheet)
345 .map_err(|source| SheetsDiffError::ReadSheetFormulas {
346 side: WorkbookSide::Old,
347 sheet: sheet.clone(),
348 source,
349 })?;
350
351 let new_range = new_workbook
352 .worksheet_formula(sheet)
353 .map_err(|source| SheetsDiffError::ReadSheetFormulas {
354 side: WorkbookSide::New,
355 sheet: sheet.clone(),
356 source,
357 })?;
358
359 let mut cell_diffs: Vec<CellDiff> = vec![];
360
361 let (start_row, start_col, end_row, end_col) = diff_range(
362 old_range.start(),
363 new_range.start(),
364 old_range.end(),
365 new_range.end(),
366 );
367
368 for row in start_row..end_row {
369 for col in start_col..end_col {
370 let empty = String::new();
371 let old_cell = old_range.get_value((row, col)).unwrap_or(&empty);
372 let new_cell = new_range.get_value((row, col)).unwrap_or(&empty);
373
374 if old_cell != new_cell {
375 let row1 = (row + 1) as usize;
376 let col1 = (col + 1) as usize;
377 cell_diffs.push(CellDiff {
378 row: row1,
379 col: col1,
380 addr: cell_pos_to_address(row1, col1),
381 kind: CellDiffKind::Formula,
382 old: if old_cell.is_empty() {
383 None
384 } else {
385 Some(old_cell.to_string())
386 },
387 new: if new_cell.is_empty() {
388 None
389 } else {
390 Some(new_cell.to_string())
391 },
392 });
393 }
394 }
395 }
396
397 if !cell_diffs.is_empty() {
398 self.cell_diffs.push(SheetCellDiff {
399 sheet: sheet.clone(),
400 cells: cell_diffs,
401 });
402 }
403 }
404
405 Ok(())
406 }
407
408 fn normalize_cell_diffs(&mut self) {
413 self.cell_diffs.sort_by(|a, b| a.sheet.cmp(&b.sheet));
414
415 let mut merged: Vec<SheetCellDiff> = vec![];
416 for entry in self.cell_diffs.drain(..) {
417 match merged.iter_mut().find(|m| m.sheet == entry.sheet) {
418 Some(existing) => existing.cells.extend(entry.cells),
419 None => merged.push(entry),
420 }
421 }
422
423 for sheet_diff in &mut merged {
424 sheet_diff.cells.sort_by(|a, b| {
425 a.row
426 .cmp(&b.row)
427 .then_with(|| a.col.cmp(&b.col))
428 .then_with(|| a.kind.cmp(&b.kind))
429 });
430 }
431
432 self.cell_diffs = merged;
433 }
434}