sheets-diff 2.4.1

Structured diff engine for Microsoft Excel .xlsx workbooks
Documentation

sheets-diff

CI crates.io docs.rs deps.rs License

Structured diff engine for Microsoft Excel .xlsx workbooks.

Compares two workbooks and returns a typed, deterministic result — not just a text diff. Application developers get the data; they decide the presentation.

Overview

sheets-diff compares .xlsx workbooks at the cell level and returns a WorkbookDiff carrying:

  • per-sheet change classification (added, removed, renamed, moved, modified);
  • per-cell typed value changes (Integer, Number, Bool, DateTime, …);
  • per-cell formula text changes;
  • structured diagnostics and per-level summary counts;
  • deterministic ordering by sheet index, then (row, col).

Current format support: .xlsx. Other spreadsheet formats (.ods, .xlsm, …) may be added in future versions. Plain-text formats such as CSV and TSV are out of scope — diffs of text files are better served by dedicated text-diff tools.

Why / When

Use sheets-diff when you need:

  • a library that returns structured data rather than printing a diff;
  • typed valuesText("100") and Integer(100) are distinct;
  • GUI or batch integration: bytes/reader inputs, progress events, cancellation;
  • no hidden side effects — the library never writes to stdout/stderr or accesses the network.

It is intentionally not a spreadsheet editor, merge engine, or formula engine.

Quick Start

use sheets_diff::compare_paths;

let diff = compare_paths("old.xlsx", "new.xlsx")?;
println!("{} cell(s) changed", diff.summary.cells_changed);

for sheet in &diff.sheets {
    for cell in &sheet.cell_diffs {
        if let Some(vc) = &cell.value {
            println!("{}: {}{}", cell.address.a1,
                vc.old.display_string(), vc.new.display_string());
        }
    }
}

Features

Cargo feature What it enables
(none) Core library — no extra deps
serde Serialize on all public model types; output::json helpers
chrono ISO-8601 string synthesis for DateTime values
cli Builds the sheets-diff binary (requires clap)

Design Notes

  • One CellDiff per address. Value and formula changes are independent sub-fields; no duplicate-address entries.
  • #[non_exhaustive] everywhere. Adding fields or variants in v2.x is additive — no forced semver bump for struct consumers.
  • Conservative by default. Sheet rename detection only fires when exactly one old and one new sheet are unmatched. Ambiguous matches surface as Added/Removed plus a diagnostic.
  • calamine 0.36 pinned. The Data enum variant set is the grounding for all CellValue conversions.
  • Superlinear paths are bounded by default; linear paths stay opt-in. Limits::default() bounds the row-alignment product (max_alignment_product, empirically set so the worst case stays under ~15ms) and the input size (max_input_bytes, 500 MiB) — both checked before the corresponding work begins. max_sheets, max_cells_read, max_cells_compared, and max_diffs_returned stay None (unbounded) unless set explicitly. Use Limits::hardened() for a stricter preset suited to untrusted input. See DiffOptions::limits / Limits docs.
  • Alignment degrades, it never errors. If the row-alignment product bound is exceeded, the affected sheet falls back to positional comparison and a alignment_bound_exceeded diagnostic is emitted — the comparison still completes.
  • #![forbid(unsafe_code)]. The crate contains no unsafe blocks.
  • ISO-typed date/time values compare via their ISO string, not a placeholder serial. Data::DateTimeIso/DurationIso cells (calamine's t="d" path) carry no genuine Excel serial; CellDateTime::has_serial distinguishes that case so comparison never treats the 0.0 placeholder as a real one. The workbook's 1900/1904 date epoch is read once per workbook and threaded through, so DateComparePolicy::NormalizeEquivalentDateTimes can actually reconcile dates across epochs.

More Detail