Skip to main content

spreadsheet_mcp/diff/
merge.rs

1use 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        // Handle errors in stream
50        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        // Get references to Ok items
58        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                        // Base is behind -> Deleted
82                        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                        // Fork is behind -> Added
90                        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                        // Same address -> Compare
99                        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, // Should be covered above
130        }
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            // Try numeric comparison with epsilon
150            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}