1use std::{fmt, fs::File, io::BufReader};
2
3use calamine::{open_workbook, Data, Reader, Xlsx};
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9pub enum CellDiffKind {
10 Value,
11 Formula,
12}
13
14impl fmt::Display for CellDiffKind {
15 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
16 match self {
17 CellDiffKind::Formula => write!(f, "formula"),
18 CellDiffKind::Value => write!(f, "value"),
19 }
20 }
21}
22
23#[derive(Clone, Debug)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26pub struct Diff {
27 pub old_filepath: String,
28 pub new_filepath: String,
29 pub sheet_diff: Vec<SheetDiff>,
30 pub cell_diffs: Vec<SheetCellDiff>,
31}
32
33#[derive(Clone, Debug)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35pub struct SheetDiff {
36 pub old: Option<String>,
37 pub new: Option<String>,
38}
39
40#[derive(Clone, Debug)]
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42pub struct SheetCellDiff {
43 pub sheet: String,
44 pub cells: Vec<CellDiff>,
45}
46
47#[derive(Clone, Debug)]
48#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
49pub struct CellDiff {
50 pub row: usize,
51 pub col: usize,
52 pub addr: String,
53 pub kind: CellDiffKind,
54 pub old: Option<String>,
55 pub new: Option<String>,
56}
57
58impl Diff {
59 pub fn new(old_filepath: &str, new_filepath: &str) -> Self {
61 let mut ret = Diff {
62 old_filepath: old_filepath.to_owned(),
63 new_filepath: new_filepath.to_owned(),
64 sheet_diff: vec![],
65 cell_diffs: vec![],
66 };
67
68 ret.collect_diff();
69
70 ret.cell_diffs.sort_by(|a, b| a.sheet.cmp(&b.sheet));
71
72 let mut merged_cell_diffs: Vec<SheetCellDiff> = vec![];
73 ret.cell_diffs.iter().for_each(|a| {
74 let found = merged_cell_diffs.iter_mut().find(|b| b.sheet == a.sheet);
75 if let Some(found) = found {
76 found.cells.extend(a.cells.clone());
77 } else {
78 merged_cell_diffs.push(a.clone());
79 }
80 });
81 merged_cell_diffs.iter_mut().for_each(|x| {
82 x.cells
83 .sort_by(|a, b| a.addr.cmp(&b.addr).then_with(|| a.kind.cmp(&b.kind)));
84 });
85 ret.cell_diffs = merged_cell_diffs;
86
87 ret
88 }
89
90 pub fn diff(&mut self) -> Diff {
93 self.clone()
94 }
95
96 pub fn unified_diff(&mut self) -> UnifiedDiff {
98 let mut ret: Vec<UnifiedDiffLine> = vec![];
99
100 if !self.sheet_diff.is_empty() {
101 ret.push(UnifiedDiffLine {
102 kind: UnifiedDiffKind::OldTitle,
103 text: format!("{} (sheet names)", self.old_filepath),
104 });
105 ret.push(UnifiedDiffLine {
106 kind: UnifiedDiffKind::NewTitle,
107 text: format!("{} (sheet names)", self.new_filepath),
108 });
109
110 self.sheet_diff.iter().for_each(|x| {
111 if let Some(sheet) = x.old.as_ref() {
112 ret.push(UnifiedDiffLine {
113 kind: UnifiedDiffKind::OldContent,
114 text: sheet.to_owned(),
115 });
116 }
117 if let Some(sheet) = x.new.as_ref() {
118 ret.push(UnifiedDiffLine {
119 kind: UnifiedDiffKind::NewContent,
120 text: sheet.to_owned(),
121 });
122 }
123 });
124 }
125
126 self.cell_diffs.iter().for_each(|x| {
127 ret.push(UnifiedDiffLine {
128 kind: UnifiedDiffKind::OldTitle,
129 text: format!("{} [{}]", self.old_filepath, x.sheet),
130 });
131 ret.push(UnifiedDiffLine {
132 kind: UnifiedDiffKind::NewTitle,
133 text: format!("{} [{}]", self.new_filepath, x.sheet),
134 });
135
136 x.cells.iter().for_each(|x| {
137 ret.push(UnifiedDiffLine {
138 kind: UnifiedDiffKind::DiffPos,
139 text: format!("{}({},{}) {}", x.addr, x.row, x.col, x.kind),
140 });
141
142 if let Some(sheet) = x.old.as_ref() {
143 ret.push(UnifiedDiffLine {
144 kind: UnifiedDiffKind::OldContent,
145 text: sheet.to_owned(),
146 });
147 }
148 if let Some(sheet) = x.new.as_ref() {
149 ret.push(UnifiedDiffLine {
150 kind: UnifiedDiffKind::NewContent,
151 text: sheet.to_owned(),
152 });
153 }
154 });
155 });
156
157 UnifiedDiff { lines: ret }
158 }
159
160 fn collect_diff(&mut self) {
162 let mut old_workbook: Xlsx<BufReader<File>> = open_workbook(self.old_filepath.as_str())
163 .expect(format!("Cannot open {}", self.old_filepath.as_str()).as_str());
164 let mut new_workbook: Xlsx<BufReader<File>> = open_workbook(self.new_filepath.as_str())
165 .expect(format!("Cannot open {}", self.new_filepath.as_str()).as_str());
166
167 let old_sheets = old_workbook.sheet_names().to_owned();
168 let new_sheets = new_workbook.sheet_names().to_owned();
169
170 self.collect_sheet_diff(&old_sheets, &new_sheets);
171
172 let same_name_sheets = filter_same_name_sheets(&old_sheets, &new_sheets);
173 self.collect_cell_value_diff(&mut old_workbook, &mut new_workbook, &same_name_sheets);
174 self.collect_cell_formula_diff(&mut old_workbook, &mut new_workbook, &same_name_sheets);
175 }
176
177 fn collect_sheet_diff(&mut self, old_sheets: &Vec<String>, new_sheets: &Vec<String>) {
179 if *old_sheets == *new_sheets {
180 return;
181 }
182
183 for sheet in old_sheets {
184 if !new_sheets.contains(sheet) {
185 self.sheet_diff.push(SheetDiff {
186 old: Some(sheet.to_owned()),
187 new: None,
188 });
189 }
190 }
191 for sheet in new_sheets {
192 if !old_sheets.contains(sheet) {
193 self.sheet_diff.push(SheetDiff {
194 old: None,
195 new: Some(sheet.to_owned()),
196 });
197 }
198 }
199 }
200
201 fn collect_cell_value_diff(
203 &mut self,
204 old_workbook: &mut Xlsx<BufReader<File>>,
205 new_workbook: &mut Xlsx<BufReader<File>>,
206 same_name_sheets: &Vec<String>,
207 ) {
208 for sheet in same_name_sheets {
209 if let (Ok(old_range), Ok(new_range)) = (
210 old_workbook.worksheet_range(sheet),
211 new_workbook.worksheet_range(sheet),
212 ) {
213 let mut cell_diffs: Vec<CellDiff> = vec![];
214
215 let max_rows = old_range.height().max(new_range.height()) as u32;
216 let max_cols = old_range.width().max(new_range.width()) as u32;
217
218 for row in 0..max_rows {
219 for col in 0..max_cols {
220 let old_cell = old_range.get_value((row, col)).unwrap_or(&Data::Empty);
221 let new_cell = new_range.get_value((row, col)).unwrap_or(&Data::Empty);
222
223 if old_cell != new_cell {
224 let row = (row + 1) as usize;
225 let col = (col + 1) as usize;
226 cell_diffs.push(CellDiff {
227 row,
228 col,
229 addr: cell_pos_to_address(row, col),
230 kind: CellDiffKind::Value,
231 old: if old_cell != &Data::Empty {
232 Some(old_cell.to_string())
233 } else {
234 None
235 },
236 new: if new_cell != &Data::Empty {
237 Some(new_cell.to_string())
238 } else {
239 None
240 },
241 });
242 }
243 }
244 }
245
246 if !cell_diffs.is_empty() {
247 let sheet_cell_diff = SheetCellDiff {
248 sheet: sheet.to_owned(),
249 cells: cell_diffs,
250 };
251 self.cell_diffs.push(sheet_cell_diff);
252 }
253 } else {
254 println!("Failed to read sheet: {}", sheet);
255 }
256 }
257 }
258
259 fn collect_cell_formula_diff(
261 &mut self,
262 old_workbook: &mut Xlsx<BufReader<File>>,
263 new_workbook: &mut Xlsx<BufReader<File>>,
264 same_name_sheets: &Vec<String>,
265 ) {
266 for sheet in same_name_sheets {
267 if let (Ok(old_range), Ok(new_range)) = (
268 old_workbook.worksheet_formula(sheet),
269 new_workbook.worksheet_formula(sheet),
270 ) {
271 let mut cell_diffs: Vec<CellDiff> = vec![];
272
273 let (old_start_row, old_start_col) = match old_range.start() {
274 Some((row, col)) => (row, col),
275 None => (u32::MAX, u32::MAX),
276 };
277 let (new_start_row, new_start_col) = match new_range.start() {
278 Some((row, col)) => (row, col),
279 None => (u32::MAX, u32::MAX),
280 };
281 let (old_end_row, old_end_col) = match old_range.end() {
282 Some((row, col)) => (row, col),
283 None => (u32::MIN, u32::MIN),
284 };
285 let (new_end_row, new_end_col) = match new_range.end() {
286 Some((row, col)) => (row, col),
287 None => (u32::MIN, u32::MIN),
288 };
289 let start_row = old_start_row.min(new_start_row);
290 let start_col = old_start_col.min(new_start_col);
291 let end_row = old_end_row.max(new_end_row);
292 let end_col = old_end_col.max(new_end_col);
293
294 for row in start_row..(end_row + 1) {
295 for col in start_col..(end_col + 1) {
296 let old_cell = match old_range.get_value((row, col)) {
297 Some(x) => &Data::String(x.to_string()),
298 None => &Data::Empty,
299 };
300 let new_cell = match new_range.get_value((row, col)) {
301 Some(x) => &Data::String(x.to_string()),
302 None => &Data::Empty,
303 };
304
305 if old_cell != new_cell {
306 let row = (row + 1) as usize;
307 let col = (col + 1) as usize;
308 cell_diffs.push(CellDiff {
309 row,
310 col,
311 addr: cell_pos_to_address(row, col),
312 kind: CellDiffKind::Formula,
313 old: if old_cell != &Data::Empty {
314 Some(old_cell.to_string())
315 } else {
316 None
317 },
318 new: if new_cell != &Data::Empty {
319 Some(new_cell.to_string())
320 } else {
321 None
322 },
323 });
324 }
325 }
326 }
327
328 if !cell_diffs.is_empty() {
329 let sheet_cell_diff = SheetCellDiff {
330 sheet: sheet.to_owned(),
331 cells: cell_diffs,
332 };
333 self.cell_diffs.push(sheet_cell_diff);
334 }
335 } else {
336 println!("Failed to read sheet: {}", sheet);
337 }
338 }
339 }
340}
341
342fn filter_same_name_sheets<'a>(
344 old_sheets: &'a Vec<String>,
345 new_sheets: &'a Vec<String>,
346) -> Vec<String> {
347 old_sheets
348 .iter()
349 .filter(|s| new_sheets.contains(s))
350 .map(|s| s.to_owned())
351 .collect()
352}
353
354fn cell_pos_to_address(row: usize, col: usize) -> String {
356 let col_letter = (col as u8 - 1) / 26;
357 let col_index = (col as u8 - 1) % 26;
358
359 let col_char = if col_letter == 0 {
360 ((b'A' + col_index) as char).to_string()
361 } else {
362 let first_char = (b'A' + col_letter - 1) as char;
363 let second_char = (b'A' + col_index) as char;
364 format!("{}{}", first_char, second_char)
365 };
366
367 format!("{}{}", col_char, row)
368}
369
370#[derive(Clone, Debug)]
372#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
373pub enum UnifiedDiffKind {
374 OldTitle,
375 NewTitle,
376 DiffPos,
377 OldContent,
378 NewContent,
379}
380
381#[derive(Clone, Debug)]
383#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
384pub struct UnifiedDiff {
385 pub lines: Vec<UnifiedDiffLine>,
386}
387
388impl fmt::Display for UnifiedDiff {
389 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
391 let str = self
392 .lines
393 .iter()
394 .map(|x| x.to_string())
395 .collect::<Vec<String>>();
396 write!(f, "{}", str.join("\n"))
397 }
398}
399
400#[derive(Clone, Debug)]
402#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
403pub struct UnifiedDiffLine {
404 pub kind: UnifiedDiffKind,
405 pub text: String,
406}
407
408impl fmt::Display for UnifiedDiffLine {
409 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
411 match self.kind {
412 UnifiedDiffKind::OldTitle => write!(f, "--- {}", self.text),
413 UnifiedDiffKind::NewTitle => write!(f, "+++ {}", self.text),
414 UnifiedDiffKind::DiffPos => write!(f, "@@ {} @@", self.text),
415 UnifiedDiffKind::OldContent => write!(f, "- {}", self.text),
416 UnifiedDiffKind::NewContent => write!(f, "+ {}", self.text),
417 }
418 }
419}