Skip to main content

formualizer_eval/builtins/lookup/
stack.rs

1//! Stack / concatenation dynamic array functions: HSTACK, VSTACK
2//!
3//! Excel semantics (baseline subset):
4//! - Each function accepts 1..N arrays/ranges; scalars treated as 1x1.
5//! - HSTACK: concatenate arrays horizontally (columns) aligning rows; differing row counts -> #VALUE!.
6//! - VSTACK: concatenate arrays vertically (rows) aligning columns; differing column counts -> #VALUE!.
7//! - Empty arguments (zero-sized ranges) are skipped; if all skipped -> empty spill.
8//! - Result collapses to scalar if 1x1 after stacking (consistent with existing dynamic functions here).
9//!
10//! TODO(excel-nuance): Propagate first error cell wise; currently a whole argument that is an Error scalar becomes a 1x1 error block.
11//! TODO(perf): Avoid intermediate full materialization by streaming row-wise/col-wise (later optimization).
12
13use super::super::utils::collapse_if_scalar;
14use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
15use crate::function::Function;
16use crate::traits::{ArgumentHandle, FunctionContext};
17use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
18use formualizer_macros::func_caps;
19
20#[derive(Debug)]
21pub struct HStackFn;
22#[derive(Debug)]
23pub struct VStackFn;
24
25fn materialize_arg<'b>(
26    arg: &ArgumentHandle<'_, 'b>,
27    ctx: &dyn FunctionContext<'b>,
28) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
29    // Similar helper to dynamic.rs (avoid cyclic import). Minimal duplication; consider refactor later.
30    if let Ok(r) = arg.as_reference_or_eval() {
31        let mut rows: Vec<Vec<LiteralValue>> = Vec::new();
32        let sheet = ctx.current_sheet();
33        let rv = ctx.resolve_range_view(&r, sheet)?;
34        rv.for_each_row(&mut |row| {
35            rows.push(row.to_vec());
36            Ok(())
37        })?;
38        Ok(rows)
39    } else {
40        let cv = arg.value()?;
41        match cv.into_literal() {
42            LiteralValue::Array(a) => Ok(a),
43            v => Ok(vec![vec![v]]),
44        }
45    }
46}
47
48impl Function for HStackFn {
49    func_caps!(PURE);
50    fn name(&self) -> &'static str {
51        "HSTACK"
52    }
53    fn min_args(&self) -> usize {
54        1
55    }
56    fn variadic(&self) -> bool {
57        true
58    }
59    fn arg_schema(&self) -> &'static [ArgSchema] {
60        use once_cell::sync::Lazy;
61        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
62            vec![ArgSchema {
63                kinds: smallvec::smallvec![ArgKind::Range, ArgKind::Any],
64                required: true,
65                by_ref: false,
66                shape: ShapeKind::Range,
67                coercion: CoercionPolicy::None,
68                max: None,
69                repeating: Some(1),
70                default: None,
71            }]
72        });
73        &SCHEMA
74    }
75    fn eval<'a, 'b, 'c>(
76        &self,
77        args: &'c [ArgumentHandle<'a, 'b>],
78        ctx: &dyn FunctionContext<'b>,
79    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
80        if args.is_empty() {
81            return Ok(crate::traits::CalcValue::Range(
82                crate::engine::range_view::RangeView::from_owned_rows(vec![], ctx.date_system()),
83            ));
84        }
85
86        let mut entries = Vec::with_capacity(args.len());
87        let mut target_rows: Option<usize> = None;
88        let mut total_cols = 0;
89
90        for a in args {
91            if let Ok(v) = a.range_view() {
92                let (rows, cols) = v.dims();
93                if rows == 0 || cols == 0 {
94                    continue;
95                }
96                if let Some(tr) = target_rows {
97                    if rows != tr {
98                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
99                            ExcelError::new(ExcelErrorKind::Value),
100                        )));
101                    }
102                } else {
103                    target_rows = Some(rows);
104                }
105                total_cols += cols;
106                entries.push(HStackEntry::View(v));
107            } else {
108                let v = a.value()?.into_literal();
109                if let Some(tr) = target_rows {
110                    if tr != 1 {
111                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
112                            ExcelError::new(ExcelErrorKind::Value),
113                        )));
114                    }
115                } else {
116                    target_rows = Some(1);
117                }
118                total_cols += 1;
119                entries.push(HStackEntry::Scalar(v));
120            }
121        }
122
123        if entries.is_empty() {
124            return Ok(crate::traits::CalcValue::Range(
125                crate::engine::range_view::RangeView::from_owned_rows(vec![], ctx.date_system()),
126            ));
127        }
128
129        let row_count = target_rows.unwrap();
130        let mut result: Vec<Vec<LiteralValue>> = Vec::with_capacity(row_count);
131        for _ in 0..row_count {
132            result.push(Vec::with_capacity(total_cols));
133        }
134
135        for entry in entries {
136            match entry {
137                HStackEntry::View(v) => {
138                    let (v_rows, v_cols) = v.dims();
139                    for (r, row) in result.iter_mut().enumerate().take(v_rows) {
140                        for c in 0..v_cols {
141                            row.push(v.get_cell(r, c));
142                        }
143                    }
144                }
145                HStackEntry::Scalar(s) => {
146                    result[0].push(s);
147                }
148            }
149        }
150
151        Ok(collapse_if_scalar(result, ctx.date_system()))
152    }
153}
154
155enum HStackEntry<'a> {
156    View(crate::engine::range_view::RangeView<'a>),
157    Scalar(LiteralValue),
158}
159
160impl Function for VStackFn {
161    func_caps!(PURE);
162    fn name(&self) -> &'static str {
163        "VSTACK"
164    }
165    fn min_args(&self) -> usize {
166        1
167    }
168    fn variadic(&self) -> bool {
169        true
170    }
171    fn arg_schema(&self) -> &'static [ArgSchema] {
172        use once_cell::sync::Lazy;
173        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
174            vec![ArgSchema {
175                kinds: smallvec::smallvec![ArgKind::Range, ArgKind::Any],
176                required: true,
177                by_ref: false,
178                shape: ShapeKind::Range,
179                coercion: CoercionPolicy::None,
180                max: None,
181                repeating: Some(1),
182                default: None,
183            }]
184        });
185        &SCHEMA
186    }
187    fn eval<'a, 'b, 'c>(
188        &self,
189        args: &'c [ArgumentHandle<'a, 'b>],
190        ctx: &dyn FunctionContext<'b>,
191    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
192        if args.is_empty() {
193            return Ok(crate::traits::CalcValue::Range(
194                crate::engine::range_view::RangeView::from_owned_rows(vec![], ctx.date_system()),
195            ));
196        }
197
198        let mut target_width: Option<usize> = None;
199        let mut total_rows = 0;
200        let mut entries = Vec::with_capacity(args.len());
201
202        for a in args {
203            if let Ok(v) = a.range_view() {
204                let (rows, cols) = v.dims();
205                if rows == 0 || cols == 0 {
206                    continue;
207                }
208                if let Some(tw) = target_width {
209                    if cols != tw {
210                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
211                            ExcelError::new(ExcelErrorKind::Value),
212                        )));
213                    }
214                } else {
215                    target_width = Some(cols);
216                }
217                total_rows += rows;
218                entries.push(VStackEntry::View(v));
219            } else {
220                let v = a.value()?.into_literal();
221                if let Some(tw) = target_width {
222                    if tw != 1 {
223                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
224                            ExcelError::new(ExcelErrorKind::Value),
225                        )));
226                    }
227                } else {
228                    target_width = Some(1);
229                }
230                total_rows += 1;
231                entries.push(VStackEntry::Scalar(v));
232            }
233        }
234
235        if entries.is_empty() {
236            return Ok(crate::traits::CalcValue::Range(
237                crate::engine::range_view::RangeView::from_owned_rows(vec![], ctx.date_system()),
238            ));
239        }
240
241        let mut result: Vec<Vec<LiteralValue>> = Vec::with_capacity(total_rows);
242        for entry in entries {
243            match entry {
244                VStackEntry::View(v) => {
245                    let _ = v.for_each_row(&mut |row| {
246                        result.push(row.to_vec());
247                        Ok(())
248                    });
249                }
250                VStackEntry::Scalar(s) => {
251                    result.push(vec![s]);
252                }
253            }
254        }
255
256        Ok(collapse_if_scalar(result, ctx.date_system()))
257    }
258}
259
260enum VStackEntry<'a> {
261    View(crate::engine::range_view::RangeView<'a>),
262    Scalar(LiteralValue),
263}
264
265pub fn register_builtins() {
266    use crate::function_registry::register_function;
267    use std::sync::Arc;
268    register_function(Arc::new(HStackFn));
269    register_function(Arc::new(VStackFn));
270}
271
272/* ───────────────────────── tests ───────────────────────── */
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::test_workbook::TestWorkbook;
277    use crate::traits::ArgumentHandle;
278    use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
279    use std::sync::Arc;
280
281    fn ref_range(r: &str, sr: i32, sc: i32, er: i32, ec: i32) -> ASTNode {
282        ASTNode::new(
283            ASTNodeType::Reference {
284                original: r.into(),
285                reference: ReferenceType::range(
286                    None,
287                    Some(sr as u32),
288                    Some(sc as u32),
289                    Some(er as u32),
290                    Some(ec as u32),
291                ),
292            },
293            None,
294        )
295    }
296
297    fn lit(v: LiteralValue) -> ASTNode {
298        ASTNode::new(ASTNodeType::Literal(v), None)
299    }
300
301    #[test]
302    fn hstack_basic_and_mismatched_rows() {
303        let wb = TestWorkbook::new().with_function(Arc::new(HStackFn));
304        let wb = wb
305            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
306            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
307            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(10))
308            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(20))
309            .with_cell_a1("Sheet1", "C1", LiteralValue::Int(100)); // single row range for mismatch
310        let ctx = wb.interpreter();
311        let left = ref_range("A1:A2", 1, 1, 2, 1);
312        let right = ref_range("B1:B2", 1, 2, 2, 2);
313        let f = ctx.context.get_function("", "HSTACK").unwrap();
314        let args = vec![
315            ArgumentHandle::new(&left, &ctx),
316            ArgumentHandle::new(&right, &ctx),
317        ];
318        let v = f
319            .dispatch(&args, &ctx.function_context(None))
320            .unwrap()
321            .into_literal();
322        match v {
323            LiteralValue::Array(a) => {
324                assert_eq!(a.len(), 2);
325                assert_eq!(
326                    a[0],
327                    vec![LiteralValue::Number(1.0), LiteralValue::Number(10.0)]
328                );
329            }
330            other => panic!("expected array got {other:?}"),
331        }
332        // mismatch rows
333        let mism = ref_range("C1:C1", 1, 3, 1, 3);
334        let args_bad = vec![
335            ArgumentHandle::new(&left, &ctx),
336            ArgumentHandle::new(&mism, &ctx),
337        ];
338        let v_bad = f
339            .dispatch(&args_bad, &ctx.function_context(None))
340            .unwrap()
341            .into_literal();
342        match v_bad {
343            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Value),
344            other => panic!("expected #VALUE! got {other:?}"),
345        }
346    }
347
348    #[test]
349    fn vstack_basic_and_mismatched_cols() {
350        let wb = TestWorkbook::new().with_function(Arc::new(VStackFn));
351        let wb = wb
352            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
353            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(10))
354            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
355            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(20))
356            .with_cell_a1("Sheet1", "C1", LiteralValue::Int(100))
357            .with_cell_a1("Sheet1", "C2", LiteralValue::Int(200));
358        let ctx = wb.interpreter();
359        let top = ref_range("A1:B1", 1, 1, 1, 2);
360        let bottom = ref_range("A2:B2", 2, 1, 2, 2);
361        let f = ctx.context.get_function("", "VSTACK").unwrap();
362        let args = vec![
363            ArgumentHandle::new(&top, &ctx),
364            ArgumentHandle::new(&bottom, &ctx),
365        ];
366        let v = f
367            .dispatch(&args, &ctx.function_context(None))
368            .unwrap()
369            .into_literal();
370        match v {
371            LiteralValue::Array(a) => {
372                assert_eq!(a.len(), 2);
373                assert_eq!(
374                    a[0],
375                    vec![LiteralValue::Number(1.0), LiteralValue::Number(10.0)]
376                );
377            }
378            other => panic!("expected array got {other:?}"),
379        }
380        // mismatched width (add 3rd column row)
381        let extra = ref_range("A1:C1", 1, 1, 1, 3);
382        let args_bad = vec![
383            ArgumentHandle::new(&top, &ctx),
384            ArgumentHandle::new(&extra, &ctx),
385        ];
386        let v_bad = f
387            .dispatch(&args_bad, &ctx.function_context(None))
388            .unwrap()
389            .into_literal();
390        match v_bad {
391            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Value),
392            other => panic!("expected #VALUE! got {other:?}"),
393        }
394    }
395
396    #[test]
397    fn hstack_scalar_and_array_collapse() {
398        let wb = TestWorkbook::new().with_function(Arc::new(HStackFn));
399        let ctx = wb.interpreter();
400        let f = ctx.context.get_function("", "HSTACK").unwrap();
401        let s1 = lit(LiteralValue::Int(5));
402        let s2 = lit(LiteralValue::Int(6));
403        let args = vec![
404            ArgumentHandle::new(&s1, &ctx),
405            ArgumentHandle::new(&s2, &ctx),
406        ];
407        let v = f
408            .dispatch(&args, &ctx.function_context(None))
409            .unwrap()
410            .into_literal();
411        // 1 row x 2 cols stays as array (not scalar collapse)
412        match v {
413            LiteralValue::Array(a) => {
414                assert_eq!(a.len(), 1);
415                assert_eq!(
416                    a[0],
417                    vec![LiteralValue::Number(5.0), LiteralValue::Number(6.0)]
418                );
419            }
420            other => panic!("expected array got {other:?}"),
421        }
422    }
423
424    #[test]
425    fn vstack_scalar_collapse_single_result() {
426        let wb = TestWorkbook::new().with_function(Arc::new(VStackFn));
427        let ctx = wb.interpreter();
428        let f = ctx.context.get_function("", "VSTACK").unwrap();
429        let lone = lit(LiteralValue::Int(9));
430        let args = vec![ArgumentHandle::new(&lone, &ctx)];
431        let v = f
432            .dispatch(&args, &ctx.function_context(None))
433            .unwrap()
434            .into_literal();
435        assert_eq!(v, LiteralValue::Int(9));
436    }
437}