sheets-diff
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 values —
Text("100")andInteger(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 compare_paths;
let diff = compare_paths?;
println!;
for sheet in &diff.sheets
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
CellDiffper 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/Removedplus a diagnostic. calamine0.36 pinned. TheDataenum variant set is the grounding for allCellValueconversions.- 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, andmax_diffs_returnedstayNone(unbounded) unless set explicitly. UseLimits::hardened()for a stricter preset suited to untrusted input. SeeDiffOptions::limits/Limitsdocs. - 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_exceededdiagnostic is emitted — the comparison still completes. #![forbid(unsafe_code)]. The crate contains nounsafeblocks.- ISO-typed date/time values compare via their ISO string, not a
placeholder serial.
Data::DateTimeIso/DurationIsocells (calamine'st="d"path) carry no genuine Excel serial;CellDateTime::has_serialdistinguishes that case so comparison never treats the0.0placeholder as a real one. The workbook's 1900/1904 date epoch is read once per workbook and threaded through, soDateComparePolicy::NormalizeEquivalentDateTimescan actually reconcile dates across epochs.
More Detail
- Full documentation (mdbook)
- Migration from v1
- RFCs — design records for every significant decision