1use crate::address::CellAddress;
7use crate::model::{
8 CellChangeKind, CellDiff, SheetChange, Severity, WorkbookDiff,
9};
10
11#[derive(Clone, Debug)]
17pub struct ViewFilter {
18 pub include_values: bool,
19 pub include_formulas: bool,
20 pub include_formatting: bool,
22 pub include_info_diagnostics: bool,
23 pub sheets: Option<Vec<usize>>,
25}
26
27impl Default for ViewFilter {
28 fn default() -> Self {
29 Self {
30 include_values: true,
31 include_formulas: true,
32 include_formatting: false,
33 include_info_diagnostics: false,
34 sheets: None,
35 }
36 }
37}
38
39#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize))]
48pub struct ChangeAnchor {
49 pub sheet_index: usize,
50 pub row: u32,
51 pub col: u32,
52}
53
54pub struct CellChangeRow<'a> {
60 pub anchor: ChangeAnchor,
62 pub sheet_name: &'a str,
63 pub address: &'a CellAddress,
64 pub change_kind: CellChangeKind,
66 pub old_display: String,
68 pub new_display: String,
70 pub formula_changed: bool,
72 pub old_formula: Option<&'a str>,
75 pub new_formula: Option<&'a str>,
77 pub max_severity: Option<Severity>,
79}
80
81impl<'a> CellChangeRow<'a> {
82 pub fn to_owned_row(&self) -> OwnedCellChangeRow {
85 OwnedCellChangeRow {
86 anchor: self.anchor.clone(),
87 sheet_name: self.sheet_name.to_owned(),
88 address: self.address.clone(),
89 change_kind: self.change_kind,
90 old_display: self.old_display.clone(),
91 new_display: self.new_display.clone(),
92 formula_changed: self.formula_changed,
93 old_formula: self.old_formula.map(|s| s.to_owned()),
94 new_formula: self.new_formula.map(|s| s.to_owned()),
95 max_severity: self.max_severity,
96 }
97 }
98}
99
100#[derive(Clone, Debug)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize))]
107pub struct OwnedCellChangeRow {
108 pub anchor: ChangeAnchor,
109 pub sheet_name: String,
110 pub address: CellAddress,
111 pub change_kind: CellChangeKind,
112 pub old_display: String,
113 pub new_display: String,
114 pub formula_changed: bool,
115 pub old_formula: Option<String>,
116 pub new_formula: Option<String>,
117 pub max_severity: Option<Severity>,
118}
119
120pub struct SheetSummaryRow<'a> {
126 pub sheet_index: usize,
127 pub name: &'a str,
128 pub change: &'a SheetChange,
129 pub cells_changed: usize,
130 pub has_diagnostics: bool,
131}
132
133pub struct DiffView<'a> {
140 pub workbook: &'a WorkbookDiff,
141}
142
143impl<'a> DiffView<'a> {
144 pub fn new(workbook: &'a WorkbookDiff) -> Self {
145 Self { workbook }
146 }
147
148 pub fn sheets(&self) -> impl Iterator<Item = SheetSummaryRow<'a>> {
154 self.workbook.sheets.iter().enumerate().map(|(i, sd)| {
155 SheetSummaryRow {
156 sheet_index: i,
157 name: sd.new_sheet.as_ref().or(sd.old_sheet.as_ref())
158 .map(|s| s.name.as_str()).unwrap_or("?"),
159 change: &sd.change,
160 cells_changed: sd.summary.cells_changed,
161 has_diagnostics: !sd.diagnostics.is_empty(),
162 }
163 })
164 }
165
166 pub fn rows(&'a self, filter: &ViewFilter) -> Vec<CellChangeRow<'a>> {
174 let mut out = Vec::new();
175 for (sheet_idx, sd) in self.workbook.sheets.iter().enumerate() {
176 if let Some(ref allowed) = filter.sheets {
177 if !allowed.contains(&sheet_idx) { continue; }
178 }
179 let sheet_name = sd.new_sheet.as_ref().or(sd.old_sheet.as_ref())
180 .map(|s| s.name.as_str()).unwrap_or("?");
181 for cd in &sd.cell_diffs {
182 if let Some(row) = cell_to_row(cd, sheet_idx, sheet_name, filter) {
183 out.push(row);
184 }
185 }
186 }
187 out
188 }
189
190 pub fn row_count(&self, filter: &ViewFilter) -> usize {
192 self.rows(filter).len()
193 }
194
195 pub fn first(&self, filter: &ViewFilter) -> Option<ChangeAnchor> {
201 self.rows(filter).into_iter().next().map(|r| r.anchor)
202 }
203
204 pub fn next_after(&self, current: &ChangeAnchor, filter: &ViewFilter) -> Option<ChangeAnchor> {
206 let mut past = false;
207 for row in self.rows(filter).into_iter() {
208 if past {
209 return Some(row.anchor);
210 }
211 if &row.anchor == current {
212 past = true;
213 }
214 }
215 None
216 }
217
218 pub fn previous_before(&self, current: &ChangeAnchor, filter: &ViewFilter) -> Option<ChangeAnchor> {
220 let mut prev: Option<ChangeAnchor> = None;
221 for row in self.rows(filter).into_iter() {
222 if &row.anchor == current {
223 return prev;
224 }
225 prev = Some(row.anchor.clone());
226 }
227 None
228 }
229
230 pub fn sheet_rows(
236 &'a self,
237 sheet_index: usize,
238 filter: &ViewFilter,
239 ) -> Vec<CellChangeRow<'a>> {
240 let mut f = filter.clone();
241 f.sheets = Some(vec![sheet_index]);
242 self.rows(&f)
243 }
244}
245
246fn cell_to_row<'a>(
251 cd: &'a CellDiff,
252 sheet_index: usize,
253 sheet_name: &'a str,
254 filter: &ViewFilter,
255) -> Option<CellChangeRow<'a>> {
256 let has_value = cd.value.is_some() && filter.include_values;
257 let has_formula = cd.formula.is_some() && filter.include_formulas;
258
259 if !has_value && !has_formula {
260 return None;
261 }
262
263 let old_display = cd.value.as_ref()
264 .map(|vc| vc.old.display_string())
265 .unwrap_or_default();
266 let new_display = cd.value.as_ref()
267 .map(|vc| vc.new.display_string())
268 .unwrap_or_default();
269
270 let max_severity = cd.diagnostics.iter()
271 .map(|d| d.severity)
272 .max();
273
274 let (old_formula, new_formula) = match &cd.formula {
276 Some(fc) => (
277 fc.old.as_ref().map(|t| t.raw.as_str()),
278 fc.new.as_ref().map(|t| t.raw.as_str()),
279 ),
280 None => (None, None),
281 };
282
283 Some(CellChangeRow {
284 anchor: ChangeAnchor {
285 sheet_index,
286 row: cd.address.row,
287 col: cd.address.col,
288 },
289 sheet_name,
290 address: &cd.address,
291 change_kind: cd.change_kind(),
292 old_display,
293 new_display,
294 formula_changed: cd.formula.is_some(),
295 old_formula,
296 new_formula,
297 max_severity,
298 })
299}