use std::fmt;
#[cfg(feature = "serde")]
use serde::Serialize;
use crate::address::{CellAddress, ComparedRange};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum Side {
Old,
New,
}
impl fmt::Display for Side {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Side::Old => f.write_str("old"),
Side::New => f.write_str("new"),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum SourceKind {
Path,
Bytes,
Reader,
Unknown,
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct SourceDescription {
pub kind: SourceKind,
pub display_name: Option<String>,
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct WorkbookSideInfo {
pub source: SourceDescription,
pub workbook_name: Option<String>,
pub sheet_count: usize,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct SheetRef {
pub name: String,
pub index: usize,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum MatchConfidence {
Exact,
High,
Medium,
Low,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum SheetMatchReason {
ExactName,
IndexAndContent,
ContentSimilarity,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum SheetChange {
Unchanged,
Modified,
Added,
Removed,
Moved,
Renamed { confidence: MatchConfidence, reason: SheetMatchReason },
RenamedAndMoved { confidence: MatchConfidence, reason: SheetMatchReason },
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct CellDateTime {
pub serial: f64,
pub is_1904: bool,
pub kind: DateTimeKind,
pub iso: Option<String>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum DateTimeKind {
DateTime,
Date,
Time,
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct CellDuration {
pub serial: f64,
pub iso: Option<String>,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum CellError {
Div0,
NA,
Name,
Null,
Num,
Ref,
Value,
GettingData,
Other(String),
}
impl fmt::Display for CellError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CellError::Div0 => f.write_str("#DIV/0!"),
CellError::NA => f.write_str("#N/A"),
CellError::Name => f.write_str("#NAME?"),
CellError::Null => f.write_str("#NULL!"),
CellError::Num => f.write_str("#NUM!"),
CellError::Ref => f.write_str("#REF!"),
CellError::Value => f.write_str("#VALUE!"),
CellError::GettingData => f.write_str("#GETTING_DATA"),
CellError::Other(s) => write!(f, "#{s}"),
}
}
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum CellValue {
Empty,
Text(String),
Integer(i64),
Number(f64),
Bool(bool),
DateTime(CellDateTime),
Duration(CellDuration),
Error(CellError),
Unsupported { display: String, reason: String },
}
impl CellValue {
pub fn display_string(&self) -> String {
match self {
CellValue::Empty => String::new(),
CellValue::Text(s) => s.clone(),
CellValue::Integer(i) => i.to_string(),
CellValue::Number(f) => f.to_string(),
CellValue::Bool(b) => b.to_string(),
CellValue::DateTime(dt) => {
dt.iso.clone().unwrap_or_else(|| dt.serial.to_string())
}
CellValue::Duration(d) => {
d.iso.clone().unwrap_or_else(|| d.serial.to_string())
}
CellValue::Error(e) => e.to_string(),
CellValue::Unsupported { display, .. } => display.clone(),
}
}
pub fn is_empty(&self) -> bool {
matches!(self, CellValue::Empty)
}
#[inline]
pub fn display_default(&self) -> String {
self.display_string()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum DisplaySource {
ReaderProvided,
SheetsDiffDefault,
ApplicationProvided,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct CellNumberFormat {
pub id: Option<u32>,
pub code: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct CellDisplay {
pub text: String,
pub format: Option<CellNumberFormat>,
pub source: DisplaySource,
}
impl CellDisplay {
pub fn from_value(value: &CellValue) -> Self {
Self {
text: value.display_default(),
format: None,
source: DisplaySource::SheetsDiffDefault,
}
}
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct CellSnapshot {
pub value: CellValue,
pub formula: Option<crate::model::FormulaText>,
pub display: Option<CellDisplay>,
}
impl CellSnapshot {
pub fn preferred_display(&self) -> String {
self.display
.as_ref()
.map(|d| d.text.clone())
.unwrap_or_else(|| self.value.display_default())
}
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum ValueDifferenceKind {
TypeChanged,
ContentChanged,
NumericOutsideTolerance,
DateTimeChanged,
ErrorKindChanged,
DisplayStringChanged,
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct ValueChange {
pub old: CellValue,
pub new: CellValue,
pub reason: ValueDifferenceKind,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct FormulaText {
pub raw: String,
pub normalized: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct FormulaChange {
pub old: Option<FormulaText>,
pub new: Option<FormulaText>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct FormatChange {
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum CellChangeKind {
Added,
Removed,
Modified,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct CellDiff {
pub address: CellAddress,
pub value: Option<ValueChange>,
pub formula: Option<FormulaChange>,
pub format: Option<FormatChange>,
pub diagnostics: Vec<Diagnostic>,
}
impl CellDiff {
pub fn change_kind(&self) -> CellChangeKind {
let has_old = self.value.as_ref().map(|v| !v.old.is_empty()).unwrap_or(false)
|| self.formula.as_ref().map(|f| f.old.is_some()).unwrap_or(false);
let has_new = self.value.as_ref().map(|v| !v.new.is_empty()).unwrap_or(false)
|| self.formula.as_ref().map(|f| f.new.is_some()).unwrap_or(false);
match (has_old, has_new) {
(false, true) => CellChangeKind::Added,
(true, false) => CellChangeKind::Removed,
_ => CellChangeKind::Modified,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum Severity {
Info,
Warning,
Error,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum DiffStage {
Open,
Metadata,
Match,
Read,
Normalize,
Compare,
Aggregate,
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct DiagnosticLocation {
pub stage: DiffStage,
pub sheet_order: Option<usize>,
pub sheet_name: Option<String>,
pub address: Option<CellAddress>,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum DiagnosticKind {
FormulaUnavailable,
FormulaCachedValueUnverified,
AmbiguousSheetMatch { candidates: Vec<SheetRef> },
UnsupportedCellValue { detail: String },
UnsupportedWorkbookFeature { feature: String },
UnsupportedWorkbookMetadata { category: String },
DefinedNameScopeUnknown,
DateTimeNotNormalized,
LimitTruncatedCells { limit: String, observed: u64 },
}
impl DiagnosticKind {
pub fn code(&self) -> &'static str {
match self {
DiagnosticKind::FormulaUnavailable => "formula_unavailable",
DiagnosticKind::FormulaCachedValueUnverified => "formula_cached_value_unverified",
DiagnosticKind::AmbiguousSheetMatch { .. } => "ambiguous_sheet_match",
DiagnosticKind::UnsupportedCellValue { .. } => "unsupported_cell_value",
DiagnosticKind::UnsupportedWorkbookFeature { .. } => "unsupported_workbook_feature",
DiagnosticKind::UnsupportedWorkbookMetadata { .. } => "unsupported_workbook_metadata",
DiagnosticKind::DefinedNameScopeUnknown => "defined_name_scope_unknown",
DiagnosticKind::DateTimeNotNormalized => "datetime_not_normalized",
DiagnosticKind::LimitTruncatedCells { .. } => "limit_truncated_cells",
}
}
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Diagnostic {
pub severity: Severity,
pub kind: DiagnosticKind,
pub location: DiagnosticLocation,
pub message: String,
}
#[derive(Clone, Default, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct SheetSummary {
pub cells_changed: usize,
pub values_changed: usize,
pub formulas_changed: usize,
}
#[derive(Clone, Default, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct DiagnosticSummary {
pub errors: usize,
pub warnings: usize,
pub info: usize,
}
#[derive(Clone, Default, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct DiffSummary {
pub sheets_added: usize,
pub sheets_removed: usize,
pub sheets_renamed: usize,
pub sheets_moved: usize,
pub sheets_changed: usize,
pub cells_changed: usize,
pub values_changed: usize,
pub formulas_changed: usize,
pub diagnostics: DiagnosticSummary,
}
#[derive(Clone, Default, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct DiffMetrics {
pub sheets_read: u32,
pub cells_read: u64,
pub cells_compared: u64,
pub diffs_emitted: u64,
pub diagnostics_emitted: u64,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct AlignmentSummary {
pub inserted_rows: usize,
pub removed_rows: usize,
pub matched_rows: usize,
pub confidence: MatchConfidence,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct SheetDiff {
pub old_sheet: Option<SheetRef>,
pub new_sheet: Option<SheetRef>,
pub change: SheetChange,
pub cell_diffs: Vec<CellDiff>,
pub compared_range: ComparedRange,
pub alignment_summary: Option<AlignmentSummary>,
pub diagnostics: Vec<Diagnostic>,
pub summary: SheetSummary,
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct WorkbookChange {
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct WorkbookObjectChange {
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct WorkbookDiff {
pub old: WorkbookSideInfo,
pub new: WorkbookSideInfo,
pub sheets: Vec<SheetDiff>,
pub workbook_changes: Vec<WorkbookChange>,
pub object_changes: Vec<WorkbookObjectChange>,
pub diagnostics: Vec<Diagnostic>,
pub summary: DiffSummary,
pub metrics: DiffMetrics,
}
impl WorkbookDiff {
pub(crate) fn derive_summary(sheets: &[SheetDiff], diagnostics: &[Diagnostic]) -> DiffSummary {
let mut s = DiffSummary::default();
for sd in sheets {
match sd.change {
SheetChange::Added => s.sheets_added += 1,
SheetChange::Removed => s.sheets_removed += 1,
SheetChange::Renamed { .. } => {
s.sheets_renamed += 1;
if !sd.cell_diffs.is_empty() {
s.sheets_changed += 1;
}
}
SheetChange::RenamedAndMoved { .. } => {
s.sheets_renamed += 1;
s.sheets_moved += 1;
if !sd.cell_diffs.is_empty() {
s.sheets_changed += 1;
}
}
SheetChange::Moved => {
s.sheets_moved += 1;
if !sd.cell_diffs.is_empty() {
s.sheets_changed += 1;
}
}
SheetChange::Modified => s.sheets_changed += 1,
SheetChange::Unchanged => {}
}
s.cells_changed += sd.summary.cells_changed;
s.values_changed += sd.summary.values_changed;
s.formulas_changed += sd.summary.formulas_changed;
}
for d in diagnostics {
match d.severity {
Severity::Error => s.diagnostics.errors += 1,
Severity::Warning => s.diagnostics.warnings += 1,
Severity::Info => s.diagnostics.info += 1,
}
}
s
}
}