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// Comparison options
80// ---------------------------------------------------------------------------
81
82/// All comparison-behaviour options.
83#[derive(Clone, Debug)]
84pub struct ComparisonOptions {
85    pub value: ValueCompareOptions,
86    pub formula: FormulaCompareMode,
87    /// Whether the formula's cached value is compared as a value change.
88    pub include_formula_cached_values: bool,
89}
90
91impl Default for ComparisonOptions {
92    fn default() -> Self {
93        Self {
94            value: ValueCompareOptions::default(),
95            formula: FormulaCompareMode::default(),
96            include_formula_cached_values: true,
97        }
98    }
99}
100
101// ---------------------------------------------------------------------------
102// Sheet matching (RFC-009)
103// ---------------------------------------------------------------------------
104
105/// How sheets are paired between the two workbooks.
106#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
107pub enum SheetMatchingMode {
108    /// Pair only sheets with the same name; others are Added/Removed.
109    ExactNameOnly,
110    /// Exact name first; then detect a rename when exactly one unmatched old and
111    /// one unmatched new sheet remain and confidence is sufficient.  Default.
112    #[default]
113    ExactNameThenConservativeRename,
114    /// Exact name first; then try pairing by sheet index.
115    ExactNameThenIndex,
116}
117
118/// Row/column alignment mode (RFC-011).
119#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
120pub enum AlignmentMode {
121    /// Positional (row N on old vs row N on new).  Default.
122    #[default]
123    Positional,
124    // Future RFC-011 modes go here.
125}
126
127/// Options controlling sheet matching and cell alignment.
128#[derive(Clone, Debug, Default)]
129pub struct MatchingOptions {
130    pub sheet_matching: SheetMatchingMode,
131    pub alignment: AlignmentMode,
132}
133
134// ---------------------------------------------------------------------------
135// Limits (RFC-012 / RFC-033 §10)
136// ---------------------------------------------------------------------------
137
138/// Resource bounds that protect against pathological workbooks.
139///
140/// `None` means no limit on that dimension.
141#[derive(Clone, Debug, Default)]
142pub struct Limits {
143    pub max_sheets: Option<u32>,
144    pub max_cells_read: Option<u64>,
145    pub max_cells_compared: Option<u64>,
146    pub max_diffs_returned: Option<u64>,
147}
148
149// ---------------------------------------------------------------------------
150// Progress and cancellation (RFC-012)
151// ---------------------------------------------------------------------------
152
153/// An event emitted during a comparison for progress reporting.
154#[derive(Clone, Debug)]
155pub enum DiffEvent {
156    Started,
157    OpeningWorkbook { side: crate::model::Side },
158    WorkbookOpened { side: crate::model::Side, sheet_count: usize },
159    MatchingSheets,
160    SheetStarted { index: usize, total: usize, name: String },
161    SheetFinished { index: usize, changed_cells: usize },
162    Finished,
163}
164
165/// Trait for receiving progress events.
166///
167/// A blanket impl covers any `FnMut(DiffEvent) + Send` closure, so callers can
168/// pass a bare closure at call sites without boilerplate (RFC-012).
169pub trait ProgressSink: Send {
170    fn on_event(&mut self, event: DiffEvent);
171}
172
173impl<F: FnMut(DiffEvent) + Send> ProgressSink for F {
174    fn on_event(&mut self, event: DiffEvent) {
175        self(event);
176    }
177}
178
179/// Trait for cancellation predicates.
180pub trait Cancellation: Send + Sync {
181    fn is_cancelled(&self) -> bool;
182}
183
184impl<F: Fn() -> bool + Send + Sync> Cancellation for F {
185    fn is_cancelled(&self) -> bool {
186        self()
187    }
188}
189
190/// Execution-mode configuration.
191#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
192pub enum ExecutionMode {
193    /// Single-threaded, deterministic.  Default.
194    #[default]
195    Sequential,
196    // Parallel added by RFC-025.
197}
198
199/// Execution, progress, and cancellation options.
200pub struct ExecutionOptions {
201    pub progress: Option<Box<dyn ProgressSink>>,
202    pub cancellation: Option<Box<dyn Cancellation>>,
203    pub mode: ExecutionMode,
204}
205
206impl Default for ExecutionOptions {
207    fn default() -> Self {
208        Self {
209            progress: None,
210            cancellation: None,
211            mode: ExecutionMode::default(),
212        }
213    }
214}
215
216// ---------------------------------------------------------------------------
217// Diagnostic options
218// ---------------------------------------------------------------------------
219
220#[derive(Clone, Debug, Default)]
221pub struct DiagnosticOptions {
222    /// Minimum severity to collect.  Defaults to `Info` (collect everything).
223    pub min_severity: Option<crate::model::Severity>,
224}
225
226// ---------------------------------------------------------------------------
227// Output options
228// ---------------------------------------------------------------------------
229
230#[derive(Clone, Debug, Default)]
231pub struct OutputOptions {
232    // Future fields: number format display policy, locale hints, etc.
233}
234
235// ---------------------------------------------------------------------------
236// DiffOptions — grouped tree (RFC-033 §11)
237// ---------------------------------------------------------------------------
238
239/// The top-level configuration entry point for a v2 comparison.
240///
241/// Construct via `DiffOptions::default()` or `DiffOptions::builder()`.
242pub struct DiffOptions {
243    pub comparison: ComparisonOptions,
244    pub matching: MatchingOptions,
245    pub limits: Limits,
246    pub execution: ExecutionOptions,
247    pub diagnostics: DiagnosticOptions,
248    pub output: OutputOptions,
249}
250
251impl Default for DiffOptions {
252    fn default() -> Self {
253        Self {
254            comparison: ComparisonOptions::default(),
255            matching: MatchingOptions::default(),
256            limits: Limits::default(),
257            execution: ExecutionOptions::default(),
258            diagnostics: DiagnosticOptions::default(),
259            output: OutputOptions::default(),
260        }
261    }
262}
263
264impl DiffOptions {
265    pub fn builder() -> DiffOptionsBuilder {
266        DiffOptionsBuilder::new()
267    }
268
269    /// Validate option combinations before I/O begins.
270    pub(crate) fn validate(&self) -> Result<(), SheetsDiffError> {
271        // NormalizedText requires a normaliser; none exists in v2.0.
272        if self.comparison.formula == FormulaCompareMode::NormalizedText
273            || self.comparison.formula == FormulaCompareMode::RawAndNormalized
274        {
275            return Err(SheetsDiffError::InvalidOptions {
276                detail: "FormulaCompareMode::NormalizedText / RawAndNormalized is not \
277                         available in v2.0; no formula normaliser is implemented yet"
278                    .into(),
279            });
280        }
281        Ok(())
282    }
283}
284
285// ---------------------------------------------------------------------------
286// Builder
287// ---------------------------------------------------------------------------
288
289/// Fluent builder for `DiffOptions`.
290///
291/// Call `.build()` to validate the combination and obtain a `DiffOptions`.
292#[derive(Default)]
293pub struct DiffOptionsBuilder {
294    opts: DiffOptions,
295}
296
297impl DiffOptionsBuilder {
298    pub fn new() -> Self {
299        Self { opts: DiffOptions::default() }
300    }
301
302    // Comparison
303
304    pub fn formula_compare(mut self, mode: FormulaCompareMode) -> Self {
305        self.opts.comparison.formula = mode;
306        self
307    }
308
309    pub fn include_formula_cached_values(mut self, yes: bool) -> Self {
310        self.opts.comparison.include_formula_cached_values = yes;
311        self
312    }
313
314    pub fn number_compare(mut self, policy: NumberComparePolicy) -> Self {
315        self.opts.comparison.value.number = policy;
316        self
317    }
318
319    pub fn numeric_type_policy(mut self, policy: NumericTypePolicy) -> Self {
320        self.opts.comparison.value.numeric_type = policy;
321        self
322    }
323
324    pub fn type_mismatch_policy(mut self, policy: TypeMismatchPolicy) -> Self {
325        self.opts.comparison.value.type_mismatch = policy;
326        self
327    }
328
329    // Matching
330
331    pub fn sheet_matching(mut self, mode: SheetMatchingMode) -> Self {
332        self.opts.matching.sheet_matching = mode;
333        self
334    }
335
336    // Limits
337
338    pub fn max_sheets(mut self, n: u32) -> Self {
339        self.opts.limits.max_sheets = Some(n);
340        self
341    }
342
343    pub fn max_cells_compared(mut self, n: u64) -> Self {
344        self.opts.limits.max_cells_compared = Some(n);
345        self
346    }
347
348    pub fn max_diffs_returned(mut self, n: u64) -> Self {
349        self.opts.limits.max_diffs_returned = Some(n);
350        self
351    }
352
353    // Execution
354
355    pub fn progress<S: ProgressSink + 'static>(mut self, sink: S) -> Self {
356        self.opts.execution.progress = Some(Box::new(sink));
357        self
358    }
359
360    pub fn cancellation<C: Cancellation + 'static>(mut self, token: C) -> Self {
361        self.opts.execution.cancellation = Some(Box::new(token));
362        self
363    }
364
365    /// Validate and return the built options.
366    pub fn build(self) -> Result<DiffOptions, SheetsDiffError> {
367        self.opts.validate()?;
368        Ok(self.opts)
369    }
370}