Skip to main content

gridline_engine/engine/
preprocess.rs

1use regex::Regex;
2
3use super::cell_ref::CellRef;
4
5/// Operation for shifting cell references in formulas
6#[derive(Clone, Copy, Debug)]
7pub enum ShiftOperation {
8    InsertRow(usize),
9    DeleteRow(usize),
10    InsertColumn(usize),
11    DeleteColumn(usize),
12}
13
14/// Shift cell references in a formula when rows/cols are inserted/deleted.
15/// Returns the updated formula string.
16///
17/// Rules:
18/// - Insert row at R: refs to row >= R become row + 1
19/// - Delete row at R: refs to row > R become row - 1; row == R becomes `#REF!`
20/// - Same logic for columns
21pub fn shift_formula_references(formula: &str, op: ShiftOperation) -> String {
22    // Handle range functions like SUM(A1:B5, ...)
23    let with_shifted_ranges = crate::builtins::range_fn_re()
24        .replace_all(formula, |caps: &regex::Captures| {
25            let func_name = &caps[1];
26            let start_ref = &caps[2];
27            let end_ref = &caps[3];
28            let rest_args = caps.get(4).map(|m| m.as_str()).unwrap_or("");
29
30            let new_start = shift_single_ref(start_ref, op);
31            let new_end = shift_single_ref(end_ref, op);
32
33            // If either ref became #REF!, return #REF!
34            if new_start == "#REF!" || new_end == "#REF!" {
35                return "#REF!".to_string();
36            }
37
38            format!("{}({}:{}{}", func_name, new_start, new_end, rest_args)
39        })
40        .to_string();
41
42    // Now shift individual cell references
43    shift_cell_refs_outside_strings(&with_shifted_ranges, op)
44}
45
46fn shift_single_ref(cell_ref_str: &str, op: ShiftOperation) -> String {
47    let Some(cr) = CellRef::from_str(cell_ref_str) else {
48        return cell_ref_str.to_string();
49    };
50
51    match op {
52        ShiftOperation::InsertRow(at_row) => {
53            if cr.row >= at_row {
54                CellRef::new(cr.row + 1, cr.col).to_string()
55            } else {
56                cr.to_string()
57            }
58        }
59        ShiftOperation::DeleteRow(at_row) => {
60            if cr.row == at_row {
61                "#REF!".to_string()
62            } else if cr.row > at_row {
63                CellRef::new(cr.row - 1, cr.col).to_string()
64            } else {
65                cr.to_string()
66            }
67        }
68        ShiftOperation::InsertColumn(at_col) => {
69            if cr.col >= at_col {
70                CellRef::new(cr.row, cr.col + 1).to_string()
71            } else {
72                cr.to_string()
73            }
74        }
75        ShiftOperation::DeleteColumn(at_col) => {
76            if cr.col == at_col {
77                "#REF!".to_string()
78            } else if cr.col > at_col {
79                CellRef::new(cr.row, cr.col - 1).to_string()
80            } else {
81                cr.to_string()
82            }
83        }
84    }
85}
86
87fn shift_cell_refs_outside_strings(script: &str, op: ShiftOperation) -> String {
88    let cell_re = Regex::new(r"\b([A-Za-z]+)([0-9]+)\b").unwrap();
89    let value_re = Regex::new(r"@([A-Za-z]+)([0-9]+)\b").unwrap();
90
91    let shift_cells = |seg: &str| {
92        // First handle @-prefixed refs (value refs)
93        let seg = value_re
94            .replace_all(seg, |caps: &regex::Captures| {
95                let cell_ref = format!("{}{}", &caps[1], &caps[2]);
96                let shifted = shift_single_ref(&cell_ref, op);
97                if shifted == "#REF!" {
98                    shifted
99                } else {
100                    format!("@{}", shifted)
101                }
102            })
103            .to_string();
104
105        // Then handle regular refs
106        cell_re
107            .replace_all(&seg, |caps: &regex::Captures| {
108                let cell_ref = format!("{}{}", &caps[1], &caps[2]);
109                shift_single_ref(&cell_ref, op)
110            })
111            .to_string()
112    };
113
114    // Process outside of string literals
115    let bytes = script.as_bytes();
116    let mut out = String::new();
117    let mut seg_start = 0;
118    let mut in_string = false;
119    let mut backslashes = 0usize;
120    let mut i = 0usize;
121
122    while i < bytes.len() {
123        let b = bytes[i];
124        if in_string {
125            if b == b'\\' {
126                backslashes += 1;
127                i += 1;
128                continue;
129            }
130            if b == b'"' && backslashes % 2 == 0 {
131                out.push_str(&script[seg_start..=i]);
132                in_string = false;
133                seg_start = i + 1;
134            }
135            backslashes = 0;
136            i += 1;
137            continue;
138        }
139
140        if b == b'"' {
141            out.push_str(&shift_cells(&script[seg_start..i]));
142            in_string = true;
143            seg_start = i;
144            backslashes = 0;
145            i += 1;
146            continue;
147        }
148
149        i += 1;
150    }
151
152    if seg_start < script.len() {
153        if in_string {
154            out.push_str(&script[seg_start..]);
155        } else {
156            out.push_str(&shift_cells(&script[seg_start..]));
157        }
158    }
159
160    out
161}
162
163/// Replace cell references like "A1" with Rhai function calls like "cell(0, 0)".
164/// Typed refs like "@A1" become "value(0, 0)" (returns Dynamic).
165/// Also transforms range functions like SUM(A1:B5, ...) into sum_range(0, 0, 4, 1, ...).
166pub fn preprocess_script(script: &str) -> String {
167    let with_ranges = crate::builtins::range_fn_re()
168        .replace_all(script, |caps: &regex::Captures| {
169            let start_ref = &caps[2];
170            let end_ref = &caps[3];
171            let rest_args = caps.get(4).map(|m| m.as_str()).unwrap_or("");
172
173            let Some(rhai_name) = crate::builtins::range_rhai_name(&caps[1]) else {
174                return caps[0].to_string();
175            };
176
177            if let (Some(start), Some(end)) =
178                (CellRef::from_str(start_ref), CellRef::from_str(end_ref))
179            {
180                format!(
181                    "{}({}, {}, {}, {}{})",
182                    rhai_name, start.row, start.col, end.row, end.col, rest_args
183                )
184            } else {
185                caps[0].to_string()
186            }
187        })
188        .to_string();
189
190    replace_cell_refs_outside_strings(&with_ranges)
191}
192
193fn replace_cell_refs_outside_strings(script: &str) -> String {
194    let cell_re = Regex::new(r"\b([A-Za-z]+)([0-9]+)\b").unwrap();
195    let value_re = Regex::new(r"@([A-Za-z]+)([0-9]+)\b").unwrap();
196
197    let replace_cells = |seg: &str| {
198        let seg = value_re
199            .replace_all(seg, |caps: &regex::Captures| {
200                let cell_ref = format!("{}{}", &caps[1], &caps[2]);
201                if let Some(cr) = CellRef::from_str(&cell_ref) {
202                    format!("value({}, {})", cr.row, cr.col)
203                } else {
204                    caps[0].to_string()
205                }
206            })
207            .to_string();
208
209        cell_re
210            .replace_all(&seg, |caps: &regex::Captures| {
211                let cell_ref = format!("{}{}", &caps[1], &caps[2]);
212                if let Some(cr) = CellRef::from_str(&cell_ref) {
213                    format!("cell({}, {})", cr.row, cr.col)
214                } else {
215                    caps[0].to_string()
216                }
217            })
218            .to_string()
219    };
220
221    let bytes = script.as_bytes();
222    let mut out = String::new();
223    let mut seg_start = 0;
224    let mut in_string = false;
225    let mut backslashes = 0usize;
226    let mut i = 0usize;
227
228    while i < bytes.len() {
229        let b = bytes[i];
230        if in_string {
231            if b == b'\\' {
232                backslashes += 1;
233                i += 1;
234                continue;
235            }
236            if b == b'"' && backslashes.is_multiple_of(2) {
237                out.push_str(&script[seg_start..=i]);
238                in_string = false;
239                seg_start = i + 1;
240            }
241            backslashes = 0;
242            i += 1;
243            continue;
244        }
245
246        if b == b'"' {
247            out.push_str(&replace_cells(&script[seg_start..i]));
248            in_string = true;
249            seg_start = i;
250            backslashes = 0;
251            i += 1;
252            continue;
253        }
254
255        i += 1;
256    }
257
258    if seg_start < script.len() {
259        if in_string {
260            out.push_str(&script[seg_start..]);
261        } else {
262            out.push_str(&replace_cells(&script[seg_start..]));
263        }
264    }
265
266    out
267}