Skip to main content

gridline_engine/engine/
deps.rs

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