Skip to main content

gridline_engine/engine/
deps.rs

1//! Dependency extraction from formula strings.
2//!
3//! Parses formula text to find all cell references (e.g., `A1`, `B2:C5`)
4//! that the formula depends on. This is used to build the dependency graph
5//! for cache invalidation and cycle detection.
6//!
7//! Handles:
8//! - Simple cell references: `A1`, `B2`
9//! - Range references in functions: `SUM(A1:B5)`
10//! - Ignores references inside string literals
11
12use regex::Regex;
13
14use super::cell_ref::CellRef;
15
16/// Extract all cell references from a script as dependencies.
17pub fn extract_dependencies(script: &str) -> Vec<CellRef> {
18    let mut deps = Vec::new();
19
20    // Ignore references inside string literals.
21    let script = strip_string_literals(script);
22
23    // Match range functions like SUM(A1:B5, ...)
24    let range_re = crate::builtins::range_fn_re();
25
26    // First, remove range function calls from the script to avoid double-counting
27    let script_without_ranges = range_re.replace_all(&script, "").to_string();
28
29    // Extract dependencies from ranges
30    for caps in range_re.captures_iter(&script) {
31        if let (Some(start), Some(end)) = (CellRef::from_str(&caps[2]), CellRef::from_str(&caps[3]))
32        {
33            let min_row = start.row.min(end.row);
34            let max_row = start.row.max(end.row);
35            let min_col = start.col.min(end.col);
36            let max_col = start.col.max(end.col);
37            for row in min_row..=max_row {
38                for col in min_col..=max_col {
39                    deps.push(CellRef::new(row, col));
40                }
41            }
42        }
43    }
44
45    // Match individual cell references like A1, B2, etc.
46    let cell_re = Regex::new(r"\b([A-Za-z]+)([0-9]+)\b").unwrap();
47
48    for caps in cell_re.captures_iter(&script_without_ranges) {
49        let cell_ref = format!("{}{}", &caps[1], &caps[2]);
50        if let Some(cr) = CellRef::from_str(&cell_ref) {
51            deps.push(cr);
52        }
53    }
54
55    deps
56}
57
58fn strip_string_literals(script: &str) -> String {
59    let mut out = String::with_capacity(script.len());
60    let mut in_string = false;
61    let mut escaped = false;
62
63    for ch in script.chars() {
64        if in_string {
65            if escaped {
66                escaped = false;
67                out.push(' ');
68                continue;
69            }
70            if ch == '\\' {
71                escaped = true;
72                out.push(' ');
73                continue;
74            }
75            if ch == '"' {
76                in_string = false;
77                out.push('"');
78            } else {
79                out.push(' ');
80            }
81        } else if ch == '"' {
82            in_string = true;
83            out.push('"');
84        } else {
85            out.push(ch);
86        }
87    }
88
89    out
90}
91
92/// Parse a cell range like "A1:B5" and return (start_row, start_col, end_row, end_col).
93pub fn parse_range(range: &str) -> Option<(usize, usize, usize, usize)> {
94    let parts: Vec<&str> = range.split(':').collect();
95    if parts.len() != 2 {
96        return None;
97    }
98    let start = CellRef::from_str(parts[0])?;
99    let end = CellRef::from_str(parts[1])?;
100    Some((start.row, start.col, end.row, end.col))
101}