formualizer_eval/builtins/
reference_fns.rs

1use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
2use crate::function::{FnCaps, Function};
3use crate::traits::{ArgumentHandle, FunctionContext};
4use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
5use formualizer_parse::parser::ReferenceType;
6
7fn number_strict_scalar() -> ArgSchema {
8    ArgSchema {
9        kinds: smallvec::smallvec![ArgKind::Number],
10        required: true,
11        by_ref: false,
12        shape: ShapeKind::Scalar,
13        coercion: CoercionPolicy::NumberStrict,
14        max: None,
15        repeating: None,
16        default: None,
17    }
18}
19
20fn arg_byref_array() -> Vec<ArgSchema> {
21    vec![
22        ArgSchema {
23            kinds: smallvec::smallvec![ArgKind::Range],
24            required: true,
25            by_ref: true,
26            shape: ShapeKind::Range,
27            coercion: CoercionPolicy::None,
28            max: None,
29            repeating: None,
30            default: None,
31        },
32        number_strict_scalar(),
33        number_strict_scalar(),
34    ]
35}
36
37fn arg_byref_reference() -> Vec<ArgSchema> {
38    vec![
39        ArgSchema {
40            kinds: smallvec::smallvec![ArgKind::Range],
41            required: true,
42            by_ref: true,
43            shape: ShapeKind::Range,
44            coercion: CoercionPolicy::None,
45            max: None,
46            repeating: None,
47            default: None,
48        },
49        number_strict_scalar(),
50        number_strict_scalar(),
51        ArgSchema {
52            // height optional
53            kinds: smallvec::smallvec![ArgKind::Number],
54            required: false,
55            by_ref: false,
56            shape: ShapeKind::Scalar,
57            coercion: CoercionPolicy::NumberStrict,
58            max: None,
59            repeating: None,
60            default: None,
61        },
62        ArgSchema {
63            // width optional
64            kinds: smallvec::smallvec![ArgKind::Number],
65            required: false,
66            by_ref: false,
67            shape: ShapeKind::Scalar,
68            coercion: CoercionPolicy::NumberStrict,
69            max: None,
70            repeating: None,
71            default: None,
72        },
73    ]
74}
75
76#[derive(Debug)]
77pub struct IndexFn;
78impl Function for IndexFn {
79    fn caps(&self) -> FnCaps {
80        FnCaps::PURE | FnCaps::RETURNS_REFERENCE
81    }
82    fn name(&self) -> &'static str {
83        "INDEX"
84    }
85    fn min_args(&self) -> usize {
86        3
87    }
88    fn arg_schema(&self) -> &'static [ArgSchema] {
89        use once_cell::sync::Lazy;
90        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(arg_byref_array);
91        &SCHEMA
92    }
93
94    fn eval_reference<'a, 'b>(
95        &self,
96        args: &'a [ArgumentHandle<'a, 'b>],
97        _ctx: &dyn FunctionContext,
98    ) -> Option<Result<ReferenceType, ExcelError>> {
99        // args: array(by_ref), row, col
100        if args.len() < 3 {
101            return Some(Err(ExcelError::new(ExcelErrorKind::Value)));
102        }
103        let base = match args[0].as_reference_or_eval() {
104            Ok(r) => r,
105            Err(e) => return Some(Err(e)),
106        };
107        let row = match args[1].value() {
108            Ok(v) => match v.as_ref() {
109                LiteralValue::Number(n) => *n as i64,
110                LiteralValue::Int(i) => *i,
111                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
112            },
113            Err(e) => return Some(Err(e)),
114        };
115        let col = match args[2].value() {
116            Ok(v) => match v.as_ref() {
117                LiteralValue::Number(n) => *n as i64,
118                LiteralValue::Int(i) => *i,
119                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
120            },
121            Err(e) => return Some(Err(e)),
122        };
123
124        // Only Range supported for now
125        let (sheet, sr, sc, er, ec) = match base {
126            ReferenceType::Range {
127                sheet,
128                start_row,
129                start_col,
130                end_row,
131                end_col,
132            } => match (start_row, start_col, end_row, end_col) {
133                (Some(sr), Some(sc), Some(er), Some(ec)) => (sheet, sr, sc, er, ec),
134                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Ref))),
135            },
136            ReferenceType::Cell { sheet, row, col } => (sheet, row, col, row, col),
137            _ => return Some(Err(ExcelError::new(ExcelErrorKind::Ref))),
138        };
139
140        // 1-based indexing per Excel
141        if row <= 0 || col <= 0 {
142            return Some(Err(ExcelError::new(ExcelErrorKind::Ref)));
143        }
144        let r = sr + (row as u32) - 1;
145        let c = sc + (col as u32) - 1;
146        if r > er || c > ec {
147            return Some(Err(ExcelError::new(ExcelErrorKind::Ref)));
148        }
149
150        Some(Ok(ReferenceType::Cell {
151            sheet,
152            row: r,
153            col: c,
154        }))
155    }
156
157    fn eval_scalar<'a, 'b>(
158        &self,
159        args: &'a [ArgumentHandle<'a, 'b>],
160        ctx: &dyn FunctionContext,
161    ) -> Result<LiteralValue, ExcelError> {
162        if let Some(Ok(r)) = self.eval_reference(args, ctx) {
163            // Materialize to value
164            match ctx.resolve_range_view(&r, "Sheet1") {
165                Ok(rv) => {
166                    let (rows, cols) = rv.dims();
167                    if rows == 1 && cols == 1 {
168                        Ok(rv.as_1x1().unwrap_or(LiteralValue::Empty))
169                    } else {
170                        let mut rows_out: Vec<Vec<LiteralValue>> = Vec::new();
171                        rv.for_each_row(&mut |row| {
172                            rows_out.push(row.to_vec());
173                            Ok(())
174                        })?;
175                        Ok(LiteralValue::Array(rows_out))
176                    }
177                }
178                Err(e) => Ok(LiteralValue::Error(e)),
179            }
180        } else {
181            Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref)))
182        }
183    }
184}
185
186#[derive(Debug)]
187pub struct OffsetFn;
188impl Function for OffsetFn {
189    fn caps(&self) -> FnCaps {
190        // OFFSET is volatile in Excel semantics
191        FnCaps::PURE | FnCaps::RETURNS_REFERENCE | FnCaps::VOLATILE
192    }
193    fn name(&self) -> &'static str {
194        "OFFSET"
195    }
196    fn min_args(&self) -> usize {
197        3
198    }
199    fn arg_schema(&self) -> &'static [ArgSchema] {
200        use once_cell::sync::Lazy;
201        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(arg_byref_reference);
202        &SCHEMA
203    }
204
205    fn eval_reference<'a, 'b>(
206        &self,
207        args: &'a [ArgumentHandle<'a, 'b>],
208        _ctx: &dyn FunctionContext,
209    ) -> Option<Result<ReferenceType, ExcelError>> {
210        if args.len() < 3 {
211            return Some(Err(ExcelError::new(ExcelErrorKind::Value)));
212        }
213        let base = match args[0].as_reference_or_eval() {
214            Ok(r) => r,
215            Err(e) => return Some(Err(e)),
216        };
217        let dr = match args[1].value() {
218            Ok(v) => match v.as_ref() {
219                LiteralValue::Number(n) => *n as i64,
220                LiteralValue::Int(i) => *i,
221                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
222            },
223            Err(e) => return Some(Err(e)),
224        };
225        let dc = match args[2].value() {
226            Ok(v) => match v.as_ref() {
227                LiteralValue::Number(n) => *n as i64,
228                LiteralValue::Int(i) => *i,
229                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
230            },
231            Err(e) => return Some(Err(e)),
232        };
233
234        let (sheet, sr, sc, er, ec) = match base {
235            ReferenceType::Range {
236                sheet,
237                start_row,
238                start_col,
239                end_row,
240                end_col,
241            } => match (start_row, start_col, end_row, end_col) {
242                (Some(sr), Some(sc), Some(er), Some(ec)) => (sheet, sr, sc, er, ec),
243                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Ref))),
244            },
245            ReferenceType::Cell { sheet, row, col } => (sheet, row, col, row, col),
246            _ => return Some(Err(ExcelError::new(ExcelErrorKind::Ref))),
247        };
248
249        let nsr = (sr as i64) + dr;
250        let nsc = (sc as i64) + dc;
251        let height = if args.len() >= 4 {
252            match args[3].value() {
253                Ok(v) => match v.as_ref() {
254                    LiteralValue::Number(n) => *n as i64,
255                    LiteralValue::Int(i) => *i,
256                    _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
257                },
258                Err(e) => return Some(Err(e)),
259            }
260        } else {
261            (er as i64) - (sr as i64) + 1
262        };
263        let width = if args.len() >= 5 {
264            match args[4].value() {
265                Ok(v) => match v.as_ref() {
266                    LiteralValue::Number(n) => *n as i64,
267                    LiteralValue::Int(i) => *i,
268                    _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
269                },
270                Err(e) => return Some(Err(e)),
271            }
272        } else {
273            (ec as i64) - (sc as i64) + 1
274        };
275
276        if nsr <= 0 || nsc <= 0 || height <= 0 || width <= 0 {
277            return Some(Err(ExcelError::new(ExcelErrorKind::Ref)));
278        }
279        let ner = nsr + height - 1;
280        let nec = nsc + width - 1;
281
282        if height == 1 && width == 1 {
283            Some(Ok(ReferenceType::Cell {
284                sheet,
285                row: nsr as u32,
286                col: nsc as u32,
287            }))
288        } else {
289            Some(Ok(ReferenceType::Range {
290                sheet,
291                start_row: Some(nsr as u32),
292                start_col: Some(nsc as u32),
293                end_row: Some(ner as u32),
294                end_col: Some(nec as u32),
295            }))
296        }
297    }
298
299    fn eval_scalar<'a, 'b>(
300        &self,
301        args: &'a [ArgumentHandle<'a, 'b>],
302        ctx: &dyn FunctionContext,
303    ) -> Result<LiteralValue, ExcelError> {
304        if let Some(Ok(r)) = self.eval_reference(args, ctx) {
305            match ctx.resolve_range_view(&r, "Sheet1") {
306                Ok(rv) => {
307                    let (rows, cols) = rv.dims();
308                    if rows == 1 && cols == 1 {
309                        Ok(rv.as_1x1().unwrap_or(LiteralValue::Empty))
310                    } else {
311                        let mut rows_out: Vec<Vec<LiteralValue>> = Vec::new();
312                        rv.for_each_row(&mut |row| {
313                            rows_out.push(row.to_vec());
314                            Ok(())
315                        })?;
316                        Ok(LiteralValue::Array(rows_out))
317                    }
318                }
319                Err(e) => Ok(LiteralValue::Error(e)),
320            }
321        } else {
322            Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref)))
323        }
324    }
325}
326
327pub fn register_builtins() {
328    crate::function_registry::register_function(std::sync::Arc::new(IndexFn));
329    crate::function_registry::register_function(std::sync::Arc::new(OffsetFn));
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::test_workbook::TestWorkbook;
336    use crate::traits::ArgumentHandle;
337    use formualizer_parse::parser::{ASTNode, ASTNodeType};
338
339    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
340        wb.interpreter()
341    }
342
343    #[test]
344    fn index_returns_reference_and_materializes_in_value_context() {
345        let wb = TestWorkbook::new()
346            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(42))
347            .with_function(std::sync::Arc::new(IndexFn));
348        let ctx = interp(&wb);
349
350        // Build INDEX(A1:C3,2,2) expecting B2
351        let array_ref = ASTNode::new(
352            ASTNodeType::Reference {
353                original: "A1:C3".into(),
354                reference: ReferenceType::Range {
355                    sheet: None,
356                    start_row: Some(1),
357                    start_col: Some(1),
358                    end_row: Some(3),
359                    end_col: Some(3),
360                },
361            },
362            None,
363        );
364        let row = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(2)), None);
365        let col = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(2)), None);
366        let call = ASTNode::new(
367            ASTNodeType::Function {
368                name: "INDEX".into(),
369                args: vec![array_ref.clone(), row.clone(), col.clone()],
370            },
371            None,
372        );
373
374        // Reference context
375        let r = ctx.evaluate_ast_as_reference(&call).expect("ref ok");
376        match r {
377            ReferenceType::Cell { row, col, .. } => {
378                assert_eq!((row, col), (2, 2));
379            }
380            _ => panic!(),
381        }
382
383        // Value context (scalar materialization)
384        let args = vec![
385            ArgumentHandle::new(&array_ref, &ctx),
386            ArgumentHandle::new(&row, &ctx),
387            ArgumentHandle::new(&col, &ctx),
388        ];
389        let f = ctx.context.get_function("", "INDEX").unwrap();
390        let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
391        assert_eq!(v, LiteralValue::Int(42));
392    }
393
394    #[test]
395    fn offset_returns_reference_and_materializes() {
396        let wb = TestWorkbook::new()
397            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
398            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(5))
399            .with_function(std::sync::Arc::new(OffsetFn));
400        let ctx = interp(&wb);
401
402        let base = ASTNode::new(
403            ASTNodeType::Reference {
404                original: "A1".into(),
405                reference: ReferenceType::Cell {
406                    sheet: None,
407                    row: 1,
408                    col: 1,
409                },
410            },
411            None,
412        );
413        let dr = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
414        let dc = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
415        let call = ASTNode::new(
416            ASTNodeType::Function {
417                name: "OFFSET".into(),
418                args: vec![base.clone(), dr.clone(), dc.clone()],
419            },
420            None,
421        );
422
423        let r = ctx.evaluate_ast_as_reference(&call).expect("ref ok");
424        match r {
425            ReferenceType::Cell { row, col, .. } => assert_eq!((row, col), (2, 2)),
426            _ => panic!(),
427        }
428
429        let args = vec![
430            ArgumentHandle::new(&base, &ctx),
431            ArgumentHandle::new(&dr, &ctx),
432            ArgumentHandle::new(&dc, &ctx),
433        ];
434        let f = ctx.context.get_function("", "OFFSET").unwrap();
435        let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
436        assert_eq!(v, LiteralValue::Int(5));
437    }
438}