spreadsheet_mcp/core/
diff.rs1#[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}