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#[derive(Clone, Debug, Default)]
263pub struct OutputOptions {
264    // Future fields: number format display policy, locale hints, etc.
265}
266
267// ---------------------------------------------------------------------------
268// DiffOptions — grouped tree (RFC-033 §11)
269// ---------------------------------------------------------------------------
270
271/// The top-level configuration entry point for a v2 comparison.
272///
273/// Construct via `DiffOptions::default()` or `DiffOptions::builder()`.
274pub struct DiffOptions {
275    pub comparison: ComparisonOptions,
276    pub matching: MatchingOptions,
277    pub limits: Limits,
278    pub execution: ExecutionOptions,
279    pub diagnostics: DiagnosticOptions,
280    pub output: OutputOptions,
281}
282
283impl Default for DiffOptions {
284    fn default() -> Self {
285        Self {
286            comparison: ComparisonOptions::default(),
287            matching: MatchingOptions::default(),
288            limits: Limits::default(),
289            execution: ExecutionOptions::default(),
290            diagnostics: DiagnosticOptions::default(),
291            output: OutputOptions::default(),
292        }
293    }
294}
295
296impl DiffOptions {
297    pub fn builder() -> DiffOptionsBuilder {
298        DiffOptionsBuilder::new()
299    }
300
301    /// Validate option combinations before I/O begins.
302    pub(crate) fn validate(&self) -> Result<(), SheetsDiffError> {
303        // NormalizedText requires a normaliser; none exists in v2.0.
304        if self.comparison.formula == FormulaCompareMode::NormalizedText
305            || self.comparison.formula == FormulaCompareMode::RawAndNormalized
306        {
307            return Err(SheetsDiffError::InvalidOptions {
308                detail: "FormulaCompareMode::NormalizedText / RawAndNormalized is not \
309                         available in v2.0; no formula normaliser is implemented yet"
310                    .into(),
311            });
312        }
313        // Style comparison requires a calamine style reader not yet available.
314        if self.comparison.format != FormatCompareMode::Ignore {
315            return Err(SheetsDiffError::InvalidOptions {
316                detail: "FormatCompareMode other than Ignore is not available in v2; \
317                         calamine 0.35 does not expose a cell-style API"
318                    .into(),
319            });
320        }
321        Ok(())
322    }
323}
324
325// ---------------------------------------------------------------------------
326// Builder
327// ---------------------------------------------------------------------------
328
329/// Fluent builder for `DiffOptions`.
330///
331/// Call `.build()` to validate the combination and obtain a `DiffOptions`.
332#[derive(Default)]
333pub struct DiffOptionsBuilder {
334    opts: DiffOptions,
335}
336
337impl DiffOptionsBuilder {
338    pub fn new() -> Self {
339        Self { opts: DiffOptions::default() }
340    }
341
342    // Comparison
343
344    pub fn formula_compare(mut self, mode: FormulaCompareMode) -> Self {
345        self.opts.comparison.formula = mode;
346        self
347    }
348
349    pub fn format_compare(mut self, mode: FormatCompareMode) -> Self {
350        self.opts.comparison.format = mode;
351        self
352    }
353
354    pub fn include_formula_cached_values(mut self, yes: bool) -> Self {
355        self.opts.comparison.include_formula_cached_values = yes;
356        self
357    }
358
359    pub fn number_compare(mut self, policy: NumberComparePolicy) -> Self {
360        self.opts.comparison.value.number = policy;
361        self
362    }
363
364    pub fn numeric_type_policy(mut self, policy: NumericTypePolicy) -> Self {
365        self.opts.comparison.value.numeric_type = policy;
366        self
367    }
368
369    pub fn type_mismatch_policy(mut self, policy: TypeMismatchPolicy) -> Self {
370        self.opts.comparison.value.type_mismatch = policy;
371        self
372    }
373
374    // Matching
375
376    pub fn sheet_matching(mut self, mode: SheetMatchingMode) -> Self {
377        self.opts.matching.sheet_matching = mode;
378        self
379    }
380
381    // Limits
382
383    pub fn max_sheets(mut self, n: u32) -> Self {
384        self.opts.limits.max_sheets = Some(n);
385        self
386    }
387
388    pub fn max_cells_compared(mut self, n: u64) -> Self {
389        self.opts.limits.max_cells_compared = Some(n);
390        self
391    }
392
393    pub fn max_diffs_returned(mut self, n: u64) -> Self {
394        self.opts.limits.max_diffs_returned = Some(n);
395        self
396    }
397
398    // Execution
399
400    pub fn progress<S: ProgressSink + 'static>(mut self, sink: S) -> Self {
401        self.opts.execution.progress = Some(Box::new(sink));
402        self
403    }
404
405    pub fn cancellation<C: Cancellation + 'static>(mut self, token: C) -> Self {
406        self.opts.execution.cancellation = Some(Box::new(token));
407        self
408    }
409
410    /// Build with a fully specified `MatchingOptions` (convenience for alignment tests).
411    pub fn build_with_matching(mut self, matching: MatchingOptions) -> Result<DiffOptions, SheetsDiffError> {
412        self.opts.matching = matching;
413        self.opts.validate()?;
414        Ok(self.opts)
415    }
416
417    /// Validate and return the built options.
418    pub fn build(self) -> Result<DiffOptions, SheetsDiffError> {
419        self.opts.validate()?;
420        Ok(self.opts)
421    }
422}