sheets-diff 2.4.1

Structured diff engine for Microsoft Excel .xlsx workbooks
Documentation
# sheets-diff

[![CI](https://github.com/forskscope/sheets-diff-rs/actions/workflows/ci.yaml/badge.svg)](https://github.com/forskscope/sheets-diff-rs/actions/workflows/ci.yaml)
[![crates.io](https://img.shields.io/crates/v/sheets-diff.svg)](https://crates.io/crates/sheets-diff)
[![docs.rs](https://docs.rs/sheets-diff/badge.svg)](https://docs.rs/sheets-diff)
[![deps.rs](https://deps.rs/crate/sheets-diff/latest/status.svg)](https://deps.rs/crate/sheets-diff)
[![License](https://img.shields.io/github/license/nabbisen/sheets-diff-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 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

| 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

- [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