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    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_preprocess_row_col_functions() {
106        // ROW() and COL() are replaced with 1-based values when context is provided
107        let cell = CellRef::new(5, 3); // Row 5, Col 3 (0-indexed) => Row 6, Col 4 (1-indexed)
108
109        assert_eq!(
110            preprocess_script_with_context("ROW()", Some(&cell)),
111            "6"
112        );
113        assert_eq!(
114            preprocess_script_with_context("COL()", Some(&cell)),
115            "4"
116        );
117        assert_eq!(
118            preprocess_script_with_context("ROW() + COL()", Some(&cell)),
119            "6 + 4"
120        );
121        // Works with other formula content
122        assert_eq!(
123            preprocess_script_with_context("A1 + ROW()", Some(&cell)),
124            "cell(0, 0) + 6"
125        );
126    }
127
128    #[test]
129    fn test_preprocess_row_col_without_context() {
130        // Without context, ROW() and COL() are left as-is
131        assert_eq!(
132            preprocess_script_with_context("ROW()", None),
133            "ROW()"
134        );
135        assert_eq!(
136            preprocess_script_with_context("COL()", None),
137            "COL()"
138        );
139    }
140
141    #[test]
142    fn test_extract_dependencies_empty() {
143        assert!(extract_dependencies("").is_empty());
144        assert!(extract_dependencies("10 + 20").is_empty());
145    }
146
147    #[test]
148    fn test_extract_dependencies_single() {
149        let deps = extract_dependencies("A1");
150        assert_eq!(deps.len(), 1);
151        assert_eq!(deps[0], CellRef::new(0, 0));
152    }
153
154    #[test]
155    fn test_extract_dependencies_multiple() {
156        let deps = extract_dependencies("A1 + B1 + C2");
157        assert_eq!(deps.len(), 3);
158        assert_eq!(deps[0], CellRef::new(0, 0));
159        assert_eq!(deps[1], CellRef::new(0, 1));
160        assert_eq!(deps[2], CellRef::new(1, 2));
161    }
162
163    #[test]
164    fn test_extract_dependencies_duplicates() {
165        let deps = extract_dependencies("A1 + A1");
166        assert_eq!(deps.len(), 2);
167    }
168
169    #[test]
170    fn test_detect_cycle_no_cycle() {
171        let grid: Grid = DashMap::new();
172        grid.insert(CellRef::new(0, 0), Cell::new_number(10.0));
173        grid.insert(CellRef::new(0, 1), Cell::new_number(20.0));
174        grid.insert(CellRef::new(0, 2), Cell::new_script("A1 + B1"));
175
176        assert!(detect_cycle(&CellRef::new(0, 2), &grid).is_none());
177    }
178
179    #[test]
180    fn test_detect_cycle_direct() {
181        let grid: Grid = DashMap::new();
182        grid.insert(CellRef::new(0, 0), Cell::new_script("B1"));
183        grid.insert(CellRef::new(0, 1), Cell::new_script("A1"));
184
185        assert!(detect_cycle(&CellRef::new(0, 0), &grid).is_some());
186        assert!(detect_cycle(&CellRef::new(0, 1), &grid).is_some());
187    }
188
189    #[test]
190    fn test_detect_cycle_indirect() {
191        let grid: Grid = DashMap::new();
192        grid.insert(CellRef::new(0, 0), Cell::new_script("B1"));
193        grid.insert(CellRef::new(0, 1), Cell::new_script("C1"));
194        grid.insert(CellRef::new(0, 2), Cell::new_script("A1"));
195
196        let cycle = detect_cycle(&CellRef::new(0, 0), &grid);
197        assert!(cycle.is_some());
198        let path = cycle.unwrap();
199        assert!(path.len() >= 3);
200    }
201
202    #[test]
203    fn test_detect_cycle_self_reference() {
204        let grid: Grid = DashMap::new();
205        grid.insert(CellRef::new(0, 0), Cell::new_script("A1"));
206
207        assert!(detect_cycle(&CellRef::new(0, 0), &grid).is_some());
208    }
209
210    #[test]
211    fn test_parse_range() {
212        let result = parse_range("A1:B5");
213        assert_eq!(result, Some((0, 0, 4, 1)));
214
215        let result = parse_range("B2:D10");
216        assert_eq!(result, Some((1, 1, 9, 3)));
217
218        let result = parse_range("A1");
219        assert_eq!(result, None);
220
221        let result = parse_range("invalid");
222        assert_eq!(result, None);
223    }
224
225    #[test]
226    fn test_preprocess_script_range_functions() {
227        assert_eq!(preprocess_script("SUM(A1:B5)"), "sum_range(0, 0, 4, 1)");
228        assert_eq!(preprocess_script("AVG(A1:A10)"), "avg_range(0, 0, 9, 0)");
229        assert_eq!(preprocess_script("COUNT(B2:D5)"), "count_range(1, 1, 4, 3)");
230        assert_eq!(preprocess_script("MIN(A1:C3)"), "min_range(0, 0, 2, 2)");
231        assert_eq!(preprocess_script("MAX(A1:Z100)"), "max_range(0, 0, 99, 25)");
232        assert_eq!(
233            preprocess_script("BARCHART(A1:A10)"),
234            "barchart_range(0, 0, 9, 0)"
235        );
236        assert_eq!(
237            preprocess_script("LINECHART(A1:A10)"),
238            "linechart_range(0, 0, 9, 0)"
239        );
240        assert_eq!(
241            preprocess_script("SCATTER(A1:B10)"),
242            "scatter_range(0, 0, 9, 1)"
243        );
244        assert_eq!(
245            preprocess_script("SCATTER(A1:B10, \"My Plot\", \"X\", \"Y\")"),
246            "scatter_range(0, 0, 9, 1, \"My Plot\", \"X\", \"Y\")"
247        );
248        assert_eq!(
249            preprocess_script("SCATTER(A1:B10, \"A1\", \"B2\", \"C3\")"),
250            "scatter_range(0, 0, 9, 1, \"A1\", \"B2\", \"C3\")"
251        );
252    }
253
254    #[test]
255    fn test_preprocess_script_mixed() {
256        assert_eq!(
257            preprocess_script("SUM(A1:A3) + B1"),
258            "sum_range(0, 0, 2, 0) + cell(0, 1)"
259        );
260        assert_eq!(
261            preprocess_script("SUM(A1:A3) * 2 + AVG(B1:B5)"),
262            "sum_range(0, 0, 2, 0) * 2 + avg_range(0, 1, 4, 1)"
263        );
264    }
265
266    #[test]
267    fn test_range_functions_evaluation() {
268        let grid: Grid = DashMap::new();
269        grid.insert(CellRef::new(0, 0), Cell::new_number(10.0));
270        grid.insert(CellRef::new(1, 0), Cell::new_number(20.0));
271        grid.insert(CellRef::new(2, 0), Cell::new_number(30.0));
272
273        let engine = create_engine(Arc::new(grid));
274
275        let result: f64 = engine.eval("sum_range(0, 0, 2, 0)").unwrap();
276        assert_eq!(result, 60.0);
277
278        let result: f64 = engine.eval("avg_range(0, 0, 2, 0)").unwrap();
279        assert_eq!(result, 20.0);
280
281        let result: f64 = engine.eval("min_range(0, 0, 2, 0)").unwrap();
282        assert_eq!(result, 10.0);
283
284        let result: f64 = engine.eval("max_range(0, 0, 2, 0)").unwrap();
285        assert_eq!(result, 30.0);
286
287        let result: f64 = engine.eval("count_range(0, 0, 2, 0)").unwrap();
288        assert_eq!(result, 3.0);
289    }
290
291    #[test]
292    fn test_typed_ref_len_over_script_string() {
293        let grid: Grid = DashMap::new();
294        grid.insert(CellRef::new(0, 2), Cell::new_number(150.0)); // C1
295        grid.insert(
296            CellRef::new(0, 1),
297            Cell::new_script("if C1 > 100 { \"expensive\" } else { \"cheap\" }"),
298        ); // B1
299
300        let engine = create_engine(Arc::new(grid));
301        let processed = preprocess_script("len(@B1)");
302        let result = eval_with_functions(&engine, &processed, None).unwrap();
303        assert_eq!(result.as_int().unwrap(), 9);
304    }
305
306    #[test]
307    fn test_extract_dependencies_with_ranges() {
308        let deps = extract_dependencies("SUM(A1:A3)");
309        assert_eq!(deps.len(), 3);
310        assert!(deps.contains(&CellRef::new(0, 0)));
311        assert!(deps.contains(&CellRef::new(1, 0)));
312        assert!(deps.contains(&CellRef::new(2, 0)));
313    }
314
315    #[test]
316    fn test_custom_functions() {
317        let grid: Grid = DashMap::new();
318        let custom_script = r#"
319            fn double(x) { x * 2.0 }
320            fn square(x) { x * x }
321        "#;
322
323        let (engine, custom_ast, error) =
324            create_engine_with_functions(Arc::new(grid), Some(custom_script));
325        assert!(error.is_none());
326        assert!(custom_ast.is_some());
327
328        let result = eval_with_functions(&engine, "double(5.0)", custom_ast.as_ref()).unwrap();
329        assert_eq!(result.as_float().unwrap(), 10.0);
330
331        let result = eval_with_functions(&engine, "square(4.0)", custom_ast.as_ref()).unwrap();
332        assert_eq!(result.as_float().unwrap(), 16.0);
333    }
334
335    #[test]
336    fn test_custom_functions_with_syntax_error() {
337        let grid: Grid = DashMap::new();
338        let bad_script = "fn broken( { }";
339
340        let (_engine, _ast, error) = create_engine_with_functions(Arc::new(grid), Some(bad_script));
341        assert!(error.is_some());
342        assert!(error.unwrap().contains("Error"));
343    }
344}