1use crate::model::{
4 CellDateTime, CellValue, FormulaChange, FormulaText, ValueChange, ValueDifferenceKind,
5};
6use crate::options::{
7 DateComparePolicy, FormulaCompareMode, NumberComparePolicy, NumericTypePolicy,
8 TypeMismatchPolicy, ValueCompareOptions,
9};
10
11pub fn compare_values(
19 old: &CellValue,
20 new: &CellValue,
21 opts: &ValueCompareOptions,
22) -> Option<ValueChange> {
23 use CellValue::*;
24
25 let reason = match (old, new) {
26 (Empty, Empty) => return None,
28 (Text(a), Text(b)) if a == b => return None,
29 (Text(a), Text(b)) => {
30 debug_assert!(a != b);
31 ValueDifferenceKind::ContentChanged
32 }
33 (Bool(a), Bool(b)) if a == b => return None,
34 (Bool(_), Bool(_)) => ValueDifferenceKind::ContentChanged,
35 (Integer(a), Integer(b)) if a == b => return None,
36 (Integer(_), Integer(_)) => ValueDifferenceKind::ContentChanged,
37 (Number(a), Number(b)) => compare_floats(*a, *b, &opts.number)?,
38 (Error(a), Error(b)) if a == b => return None,
39 (Error(_), Error(_)) => ValueDifferenceKind::ErrorKindChanged,
40 (DateTime(a), DateTime(b)) => {
41 if datetime_equal(a, b) {
42 return None;
43 }
44 match opts.date {
45 DateComparePolicy::ExactRepresentation => ValueDifferenceKind::DateTimeChanged,
46 DateComparePolicy::NormalizeEquivalentDateTimes => {
47 if a.has_serial
51 && b.has_serial
52 && normalized_serial_eq(a.serial, a.is_1904, b.serial, b.is_1904)
53 {
54 return None;
55 }
56 ValueDifferenceKind::DateTimeChanged
57 }
58 }
59 }
60 (Duration(a), Duration(b)) => {
61 let equal = match (&a.iso, &b.iso) {
62 (Some(ai), Some(bi)) => ai == bi,
67 (None, None) => a.serial == b.serial,
68 _ => false,
72 };
73 if equal {
74 return None;
75 }
76 ValueDifferenceKind::ContentChanged
77 }
78 (Unsupported { display: a, .. }, Unsupported { display: b, .. }) if a == b => return None,
79 (Unsupported { .. }, Unsupported { .. }) => ValueDifferenceKind::ContentChanged,
80
81 (Integer(i), Number(f)) | (Number(f), Integer(i)) => match opts.numeric_type {
83 NumericTypePolicy::PreserveType => ValueDifferenceKind::TypeChanged,
84 NumericTypePolicy::CompareMathematicalValue => {
85 if *i as f64 == *f {
86 return None;
87 }
88 ValueDifferenceKind::ContentChanged
89 }
90 },
91
92 _ => match opts.type_mismatch {
94 TypeMismatchPolicy::Different => ValueDifferenceKind::TypeChanged,
95 TypeMismatchPolicy::CompareDisplayString => {
96 let a_str = old.display_string();
97 let b_str = new.display_string();
98 if a_str == b_str {
99 return None;
100 }
101 ValueDifferenceKind::DisplayStringChanged
102 }
103 },
104 };
105
106 Some(ValueChange {
107 old: old.clone(),
108 new: new.clone(),
109 reason,
110 })
111}
112
113fn datetime_equal(a: &CellDateTime, b: &CellDateTime) -> bool {
130 match (a.has_serial, b.has_serial) {
131 (true, true) => a.serial == b.serial && a.is_1904 == b.is_1904 && a.kind == b.kind,
132 (false, false) => a.iso == b.iso,
133 _ => false,
134 }
135}
136
137fn compare_floats(a: f64, b: f64, policy: &NumberComparePolicy) -> Option<ValueDifferenceKind> {
138 let equal = match policy {
139 NumberComparePolicy::Exact => a == b || (a.is_nan() && b.is_nan()),
140 NumberComparePolicy::AbsoluteTolerance(tol) => (a - b).abs() <= *tol,
141 NumberComparePolicy::RelativeTolerance(tol) => {
142 let denom = a.abs().max(b.abs());
143 denom == 0.0 || (a - b).abs() / denom <= *tol
144 }
145 NumberComparePolicy::AbsoluteOrRelative { abs, rel } => {
146 let abs_ok = (a - b).abs() <= *abs;
147 let denom = a.abs().max(b.abs());
148 let rel_ok = denom == 0.0 || (a - b).abs() / denom <= *rel;
149 abs_ok || rel_ok
150 }
151 };
152 if equal {
153 None
154 } else {
155 Some(ValueDifferenceKind::NumericOutsideTolerance)
156 }
157}
158
159fn normalized_serial_eq(a_serial: f64, a_1904: bool, b_serial: f64, b_1904: bool) -> bool {
162 const OFFSET: f64 = 1462.0;
163 let a_norm = if a_1904 { a_serial + OFFSET } else { a_serial };
164 let b_norm = if b_1904 { b_serial + OFFSET } else { b_serial };
165 a_norm == b_norm
166}
167
168pub fn compare_formulas(
177 old_formula: Option<&str>,
178 new_formula: Option<&str>,
179 mode: FormulaCompareMode,
180) -> Option<FormulaChange> {
181 if mode == FormulaCompareMode::Ignore {
182 return None;
183 }
184
185 let old_text = old_formula.map(|r| FormulaText {
186 raw: r.to_owned(),
187 normalized: None, });
189 let new_text = new_formula.map(|r| FormulaText {
190 raw: r.to_owned(),
191 normalized: None,
192 });
193
194 let old_raw = old_formula.unwrap_or("");
196 let new_raw = new_formula.unwrap_or("");
197 if old_raw == new_raw {
198 return None;
199 }
200
201 Some(FormulaChange {
202 old: old_text,
203 new: new_text,
204 })
205}
206
207#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::model::{CellDuration, CellValue, DateTimeKind};
215 use crate::options::{DateComparePolicy, ValueCompareOptions};
216
217 fn opts() -> ValueCompareOptions {
218 ValueCompareOptions::default()
219 }
220
221 fn iso_only_dt(iso: &str) -> CellDateTime {
222 CellDateTime {
223 serial: 0.0,
224 is_1904: false,
225 kind: DateTimeKind::DateTime,
226 iso: Some(iso.to_string()),
227 has_serial: false,
228 }
229 }
230
231 fn serial_dt(serial: f64, is_1904: bool) -> CellDateTime {
232 CellDateTime {
233 serial,
234 is_1904,
235 kind: DateTimeKind::DateTime,
236 iso: None,
237 has_serial: true,
238 }
239 }
240
241 #[test]
244 fn iso_only_datetimes_with_same_iso_are_equal() {
245 let a = iso_only_dt("2024-01-01T00:00:00");
246 let b = iso_only_dt("2024-01-01T00:00:00");
247 assert!(
248 compare_values(&CellValue::DateTime(a), &CellValue::DateTime(b), &opts()).is_none()
249 );
250 }
251
252 #[test]
253 fn iso_only_datetimes_with_different_iso_are_reported_changed() {
254 let a = iso_only_dt("2024-01-01T00:00:00");
258 let b = iso_only_dt("2099-12-31T23:59:59");
259 let r = compare_values(&CellValue::DateTime(a), &CellValue::DateTime(b), &opts()).unwrap();
260 assert_eq!(r.reason, ValueDifferenceKind::DateTimeChanged);
261 }
262
263 #[test]
264 fn iso_only_durations_with_different_iso_are_reported_changed() {
265 let a = CellValue::Duration(CellDuration {
267 serial: 0.0,
268 iso: Some("PT1H".to_string()),
269 });
270 let b = CellValue::Duration(CellDuration {
271 serial: 0.0,
272 iso: Some("PT99H".to_string()),
273 });
274 let r = compare_values(&a, &b, &opts()).unwrap();
275 assert_eq!(r.reason, ValueDifferenceKind::ContentChanged);
276 }
277
278 #[test]
279 fn iso_only_durations_with_same_iso_are_equal() {
280 let a = CellValue::Duration(CellDuration {
281 serial: 0.0,
282 iso: Some("PT1H30M".to_string()),
283 });
284 let b = CellValue::Duration(CellDuration {
285 serial: 0.0,
286 iso: Some("PT1H30M".to_string()),
287 });
288 assert!(compare_values(&a, &b, &opts()).is_none());
289 }
290
291 #[test]
292 fn mixed_serial_and_iso_datetime_never_silently_equal() {
293 let serial_based = serial_dt(0.0, false);
299 let iso_only = iso_only_dt("2024-01-01T00:00:00");
300 let mut o = opts();
301
302 o.date = DateComparePolicy::ExactRepresentation;
303 let r = compare_values(
304 &CellValue::DateTime(serial_based.clone()),
305 &CellValue::DateTime(iso_only.clone()),
306 &o,
307 );
308 assert!(
309 r.is_some(),
310 "mixed representation must not be silently equal"
311 );
312
313 o.date = DateComparePolicy::NormalizeEquivalentDateTimes;
315 let r = compare_values(
316 &CellValue::DateTime(serial_based),
317 &CellValue::DateTime(iso_only),
318 &o,
319 );
320 assert!(
321 r.is_some(),
322 "mixed representation must not be silently equal under NormalizeEquivalentDateTimes either"
323 );
324 }
325
326 #[test]
327 fn normalize_equivalent_datetimes_reconciles_1900_and_1904_systems() {
328 let system_1900 = serial_dt(45000.0, false);
333 let system_1904 = serial_dt(45000.0 - 1462.0, true);
334
335 let mut o = opts();
336 o.date = DateComparePolicy::ExactRepresentation;
337 let r = compare_values(
338 &CellValue::DateTime(system_1900.clone()),
339 &CellValue::DateTime(system_1904.clone()),
340 &o,
341 );
342 assert!(
343 r.is_some(),
344 "ExactRepresentation must not conflate the two epochs"
345 );
346
347 o.date = DateComparePolicy::NormalizeEquivalentDateTimes;
348 let r = compare_values(
349 &CellValue::DateTime(system_1900),
350 &CellValue::DateTime(system_1904),
351 &o,
352 );
353 assert!(
354 r.is_none(),
355 "NormalizeEquivalentDateTimes must recognise the same instant across epochs"
356 );
357 }
358
359 #[test]
360 fn equal_texts_produce_no_change() {
361 let r = compare_values(
362 &CellValue::Text("x".into()),
363 &CellValue::Text("x".into()),
364 &opts(),
365 );
366 assert!(r.is_none());
367 }
368
369 #[test]
370 fn different_texts_produce_content_changed() {
371 let r = compare_values(
372 &CellValue::Text("a".into()),
373 &CellValue::Text("b".into()),
374 &opts(),
375 )
376 .unwrap();
377 assert_eq!(r.reason, ValueDifferenceKind::ContentChanged);
378 }
379
380 #[test]
381 fn integer_vs_number_is_type_changed_by_default() {
382 let r = compare_values(&CellValue::Integer(1), &CellValue::Number(1.0), &opts()).unwrap();
383 assert_eq!(r.reason, ValueDifferenceKind::TypeChanged);
384 }
385
386 #[test]
387 fn integer_vs_number_equal_when_math_policy() {
388 let mut o = opts();
389 o.numeric_type = NumericTypePolicy::CompareMathematicalValue;
390 let r = compare_values(&CellValue::Integer(1), &CellValue::Number(1.0), &o);
391 assert!(r.is_none());
392 }
393
394 #[test]
395 fn text_vs_integer_is_type_changed() {
396 let r = compare_values(
397 &CellValue::Text("100".into()),
398 &CellValue::Integer(100),
399 &opts(),
400 )
401 .unwrap();
402 assert_eq!(r.reason, ValueDifferenceKind::TypeChanged);
403 }
404
405 #[test]
406 fn equal_booleans_produce_no_change() {
407 let r = compare_values(&CellValue::Bool(true), &CellValue::Bool(true), &opts());
408 assert!(r.is_none());
409 }
410
411 #[test]
412 fn empty_vs_empty_produces_no_change() {
413 let r = compare_values(&CellValue::Empty, &CellValue::Empty, &opts());
414 assert!(r.is_none());
415 }
416
417 #[test]
418 fn formula_ignore_returns_none() {
419 let r = compare_formulas(Some("=A1"), Some("=B1"), FormulaCompareMode::Ignore);
420 assert!(r.is_none());
421 }
422
423 #[test]
424 fn equal_formulas_return_none() {
425 let r = compare_formulas(Some("=A1+B1"), Some("=A1+B1"), FormulaCompareMode::RawText);
426 assert!(r.is_none());
427 }
428
429 #[test]
430 fn different_formulas_return_change() {
431 let r = compare_formulas(
432 Some("=A1+B1"),
433 Some("=A1+B1+C1"),
434 FormulaCompareMode::RawText,
435 )
436 .unwrap();
437 assert_eq!(r.old.as_ref().unwrap().raw, "=A1+B1");
438 assert_eq!(r.new.as_ref().unwrap().raw, "=A1+B1+C1");
439 }
440
441 #[test]
442 fn formula_added() {
443 let r = compare_formulas(None, Some("=SUM(A1:A10)"), FormulaCompareMode::RawText).unwrap();
444 assert!(r.old.is_none());
445 assert!(r.new.is_some());
446 }
447}