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 #[inline]
253 pub fn display_default(&self) -> String {
254 self.display_string()
255 }
256}
257
258#[derive(Clone, Copy, PartialEq, Eq, Debug)]
264#[cfg_attr(feature = "serde", derive(Serialize))]
265pub enum DisplaySource {
266 ReaderProvided,
268 SheetsDiffDefault,
270 ApplicationProvided,
272}
273
274#[derive(Clone, PartialEq, Eq, Debug, Default)]
280#[cfg_attr(feature = "serde", derive(Serialize))]
281pub struct CellNumberFormat {
282 pub id: Option<u32>,
284 pub code: Option<String>,
286}
287
288#[derive(Clone, PartialEq, Eq, Debug)]
293#[cfg_attr(feature = "serde", derive(Serialize))]
294pub struct CellDisplay {
295 pub text: String,
297 pub format: Option<CellNumberFormat>,
299 pub source: DisplaySource,
300}
301
302impl CellDisplay {
303 pub fn from_value(value: &CellValue) -> Self {
305 Self {
306 text: value.display_default(),
307 format: None,
308 source: DisplaySource::SheetsDiffDefault,
309 }
310 }
311}
312
313#[derive(Clone, PartialEq, Debug)]
319#[cfg_attr(feature = "serde", derive(Serialize))]
320pub struct CellSnapshot {
321 pub value: CellValue,
322 pub formula: Option<crate::model::FormulaText>,
323 pub display: Option<CellDisplay>,
324}
325
326impl CellSnapshot {
327 pub fn preferred_display(&self) -> String {
330 self.display
331 .as_ref()
332 .map(|d| d.text.clone())
333 .unwrap_or_else(|| self.value.display_default())
334 }
335}
336
337#[non_exhaustive]
343#[derive(Clone, PartialEq, Eq, Debug)]
344#[cfg_attr(feature = "serde", derive(Serialize))]
345pub enum ValueDifferenceKind {
346 TypeChanged,
348 ContentChanged,
350 NumericOutsideTolerance,
352 DateTimeChanged,
354 ErrorKindChanged,
356 DisplayStringChanged,
358}
359
360#[derive(Clone, PartialEq, Debug)]
362#[cfg_attr(feature = "serde", derive(Serialize))]
363pub struct ValueChange {
364 pub old: CellValue,
365 pub new: CellValue,
366 pub reason: ValueDifferenceKind,
367}
368
369#[derive(Clone, PartialEq, Eq, Debug)]
371#[cfg_attr(feature = "serde", derive(Serialize))]
372pub struct FormulaText {
373 pub raw: String,
374 pub normalized: Option<String>,
377}
378
379#[derive(Clone, PartialEq, Eq, Debug)]
383#[cfg_attr(feature = "serde", derive(Serialize))]
384pub struct FormulaChange {
385 pub old: Option<FormulaText>,
386 pub new: Option<FormulaText>,
387}
388
389#[derive(Clone, PartialEq, Eq, Debug)]
391#[cfg_attr(feature = "serde", derive(Serialize))]
392pub struct FormatChange {
393 }
395
396#[derive(Clone, Copy, PartialEq, Eq, Debug)]
398#[cfg_attr(feature = "serde", derive(Serialize))]
399pub enum CellChangeKind {
400 Added,
401 Removed,
402 Modified,
403}
404
405#[non_exhaustive]
410#[derive(Clone, PartialEq, Debug)]
411#[cfg_attr(feature = "serde", derive(Serialize))]
412pub struct CellDiff {
413 pub address: CellAddress,
414 pub value: Option<ValueChange>,
415 pub formula: Option<FormulaChange>,
416 pub format: Option<FormatChange>,
418 pub diagnostics: Vec<Diagnostic>,
419}
420
421impl CellDiff {
422 pub fn change_kind(&self) -> CellChangeKind {
428 let has_old = self.value.as_ref().map(|v| !v.old.is_empty()).unwrap_or(false)
429 || self.formula.as_ref().map(|f| f.old.is_some()).unwrap_or(false);
430 let has_new = self.value.as_ref().map(|v| !v.new.is_empty()).unwrap_or(false)
431 || self.formula.as_ref().map(|f| f.new.is_some()).unwrap_or(false);
432 match (has_old, has_new) {
433 (false, true) => CellChangeKind::Added,
434 (true, false) => CellChangeKind::Removed,
435 _ => CellChangeKind::Modified,
436 }
437 }
438}
439
440#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
446#[cfg_attr(feature = "serde", derive(Serialize))]
447pub enum Severity {
448 Info,
449 Warning,
450 Error,
451}
452
453#[derive(Clone, Copy, PartialEq, Eq, Debug)]
455#[cfg_attr(feature = "serde", derive(Serialize))]
456pub enum DiffStage {
457 Open,
458 Metadata,
459 Match,
460 Read,
461 Normalize,
462 Compare,
463 Aggregate,
464}
465
466#[derive(Clone, PartialEq, Debug)]
468#[cfg_attr(feature = "serde", derive(Serialize))]
469pub struct DiagnosticLocation {
470 pub stage: DiffStage,
471 pub sheet_order: Option<usize>,
473 pub sheet_name: Option<String>,
474 pub address: Option<CellAddress>,
475}
476
477#[non_exhaustive]
482#[derive(Clone, PartialEq, Eq, Debug)]
483#[cfg_attr(feature = "serde", derive(Serialize))]
484pub enum DiagnosticKind {
485 FormulaUnavailable,
486 FormulaCachedValueUnverified,
487 AmbiguousSheetMatch { candidates: Vec<SheetRef> },
488 UnsupportedCellValue { detail: String },
489 UnsupportedWorkbookFeature { feature: String },
490 UnsupportedWorkbookMetadata { category: String },
491 DefinedNameScopeUnknown,
492 DateTimeNotNormalized,
493 LimitTruncatedCells { limit: String, observed: u64 },
494}
495
496impl DiagnosticKind {
497 pub fn code(&self) -> &'static str {
500 match self {
501 DiagnosticKind::FormulaUnavailable => "formula_unavailable",
502 DiagnosticKind::FormulaCachedValueUnverified => "formula_cached_value_unverified",
503 DiagnosticKind::AmbiguousSheetMatch { .. } => "ambiguous_sheet_match",
504 DiagnosticKind::UnsupportedCellValue { .. } => "unsupported_cell_value",
505 DiagnosticKind::UnsupportedWorkbookFeature { .. } => "unsupported_workbook_feature",
506 DiagnosticKind::UnsupportedWorkbookMetadata { .. } => "unsupported_workbook_metadata",
507 DiagnosticKind::DefinedNameScopeUnknown => "defined_name_scope_unknown",
508 DiagnosticKind::DateTimeNotNormalized => "datetime_not_normalized",
509 DiagnosticKind::LimitTruncatedCells { .. } => "limit_truncated_cells",
510 }
511 }
512}
513
514#[derive(Clone, PartialEq, Debug)]
516#[cfg_attr(feature = "serde", derive(Serialize))]
517pub struct Diagnostic {
518 pub severity: Severity,
519 pub kind: DiagnosticKind,
520 pub location: DiagnosticLocation,
521 pub message: String,
523}
524
525#[derive(Clone, Default, PartialEq, Debug)]
531#[cfg_attr(feature = "serde", derive(Serialize))]
532pub struct SheetSummary {
533 pub cells_changed: usize,
534 pub values_changed: usize,
535 pub formulas_changed: usize,
536}
537
538#[derive(Clone, Default, PartialEq, Debug)]
540#[cfg_attr(feature = "serde", derive(Serialize))]
541pub struct DiagnosticSummary {
542 pub errors: usize,
543 pub warnings: usize,
544 pub info: usize,
545}
546
547#[derive(Clone, Default, PartialEq, Debug)]
549#[cfg_attr(feature = "serde", derive(Serialize))]
550pub struct DiffSummary {
551 pub sheets_added: usize,
552 pub sheets_removed: usize,
553 pub sheets_renamed: usize,
554 pub sheets_moved: usize,
555 pub sheets_changed: usize,
556 pub cells_changed: usize,
557 pub values_changed: usize,
558 pub formulas_changed: usize,
559 pub diagnostics: DiagnosticSummary,
560}
561
562#[derive(Clone, Default, PartialEq, Debug)]
567#[cfg_attr(feature = "serde", derive(Serialize))]
568pub struct DiffMetrics {
569 pub sheets_read: u32,
570 pub cells_read: u64,
571 pub cells_compared: u64,
572 pub diffs_emitted: u64,
573 pub diagnostics_emitted: u64,
574}
575
576#[non_exhaustive]
584#[derive(Clone, PartialEq, Debug)]
585#[cfg_attr(feature = "serde", derive(Serialize))]
586pub struct AlignmentSummary {
587 pub inserted_rows: usize,
588 pub removed_rows: usize,
589 pub matched_rows: usize,
590 pub confidence: MatchConfidence,
591}
592
593#[non_exhaustive]
595#[derive(Clone, PartialEq, Debug)]
596#[cfg_attr(feature = "serde", derive(Serialize))]
597pub struct SheetDiff {
598 pub old_sheet: Option<SheetRef>,
600 pub new_sheet: Option<SheetRef>,
602 pub change: SheetChange,
603 pub cell_diffs: Vec<CellDiff>,
605 pub compared_range: ComparedRange,
606 pub alignment_summary: Option<AlignmentSummary>,
608 pub diagnostics: Vec<Diagnostic>,
609 pub summary: SheetSummary,
610}
611
612#[non_exhaustive]
618#[derive(Clone, PartialEq, Debug)]
619#[cfg_attr(feature = "serde", derive(Serialize))]
620pub struct WorkbookChange {
621 }
623
624#[non_exhaustive]
626#[derive(Clone, PartialEq, Debug)]
627#[cfg_attr(feature = "serde", derive(Serialize))]
628pub struct WorkbookObjectChange {
629 }
631
632#[non_exhaustive]
642#[derive(Clone, PartialEq, Debug)]
643#[cfg_attr(feature = "serde", derive(Serialize))]
644pub struct WorkbookDiff {
645 pub old: WorkbookSideInfo,
646 pub new: WorkbookSideInfo,
647 pub sheets: Vec<SheetDiff>,
650 pub workbook_changes: Vec<WorkbookChange>,
652 pub object_changes: Vec<WorkbookObjectChange>,
654 pub diagnostics: Vec<Diagnostic>,
655 pub summary: DiffSummary,
656 pub metrics: DiffMetrics,
658}
659
660impl WorkbookDiff {
665 pub(crate) fn derive_summary(sheets: &[SheetDiff], diagnostics: &[Diagnostic]) -> DiffSummary {
666 let mut s = DiffSummary::default();
667 for sd in sheets {
668 match sd.change {
669 SheetChange::Added => s.sheets_added += 1,
670 SheetChange::Removed => s.sheets_removed += 1,
671 SheetChange::Renamed { .. } => {
672 s.sheets_renamed += 1;
673 if !sd.cell_diffs.is_empty() {
674 s.sheets_changed += 1;
675 }
676 }
677 SheetChange::RenamedAndMoved { .. } => {
678 s.sheets_renamed += 1;
679 s.sheets_moved += 1;
680 if !sd.cell_diffs.is_empty() {
681 s.sheets_changed += 1;
682 }
683 }
684 SheetChange::Moved => {
685 s.sheets_moved += 1;
686 if !sd.cell_diffs.is_empty() {
687 s.sheets_changed += 1;
688 }
689 }
690 SheetChange::Modified => s.sheets_changed += 1,
691 SheetChange::Unchanged => {}
692 }
693 s.cells_changed += sd.summary.cells_changed;
694 s.values_changed += sd.summary.values_changed;
695 s.formulas_changed += sd.summary.formulas_changed;
696 }
697 for d in diagnostics {
698 match d.severity {
699 Severity::Error => s.diagnostics.errors += 1,
700 Severity::Warning => s.diagnostics.warnings += 1,
701 Severity::Info => s.diagnostics.info += 1,
702 }
703 }
704 s
705 }
706}