1use std::fmt;
8
9#[cfg(feature = "serde")]
10use serde::Serialize;
11
12use crate::address::{CellAddress, ComparedRange};
13
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20#[cfg_attr(feature = "serde", derive(Serialize))]
21pub enum Side {
22 Old,
23 New,
24}
25
26impl fmt::Display for Side {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Side::Old => f.write_str("old"),
30 Side::New => f.write_str("new"),
31 }
32 }
33}
34
35#[derive(Clone, PartialEq, Eq, Debug)]
41#[cfg_attr(feature = "serde", derive(Serialize))]
42pub enum SourceKind {
43 Path,
44 Bytes,
45 Reader,
46 Unknown,
47}
48
49#[derive(Clone, PartialEq, Debug)]
54#[cfg_attr(feature = "serde", derive(Serialize))]
55pub struct SourceDescription {
56 pub kind: SourceKind,
57 pub display_name: Option<String>,
58}
59
60#[derive(Clone, PartialEq, Debug)]
65#[cfg_attr(feature = "serde", derive(Serialize))]
66pub struct WorkbookSideInfo {
67 pub source: SourceDescription,
68 pub workbook_name: Option<String>,
69 pub sheet_count: usize,
70}
71
72#[derive(Clone, PartialEq, Eq, Debug)]
80#[cfg_attr(feature = "serde", derive(Serialize))]
81pub struct SheetRef {
82 pub name: String,
83 pub index: usize,
84}
85
86#[derive(Clone, Copy, PartialEq, Eq, Debug)]
92#[cfg_attr(feature = "serde", derive(Serialize))]
93pub enum MatchConfidence {
94 Exact,
95 High,
96 Medium,
97 Low,
98}
99
100#[non_exhaustive]
102#[derive(Clone, PartialEq, Eq, Debug)]
103#[cfg_attr(feature = "serde", derive(Serialize))]
104pub enum SheetMatchReason {
105 ExactName,
106 IndexAndContent,
107 ContentSimilarity,
108}
109
110#[non_exhaustive]
115#[derive(Clone, PartialEq, Eq, Debug)]
116#[cfg_attr(feature = "serde", derive(Serialize))]
117pub enum SheetChange {
118 Unchanged,
120 Modified,
122 Added,
124 Removed,
126 Moved,
128 Renamed { confidence: MatchConfidence, reason: SheetMatchReason },
130 RenamedAndMoved { confidence: MatchConfidence, reason: SheetMatchReason },
132}
133
134#[derive(Clone, PartialEq, Debug)]
145#[cfg_attr(feature = "serde", derive(Serialize))]
146pub struct CellDateTime {
147 pub serial: f64,
148 pub is_1904: bool,
149 pub kind: DateTimeKind,
150 pub iso: Option<String>,
151}
152
153#[derive(Clone, Copy, PartialEq, Eq, Debug)]
155#[cfg_attr(feature = "serde", derive(Serialize))]
156pub enum DateTimeKind {
157 DateTime,
158 Date,
159 Time,
160}
161
162#[derive(Clone, PartialEq, Debug)]
164#[cfg_attr(feature = "serde", derive(Serialize))]
165pub struct CellDuration {
166 pub serial: f64,
167 pub iso: Option<String>,
168}
169
170#[non_exhaustive]
174#[derive(Clone, PartialEq, Eq, Debug)]
175#[cfg_attr(feature = "serde", derive(Serialize))]
176pub enum CellError {
177 Div0,
178 NA,
179 Name,
180 Null,
181 Num,
182 Ref,
183 Value,
184 GettingData,
185 Other(String),
186}
187
188impl fmt::Display for CellError {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 match self {
191 CellError::Div0 => f.write_str("#DIV/0!"),
192 CellError::NA => f.write_str("#N/A"),
193 CellError::Name => f.write_str("#NAME?"),
194 CellError::Null => f.write_str("#NULL!"),
195 CellError::Num => f.write_str("#NUM!"),
196 CellError::Ref => f.write_str("#REF!"),
197 CellError::Value => f.write_str("#VALUE!"),
198 CellError::GettingData => f.write_str("#GETTING_DATA"),
199 CellError::Other(s) => write!(f, "#{s}"),
200 }
201 }
202}
203
204#[non_exhaustive]
211#[derive(Clone, PartialEq, Debug)]
212#[cfg_attr(feature = "serde", derive(Serialize))]
213pub enum CellValue {
214 Empty,
215 Text(String),
216 Integer(i64),
217 Number(f64),
218 Bool(bool),
219 DateTime(CellDateTime),
220 Duration(CellDuration),
221 Error(CellError),
222 Unsupported { display: String, reason: String },
223}
224
225impl CellValue {
226 pub fn display_string(&self) -> String {
229 match self {
230 CellValue::Empty => String::new(),
231 CellValue::Text(s) => s.clone(),
232 CellValue::Integer(i) => i.to_string(),
233 CellValue::Number(f) => f.to_string(),
234 CellValue::Bool(b) => b.to_string(),
235 CellValue::DateTime(dt) => {
236 dt.iso.clone().unwrap_or_else(|| dt.serial.to_string())
237 }
238 CellValue::Duration(d) => {
239 d.iso.clone().unwrap_or_else(|| d.serial.to_string())
240 }
241 CellValue::Error(e) => e.to_string(),
242 CellValue::Unsupported { display, .. } => display.clone(),
243 }
244 }
245
246 pub fn is_empty(&self) -> bool {
248 matches!(self, CellValue::Empty)
249 }
250}
251
252#[non_exhaustive]
258#[derive(Clone, PartialEq, Eq, Debug)]
259#[cfg_attr(feature = "serde", derive(Serialize))]
260pub enum ValueDifferenceKind {
261 TypeChanged,
263 ContentChanged,
265 NumericOutsideTolerance,
267 DateTimeChanged,
269 ErrorKindChanged,
271 DisplayStringChanged,
273}
274
275#[derive(Clone, PartialEq, Debug)]
277#[cfg_attr(feature = "serde", derive(Serialize))]
278pub struct ValueChange {
279 pub old: CellValue,
280 pub new: CellValue,
281 pub reason: ValueDifferenceKind,
282}
283
284#[derive(Clone, PartialEq, Eq, Debug)]
286#[cfg_attr(feature = "serde", derive(Serialize))]
287pub struct FormulaText {
288 pub raw: String,
289 pub normalized: Option<String>,
292}
293
294#[derive(Clone, PartialEq, Eq, Debug)]
298#[cfg_attr(feature = "serde", derive(Serialize))]
299pub struct FormulaChange {
300 pub old: Option<FormulaText>,
301 pub new: Option<FormulaText>,
302}
303
304#[derive(Clone, PartialEq, Eq, Debug)]
306#[cfg_attr(feature = "serde", derive(Serialize))]
307pub struct FormatChange {
308 }
310
311#[derive(Clone, Copy, PartialEq, Eq, Debug)]
313#[cfg_attr(feature = "serde", derive(Serialize))]
314pub enum CellChangeKind {
315 Added,
316 Removed,
317 Modified,
318}
319
320#[non_exhaustive]
325#[derive(Clone, PartialEq, Debug)]
326#[cfg_attr(feature = "serde", derive(Serialize))]
327pub struct CellDiff {
328 pub address: CellAddress,
329 pub value: Option<ValueChange>,
330 pub formula: Option<FormulaChange>,
331 pub format: Option<FormatChange>,
333 pub diagnostics: Vec<Diagnostic>,
334}
335
336impl CellDiff {
337 pub fn change_kind(&self) -> CellChangeKind {
343 let has_old = self.value.as_ref().map(|v| !v.old.is_empty()).unwrap_or(false)
344 || self.formula.as_ref().map(|f| f.old.is_some()).unwrap_or(false);
345 let has_new = self.value.as_ref().map(|v| !v.new.is_empty()).unwrap_or(false)
346 || self.formula.as_ref().map(|f| f.new.is_some()).unwrap_or(false);
347 match (has_old, has_new) {
348 (false, true) => CellChangeKind::Added,
349 (true, false) => CellChangeKind::Removed,
350 _ => CellChangeKind::Modified,
351 }
352 }
353}
354
355#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
361#[cfg_attr(feature = "serde", derive(Serialize))]
362pub enum Severity {
363 Info,
364 Warning,
365 Error,
366}
367
368#[derive(Clone, Copy, PartialEq, Eq, Debug)]
370#[cfg_attr(feature = "serde", derive(Serialize))]
371pub enum DiffStage {
372 Open,
373 Metadata,
374 Match,
375 Read,
376 Normalize,
377 Compare,
378 Aggregate,
379}
380
381#[derive(Clone, PartialEq, Debug)]
383#[cfg_attr(feature = "serde", derive(Serialize))]
384pub struct DiagnosticLocation {
385 pub stage: DiffStage,
386 pub sheet_order: Option<usize>,
388 pub sheet_name: Option<String>,
389 pub address: Option<CellAddress>,
390}
391
392#[non_exhaustive]
397#[derive(Clone, PartialEq, Eq, Debug)]
398#[cfg_attr(feature = "serde", derive(Serialize))]
399pub enum DiagnosticKind {
400 FormulaUnavailable,
401 FormulaCachedValueUnverified,
402 AmbiguousSheetMatch { candidates: Vec<SheetRef> },
403 UnsupportedCellValue { detail: String },
404 UnsupportedWorkbookFeature { feature: String },
405 UnsupportedWorkbookMetadata { category: String },
406 DefinedNameScopeUnknown,
407 DateTimeNotNormalized,
408 LimitTruncatedCells { limit: String, observed: u64 },
409}
410
411impl DiagnosticKind {
412 pub fn code(&self) -> &'static str {
415 match self {
416 DiagnosticKind::FormulaUnavailable => "formula_unavailable",
417 DiagnosticKind::FormulaCachedValueUnverified => "formula_cached_value_unverified",
418 DiagnosticKind::AmbiguousSheetMatch { .. } => "ambiguous_sheet_match",
419 DiagnosticKind::UnsupportedCellValue { .. } => "unsupported_cell_value",
420 DiagnosticKind::UnsupportedWorkbookFeature { .. } => "unsupported_workbook_feature",
421 DiagnosticKind::UnsupportedWorkbookMetadata { .. } => "unsupported_workbook_metadata",
422 DiagnosticKind::DefinedNameScopeUnknown => "defined_name_scope_unknown",
423 DiagnosticKind::DateTimeNotNormalized => "datetime_not_normalized",
424 DiagnosticKind::LimitTruncatedCells { .. } => "limit_truncated_cells",
425 }
426 }
427}
428
429#[derive(Clone, PartialEq, Debug)]
431#[cfg_attr(feature = "serde", derive(Serialize))]
432pub struct Diagnostic {
433 pub severity: Severity,
434 pub kind: DiagnosticKind,
435 pub location: DiagnosticLocation,
436 pub message: String,
438}
439
440#[derive(Clone, Default, PartialEq, Debug)]
446#[cfg_attr(feature = "serde", derive(Serialize))]
447pub struct SheetSummary {
448 pub cells_changed: usize,
449 pub values_changed: usize,
450 pub formulas_changed: usize,
451}
452
453#[derive(Clone, Default, PartialEq, Debug)]
455#[cfg_attr(feature = "serde", derive(Serialize))]
456pub struct DiagnosticSummary {
457 pub errors: usize,
458 pub warnings: usize,
459 pub info: usize,
460}
461
462#[derive(Clone, Default, PartialEq, Debug)]
464#[cfg_attr(feature = "serde", derive(Serialize))]
465pub struct DiffSummary {
466 pub sheets_added: usize,
467 pub sheets_removed: usize,
468 pub sheets_renamed: usize,
469 pub sheets_moved: usize,
470 pub sheets_changed: usize,
471 pub cells_changed: usize,
472 pub values_changed: usize,
473 pub formulas_changed: usize,
474 pub diagnostics: DiagnosticSummary,
475}
476
477#[non_exhaustive]
483#[derive(Clone, PartialEq, Debug)]
484#[cfg_attr(feature = "serde", derive(Serialize))]
485pub struct AlignmentSummary {
486 }
488
489#[non_exhaustive]
491#[derive(Clone, PartialEq, Debug)]
492#[cfg_attr(feature = "serde", derive(Serialize))]
493pub struct SheetDiff {
494 pub old_sheet: Option<SheetRef>,
496 pub new_sheet: Option<SheetRef>,
498 pub change: SheetChange,
499 pub cell_diffs: Vec<CellDiff>,
501 pub compared_range: ComparedRange,
502 pub alignment_summary: Option<AlignmentSummary>,
504 pub diagnostics: Vec<Diagnostic>,
505 pub summary: SheetSummary,
506}
507
508#[non_exhaustive]
514#[derive(Clone, PartialEq, Debug)]
515#[cfg_attr(feature = "serde", derive(Serialize))]
516pub struct WorkbookChange {
517 }
519
520#[non_exhaustive]
522#[derive(Clone, PartialEq, Debug)]
523#[cfg_attr(feature = "serde", derive(Serialize))]
524pub struct WorkbookObjectChange {
525 }
527
528#[non_exhaustive]
538#[derive(Clone, PartialEq, Debug)]
539#[cfg_attr(feature = "serde", derive(Serialize))]
540pub struct WorkbookDiff {
541 pub old: WorkbookSideInfo,
542 pub new: WorkbookSideInfo,
543 pub sheets: Vec<SheetDiff>,
546 pub workbook_changes: Vec<WorkbookChange>,
548 pub object_changes: Vec<WorkbookObjectChange>,
550 pub diagnostics: Vec<Diagnostic>,
551 pub summary: DiffSummary,
552}
553
554impl WorkbookDiff {
559 pub(crate) fn derive_summary(sheets: &[SheetDiff], diagnostics: &[Diagnostic]) -> DiffSummary {
560 let mut s = DiffSummary::default();
561 for sd in sheets {
562 match sd.change {
563 SheetChange::Added => s.sheets_added += 1,
564 SheetChange::Removed => s.sheets_removed += 1,
565 SheetChange::Renamed { .. } => {
566 s.sheets_renamed += 1;
567 if !sd.cell_diffs.is_empty() {
568 s.sheets_changed += 1;
569 }
570 }
571 SheetChange::RenamedAndMoved { .. } => {
572 s.sheets_renamed += 1;
573 s.sheets_moved += 1;
574 if !sd.cell_diffs.is_empty() {
575 s.sheets_changed += 1;
576 }
577 }
578 SheetChange::Moved => {
579 s.sheets_moved += 1;
580 if !sd.cell_diffs.is_empty() {
581 s.sheets_changed += 1;
582 }
583 }
584 SheetChange::Modified => s.sheets_changed += 1,
585 SheetChange::Unchanged => {}
586 }
587 s.cells_changed += sd.summary.cells_changed;
588 s.values_changed += sd.summary.values_changed;
589 s.formulas_changed += sd.summary.formulas_changed;
590 }
591 for d in diagnostics {
592 match d.severity {
593 Severity::Error => s.diagnostics.errors += 1,
594 Severity::Warning => s.diagnostics.warnings += 1,
595 Severity::Info => s.diagnostics.info += 1,
596 }
597 }
598 s
599 }
600}