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