sheets_diff/output/
text.rs1use std::fmt::Write as FmtWrite;
7
8use crate::model::{CellDiff, Diagnostic, Severity, SheetChange, SheetDiff, WorkbookDiff};
9
10pub fn render_summary(diff: &WorkbookDiff) -> String {
16 let mut out = String::new();
17 let s = &diff.summary;
18
19 let old_name = diff.old.source.display_name.as_deref().unwrap_or("old");
20 let new_name = diff.new.source.display_name.as_deref().unwrap_or("new");
21 writeln!(out, "sheets-diff: {old_name} → {new_name}").unwrap();
22 writeln!(
23 out,
24 " sheets : {} added, {} removed, {} renamed, {} changed",
25 s.sheets_added, s.sheets_removed, s.sheets_renamed, s.sheets_changed
26 )
27 .unwrap();
28 writeln!(
29 out,
30 " cells : {} changed ({} value, {} formula)",
31 s.cells_changed, s.values_changed, s.formulas_changed
32 )
33 .unwrap();
34
35 if s.diagnostics.errors + s.diagnostics.warnings > 0 {
36 writeln!(
37 out,
38 " diagnostics: {} error(s), {} warning(s)",
39 s.diagnostics.errors, s.diagnostics.warnings
40 )
41 .unwrap();
42 }
43
44 for sd in &diff.sheets {
45 write_sheet_line(&mut out, sd);
46 }
47
48 out
49}
50
51fn write_sheet_line(out: &mut String, sd: &SheetDiff) {
52 let label = sheet_label(sd);
53 let tag = match &sd.change {
54 SheetChange::Added => " [added]",
55 SheetChange::Removed => " [removed]",
56 SheetChange::Renamed { .. } => " [renamed]",
57 SheetChange::RenamedAndMoved { .. } => " [renamed+moved]",
58 SheetChange::Moved => " [moved]",
59 SheetChange::Unchanged => return,
60 SheetChange::Modified => "",
61 };
62 let cells = sd.summary.cells_changed;
63 if cells > 0 || !tag.is_empty() {
64 writeln!(out, " sheet '{label}'{tag}: {cells} cell(s) changed").unwrap();
65 }
66}
67
68pub fn render_unified(diff: &WorkbookDiff) -> String {
74 let mut out = String::new();
75
76 let old_name = diff.old.source.display_name.as_deref().unwrap_or("old");
77 let new_name = diff.new.source.display_name.as_deref().unwrap_or("new");
78 writeln!(out, "--- {old_name}").unwrap();
79 writeln!(out, "+++ {new_name}").unwrap();
80
81 for sd in &diff.sheets {
82 let has_cell_changes = !sd.cell_diffs.is_empty();
83 let is_structural =
84 matches!(sd.change, SheetChange::Added | SheetChange::Removed);
85
86 if !has_cell_changes && !is_structural {
87 continue;
88 }
89
90 let label = sheet_label(sd);
91 writeln!(out, "@@ sheet: {label} @@").unwrap();
92
93 match &sd.change {
94 SheetChange::Added => {
95 writeln!(out, "+[sheet added]").unwrap();
96 }
97 SheetChange::Removed => {
98 writeln!(out, "-[sheet removed]").unwrap();
99 }
100 SheetChange::Renamed { .. } | SheetChange::RenamedAndMoved { .. } => {
101 let old_n = sd.old_sheet.as_ref().map(|s| s.name.as_str()).unwrap_or("?");
102 let new_n = sd.new_sheet.as_ref().map(|s| s.name.as_str()).unwrap_or("?");
103 writeln!(out, " [renamed: '{old_n}' → '{new_n}']").unwrap();
104 }
105 _ => {}
106 }
107
108 for cd in &sd.cell_diffs {
109 write_cell_diff(&mut out, cd);
110 }
111 }
112
113 write_diagnostics_section(&mut out, &diff.diagnostics);
114 out
115}
116
117fn write_cell_diff(out: &mut String, cd: &CellDiff) {
118 let addr = &cd.address.a1;
119 if let Some(vc) = &cd.value {
120 writeln!(out, "-{addr}\t{}", vc.old.display_string()).unwrap();
121 writeln!(out, "+{addr}\t{}", vc.new.display_string()).unwrap();
122 }
123 if let Some(fc) = &cd.formula {
124 let old_f = fc.old.as_ref().map(|t| t.raw.as_str()).unwrap_or("");
125 let new_f = fc.new.as_ref().map(|t| t.raw.as_str()).unwrap_or("");
126 if !old_f.is_empty() || fc.old.is_none() && fc.new.is_some() {
127 writeln!(out, "-{addr}~\t{old_f}").unwrap();
129 }
130 if !new_f.is_empty() || fc.new.is_none() && fc.old.is_some() {
131 writeln!(out, "+{addr}~\t{new_f}").unwrap();
132 }
133 }
134}
135
136fn write_diagnostics_section(out: &mut String, diagnostics: &[Diagnostic]) {
137 let shown: Vec<_> = diagnostics
138 .iter()
139 .filter(|d| d.severity >= Severity::Warning)
140 .collect();
141 if shown.is_empty() {
142 return;
143 }
144 writeln!(out, "\n# Diagnostics").unwrap();
145 for d in shown {
146 let prefix = match d.severity {
147 Severity::Error => "ERROR",
148 Severity::Warning => "WARN",
149 Severity::Info => "INFO",
150 };
151 writeln!(out, " [{prefix}] {} — {}", d.kind.code(), d.message).unwrap();
152 }
153}
154
155fn sheet_label(sd: &SheetDiff) -> String {
160 sd.new_sheet
161 .as_ref()
162 .or(sd.old_sheet.as_ref())
163 .map(|s| s.name.clone())
164 .unwrap_or_else(|| "?".into())
165}