Skip to main content

sheets_diff/
options.rs

1//! Comparison options, builder, and related policy enums (RFC-006, RFC-033 §11).
2
3use crate::error::SheetsDiffError;
4
5// ---------------------------------------------------------------------------
6// Formula comparison (RFC-018)
7// ---------------------------------------------------------------------------
8
9/// How formula text is compared when both sides have a formula.
10#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
11pub enum FormulaCompareMode {
12    /// Compare raw formula strings exactly.  Default.
13    #[default]
14    RawText,
15    /// Compare normalised formula strings.  Requires a normaliser feature;
16    /// returns `InvalidOptions` if selected without one.
17    NormalizedText,
18    /// Compare both raw and normalised; emits both in `FormulaText`.
19    RawAndNormalized,
20    /// Do not compare formulas at all.
21    Ignore,
22}
23
24// ---------------------------------------------------------------------------
25// Numeric / value comparison (RFC-019)
26// ---------------------------------------------------------------------------
27
28/// How two floating-point numbers are compared.
29#[derive(Clone, Copy, PartialEq, Debug, Default)]
30pub enum NumberComparePolicy {
31    /// Bit-faithful parsed equality.  Default.
32    #[default]
33    Exact,
34    AbsoluteTolerance(f64),
35    RelativeTolerance(f64),
36    AbsoluteOrRelative { abs: f64, rel: f64 },
37}
38
39/// Whether `Integer` vs `Number` is treated as a type change.
40#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
41pub enum NumericTypePolicy {
42    /// `Integer(1)` and `Number(1.0)` are **different** (TypeChanged).  Default.
43    #[default]
44    PreserveType,
45    /// Compare by mathematical value; `Integer(1)` and `Number(1.0)` are equal.
46    CompareMathematicalValue,
47}
48
49/// How date/time values are compared.
50#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
51pub enum DateComparePolicy {
52    /// Compare the raw serial and `is_1904` flag.  Default.
53    #[default]
54    ExactRepresentation,
55    /// Attempt to normalise equivalent date-times before comparing.
56    NormalizeEquivalentDateTimes,
57}
58
59/// How a typed value is compared against a value of a different type.
60#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
61pub enum TypeMismatchPolicy {
62    /// Different types are always `TypeChanged`.  Default.
63    #[default]
64    Different,
65    /// Compare their display strings instead (for human-friendly reports only).
66    CompareDisplayString,
67}
68
69/// All value-comparison policy fields grouped together.
70#[derive(Clone, Debug, Default)]
71pub struct ValueCompareOptions {
72    pub number: NumberComparePolicy,
73    pub numeric_type: NumericTypePolicy,
74    pub date: DateComparePolicy,
75    pub type_mismatch: TypeMismatchPolicy,
76}
77
78// ---------------------------------------------------------------------------
79// Format / style comparison (RFC-022)
80// ---------------------------------------------------------------------------
81
82/// Controls whether cell formatting (number format, font, fill, …) is compared.
83///
84/// Default is `Ignore` — calamine 0.35 does not expose a cell-style API, so
85/// `AllAvailable` emits an `UnsupportedWorkbookFeature` diagnostic at runtime.
86#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
87pub enum FormatCompareMode {
88    /// Ignore all formatting differences (default).
89    #[default]
90    Ignore,
91    /// Compare number-format strings only (future, requires style reader).
92    NumberFormatOnly,
93    /// Compare all available style fields (future, best-effort).
94    AllAvailable,
95}
96
97// ---------------------------------------------------------------------------
98// Comparison options
99// ---------------------------------------------------------------------------
100
101/// All comparison-behaviour options.
102#[derive(Clone, Debug)]
103pub struct ComparisonOptions {
104    pub value: ValueCompareOptions,
105    pub formula: FormulaCompareMode,
106    /// Whether the formula's cached value is compared as a value change.
107    pub include_formula_cached_values: bool,
108    /// Cell formatting comparison mode (RFC-022). Default: `Ignore`.
109    pub format: FormatCompareMode,
110}
111
112impl Default for ComparisonOptions {
113    fn default() -> Self {
114        Self {
115            value: ValueCompareOptions::default(),
116            formula: FormulaCompareMode::default(),
117            include_formula_cached_values: true,
118            format: FormatCompareMode::default(),
119        }
120    }
121}
122
123// ---------------------------------------------------------------------------
124// Sheet matching (RFC-009)
125// ---------------------------------------------------------------------------
126
127/// How sheets are paired between the two workbooks.
128#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
129pub enum SheetMatchingMode {
130    /// Pair only sheets with the same name; others are Added/Removed.
131    ExactNameOnly,
132    /// Exact name first; then detect a rename when exactly one unmatched old and
133    /// one unmatched new sheet remain and confidence is sufficient.  Default.
134    #[default]
135    ExactNameThenConservativeRename,
136    /// Exact name first; then try pairing by sheet index.
137    ExactNameThenIndex,
138}
139
140/// Row/column alignment mode (RFC-011).
141#[allow(dead_code)]
142#[derive(Clone, Debug, Default)]
143pub enum AlignmentMode {
144    /// Positional (row N on old vs row N on new).  Default.
145    #[default]
146    Positional,
147    /// Match rows by the values in the specified key columns (1-based).
148    /// Reduces cascades after row insertion/deletion.
149    RowKey { columns: Vec<u32> },
150    /// Match rows by a hash of selected cell values (content similarity).
151    /// `sample_columns` limits which columns contribute to the signature;
152    /// `None` means all columns.
153    RowSignature { sample_columns: Option<Vec<u32>> },
154    /// Match rows using the first row as a column-header identity.
155    #[allow(dead_code)]
156    HeaderColumn,
157}
158
159/// Options controlling sheet matching and cell alignment.
160#[derive(Clone, Debug, Default)]
161pub struct MatchingOptions {
162    pub sheet_matching: SheetMatchingMode,
163    pub alignment: AlignmentMode,
164}
165
166// ---------------------------------------------------------------------------
167// Limits (RFC-012 / RFC-033 §10)
168// ---------------------------------------------------------------------------
169
170/// Resource bounds that protect against pathological workbooks.
171///
172/// `None` means no limit on that dimension.
173#[derive(Clone, Debug, Default)]
174pub struct Limits {
175    pub max_sheets: Option<u32>,
176    pub max_cells_read: Option<u64>,
177    pub max_cells_compared: Option<u64>,
178    pub max_diffs_returned: Option<u64>,
179}
180
181// ---------------------------------------------------------------------------
182// Progress and cancellation (RFC-012)
183// ---------------------------------------------------------------------------
184
185/// An event emitted during a comparison for progress reporting.
186#[derive(Clone, Debug)]
187pub enum DiffEvent {
188    Started,
189    OpeningWorkbook { side: crate::model::Side },
190    WorkbookOpened { side: crate::model::Side, sheet_count: usize },
191    MatchingSheets,
192    SheetStarted { index: usize, total: usize, name: String },
193    SheetFinished { index: usize, changed_cells: usize },
194    Finished,
195}
196
197/// Trait for receiving progress events.
198///
199/// A blanket impl covers any `FnMut(DiffEvent) + Send` closure, so callers can
200/// pass a bare closure at call sites without boilerplate (RFC-012).
201pub trait ProgressSink: Send {
202    fn on_event(&mut self, event: DiffEvent);
203}
204
205impl<F: FnMut(DiffEvent) + Send> ProgressSink for F {
206    fn on_event(&mut self, event: DiffEvent) {
207        self(event);
208    }
209}
210
211/// Trait for cancellation predicates.
212pub trait Cancellation: Send + Sync {
213    fn is_cancelled(&self) -> bool;
214}
215
216impl<F: Fn() -> bool + Send + Sync> Cancellation for F {
217    fn is_cancelled(&self) -> bool {
218        self()
219    }
220}
221
222/// Execution-mode configuration.
223#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
224pub enum ExecutionMode {
225    /// Single-threaded, deterministic.  Default.
226    #[default]
227    Sequential,
228    // Parallel added by RFC-025.
229}
230
231/// Execution, progress, and cancellation options.
232pub struct ExecutionOptions {
233    pub progress: Option<Box<dyn ProgressSink>>,
234    pub cancellation: Option<Box<dyn Cancellation>>,
235    pub mode: ExecutionMode,
236}
237
238impl Default for ExecutionOptions {
239    fn default() -> Self {
240        Self {
241            progress: None,
242            cancellation: None,
243            mode: ExecutionMode::default(),
244        }
245    }
246}
247
248// ---------------------------------------------------------------------------
249// Diagnostic options
250// ---------------------------------------------------------------------------
251
252#[derive(Clone, Debug, Default)]
253pub struct DiagnosticOptions {
254    /// Minimum severity to collect.  Defaults to `Info` (collect everything).
255    pub min_severity: Option<crate::model::Severity>,
256}
257
258// ---------------------------------------------------------------------------
259// Output options
260// ---------------------------------------------------------------------------
261
262/// Output and presentation options.
263#[derive(Clone, Debug)]
264pub struct OutputOptions {
265    /// How non-cell workbook objects are handled (RFC-023).
266    pub objects: crate::objects::ObjectCompareMode,
267}
268
269impl Default for OutputOptions {
270    fn default() -> Self {
271        Self { objects: crate::objects::ObjectCompareMode::WarnIfPresent }
272    }
273}
274
275// ---------------------------------------------------------------------------
276// DiffOptions — grouped tree (RFC-033 §11)
277// ---------------------------------------------------------------------------
278
279/// The top-level configuration entry point for a v2 comparison.
280///
281/// Construct via `DiffOptions::default()` or `DiffOptions::builder()`.
282pub struct DiffOptions {
283    pub comparison: ComparisonOptions,
284    pub matching: MatchingOptions,
285    pub limits: Limits,
286    pub execution: ExecutionOptions,
287    pub diagnostics: DiagnosticOptions,
288    pub output: OutputOptions,
289}
290
291impl Default for DiffOptions {
292    fn default() -> Self {
293        Self {
294            comparison: ComparisonOptions::default(),
295            matching: MatchingOptions::default(),
296            limits: Limits::default(),
297            execution: ExecutionOptions::default(),
298            diagnostics: DiagnosticOptions::default(),
299            output: OutputOptions::default(),
300        }
301    }
302}
303
304impl DiffOptions {
305    pub fn builder() -> DiffOptionsBuilder {
306        DiffOptionsBuilder::new()
307    }
308
309    /// Validate option combinations before I/O begins.
310    pub(crate) fn validate(&self) -> Result<(), SheetsDiffError> {
311        // NormalizedText requires a normaliser; none exists in v2.0.
312        if self.comparison.formula == FormulaCompareMode::NormalizedText
313            || self.comparison.formula == FormulaCompareMode::RawAndNormalized
314        {
315            return Err(SheetsDiffError::InvalidOptions {
316                detail: "FormulaCompareMode::NormalizedText / RawAndNormalized is not \
317                         available in v2.0; no formula normaliser is implemented yet"
318                    .into(),
319            });
320        }
321        // Style comparison requires a calamine style reader not yet available.
322        if self.comparison.format != FormatCompareMode::Ignore {
323            return Err(SheetsDiffError::InvalidOptions {
324                detail: "FormatCompareMode other than Ignore is not available in v2; \
325                         calamine 0.35 does not expose a cell-style API"
326                    .into(),
327            });
328        }
329        Ok(())
330    }
331}
332
333// ---------------------------------------------------------------------------
334// Builder
335// ---------------------------------------------------------------------------
336
337/// Fluent builder for `DiffOptions`.
338///
339/// Call `.build()` to validate the combination and obtain a `DiffOptions`.
340#[derive(Default)]
341pub struct DiffOptionsBuilder {
342    opts: DiffOptions,
343}
344
345impl DiffOptionsBuilder {
346    pub fn new() -> Self {
347        Self { opts: DiffOptions::default() }
348    }
349
350    // Comparison
351
352    pub fn formula_compare(mut self, mode: FormulaCompareMode) -> Self {
353        self.opts.comparison.formula = mode;
354        self
355    }
356
357    pub fn format_compare(mut self, mode: FormatCompareMode) -> Self {
358        self.opts.comparison.format = mode;
359        self
360    }
361
362    /// Set the object comparison mode (RFC-023).
363    pub fn object_mode(mut self, mode: crate::objects::ObjectCompareMode) -> Self {
364        self.opts.output.objects = mode;
365        self
366    }
367
368    /// Set the execution mode (RFC-025).
369    pub fn execution_mode(mut self, mode: ExecutionMode) -> Self {
370        self.opts.execution.mode = mode;
371        self
372    }
373
374    pub fn include_formula_cached_values(mut self, yes: bool) -> Self {
375        self.opts.comparison.include_formula_cached_values = yes;
376        self
377    }
378
379    pub fn number_compare(mut self, policy: NumberComparePolicy) -> Self {
380        self.opts.comparison.value.number = policy;
381        self
382    }
383
384    pub fn numeric_type_policy(mut self, policy: NumericTypePolicy) -> Self {
385        self.opts.comparison.value.numeric_type = policy;
386        self
387    }
388
389    pub fn type_mismatch_policy(mut self, policy: TypeMismatchPolicy) -> Self {
390        self.opts.comparison.value.type_mismatch = policy;
391        self
392    }
393
394    // Matching
395
396    pub fn sheet_matching(mut self, mode: SheetMatchingMode) -> Self {
397        self.opts.matching.sheet_matching = mode;
398        self
399    }
400
401    // Limits
402
403    pub fn max_sheets(mut self, n: u32) -> Self {
404        self.opts.limits.max_sheets = Some(n);
405        self
406    }
407
408    pub fn max_cells_compared(mut self, n: u64) -> Self {
409        self.opts.limits.max_cells_compared = Some(n);
410        self
411    }
412
413    pub fn max_diffs_returned(mut self, n: u64) -> Self {
414        self.opts.limits.max_diffs_returned = Some(n);
415        self
416    }
417
418    // Execution
419
420    pub fn progress<S: ProgressSink + 'static>(mut self, sink: S) -> Self {
421        self.opts.execution.progress = Some(Box::new(sink));
422        self
423    }
424
425    pub fn cancellation<C: Cancellation + 'static>(mut self, token: C) -> Self {
426        self.opts.execution.cancellation = Some(Box::new(token));
427        self
428    }
429
430    /// Build with a fully specified `MatchingOptions` (convenience for alignment tests).
431    pub fn build_with_matching(mut self, matching: MatchingOptions) -> Result<DiffOptions, SheetsDiffError> {
432        self.opts.matching = matching;
433        self.opts.validate()?;
434        Ok(self.opts)
435    }
436
437    /// Validate and return the built options.
438    pub fn build(self) -> Result<DiffOptions, SheetsDiffError> {
439        self.opts.validate()?;
440        Ok(self.opts)
441    }
442}