Skip to main content

gridline_engine/
lib.rs

1//! gridline_engine - Spreadsheet engine + Rhai integration.
2
3pub(crate) mod builtins;
4pub mod engine;
5pub mod plot;
6
7#[cfg(test)]
8mod tests {
9    use crate::engine::{*, preprocess_script_with_context};
10    use dashmap::DashMap;
11
12    #[test]
13    fn test_from_str_single_letter_columns() {
14        let a1 = CellRef::from_str("A1").unwrap();
15        assert_eq!(a1.row, 0);
16        assert_eq!(a1.col, 0);
17
18        let b1 = CellRef::from_str("B1").unwrap();
19        assert_eq!(b1.row, 0);
20        assert_eq!(b1.col, 1);
21
22        let z1 = CellRef::from_str("Z1").unwrap();
23        assert_eq!(z1.row, 0);
24        assert_eq!(z1.col, 25);
25    }
26
27    #[test]
28    fn test_from_str_multi_letter_columns() {
29        let aa1 = CellRef::from_str("AA1").unwrap();
30        assert_eq!(aa1.col, 26);
31
32        let ab1 = CellRef::from_str("AB1").unwrap();
33        assert_eq!(ab1.col, 27);
34
35        let az1 = CellRef::from_str("AZ1").unwrap();
36        assert_eq!(az1.col, 51);
37
38        let ba1 = CellRef::from_str("BA1").unwrap();
39        assert_eq!(ba1.col, 52);
40    }
41
42    #[test]
43    fn test_from_str_row_numbers() {
44        let a1 = CellRef::from_str("A1").unwrap();
45        assert_eq!(a1.row, 0);
46
47        let a10 = CellRef::from_str("A10").unwrap();
48        assert_eq!(a10.row, 9);
49
50        let a100 = CellRef::from_str("A100").unwrap();
51        assert_eq!(a100.row, 99);
52    }
53
54    #[test]
55    fn test_from_str_case_insensitive() {
56        let lower = CellRef::from_str("a1").unwrap();
57        assert_eq!(lower.row, 0);
58        assert_eq!(lower.col, 0);
59
60        let mixed = CellRef::from_str("aA1").unwrap();
61        assert_eq!(mixed.col, 26);
62    }
63
64    #[test]
65    fn test_from_str_invalid_inputs() {
66        assert!(CellRef::from_str("").is_none());
67        assert!(CellRef::from_str("123").is_none());
68        assert!(CellRef::from_str("ABC").is_none());
69        assert!(CellRef::from_str("A0").is_none());
70        assert!(CellRef::from_str("1A").is_none());
71        assert!(CellRef::from_str("A 1").is_none());
72    }
73
74    #[test]
75    fn test_preprocess_script_simple() {
76        assert_eq!(preprocess_script("A1"), "cell(0, 0)");
77        assert_eq!(preprocess_script("B1"), "cell(0, 1)");
78        assert_eq!(preprocess_script("A2"), "cell(1, 0)");
79    }
80
81    #[test]
82    fn test_preprocess_script_typed_refs() {
83        assert_eq!(preprocess_script("@A1"), "value(0, 0)");
84        assert_eq!(preprocess_script("len(@B1)"), "len(value(0, 1))");
85        assert_eq!(preprocess_script("@A1 + B1"), "value(0, 0) + cell(0, 1)");
86    }
87
88    #[test]
89    fn test_preprocess_script_expression() {
90        assert_eq!(preprocess_script("A1 + B1"), "cell(0, 0) + cell(0, 1)");
91        assert_eq!(
92            preprocess_script("A1 * B2 + C3"),
93            "cell(0, 0) * cell(1, 1) + cell(2, 2)"
94        );
95    }
96
97    #[test]
98    fn test_preprocess_script_preserves_other_content() {
99        assert_eq!(preprocess_script("A1 + 10"), "cell(0, 0) + 10");
100        assert_eq!(preprocess_script("print(A1)"), "print(cell(0, 0))");
101    }
102
103    #[test]
104    fn test_preprocess_row_col_functions() {
105        // ROW() and COL() are replaced with 1-based values when context is provided
106        let cell = CellRef::new(5, 3); // Row 5, Col 3 (0-indexed) => Row 6, Col 4 (1-indexed)
107
108        assert_eq!(
109            preprocess_script_with_context("ROW()", Some(&cell)),
110            "6"
111        );
112        assert_eq!(
113            preprocess_script_with_context("COL()", Some(&cell)),
114            "4"
115        );
116        assert_eq!(
117            preprocess_script_with_context("ROW() + COL()", Some(&cell)),
118            "6 + 4"
119        );
120        // Works with other formula content
121        assert_eq!(
122            preprocess_script_with_context("A1 + ROW()", Some(&cell)),
123            "cell(0, 0) + 6"
124        );
125    }
126
127    #[test]
128    fn test_preprocess_row_col_without_context() {
129        // Without context, ROW() and COL() are left as-is
130        assert_eq!(
131            preprocess_script_with_context("ROW()", None),
132            "ROW()"
133        );
134        assert_eq!(
135            preprocess_script_with_context("COL()", None),
136            "COL()"
137        );
138    }
139
140    #[test]
141    fn test_extract_dependencies_empty() {
142        assert!(extract_dependencies("").is_empty());
143        assert!(extract_dependencies("10 + 20").is_empty());
144    }
145
146    #[test]
147    fn test_extract_dependencies_single() {
148        let deps = extract_dependencies("A1");
149        assert_eq!(deps.len(), 1);
150        assert_eq!(deps[0], CellRef::new(0, 0));
151    }
152
153    #[test]
154    fn test_extract_dependencies_multiple() {
155        let deps = extract_dependencies("A1 + B1 + C2");
156        assert_eq!(deps.len(), 3);
157        assert_eq!(deps[0], CellRef::new(0, 0));
158        assert_eq!(deps[1], CellRef::new(0, 1));
159        assert_eq!(deps[2], CellRef::new(1, 2));
160    }
161
162    #[test]
163    fn test_extract_dependencies_duplicates() {
164        let deps = extract_dependencies("A1 + A1");
165        assert_eq!(deps.len(), 2);
166    }
167
168    #[test]
169    fn test_detect_cycle_no_cycle() {
170        let grid: Grid = std::sync::Arc::new(DashMap::new());
171        grid.insert(CellRef::new(0, 0), Cell::new_number(10.0));
172        grid.insert(CellRef::new(0, 1), Cell::new_number(20.0));
173        grid.insert(CellRef::new(0, 2), Cell::new_script("A1 + B1"));
174
175        assert!(detect_cycle(&CellRef::new(0, 2), &grid).is_none());
176    }
177
178    #[test]
179    fn test_detect_cycle_direct() {
180        let grid: Grid = std::sync::Arc::new(DashMap::new());
181        grid.insert(CellRef::new(0, 0), Cell::new_script("B1"));
182        grid.insert(CellRef::new(0, 1), Cell::new_script("A1"));
183
184        assert!(detect_cycle(&CellRef::new(0, 0), &grid).is_some());
185        assert!(detect_cycle(&CellRef::new(0, 1), &grid).is_some());
186    }
187
188    #[test]
189    fn test_detect_cycle_indirect() {
190        let grid: Grid = std::sync::Arc::new(DashMap::new());
191        grid.insert(CellRef::new(0, 0), Cell::new_script("B1"));
192        grid.insert(CellRef::new(0, 1), Cell::new_script("C1"));
193        grid.insert(CellRef::new(0, 2), Cell::new_script("A1"));
194
195        let cycle = detect_cycle(&CellRef::new(0, 0), &grid);
196        assert!(cycle.is_some());
197        let path = cycle.unwrap();
198        assert!(path.len() >= 3);
199    }
200
201    #[test]
202    fn test_detect_cycle_self_reference() {
203        let grid: Grid = std::sync::Arc::new(DashMap::new());
204        grid.insert(CellRef::new(0, 0), Cell::new_script("A1"));
205
206        assert!(detect_cycle(&CellRef::new(0, 0), &grid).is_some());
207    }
208
209    #[test]
210    fn test_parse_range() {
211        let result = parse_range("A1:B5");
212        assert_eq!(result, Some((0, 0, 4, 1)));
213
214        let result = parse_range("B2:D10");
215        assert_eq!(result, Some((1, 1, 9, 3)));
216
217        let result = parse_range("A1");
218        assert_eq!(result, None);
219
220        let result = parse_range("invalid");
221        assert_eq!(result, None);
222    }
223
224    #[test]
225    fn test_preprocess_script_range_functions() {
226        assert_eq!(preprocess_script("SUM(A1:B5)"), "sum_range(0, 0, 4, 1)");
227        assert_eq!(preprocess_script("AVG(A1:A10)"), "avg_range(0, 0, 9, 0)");
228        assert_eq!(preprocess_script("COUNT(B2:D5)"), "count_range(1, 1, 4, 3)");
229        assert_eq!(preprocess_script("MIN(A1:C3)"), "min_range(0, 0, 2, 2)");
230        assert_eq!(preprocess_script("MAX(A1:Z100)"), "max_range(0, 0, 99, 25)");
231        assert_eq!(
232            preprocess_script("BARCHART(A1:A10)"),
233            "barchart_range(0, 0, 9, 0)"
234        );
235        assert_eq!(
236            preprocess_script("LINECHART(A1:A10)"),
237            "linechart_range(0, 0, 9, 0)"
238        );
239        assert_eq!(
240            preprocess_script("SCATTER(A1:B10)"),
241            "scatter_range(0, 0, 9, 1)"
242        );
243        assert_eq!(
244            preprocess_script("SCATTER(A1:B10, \"My Plot\", \"X\", \"Y\")"),
245            "scatter_range(0, 0, 9, 1, \"My Plot\", \"X\", \"Y\")"
246        );
247        assert_eq!(
248            preprocess_script("SCATTER(A1:B10, \"A1\", \"B2\", \"C3\")"),
249            "scatter_range(0, 0, 9, 1, \"A1\", \"B2\", \"C3\")"
250        );
251    }
252
253    #[test]
254    fn test_preprocess_script_mixed() {
255        assert_eq!(
256            preprocess_script("SUM(A1:A3) + B1"),
257            "sum_range(0, 0, 2, 0) + cell(0, 1)"
258        );
259        assert_eq!(
260            preprocess_script("SUM(A1:A3) * 2 + AVG(B1:B5)"),
261            "sum_range(0, 0, 2, 0) * 2 + avg_range(0, 1, 4, 1)"
262        );
263    }
264
265    #[test]
266    fn test_range_functions_evaluation() {
267        let grid: Grid = std::sync::Arc::new(DashMap::new());
268        grid.insert(CellRef::new(0, 0), Cell::new_number(10.0));
269        grid.insert(CellRef::new(1, 0), Cell::new_number(20.0));
270        grid.insert(CellRef::new(2, 0), Cell::new_number(30.0));
271
272        let engine = create_engine(grid);
273
274        let result: f64 = engine.eval("sum_range(0, 0, 2, 0)").unwrap();
275        assert_eq!(result, 60.0);
276
277        let result: f64 = engine.eval("avg_range(0, 0, 2, 0)").unwrap();
278        assert_eq!(result, 20.0);
279
280        let result: f64 = engine.eval("min_range(0, 0, 2, 0)").unwrap();
281        assert_eq!(result, 10.0);
282
283        let result: f64 = engine.eval("max_range(0, 0, 2, 0)").unwrap();
284        assert_eq!(result, 30.0);
285
286        let result: f64 = engine.eval("count_range(0, 0, 2, 0)").unwrap();
287        assert_eq!(result, 3.0);
288    }
289
290    #[test]
291    fn test_typed_ref_len_over_script_string() {
292        let grid: Grid = std::sync::Arc::new(DashMap::new());
293        grid.insert(CellRef::new(0, 2), Cell::new_number(150.0)); // C1
294        grid.insert(
295            CellRef::new(0, 1),
296            Cell::new_script("if C1 > 100 { \"expensive\" } else { \"cheap\" }"),
297        ); // B1
298
299        let engine = create_engine(grid);
300        let processed = preprocess_script("len(@B1)");
301        let result = eval_with_functions(&engine, &processed, None).unwrap();
302        assert_eq!(result.as_int().unwrap(), 9);
303    }
304
305    #[test]
306    fn test_extract_dependencies_with_ranges() {
307        let deps = extract_dependencies("SUM(A1:A3)");
308        assert_eq!(deps.len(), 3);
309        assert!(deps.contains(&CellRef::new(0, 0)));
310        assert!(deps.contains(&CellRef::new(1, 0)));
311        assert!(deps.contains(&CellRef::new(2, 0)));
312    }
313
314    #[test]
315    fn test_custom_functions() {
316        let grid: Grid = std::sync::Arc::new(DashMap::new());
317        let custom_script = r#"
318            fn double(x) { x * 2.0 }
319            fn square(x) { x * x }
320        "#;
321
322        let (engine, custom_ast, error) =
323            create_engine_with_functions(grid, Some(custom_script));
324        assert!(error.is_none());
325        assert!(custom_ast.is_some());
326
327        let result = eval_with_functions(&engine, "double(5.0)", custom_ast.as_ref()).unwrap();
328        assert_eq!(result.as_float().unwrap(), 10.0);
329
330        let result = eval_with_functions(&engine, "square(4.0)", custom_ast.as_ref()).unwrap();
331        assert_eq!(result.as_float().unwrap(), 16.0);
332    }
333
334    #[test]
335    fn test_custom_functions_with_syntax_error() {
336        let grid: Grid = std::sync::Arc::new(DashMap::new());
337        let bad_script = "fn broken( { }";
338
339        let (_engine, _ast, error) = create_engine_with_functions(grid, Some(bad_script));
340        assert!(error.is_some());
341        assert!(error.unwrap().contains("Error"));
342    }
343}