1use crate::model::{
4 CellValue, FormulaChange, FormulaText, ValueChange, ValueDifferenceKind,
5};
6use crate::options::{
7 DateComparePolicy, FormulaCompareMode, NumberComparePolicy, NumericTypePolicy,
8 TypeMismatchPolicy, ValueCompareOptions,
9};
10
11#[doc(hidden)]
13pub use self::compare_values as compare_values_pub;
14
15pub fn compare_values(
23 old: &CellValue,
24 new: &CellValue,
25 opts: &ValueCompareOptions,
26) -> Option<ValueChange> {
27 use CellValue::*;
28
29 let reason = match (old, new) {
30 (Empty, Empty) => return None,
32 (Text(a), Text(b)) if a == b => return None,
33 (Text(a), Text(b)) => {
34 debug_assert!(a != b);
35 ValueDifferenceKind::ContentChanged
36 }
37 (Bool(a), Bool(b)) if a == b => return None,
38 (Bool(_), Bool(_)) => ValueDifferenceKind::ContentChanged,
39 (Integer(a), Integer(b)) if a == b => return None,
40 (Integer(_), Integer(_)) => ValueDifferenceKind::ContentChanged,
41 (Number(a), Number(b)) => match compare_floats(*a, *b, &opts.number) {
42 Some(r) => r,
43 None => return None,
44 },
45 (Error(a), Error(b)) if a == b => return None,
46 (Error(_), Error(_)) => ValueDifferenceKind::ErrorKindChanged,
47 (DateTime(a), DateTime(b)) => {
48 if a.serial == b.serial && a.is_1904 == b.is_1904 && a.kind == b.kind {
49 return None;
50 }
51 match opts.date {
52 DateComparePolicy::ExactRepresentation => ValueDifferenceKind::DateTimeChanged,
53 DateComparePolicy::NormalizeEquivalentDateTimes => {
54 if normalized_serial_eq(a.serial, a.is_1904, b.serial, b.is_1904) {
56 return None;
57 }
58 ValueDifferenceKind::DateTimeChanged
59 }
60 }
61 }
62 (Duration(a), Duration(b)) => {
63 if a.serial == b.serial {
64 return None;
65 }
66 ValueDifferenceKind::ContentChanged
67 }
68 (Unsupported { display: a, .. }, Unsupported { display: b, .. }) if a == b => {
69 return None
70 }
71 (Unsupported { .. }, Unsupported { .. }) => ValueDifferenceKind::ContentChanged,
72
73 (Integer(i), Number(f)) | (Number(f), Integer(i)) => {
75 match opts.numeric_type {
76 NumericTypePolicy::PreserveType => ValueDifferenceKind::TypeChanged,
77 NumericTypePolicy::CompareMathematicalValue => {
78 if *i as f64 == *f {
79 return None;
80 }
81 ValueDifferenceKind::ContentChanged
82 }
83 }
84 }
85
86 _ => match opts.type_mismatch {
88 TypeMismatchPolicy::Different => ValueDifferenceKind::TypeChanged,
89 TypeMismatchPolicy::CompareDisplayString => {
90 let a_str = old.display_string();
91 let b_str = new.display_string();
92 if a_str == b_str {
93 return None;
94 }
95 ValueDifferenceKind::DisplayStringChanged
96 }
97 },
98 };
99
100 Some(ValueChange { old: old.clone(), new: new.clone(), reason })
101}
102
103fn compare_floats(
104 a: f64,
105 b: f64,
106 policy: &NumberComparePolicy,
107) -> Option<ValueDifferenceKind> {
108 let equal = match policy {
109 NumberComparePolicy::Exact => a == b || (a.is_nan() && b.is_nan()),
110 NumberComparePolicy::AbsoluteTolerance(tol) => (a - b).abs() <= *tol,
111 NumberComparePolicy::RelativeTolerance(tol) => {
112 let denom = a.abs().max(b.abs());
113 denom == 0.0 || (a - b).abs() / denom <= *tol
114 }
115 NumberComparePolicy::AbsoluteOrRelative { abs, rel } => {
116 let abs_ok = (a - b).abs() <= *abs;
117 let denom = a.abs().max(b.abs());
118 let rel_ok = denom == 0.0 || (a - b).abs() / denom <= *rel;
119 abs_ok || rel_ok
120 }
121 };
122 if equal {
123 None
124 } else {
125 Some(ValueDifferenceKind::NumericOutsideTolerance)
126 }
127}
128
129fn normalized_serial_eq(a_serial: f64, a_1904: bool, b_serial: f64, b_1904: bool) -> bool {
132 const OFFSET: f64 = 1462.0;
133 let a_norm = if a_1904 { a_serial + OFFSET } else { a_serial };
134 let b_norm = if b_1904 { b_serial + OFFSET } else { b_serial };
135 a_norm == b_norm
136}
137
138pub fn compare_formulas(
147 old_formula: Option<&str>,
148 new_formula: Option<&str>,
149 mode: FormulaCompareMode,
150) -> Option<FormulaChange> {
151 if mode == FormulaCompareMode::Ignore {
152 return None;
153 }
154
155 let old_text = old_formula.map(|r| FormulaText {
156 raw: r.to_owned(),
157 normalized: None, });
159 let new_text = new_formula.map(|r| FormulaText {
160 raw: r.to_owned(),
161 normalized: None,
162 });
163
164 let old_raw = old_formula.unwrap_or("");
166 let new_raw = new_formula.unwrap_or("");
167 if old_raw == new_raw {
168 return None;
169 }
170
171 Some(FormulaChange { old: old_text, new: new_text })
172}
173
174#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::model::CellValue;
182 use crate::options::ValueCompareOptions;
183
184 fn opts() -> ValueCompareOptions {
185 ValueCompareOptions::default()
186 }
187
188 #[test]
189 fn equal_texts_produce_no_change() {
190 let r = compare_values(&CellValue::Text("x".into()), &CellValue::Text("x".into()), &opts());
191 assert!(r.is_none());
192 }
193
194 #[test]
195 fn different_texts_produce_content_changed() {
196 let r = compare_values(
197 &CellValue::Text("a".into()),
198 &CellValue::Text("b".into()),
199 &opts(),
200 )
201 .unwrap();
202 assert_eq!(r.reason, ValueDifferenceKind::ContentChanged);
203 }
204
205 #[test]
206 fn integer_vs_number_is_type_changed_by_default() {
207 let r = compare_values(&CellValue::Integer(1), &CellValue::Number(1.0), &opts()).unwrap();
208 assert_eq!(r.reason, ValueDifferenceKind::TypeChanged);
209 }
210
211 #[test]
212 fn integer_vs_number_equal_when_math_policy() {
213 let mut o = opts();
214 o.numeric_type = NumericTypePolicy::CompareMathematicalValue;
215 let r = compare_values(&CellValue::Integer(1), &CellValue::Number(1.0), &o);
216 assert!(r.is_none());
217 }
218
219 #[test]
220 fn text_vs_integer_is_type_changed() {
221 let r = compare_values(
222 &CellValue::Text("100".into()),
223 &CellValue::Integer(100),
224 &opts(),
225 )
226 .unwrap();
227 assert_eq!(r.reason, ValueDifferenceKind::TypeChanged);
228 }
229
230 #[test]
231 fn equal_booleans_produce_no_change() {
232 let r = compare_values(&CellValue::Bool(true), &CellValue::Bool(true), &opts());
233 assert!(r.is_none());
234 }
235
236 #[test]
237 fn empty_vs_empty_produces_no_change() {
238 let r = compare_values(&CellValue::Empty, &CellValue::Empty, &opts());
239 assert!(r.is_none());
240 }
241
242 #[test]
243 fn formula_ignore_returns_none() {
244 let r = compare_formulas(Some("=A1"), Some("=B1"), FormulaCompareMode::Ignore);
245 assert!(r.is_none());
246 }
247
248 #[test]
249 fn equal_formulas_return_none() {
250 let r = compare_formulas(Some("=A1+B1"), Some("=A1+B1"), FormulaCompareMode::RawText);
251 assert!(r.is_none());
252 }
253
254 #[test]
255 fn different_formulas_return_change() {
256 let r = compare_formulas(Some("=A1+B1"), Some("=A1+B1+C1"), FormulaCompareMode::RawText)
257 .unwrap();
258 assert_eq!(r.old.as_ref().unwrap().raw, "=A1+B1");
259 assert_eq!(r.new.as_ref().unwrap().raw, "=A1+B1+C1");
260 }
261
262 #[test]
263 fn formula_added() {
264 let r = compare_formulas(None, Some("=SUM(A1:A10)"), FormulaCompareMode::RawText).unwrap();
265 assert!(r.old.is_none());
266 assert!(r.new.is_some());
267 }
268}