# sheets-diff
[](https://github.com/forskscope/sheets-diff-rs/actions/workflows/ci.yaml)
[](https://crates.io/crates/sheets-diff)
[](https://docs.rs/sheets-diff)
[](https://deps.rs/crate/sheets-diff)
[](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 values** — `Text("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
```rust
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
| *(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
- [Full documentation](docs/src/SUMMARY.md) *(mdbook)*
- [Migration from v1](docs/src/migration/v1-to-v2.md)
- [RFCs](rfcs/) — design records for every significant decision