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 = matches!(sd.change, SheetChange::Added | SheetChange::Removed);
84
85 if !has_cell_changes && !is_structural {
86 continue;
87 }
88
89 let label = sheet_label(sd);
90 writeln!(out, "@@ sheet: {label} @@").unwrap();
91
92 match &sd.change {
93 SheetChange::Added => {
94 writeln!(out, "+[sheet added]").unwrap();
95 }
96 SheetChange::Removed => {
97 writeln!(out, "-[sheet removed]").unwrap();
98 }
99 SheetChange::Renamed { .. } | SheetChange::RenamedAndMoved { .. } => {
100 let old_n = sd
101 .old_sheet
102 .as_ref()
103 .map(|s| s.name.as_str())
104 .unwrap_or("?");
105 let new_n = sd
106 .new_sheet
107 .as_ref()
108 .map(|s| s.name.as_str())
109 .unwrap_or("?");
110 writeln!(out, " [renamed: '{old_n}' → '{new_n}']").unwrap();
111 }
112 _ => {}
113 }
114
115 for cd in &sd.cell_diffs {
116 write_cell_diff(&mut out, cd);
117 }
118 }
119
120 write_diagnostics_section(&mut out, &diff.diagnostics);
121 out
122}
123
124fn write_cell_diff(out: &mut String, cd: &CellDiff) {
125 let addr = &cd.address.a1;
126 if let Some(vc) = &cd.value {
127 writeln!(out, "-{addr}\t{}", vc.old.display_string()).unwrap();
128 writeln!(out, "+{addr}\t{}", vc.new.display_string()).unwrap();
129 }
130 if let Some(fc) = &cd.formula {
131 let old_f = fc.old.as_ref().map(|t| t.raw.as_str()).unwrap_or("");
132 let new_f = fc.new.as_ref().map(|t| t.raw.as_str()).unwrap_or("");
133 if !old_f.is_empty() || fc.old.is_none() && fc.new.is_some() {
134 writeln!(out, "-{addr}~\t{old_f}").unwrap();
136 }
137 if !new_f.is_empty() || fc.new.is_none() && fc.old.is_some() {
138 writeln!(out, "+{addr}~\t{new_f}").unwrap();
139 }
140 }
141}
142
143fn write_diagnostics_section(out: &mut String, diagnostics: &[Diagnostic]) {
144 let shown: Vec<_> = diagnostics
145 .iter()
146 .filter(|d| d.severity >= Severity::Warning)
147 .collect();
148 if shown.is_empty() {
149 return;
150 }
151 writeln!(out, "\n# Diagnostics").unwrap();
152 for d in shown {
153 let prefix = match d.severity {
154 Severity::Error => "ERROR",
155 Severity::Warning => "WARN",
156 Severity::Info => "INFO",
157 };
158 writeln!(out, " [{prefix}] {} — {}", d.kind.code(), d.message).unwrap();
159 }
160}
161
162fn sheet_label(sd: &SheetDiff) -> String {
167 sd.new_sheet
168 .as_ref()
169 .or(sd.old_sheet.as_ref())
170 .map(|s| s.name.clone())
171 .unwrap_or_else(|| "?".into())
172}