Skip to main content

spreadsheet_mcp/core/
diff.rs

1#[cfg(not(feature = "recalc"))]
2use crate::core::types::{BasicDiffChange, BasicDiffResponse};
3#[cfg(not(feature = "recalc"))]
4use anyhow::Context;
5use anyhow::Result;
6use serde_json::Value;
7use std::path::Path;
8
9#[cfg(feature = "recalc")]
10pub fn calculate_changeset(
11    base_path: &Path,
12    fork_path: &Path,
13    sheet_filter: Option<&str>,
14) -> Result<Vec<crate::diff::Change>> {
15    crate::diff::calculate_changeset(base_path, fork_path, sheet_filter)
16}
17
18pub fn diff_workbooks_json(original: &Path, modified: &Path) -> Result<Value> {
19    #[cfg(feature = "recalc")]
20    {
21        let changes = calculate_changeset(original, modified, None)?;
22        Ok(serde_json::json!({
23            "original": original.display().to_string(),
24            "modified": modified.display().to_string(),
25            "change_count": changes.len(),
26            "changes": changes,
27        }))
28    }
29
30    #[cfg(not(feature = "recalc"))]
31    {
32        let response = basic_diff_workbooks(original, modified)?;
33        Ok(serde_json::to_value(response)?)
34    }
35}
36
37#[cfg(not(feature = "recalc"))]
38#[derive(Debug, Clone)]
39struct CellSnapshot {
40    value: String,
41    formula: Option<String>,
42}
43
44#[cfg(not(feature = "recalc"))]
45fn basic_diff_workbooks(original: &Path, modified: &Path) -> Result<BasicDiffResponse> {
46    use std::collections::BTreeSet;
47
48    let original_cells = collect_cells(original)?;
49    let modified_cells = collect_cells(modified)?;
50
51    let mut keys = BTreeSet::new();
52    keys.extend(original_cells.keys().cloned());
53    keys.extend(modified_cells.keys().cloned());
54
55    let mut changes = Vec::new();
56    for (sheet, address) in keys {
57        let original_cell = original_cells.get(&(sheet.clone(), address.clone()));
58        let modified_cell = modified_cells.get(&(sheet.clone(), address.clone()));
59
60        if cells_equal(original_cell, modified_cell) {
61            continue;
62        }
63
64        let change_type = match (original_cell, modified_cell) {
65            (None, Some(_)) => "added",
66            (Some(_), None) => "removed",
67            (Some(orig), Some(next))
68                if orig.formula != next.formula && orig.value != next.value =>
69            {
70                "formula_and_value_changed"
71            }
72            (Some(orig), Some(next)) if orig.formula != next.formula => "formula_changed",
73            _ => "value_changed",
74        }
75        .to_string();
76
77        changes.push(BasicDiffChange {
78            sheet,
79            address,
80            change_type,
81            original_value: original_cell.map(|cell| cell.value.clone()),
82            original_formula: original_cell.and_then(|cell| cell.formula.clone()),
83            modified_value: modified_cell.map(|cell| cell.value.clone()),
84            modified_formula: modified_cell.and_then(|cell| cell.formula.clone()),
85        });
86    }
87
88    Ok(BasicDiffResponse {
89        original: original.display().to_string(),
90        modified: modified.display().to_string(),
91        change_count: changes.len(),
92        changes,
93    })
94}
95
96#[cfg(not(feature = "recalc"))]
97fn collect_cells(
98    path: &Path,
99) -> Result<std::collections::BTreeMap<(String, String), CellSnapshot>> {
100    let book = umya_spreadsheet::reader::xlsx::read(path)
101        .with_context(|| format!("failed to read workbook '{}'", path.display()))?;
102    let mut cells = std::collections::BTreeMap::new();
103
104    for sheet in book.get_sheet_collection() {
105        let sheet_name = sheet.get_name().to_string();
106        for cell in sheet.get_cell_collection() {
107            let address = cell.get_coordinate().get_coordinate().to_string();
108            let value = cell.get_value().to_string();
109            let formula = if cell.is_formula() {
110                Some(cell.get_formula().to_string())
111            } else {
112                None
113            };
114
115            cells.insert(
116                (sheet_name.clone(), address),
117                CellSnapshot { value, formula },
118            );
119        }
120    }
121
122    Ok(cells)
123}
124
125#[cfg(not(feature = "recalc"))]
126fn cells_equal(left: Option<&CellSnapshot>, right: Option<&CellSnapshot>) -> bool {
127    match (left, right) {
128        (None, None) => true,
129        (Some(a), Some(b)) => a.value == b.value && a.formula == b.formula,
130        _ => false,
131    }
132}