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.
212///
213/// A blanket impl covers any `Fn() -> bool + Send + Sync`, so the common case
214/// is a closure. The single most common adapter is an `Arc<AtomicBool>` shared
215/// with a GUI "Cancel" button:
216///
217/// ```
218/// use std::sync::Arc;
219/// use std::sync::atomic::{AtomicBool, Ordering};
220/// use sheets_diff::DiffOptions;
221///
222/// let cancel_flag = Arc::new(AtomicBool::new(false));
223/// let flag = cancel_flag.clone();
224/// let opts = DiffOptions::builder()
225///     .cancellation(move || flag.load(Ordering::Relaxed))
226///     .build()
227///     .unwrap();
228/// // Setting `cancel_flag` to true from another thread causes the next
229/// // cancellation check to abort the diff with `SheetsDiffError::Cancelled`.
230/// ```
231///
232/// # Cancellation latency
233///
234/// `is_cancelled()` is polled **once before each sheet pair** is processed.
235/// On a workbook with many sheets, cancellation is observed promptly. On a
236/// single very large sheet, cancellation is **not** observed mid-sheet in the
237/// current implementation — it fires before the next sheet begins. If you need
238/// sub-sheet cancellation latency for huge single-sheet workbooks, also set a
239/// `max_cells_read` / `max_cells_compared` bound so the diff returns within a
240/// predictable amount of work.
241pub trait Cancellation: Send + Sync {
242    fn is_cancelled(&self) -> bool;
243}
244
245impl<F: Fn() -> bool + Send + Sync> Cancellation for F {
246    fn is_cancelled(&self) -> bool {
247        self()
248    }
249}
250
251/// Execution-mode configuration.
252#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
253pub enum ExecutionMode {
254    /// Single-threaded, deterministic.  Default.
255    #[default]
256    Sequential,
257    // Parallel added by RFC-025.
258}
259
260/// Execution, progress, and cancellation options.
261pub struct ExecutionOptions {
262    pub progress: Option<Box<dyn ProgressSink>>,
263    pub cancellation: Option<Box<dyn Cancellation>>,
264    pub mode: ExecutionMode,
265}
266
267impl Default for ExecutionOptions {
268    fn default() -> Self {
269        Self {
270            progress: None,
271            cancellation: None,
272            mode: ExecutionMode::default(),
273        }
274    }
275}
276
277// ---------------------------------------------------------------------------
278// Diagnostic options
279// ---------------------------------------------------------------------------
280
281#[derive(Clone, Debug, Default)]
282pub struct DiagnosticOptions {
283    /// Minimum severity to collect.  Defaults to `Info` (collect everything).
284    pub min_severity: Option<crate::model::Severity>,
285}
286
287// ---------------------------------------------------------------------------
288// Output options
289// ---------------------------------------------------------------------------
290
291/// Output and presentation options.
292#[derive(Clone, Debug)]
293pub struct OutputOptions {
294    /// How non-cell workbook objects are handled (RFC-023).
295    pub objects: crate::objects::ObjectCompareMode,
296}
297
298impl Default for OutputOptions {
299    fn default() -> Self {
300        Self { objects: crate::objects::ObjectCompareMode::WarnIfPresent }
301    }
302}
303
304// ---------------------------------------------------------------------------
305// DiffOptions — grouped tree (RFC-033 §11)
306// ---------------------------------------------------------------------------
307
308/// The top-level configuration entry point for a v2 comparison.
309///
310/// Construct via `DiffOptions::default()` or `DiffOptions::builder()`.
311pub struct DiffOptions {
312    pub comparison: ComparisonOptions,
313    pub matching: MatchingOptions,
314    pub limits: Limits,
315    pub execution: ExecutionOptions,
316    pub diagnostics: DiagnosticOptions,
317    pub output: OutputOptions,
318}
319
320impl Default for DiffOptions {
321    fn default() -> Self {
322        Self {
323            comparison: ComparisonOptions::default(),
324            matching: MatchingOptions::default(),
325            limits: Limits::default(),
326            execution: ExecutionOptions::default(),
327            diagnostics: DiagnosticOptions::default(),
328            output: OutputOptions::default(),
329        }
330    }
331}
332
333impl DiffOptions {
334    pub fn builder() -> DiffOptionsBuilder {
335        DiffOptionsBuilder::new()
336    }
337
338    /// Validate option combinations before I/O begins.
339    pub(crate) fn validate(&self) -> Result<(), SheetsDiffError> {
340        // NormalizedText requires a normaliser; none exists in v2.0.
341        if self.comparison.formula == FormulaCompareMode::NormalizedText
342            || self.comparison.formula == FormulaCompareMode::RawAndNormalized
343        {
344            return Err(SheetsDiffError::InvalidOptions {
345                detail: "FormulaCompareMode::NormalizedText / RawAndNormalized is not \
346                         available in v2.0; no formula normaliser is implemented yet"
347                    .into(),
348            });
349        }
350        // Style comparison requires a calamine style reader not yet available.
351        if self.comparison.format != FormatCompareMode::Ignore {
352            return Err(SheetsDiffError::InvalidOptions {
353                detail: "FormatCompareMode other than Ignore is not available in v2; \
354                         calamine 0.35 does not expose a cell-style API"
355                    .into(),
356            });
357        }
358        Ok(())
359    }
360}
361
362// ---------------------------------------------------------------------------
363// Builder
364// ---------------------------------------------------------------------------
365
366/// Fluent builder for `DiffOptions`.
367///
368/// Call `.build()` to validate the combination and obtain a `DiffOptions`.
369#[derive(Default)]
370pub struct DiffOptionsBuilder {
371    opts: DiffOptions,
372}
373
374impl DiffOptionsBuilder {
375    pub fn new() -> Self {
376        Self { opts: DiffOptions::default() }
377    }
378
379    // Comparison
380
381    pub fn formula_compare(mut self, mode: FormulaCompareMode) -> Self {
382        self.opts.comparison.formula = mode;
383        self
384    }
385
386    pub fn format_compare(mut self, mode: FormatCompareMode) -> Self {
387        self.opts.comparison.format = mode;
388        self
389    }
390
391    /// Set the object comparison mode (RFC-023).
392    pub fn object_mode(mut self, mode: crate::objects::ObjectCompareMode) -> Self {
393        self.opts.output.objects = mode;
394        self
395    }
396
397    /// Set the execution mode (RFC-025).
398    pub fn execution_mode(mut self, mode: ExecutionMode) -> Self {
399        self.opts.execution.mode = mode;
400        self
401    }
402
403    pub fn include_formula_cached_values(mut self, yes: bool) -> Self {
404        self.opts.comparison.include_formula_cached_values = yes;
405        self
406    }
407
408    pub fn number_compare(mut self, policy: NumberComparePolicy) -> Self {
409        self.opts.comparison.value.number = policy;
410        self
411    }
412
413    pub fn numeric_type_policy(mut self, policy: NumericTypePolicy) -> Self {
414        self.opts.comparison.value.numeric_type = policy;
415        self
416    }
417
418    pub fn type_mismatch_policy(mut self, policy: TypeMismatchPolicy) -> Self {
419        self.opts.comparison.value.type_mismatch = policy;
420        self
421    }
422
423    pub fn number_compare_policy(mut self, policy: NumberComparePolicy) -> Self {
424        self.opts.comparison.value.number = policy;
425        self
426    }
427
428    // Matching
429
430    pub fn sheet_matching(mut self, mode: SheetMatchingMode) -> Self {
431        self.opts.matching.sheet_matching = mode;
432        self
433    }
434
435    // Limits
436
437    pub fn max_sheets(mut self, n: u32) -> Self {
438        self.opts.limits.max_sheets = Some(n);
439        self
440    }
441
442    pub fn max_cells_compared(mut self, n: u64) -> Self {
443        self.opts.limits.max_cells_compared = Some(n);
444        self
445    }
446
447    pub fn max_diffs_returned(mut self, n: u64) -> Self {
448        self.opts.limits.max_diffs_returned = Some(n);
449        self
450    }
451
452    // Execution
453
454    pub fn progress<S: ProgressSink + 'static>(mut self, sink: S) -> Self {
455        self.opts.execution.progress = Some(Box::new(sink));
456        self
457    }
458
459    pub fn cancellation<C: Cancellation + 'static>(mut self, token: C) -> Self {
460        self.opts.execution.cancellation = Some(Box::new(token));
461        self
462    }
463
464    /// Build with a fully specified `MatchingOptions` (convenience for alignment tests).
465    pub fn build_with_matching(mut self, matching: MatchingOptions) -> Result<DiffOptions, SheetsDiffError> {
466        self.opts.matching = matching;
467        self.opts.validate()?;
468        Ok(self.opts)
469    }
470
471    /// Validate and return the built options.
472    pub fn build(self) -> Result<DiffOptions, SheetsDiffError> {
473        self.opts.validate()?;
474        Ok(self.opts)
475    }
476}