spreadsheet_mcp/diff/
merge.rs1use super::cells::RawCell;
2use anyhow::Result;
3use schemars::JsonSchema;
4use serde::Serialize;
5use std::cmp::Ordering;
6
7#[derive(Debug, Serialize, Clone, JsonSchema)]
8#[serde(tag = "type", rename_all = "snake_case")]
9pub enum CellDiff {
10 Added {
11 address: String,
12 value: Option<String>,
13 formula: Option<String>,
14 },
15 Deleted {
16 address: String,
17 old_value: Option<String>,
18 },
19 Modified {
20 address: String,
21 subtype: ModificationType,
22 old_value: Option<String>,
23 new_value: Option<String>,
24 old_formula: Option<String>,
25 new_formula: Option<String>,
26 old_style_id: Option<u32>,
27 new_style_id: Option<u32>,
28 },
29}
30
31#[derive(Debug, Serialize, Clone, JsonSchema)]
32#[serde(rename_all = "snake_case")]
33pub enum ModificationType {
34 FormulaEdit,
35 RecalcResult,
36 ValueEdit,
37 StyleEdit,
38}
39
40pub fn diff_streams(
41 base: impl Iterator<Item = Result<RawCell>>,
42 fork: impl Iterator<Item = Result<RawCell>>,
43) -> Result<Vec<CellDiff>> {
44 let mut diffs = Vec::new();
45 let mut base_iter = base.peekable();
46 let mut fork_iter = fork.peekable();
47
48 loop {
49 if let Some(Err(_)) = base_iter.peek() {
51 return Err(base_iter.next().unwrap().unwrap_err());
52 }
53 if let Some(Err(_)) = fork_iter.peek() {
54 return Err(fork_iter.next().unwrap().unwrap_err());
55 }
56
57 let b_opt = base_iter.peek().map(|r| r.as_ref().unwrap());
59 let f_opt = fork_iter.peek().map(|r| r.as_ref().unwrap());
60
61 match (b_opt, f_opt) {
62 (None, None) => break,
63 (Some(b), None) => {
64 diffs.push(CellDiff::Deleted {
65 address: b.address.original.clone(),
66 old_value: b.value.clone(),
67 });
68 base_iter.next();
69 }
70 (None, Some(f)) => {
71 diffs.push(CellDiff::Added {
72 address: f.address.original.clone(),
73 value: f.value.clone(),
74 formula: f.formula.clone(),
75 });
76 fork_iter.next();
77 }
78 (Some(b), Some(f)) => {
79 match b.address.cmp(&f.address) {
80 Ordering::Less => {
81 diffs.push(CellDiff::Deleted {
83 address: b.address.original.clone(),
84 old_value: b.value.clone(),
85 });
86 base_iter.next();
87 }
88 Ordering::Greater => {
89 diffs.push(CellDiff::Added {
91 address: f.address.original.clone(),
92 value: f.value.clone(),
93 formula: f.formula.clone(),
94 });
95 fork_iter.next();
96 }
97 Ordering::Equal => {
98 if let Some(diff) = compare_cells(b, f) {
100 diffs.push(diff);
101 }
102 base_iter.next();
103 fork_iter.next();
104 }
105 }
106 }
107 }
108 }
109
110 Ok(diffs)
111}
112
113fn compare_cells(base: &RawCell, fork: &RawCell) -> Option<CellDiff> {
114 let formula_changed = base.formula != fork.formula;
115 let value_changed = !values_equal(&base.value, &fork.value);
116 let style_changed = base.style_id != fork.style_id;
117
118 if !formula_changed && !value_changed && !style_changed {
119 return None;
120 }
121
122 let subtype = if style_changed && !formula_changed && !value_changed {
123 ModificationType::StyleEdit
124 } else {
125 match (formula_changed, value_changed, fork.formula.is_some()) {
126 (true, _, _) => ModificationType::FormulaEdit,
127 (false, true, true) => ModificationType::RecalcResult,
128 (false, true, false) => ModificationType::ValueEdit,
129 _ => return None, }
131 };
132
133 Some(CellDiff::Modified {
134 address: fork.address.original.clone(),
135 subtype,
136 old_value: base.value.clone(),
137 new_value: fork.value.clone(),
138 old_formula: base.formula.clone(),
139 new_formula: fork.formula.clone(),
140 old_style_id: if style_changed { base.style_id } else { None },
141 new_style_id: if style_changed { fork.style_id } else { None },
142 })
143}
144
145fn values_equal(a: &Option<String>, b: &Option<String>) -> bool {
146 match (a, b) {
147 (None, None) => true,
148 (Some(a), Some(b)) => {
149 if let (Ok(fa), Ok(fb)) = (a.parse::<f64>(), b.parse::<f64>()) {
151 (fa - fb).abs() < 1e-9
152 } else {
153 a == b
154 }
155 }
156 _ => false,
157 }
158}