Skip to main content

truecalc_core/eval/functions/array/
mod.rs

1//! Array and matrix functions for Google Sheets compatibility.
2
3use crate::eval::coercion::to_bool;
4use crate::eval::{evaluate_expr, EvalCtx};
5use crate::parser::ast::Expr;
6use crate::types::{ErrorKind, Value};
7
8use super::{check_arity, check_arity_len, EagerFn, EvalOp, FunctionKind, FunctionMeta, Registry};
9
10// ── 2D array helpers ──────────────────────────────────────────────────────────
11
12/// Convert a Value into a 2D grid (Vec<Vec<Value>>).
13/// - Nested Array (2D): outer = rows, inner = cols
14/// - Flat Array (1D): one row
15/// - Scalar: 1x1
16pub fn to_2d(v: &Value) -> Vec<Vec<Value>> {
17    match v {
18        Value::Array(outer) => {
19            if outer.iter().any(|e| matches!(e, Value::Array(_))) {
20                outer
21                    .iter()
22                    .map(|row| match row {
23                        Value::Array(cols) => cols.clone(),
24                        other => vec![other.clone()],
25                    })
26                    .collect()
27            } else {
28                vec![outer.clone()] // 1-D flat array → single row
29            }
30        }
31        other => vec![vec![other.clone()]], // scalar → 1×1
32    }
33}
34
35/// Convert a 2D grid back to a Value.
36/// - Empty grid → empty Array
37/// - Single row → flat Array
38/// - Multiple rows → nested Array of row Arrays
39pub fn from_2d(rows: Vec<Vec<Value>>) -> Value {
40    if rows.is_empty() {
41        return Value::Array(vec![]);
42    }
43    if rows.len() == 1 {
44        return Value::Array(rows.into_iter().next().unwrap());
45    }
46    Value::Array(rows.into_iter().map(Value::Array).collect())
47}
48
49/// Flatten a Value to a 1D Vec<Value> (row-major order).
50pub fn flatten_val(v: &Value) -> Vec<Value> {
51    match v {
52        Value::Array(outer) => {
53            if outer.iter().any(|e| matches!(e, Value::Array(_))) {
54                outer
55                    .iter()
56                    .flat_map(|row| match row {
57                        Value::Array(cols) => cols.clone(),
58                        other => vec![other.clone()],
59                    })
60                    .collect()
61            } else {
62                outer.clone()
63            }
64        }
65        other => vec![other.clone()],
66    }
67}
68
69/// Convert a Value to f64 for numeric computations.
70fn to_f64(v: &Value) -> Option<f64> {
71    match v {
72        Value::Number(n) => Some(*n),
73        Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
74        _ => None,
75    }
76}
77
78
79
80// ── ROWS ─────────────────────────────────────────────────────────────────────
81
82pub(crate) fn rows_fn(args: &[Value]) -> Value {
83    if let Some(e) = check_arity(args, 1, 1) {
84        return e;
85    }
86    let grid = to_2d(&args[0]);
87    Value::Number(grid.len() as f64)
88}
89
90// ── COLUMNS ───────────────────────────────────────────────────────────────────
91
92pub(crate) fn columns_fn(args: &[Value]) -> Value {
93    if let Some(e) = check_arity(args, 1, 1) {
94        return e;
95    }
96    let grid = to_2d(&args[0]);
97    let cols = grid.first().map(|r| r.len()).unwrap_or(0);
98    Value::Number(cols as f64)
99}
100
101// ── TRANSPOSE ─────────────────────────────────────────────────────────────────
102
103pub(crate) fn transpose_fn(args: &[Value]) -> Value {
104    if let Some(e) = check_arity(args, 1, 1) {
105        return e;
106    }
107    let grid = to_2d(&args[0]);
108    if grid.is_empty() {
109        return Value::Array(vec![]);
110    }
111    let nrows = grid.len();
112    let ncols = grid[0].len();
113    let transposed: Vec<Vec<Value>> = (0..ncols)
114        .map(|c| (0..nrows).map(|r| grid[r][c].clone()).collect())
115        .collect();
116    from_2d(transposed)
117}
118
119// ── ARRAY_CONSTRAIN ───────────────────────────────────────────────────────────
120
121pub(crate) fn array_constrain_fn(args: &[Value]) -> Value {
122    if let Some(e) = check_arity(args, 3, 3) {
123        return e;
124    }
125    let grid = to_2d(&args[0]);
126    let num_rows = match to_f64(&args[1]) {
127        Some(n) if n >= 1.0 => n as usize,
128        Some(n) if n < 0.0 => return Value::Error(ErrorKind::Num),
129        Some(_) => return Value::Error(ErrorKind::Ref),
130        None => return Value::Error(ErrorKind::Value),
131    };
132    let num_cols = match to_f64(&args[2]) {
133        Some(n) if n >= 1.0 => n as usize,
134        Some(n) if n < 0.0 => return Value::Error(ErrorKind::Num),
135        Some(_) => return Value::Error(ErrorKind::Ref),
136        None => return Value::Error(ErrorKind::Value),
137    };
138    let rows_to_take = num_rows.min(grid.len());
139    let result: Vec<Vec<Value>> = grid[..rows_to_take]
140        .iter()
141        .map(|row| {
142            let cols_to_take = num_cols.min(row.len());
143            row[..cols_to_take].to_vec()
144        })
145        .collect();
146    from_2d(result)
147}
148
149// ── CHOOSECOLS ────────────────────────────────────────────────────────────────
150
151fn choosecols_fn(args: &[Value]) -> Value {
152    if let Some(e) = check_arity(args, 2, usize::MAX) {
153        return e;
154    }
155    let grid = to_2d(&args[0]);
156    let ncols = grid.first().map(|r| r.len()).unwrap_or(0);
157    let mut selected_cols: Vec<usize> = Vec::new();
158    for col_arg in &args[1..] {
159        match to_f64(col_arg) {
160            Some(0.0) => return Value::Error(ErrorKind::Value),
161            Some(n) => {
162                let idx = if n < 0.0 {
163                    let i = (ncols as isize + n as isize) as usize;
164                    if n as isize + (ncols as isize) < 0 {
165                        return Value::Error(ErrorKind::Value);
166                    }
167                    i
168                } else {
169                    let i = n as usize - 1;
170                    if i >= ncols {
171                        return Value::Error(ErrorKind::Value);
172                    }
173                    i
174                };
175                selected_cols.push(idx);
176            }
177            None => return Value::Error(ErrorKind::Value),
178        }
179    }
180    let result: Vec<Vec<Value>> = grid
181        .iter()
182        .map(|row| {
183            selected_cols
184                .iter()
185                .map(|&c| row.get(c).cloned().unwrap_or(Value::Empty))
186                .collect()
187        })
188        .collect();
189    from_2d(result)
190}
191
192// ── CHOOSEROWS ────────────────────────────────────────────────────────────────
193
194fn chooserows_fn(args: &[Value]) -> Value {
195    if let Some(e) = check_arity(args, 2, usize::MAX) {
196        return e;
197    }
198    let grid = to_2d(&args[0]);
199    let nrows = grid.len();
200    let mut selected_rows: Vec<usize> = Vec::new();
201    for row_arg in &args[1..] {
202        match to_f64(row_arg) {
203            Some(0.0) => return Value::Error(ErrorKind::Value),
204            Some(n) => {
205                let idx = if n < 0.0 {
206                    let i = (nrows as isize + n as isize) as usize;
207                    if n as isize + (nrows as isize) < 0 {
208                        return Value::Error(ErrorKind::Value);
209                    }
210                    i
211                } else {
212                    let i = n as usize - 1;
213                    if i >= nrows {
214                        return Value::Error(ErrorKind::Value);
215                    }
216                    i
217                };
218                selected_rows.push(idx);
219            }
220            None => return Value::Error(ErrorKind::Value),
221        }
222    }
223    let result: Vec<Vec<Value>> = selected_rows
224        .iter()
225        .map(|&r| grid.get(r).cloned().unwrap_or_default())
226        .collect();
227    from_2d(result)
228}
229
230// ── FLATTEN ───────────────────────────────────────────────────────────────────
231// Returns a single-column (ROWS=n, COLS=1) array.
232// Google Sheets FLATTEN accepts multiple arguments and concatenates them.
233
234pub(crate) fn flatten_fn(args: &[Value]) -> Value {
235    if let Some(e) = check_arity(args, 1, usize::MAX) {
236        return e;
237    }
238    let mut flat: Vec<Value> = Vec::new();
239    for arg in args {
240        flat.extend(flatten_val(arg));
241    }
242    // Return as column vector (nested array of single-element rows)
243    let col: Vec<Vec<Value>> = flat.into_iter().map(|v| vec![v]).collect();
244    from_2d(col)
245}
246
247// ── HSTACK ────────────────────────────────────────────────────────────────────
248
249fn hstack_fn(args: &[Value]) -> Value {
250    if let Some(e) = check_arity(args, 1, usize::MAX) {
251        return e;
252    }
253    let grids: Vec<Vec<Vec<Value>>> = args.iter().map(to_2d).collect();
254    let nrows = grids.iter().map(|g| g.len()).max().unwrap_or(0);
255    let result: Vec<Vec<Value>> = (0..nrows)
256        .map(|r| {
257            grids
258                .iter()
259                .flat_map(|g| {
260                    g.get(r).cloned().unwrap_or_default()
261                })
262                .collect()
263        })
264        .collect();
265    from_2d(result)
266}
267
268// ── VSTACK ────────────────────────────────────────────────────────────────────
269
270fn vstack_fn(args: &[Value]) -> Value {
271    if let Some(e) = check_arity(args, 1, usize::MAX) {
272        return e;
273    }
274    let mut result: Vec<Vec<Value>> = Vec::new();
275    for arg in args {
276        let grid = to_2d(arg);
277        result.extend(grid);
278    }
279    from_2d(result)
280}
281
282// ── TOCOL ─────────────────────────────────────────────────────────────────────
283// Converts array to column vector (many rows, 1 col)
284// ignore: 0=keep all, 1=ignore blanks, 2=ignore errors, 3=ignore both
285// scan_by_col: if TRUE, scan column-major instead of row-major
286
287fn tocol_fn(args: &[Value]) -> Value {
288    if let Some(e) = check_arity(args, 1, 3) {
289        return e;
290    }
291    let ignore = if let Some(m) = args.get(1) {
292        match to_f64(m) {
293            Some(n) if (0.0..=3.0).contains(&n) => n as u8,
294            _ => return Value::Error(ErrorKind::Value),
295        }
296    } else {
297        0
298    };
299    let scan_by_col = args.get(2).map(|v| matches!(v, Value::Bool(true))).unwrap_or(false);
300
301    let flat = if scan_by_col {
302        // column-major order
303        let grid = to_2d(&args[0]);
304        let ncols = grid.first().map(|r| r.len()).unwrap_or(0);
305        let mut out = Vec::new();
306        for c in 0..ncols {
307            for row in &grid {
308                out.push(row[c].clone());
309            }
310        }
311        out
312    } else {
313        flatten_val(&args[0])
314    };
315
316    let filtered: Vec<Value> = flat.into_iter().filter(|v| {
317        // Only a genuinely blank cell counts as blank: an empty string is a
318        // value, so `ignore` modes 1 and 3 keep it.
319        let is_blank = matches!(v, Value::Empty);
320        let is_error = v.is_error();
321        if ignore == 1 && is_blank { return false; }
322        if ignore == 2 && is_error { return false; }
323        if ignore == 3 && (is_blank || is_error) { return false; }
324        true
325    }).collect();
326
327    let col: Vec<Vec<Value>> = filtered.into_iter().map(|v| vec![v]).collect();
328    from_2d(col)
329}
330
331// ── TOROW ─────────────────────────────────────────────────────────────────────
332// Converts array to row vector (1 row, many cols)
333// ignore: 0=keep all, 1=ignore blanks, 2=ignore errors, 3=ignore both
334// scan_by_col: if TRUE, scan column-major instead of row-major
335
336fn torow_fn(args: &[Value]) -> Value {
337    if let Some(e) = check_arity(args, 1, 3) {
338        return e;
339    }
340    let ignore = if let Some(m) = args.get(1) {
341        match to_f64(m) {
342            Some(n) if (0.0..=3.0).contains(&n) => n as u8,
343            _ => return Value::Error(ErrorKind::Value),
344        }
345    } else {
346        0
347    };
348    let scan_by_col = args.get(2).map(|v| matches!(v, Value::Bool(true))).unwrap_or(false);
349
350    let flat = if scan_by_col {
351        // column-major order
352        let grid = to_2d(&args[0]);
353        let ncols = grid.first().map(|r| r.len()).unwrap_or(0);
354        let mut out = Vec::new();
355        for c in 0..ncols {
356            for row in &grid {
357                out.push(row[c].clone());
358            }
359        }
360        out
361    } else {
362        flatten_val(&args[0])
363    };
364
365    let filtered: Vec<Value> = flat.into_iter().filter(|v| {
366        // Only a genuinely blank cell counts as blank: an empty string is a
367        // value, so `ignore` modes 1 and 3 keep it.
368        let is_blank = matches!(v, Value::Empty);
369        let is_error = v.is_error();
370        if ignore == 1 && is_blank { return false; }
371        if ignore == 2 && is_error { return false; }
372        if ignore == 3 && (is_blank || is_error) { return false; }
373        true
374    }).collect();
375
376    Value::Array(filtered)
377}
378
379// ── WRAPCOLS ──────────────────────────────────────────────────────────────────
380// WRAPCOLS(vector, wrap_count) — split into columns of wrap_count rows
381// Result: ceil(n/wrap_count) columns, wrap_count rows (pad last col with Empty)
382
383fn wrapcols_fn(args: &[Value]) -> Value {
384    if let Some(e) = check_arity(args, 2, 3) {
385        return e;
386    }
387    let flat = flatten_val(&args[0]);
388    let wrap_count = match to_f64(&args[1]) {
389        Some(n) if n >= 1.0 => n as usize,
390        Some(_) => return Value::Error(ErrorKind::Num),
391        None => return Value::Error(ErrorKind::Value),
392    };
393    let pad = args.get(2).cloned().unwrap_or(Value::Empty);
394
395    // Split into columns of wrap_count elements each
396    let ncols = flat.len().div_ceil(wrap_count);
397    let nrows = wrap_count;
398
399    // Build column-major layout, then transpose to row-major
400    let grid: Vec<Vec<Value>> = (0..nrows)
401        .map(|r| {
402            (0..ncols)
403                .map(|c| {
404                    let idx = c * wrap_count + r;
405                    flat.get(idx).cloned().unwrap_or_else(|| pad.clone())
406                })
407                .collect()
408        })
409        .collect();
410    from_2d(grid)
411}
412
413// ── WRAPROWS ──────────────────────────────────────────────────────────────────
414// WRAPROWS(vector, wrap_count) — split into rows of wrap_count cols
415
416fn wraprows_fn(args: &[Value]) -> Value {
417    if let Some(e) = check_arity(args, 2, 3) {
418        return e;
419    }
420    let flat = flatten_val(&args[0]);
421    let wrap_count = match to_f64(&args[1]) {
422        Some(n) if n >= 1.0 => n as usize,
423        Some(_) => return Value::Error(ErrorKind::Num),
424        None => return Value::Error(ErrorKind::Value),
425    };
426    let pad = args.get(2).cloned().unwrap_or(Value::Empty);
427
428    let nrows = flat.len().div_ceil(wrap_count);
429    let grid: Vec<Vec<Value>> = (0..nrows)
430        .map(|r| {
431            (0..wrap_count)
432                .map(|c| {
433                    let idx = r * wrap_count + c;
434                    flat.get(idx).cloned().unwrap_or_else(|| pad.clone())
435                })
436                .collect()
437        })
438        .collect();
439    from_2d(grid)
440}
441
442// ── SORT ──────────────────────────────────────────────────────────────────────
443
444pub(crate) fn sort_fn(args: &[Value]) -> Value {
445    if let Some(e) = check_arity(args, 1, 4) {
446        return e;
447    }
448    let is_1d = matches!(&args[0], Value::Array(outer) if !outer.iter().any(|e| matches!(e, Value::Array(_))));
449
450    // Google Sheets semantics: a flat 1-D row array is treated as a single row.
451    // SORT sorts *rows*; with only one row nothing changes regardless of parameters.
452    // Exception: if by_col=TRUE is requested on a 1D array, GS returns #N/A.
453    if is_1d {
454        let by_col = args.get(3).map(|v| matches!(v, Value::Bool(true))).unwrap_or(false);
455        if by_col {
456            return Value::Error(ErrorKind::NA);
457        }
458        return args[0].clone();
459    }
460
461    let mut grid = to_2d(&args[0]);
462    let sort_col = if args.len() >= 2 {
463        match to_f64(&args[1]) {
464            Some(n) if n >= 1.0 => n as usize - 1,
465            Some(_) => return Value::Error(ErrorKind::Value),
466            None => 0,
467        }
468    } else {
469        0
470    };
471    let ascending = if args.len() >= 3 {
472        match &args[2] {
473            Value::Number(n) => *n >= 0.0,
474            Value::Bool(b) => *b,
475            _ => true,
476        }
477    } else {
478        true
479    };
480
481    grid.sort_by(|a, b| {
482        let va = a.get(sort_col).unwrap_or(&Value::Empty);
483        let vb = b.get(sort_col).unwrap_or(&Value::Empty);
484        let cmp = compare_values_sort(va, vb);
485        if ascending { cmp } else { cmp.reverse() }
486    });
487    from_2d(grid)
488}
489
490fn compare_values_sort(a: &Value, b: &Value) -> std::cmp::Ordering {
491    match (a, b) {
492        (Value::Number(x), Value::Number(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
493        (Value::Text(x), Value::Text(y)) => x.cmp(y),
494        (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
495        // Zone-aware instants sort by the absolute instant.
496        (Value::Zoned(x), Value::Zoned(y)) => x.utc_nanos.cmp(&y.utc_nanos),
497        // Sparklines: only equality is observed (google.tsv), and no ordering
498        // between two different ones is — matching the `=`/`<`/`>` operators and
499        // their EQ/LT/GT aliases, which report no ordering relation either. The
500        // sort is stable, so sparklines keep their input order.
501        (Value::Sparkline(_), Value::Sparkline(_)) => std::cmp::Ordering::Equal,
502        _ => std::cmp::Ordering::Equal,
503    }
504}
505
506// ── SORTBY ────────────────────────────────────────────────────────────────────
507
508fn sortby_fn(args: &[Value]) -> Value {
509    if let Some(e) = check_arity(args, 2, usize::MAX) {
510        return e;
511    }
512    let is_1d = matches!(&args[0], Value::Array(outer) if !outer.iter().any(|e| matches!(e, Value::Array(_))));
513
514    if is_1d {
515        // 1D: treat each element as a separate item to sort
516        let elems = flatten_val(&args[0]);
517        let n = elems.len();
518
519        let mut sort_keys: Vec<(Vec<Value>, bool)> = Vec::new();
520        let mut i = 1;
521        while i < args.len() {
522            let key_vals = flatten_val(&args[i]);
523            if key_vals.len() != n {
524                return Value::Error(ErrorKind::Value);
525            }
526            let ascending = if i + 1 < args.len() {
527                match to_f64(&args[i + 1]) {
528                    Some(v) => v >= 0.0,
529                    None => true,
530                }
531            } else {
532                true
533            };
534            sort_keys.push((key_vals, ascending));
535            i += 2;
536        }
537
538        let mut indices: Vec<usize> = (0..n).collect();
539        indices.sort_by(|&ra, &rb| {
540            for (keys, asc) in &sort_keys {
541                let va = keys.get(ra).unwrap_or(&Value::Empty);
542                let vb = keys.get(rb).unwrap_or(&Value::Empty);
543                let cmp = compare_values_sort(va, vb);
544                if cmp != std::cmp::Ordering::Equal {
545                    return if *asc { cmp } else { cmp.reverse() };
546                }
547            }
548            std::cmp::Ordering::Equal
549        });
550
551        return Value::Array(indices.iter().map(|&r| elems[r].clone()).collect());
552    }
553
554    let grid = to_2d(&args[0]);
555    let nrows = grid.len();
556
557    // Collect (sort_key_array, order) pairs
558    let mut sort_keys: Vec<(Vec<Value>, bool)> = Vec::new();
559    let mut i = 1;
560    while i < args.len() {
561        let key_vals = flatten_val(&args[i]);
562        if key_vals.len() != nrows && nrows > 1 {
563            return Value::Error(ErrorKind::Value);
564        }
565        let ascending = if i + 1 < args.len() {
566            match to_f64(&args[i + 1]) {
567                Some(n) => n >= 0.0,
568                None => true,
569            }
570        } else {
571            true
572        };
573        sort_keys.push((key_vals, ascending));
574        i += 2;
575    }
576
577    let mut indices: Vec<usize> = (0..nrows).collect();
578    indices.sort_by(|&ra, &rb| {
579        for (keys, asc) in &sort_keys {
580            let va = keys.get(ra).unwrap_or(&Value::Empty);
581            let vb = keys.get(rb).unwrap_or(&Value::Empty);
582            let cmp = compare_values_sort(va, vb);
583            if cmp != std::cmp::Ordering::Equal {
584                return if *asc { cmp } else { cmp.reverse() };
585            }
586        }
587        std::cmp::Ordering::Equal
588    });
589
590    let sorted: Vec<Vec<Value>> = indices.iter().map(|&r| grid[r].clone()).collect();
591    drop(grid);
592    from_2d(sorted)
593}
594
595// ── UNIQUE ────────────────────────────────────────────────────────────────────
596
597pub(crate) fn unique_fn(args: &[Value]) -> Value {
598    if let Some(e) = check_arity(args, 1, 3) {
599        return e;
600    }
601    let is_1d = matches!(&args[0], Value::Array(outer) if !outer.iter().any(|e| matches!(e, Value::Array(_))));
602    let grid = to_2d(&args[0]);
603    // by_col defaults to false (deduplicate rows)
604    let by_col = args.get(1).map(|v| matches!(v, Value::Bool(true))).unwrap_or(false);
605    let exactly_once = args.get(2).map(|v| matches!(v, Value::Bool(true))).unwrap_or(false);
606
607    // Google Sheets semantics: a flat 1-D row array is treated as a single row.
608    // UNIQUE with by_col=FALSE deduplicates rows; with only one row, it is
609    // always unique and is returned as-is (regardless of exactly_once).
610    if is_1d && !by_col {
611        return args[0].clone();
612    }
613
614    if by_col {
615        // Deduplicate columns
616        let nrows = grid.len();
617        if nrows == 0 {
618            return from_2d(vec![]);
619        }
620        let ncols = grid[0].len();
621        // Build column-major representation
622        let columns: Vec<Vec<Value>> = (0..ncols)
623            .map(|c| grid.iter().map(|row| row[c].clone()).collect())
624            .collect();
625        let mut seen_cols: Vec<Vec<Value>> = Vec::new();
626        let mut counts: Vec<usize> = Vec::new();
627        for col in columns {
628            if let Some(pos) = seen_cols.iter().position(|sc| sc == &col) {
629                counts[pos] += 1;
630            } else {
631                seen_cols.push(col);
632                counts.push(1);
633            }
634        }
635        let result_cols: Vec<Vec<Value>> = seen_cols
636            .into_iter()
637            .zip(counts)
638            .filter(|(_, cnt)| !exactly_once || *cnt == 1)
639            .map(|(col, _)| col)
640            .collect();
641        // Transpose back to row-major
642        let ncols2 = result_cols.len();
643        let result: Vec<Vec<Value>> = (0..nrows)
644            .map(|r| (0..ncols2).map(|c| result_cols[c][r].clone()).collect())
645            .collect();
646        return from_2d(result);
647    }
648
649    // Deduplicate rows
650    let mut seen_rows: Vec<Vec<Value>> = Vec::new();
651    let mut counts: Vec<usize> = Vec::new();
652    for row in &grid {
653        if let Some(pos) = seen_rows.iter().position(|sr| sr == row) {
654            counts[pos] += 1;
655        } else {
656            seen_rows.push(row.clone());
657            counts.push(1);
658        }
659    }
660    let result: Vec<Vec<Value>> = seen_rows
661        .into_iter()
662        .zip(counts)
663        .filter(|(_, cnt)| !exactly_once || *cnt == 1)
664        .map(|(row, _)| row)
665        .collect();
666    from_2d(result)
667}
668
669// ── SUMPRODUCT ────────────────────────────────────────────────────────────────
670
671pub(crate) fn sumproduct_fn(args: &[Value]) -> Value {
672    if let Some(e) = check_arity(args, 1, usize::MAX) {
673        return e;
674    }
675    let arrays: Vec<Vec<Value>> = args.iter().map(flatten_val).collect();
676    let len = arrays[0].len();
677    // All arrays must have the same length
678    for arr in &arrays[1..] {
679        if arr.len() != len {
680            return Value::Error(ErrorKind::Value);
681        }
682    }
683    let mut sum = 0.0;
684    for i in 0..len {
685        let mut prod = 1.0;
686        for arr in &arrays {
687            prod *= to_f64(&arr[i]).unwrap_or(0.0);
688        }
689        sum += prod;
690    }
691    Value::Number(sum)
692}
693
694// ── SUMXMY2 ───────────────────────────────────────────────────────────────────
695
696fn sumxmy2_fn(args: &[Value]) -> Value {
697    if let Some(e) = check_arity(args, 2, 2) {
698        return e;
699    }
700    let xs = flatten_val(&args[0]);
701    let ys = flatten_val(&args[1]);
702    if xs.len() != ys.len() {
703        return Value::Error(ErrorKind::NA);
704    }
705    let mut sum = 0.0;
706    for (x, y) in xs.iter().zip(ys.iter()) {
707        // Only numeric values contribute; text, booleans, errors, empty are skipped.
708        if let (Value::Number(xn), Value::Number(yn)) = (x, y) {
709            sum += (*xn - *yn).powi(2);
710        }
711    }
712    Value::Number(sum)
713}
714
715// ── SUMX2MY2 ──────────────────────────────────────────────────────────────────
716
717fn sumx2my2_fn(args: &[Value]) -> Value {
718    if let Some(e) = check_arity(args, 2, 2) {
719        return e;
720    }
721    let xs = flatten_val(&args[0]);
722    let ys = flatten_val(&args[1]);
723    if xs.len() != ys.len() {
724        return Value::Error(ErrorKind::NA);
725    }
726    let mut sum = 0.0;
727    for (x, y) in xs.iter().zip(ys.iter()) {
728        if let (Value::Number(xn), Value::Number(yn)) = (x, y) {
729            sum += *xn * *xn - *yn * *yn;
730        }
731    }
732    Value::Number(sum)
733}
734
735// ── SUMX2PY2 ──────────────────────────────────────────────────────────────────
736
737fn sumx2py2_fn(args: &[Value]) -> Value {
738    if let Some(e) = check_arity(args, 2, 2) {
739        return e;
740    }
741    let xs = flatten_val(&args[0]);
742    let ys = flatten_val(&args[1]);
743    if xs.len() != ys.len() {
744        return Value::Error(ErrorKind::NA);
745    }
746    let mut sum = 0.0;
747    for (x, y) in xs.iter().zip(ys.iter()) {
748        if let (Value::Number(xn), Value::Number(yn)) = (x, y) {
749            sum += *xn * *xn + *yn * *yn;
750        }
751    }
752    Value::Number(sum)
753}
754
755// ── MMULT ─────────────────────────────────────────────────────────────────────
756
757fn mmult_fn(args: &[Value]) -> Value {
758    if let Some(e) = check_arity(args, 2, 2) {
759        return e;
760    }
761    let a = to_2d(&args[0]);
762    let b = to_2d(&args[1]);
763    if a.iter().chain(b.iter()).any(|row| row.iter().any(|v| matches!(v, Value::Bool(_)))) {
764        return Value::Error(ErrorKind::Value);
765    }
766    let n = a.first().map(|r| r.len()).unwrap_or(0);
767    let p = b.first().map(|r| r.len()).unwrap_or(0);
768    if b.len() != n {
769        return Value::Error(ErrorKind::Value);
770    }
771    // Convert to f64 matrices for computation
772    let af: Vec<Vec<f64>> = a.iter().map(|row| {
773        row.iter().map(|v| to_f64(v).unwrap_or(f64::NAN)).collect()
774    }).collect();
775    let bf: Vec<Vec<f64>> = b.iter().map(|row| {
776        row.iter().map(|v| to_f64(v).unwrap_or(f64::NAN)).collect()
777    }).collect();
778    if af.iter().any(|r| r.iter().any(|v| v.is_nan())) || bf.iter().any(|r| r.iter().any(|v| v.is_nan())) {
779        return Value::Error(ErrorKind::Value);
780    }
781    let result: Vec<Vec<Value>> = af.iter().map(|row_a| {
782        (0..p).map(|j| {
783            let sum: f64 = row_a.iter().enumerate().map(|(k, &av)| av * bf[k][j]).sum();
784            Value::Number(sum)
785        }).collect()
786    }).collect();
787    from_2d(result)
788}
789
790// ── MDETERM ───────────────────────────────────────────────────────────────────
791
792fn mdeterm_fn(args: &[Value]) -> Value {
793    if let Some(e) = check_arity(args, 1, 1) {
794        return e;
795    }
796    let grid = to_2d(&args[0]);
797    let n = grid.len();
798    if n == 0 {
799        return Value::Error(ErrorKind::Value);
800    }
801    for row in &grid {
802        if row.len() != n {
803            return Value::Error(ErrorKind::Value);
804        }
805    }
806    if grid.iter().any(|row| row.iter().any(|v| matches!(v, Value::Bool(_)))) {
807        return Value::Error(ErrorKind::Value);
808    }
809    // Convert to f64 matrix
810    let mut mat: Vec<Vec<f64>> = Vec::with_capacity(n);
811    for row in &grid {
812        let mut r = Vec::with_capacity(n);
813        for v in row {
814            match to_f64(v) {
815                Some(x) => r.push(x),
816                None => return Value::Error(ErrorKind::Value),
817            }
818        }
819        mat.push(r);
820    }
821    Value::Number(determinant(&mat))
822}
823
824fn determinant(mat: &[Vec<f64>]) -> f64 {
825    let n = mat.len();
826    if n == 1 {
827        return mat[0][0];
828    }
829    if n == 2 {
830        return mat[0][0] * mat[1][1] - mat[0][1] * mat[1][0];
831    }
832    let mut det = 0.0;
833    for c in 0..n {
834        let minor: Vec<Vec<f64>> = (1..n)
835            .map(|r| {
836                (0..n)
837                    .filter(|&cc| cc != c)
838                    .map(|cc| mat[r][cc])
839                    .collect()
840            })
841            .collect();
842        let sign = if c % 2 == 0 { 1.0 } else { -1.0 };
843        det += sign * mat[0][c] * determinant(&minor);
844    }
845    det
846}
847
848// ── MINVERSE ──────────────────────────────────────────────────────────────────
849
850fn minverse_fn(args: &[Value]) -> Value {
851    if let Some(e) = check_arity(args, 1, 1) {
852        return e;
853    }
854    let grid = to_2d(&args[0]);
855    let n = grid.len();
856    if n == 0 {
857        return Value::Error(ErrorKind::Value);
858    }
859    for row in &grid {
860        if row.len() != n {
861            return Value::Error(ErrorKind::Value);
862        }
863    }
864    if grid.iter().any(|row| row.iter().any(|v| matches!(v, Value::Bool(_)))) {
865        return Value::Error(ErrorKind::Value);
866    }
867    let mut mat: Vec<Vec<f64>> = Vec::with_capacity(n);
868    for row in &grid {
869        let mut r = Vec::with_capacity(n);
870        for v in row {
871            match to_f64(v) {
872                Some(x) => r.push(x),
873                None => return Value::Error(ErrorKind::Value),
874            }
875        }
876        mat.push(r);
877    }
878    match invert_matrix(mat) {
879        Some(inv) => from_2d(inv.into_iter().map(|r| r.into_iter().map(Value::Number).collect()).collect()),
880        None => Value::Error(ErrorKind::Num),
881    }
882}
883
884fn invert_matrix(mut mat: Vec<Vec<f64>>) -> Option<Vec<Vec<f64>>> {
885    let n = mat.len();
886    // Augment with identity
887    let mut inv: Vec<Vec<f64>> = (0..n)
888        .map(|i| (0..n).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
889        .collect();
890    for col in 0..n {
891        // Find pivot
892        let pivot = (col..n).max_by(|&a, &b| mat[a][col].abs().partial_cmp(&mat[b][col].abs()).unwrap_or(std::cmp::Ordering::Equal))?;
893        if mat[pivot][col].abs() < 1e-12 {
894            return None; // singular
895        }
896        mat.swap(col, pivot);
897        inv.swap(col, pivot);
898        let div = mat[col][col];
899        for j in 0..n {
900            mat[col][j] /= div;
901            inv[col][j] /= div;
902        }
903        for r in 0..n {
904            if r != col {
905                let factor = mat[r][col];
906                for j in 0..n {
907                    mat[r][j] -= factor * mat[col][j];
908                    inv[r][j] -= factor * inv[col][j];
909                }
910            }
911        }
912    }
913    Some(inv)
914}
915
916// ── FREQUENCY ─────────────────────────────────────────────────────────────────
917// Array-spill function; Google Sheets returns #REF! in scalar (non-array-formula) context.
918
919fn frequency_fn(args: &[Value]) -> Value {
920    if let Some(e) = check_arity(args, 2, 2) {
921        return e;
922    }
923    // Only numeric values are counted; text, booleans and blanks are ignored.
924    let data: Vec<f64> = flatten_val(&args[0])
925        .iter()
926        .filter_map(|v| if let Value::Number(n) = v { Some(*n) } else { None })
927        .collect();
928    let bins_raw = flatten_val(&args[1]);
929    // Empty bins array → #REF! (Google Sheets behaviour)
930    if bins_raw.is_empty() || matches!(bins_raw.as_slice(), [Value::Empty]) {
931        return Value::Error(ErrorKind::Ref);
932    }
933    // Also treat an array whose only element is Empty as empty
934    let all_empty = bins_raw.iter().all(|v| matches!(v, Value::Empty));
935    if all_empty {
936        return Value::Error(ErrorKind::Ref);
937    }
938    let bins: Vec<f64> = bins_raw
939        .iter()
940        .filter_map(|v| if let Value::Number(n) = v { Some(*n) } else { None })
941        .collect();
942    if bins.is_empty() {
943        return Value::Error(ErrorKind::Ref);
944    }
945    // One bucket per bin, plus a final "greater than the last bin" bucket.
946    let mut counts = vec![0i64; bins.len() + 1];
947    for &x in &data {
948        let mut placed = false;
949        for (i, &b) in bins.iter().enumerate() {
950            if x <= b {
951                counts[i] += 1;
952                placed = true;
953                break;
954            }
955        }
956        if !placed {
957            counts[bins.len()] += 1;
958        }
959    }
960    // Sheets returns a vertical (column) array of length bins+1.
961    let col: Vec<Vec<Value>> = counts
962        .into_iter()
963        .map(|c| vec![Value::Number(c as f64)])
964        .collect();
965    from_2d(col)
966}
967
968// ── LINEST ────────────────────────────────────────────────────────────────────
969// LINEST(known_y, [known_x], [const], [stats]) → returns 1-row array [slope, intercept, ...]
970
971fn linest_fn(args: &[Value]) -> Value {
972    if let Some(e) = check_arity(args, 1, 4) {
973        return e;
974    }
975    let ys = flatten_val(&args[0]);
976    let n = ys.len();
977    // boolean or text y-values → #VALUE!
978    if ys.iter().any(|v| matches!(v, Value::Bool(_) | Value::Text(_))) {
979        return Value::Error(ErrorKind::Value);
980    }
981    if n < 2 {
982        return Value::Error(ErrorKind::NA);
983    }
984    let xs: Vec<f64> = if args.len() >= 2 {
985        let xv = flatten_val(&args[1]);
986        if xv.len() != n {
987            return Value::Error(ErrorKind::Ref);
988        }
989        xv.iter().filter_map(to_f64).collect()
990    } else {
991        (1..=n).map(|i| i as f64).collect()
992    };
993    if xs.len() != n {
994        return Value::Error(ErrorKind::Ref);
995    }
996    let y_vals: Vec<f64> = ys.iter().filter_map(to_f64).collect();
997    if y_vals.len() != n {
998        return Value::Error(ErrorKind::Value);
999    }
1000    let (slope, intercept) = simple_linear_regression(&xs, &y_vals);
1001    Value::Array(vec![Value::Number(slope), Value::Number(intercept)])
1002}
1003
1004fn simple_linear_regression(xs: &[f64], ys: &[f64]) -> (f64, f64) {
1005    let n = xs.len() as f64;
1006    let sum_x: f64 = xs.iter().sum();
1007    let sum_y: f64 = ys.iter().sum();
1008    let sum_xy: f64 = xs.iter().zip(ys.iter()).map(|(x, y)| x * y).sum();
1009    let sum_xx: f64 = xs.iter().map(|x| x * x).sum();
1010    let denom = n * sum_xx - sum_x * sum_x;
1011    if denom.abs() < 1e-15 {
1012        let intercept = sum_y / n;
1013        return (0.0, intercept);
1014    }
1015    let slope = (n * sum_xy - sum_x * sum_y) / denom;
1016    let intercept = (sum_y - slope * sum_x) / n;
1017    (slope, intercept)
1018}
1019
1020// ── LOGEST ────────────────────────────────────────────────────────────────────
1021// LOGEST(known_y, [known_x], [const], [stats]) → returns 1-row array [base, intercept, ...]
1022
1023fn logest_fn(args: &[Value]) -> Value {
1024    if let Some(e) = check_arity(args, 1, 4) {
1025        return e;
1026    }
1027    let ys = flatten_val(&args[0]);
1028    let n = ys.len();
1029    // boolean y-values → #VALUE! (TRUE would coerce to 1 but GS errors)
1030    if ys.iter().any(|v| matches!(v, Value::Bool(_))) {
1031        return Value::Error(ErrorKind::Value);
1032    }
1033    if n < 2 {
1034        return Value::Error(ErrorKind::NA);
1035    }
1036    let xs: Vec<f64> = if args.len() >= 2 {
1037        let xv = flatten_val(&args[1]);
1038        if xv.len() != n {
1039            return Value::Error(ErrorKind::Ref);
1040        }
1041        xv.iter().filter_map(to_f64).collect()
1042    } else {
1043        (1..=n).map(|i| i as f64).collect()
1044    };
1045    if xs.len() != n {
1046        return Value::Error(ErrorKind::Ref);
1047    }
1048    let y_vals: Vec<f64> = ys.iter().filter_map(to_f64).collect();
1049    if y_vals.len() != n {
1050        return Value::Error(ErrorKind::Value);
1051    }
1052    // Take log of y values
1053    let log_y: Vec<f64> = y_vals.iter().map(|&y| libm::log(y)).collect();
1054    if log_y.iter().any(|v| v.is_nan() || v.is_infinite()) {
1055        return Value::Error(ErrorKind::Num);
1056    }
1057    let (log_base, log_intercept) = simple_linear_regression(&xs, &log_y);
1058    let base = libm::exp(log_base);
1059    let intercept = libm::exp(log_intercept);
1060    Value::Array(vec![Value::Number(base), Value::Number(intercept)])
1061}
1062
1063// ── TREND ─────────────────────────────────────────────────────────────────────
1064// TREND(known_y, [known_x], [new_x], [const]) → array of fitted/predicted values
1065
1066fn trend_fn(args: &[Value]) -> Value {
1067    if let Some(e) = check_arity(args, 1, 4) {
1068        return e;
1069    }
1070    let ys = flatten_val(&args[0]);
1071    let n = ys.len();
1072    // boolean or text y-values → #VALUE!
1073    if ys.iter().any(|v| matches!(v, Value::Bool(_) | Value::Text(_))) {
1074        return Value::Error(ErrorKind::Value);
1075    }
1076    if n < 2 {
1077        return Value::Error(ErrorKind::NA);
1078    }
1079    let xs: Vec<f64> = if args.len() >= 2 {
1080        let xv = flatten_val(&args[1]);
1081        if xv.len() != n {
1082            return Value::Error(ErrorKind::Ref);
1083        }
1084        xv.iter().filter_map(to_f64).collect()
1085    } else {
1086        (1..=n).map(|i| i as f64).collect()
1087    };
1088    if xs.len() != n {
1089        return Value::Error(ErrorKind::Ref);
1090    }
1091    let y_vals: Vec<f64> = ys.iter().filter_map(to_f64).collect();
1092    if y_vals.len() != n {
1093        return Value::Error(ErrorKind::Value);
1094    }
1095    let new_xs: Vec<f64> = if args.len() >= 3 {
1096        flatten_val(&args[2]).iter().filter_map(to_f64).collect()
1097    } else {
1098        xs.clone()
1099    };
1100    let (slope, intercept) = simple_linear_regression(&xs, &y_vals);
1101    let result: Vec<Value> = new_xs.iter().map(|&x| Value::Number(slope * x + intercept)).collect();
1102    Value::Array(result)
1103}
1104
1105// ── GROWTH ────────────────────────────────────────────────────────────────────
1106// GROWTH(known_y, [known_x], [new_x], [const]) → exponential predictions
1107
1108fn growth_fn(args: &[Value]) -> Value {
1109    if let Some(e) = check_arity(args, 1, 4) {
1110        return e;
1111    }
1112    let ys = flatten_val(&args[0]);
1113    let n = ys.len();
1114    // boolean y-values → #VALUE! (GS errors on TRUE/FALSE in y)
1115    if ys.iter().any(|v| matches!(v, Value::Bool(_))) {
1116        return Value::Error(ErrorKind::Value);
1117    }
1118    if n < 2 {
1119        return Value::Error(ErrorKind::NA);
1120    }
1121    let xs: Vec<f64> = if args.len() >= 2 {
1122        let xv = flatten_val(&args[1]);
1123        if xv.len() != n {
1124            return Value::Error(ErrorKind::Ref);
1125        }
1126        xv.iter().filter_map(to_f64).collect()
1127    } else {
1128        (1..=n).map(|i| i as f64).collect()
1129    };
1130    if xs.len() != n {
1131        return Value::Error(ErrorKind::Ref);
1132    }
1133    let y_vals: Vec<f64> = ys.iter().filter_map(to_f64).collect();
1134    if y_vals.len() != n {
1135        return Value::Error(ErrorKind::Value);
1136    }
1137    let log_y: Vec<f64> = y_vals.iter().map(|&y| libm::log(y)).collect();
1138    if log_y.iter().any(|v| v.is_nan() || v.is_infinite()) {
1139        return Value::Error(ErrorKind::Num);
1140    }
1141    let new_xs: Vec<f64> = if args.len() >= 3 && !matches!(args[2], Value::Empty) {
1142        let vals: Vec<f64> = flatten_val(&args[2]).iter().filter_map(to_f64).collect();
1143        if vals.is_empty() { xs.clone() } else { vals }
1144    } else {
1145        xs.clone()
1146    };
1147    // b param: TRUE (default) = compute intercept normally;
1148    //          FALSE = force intercept through origin (ln(b)=0, so b=1)
1149    let use_intercept = if args.len() >= 4 {
1150        match &args[3] {
1151            Value::Bool(b) => *b,
1152            Value::Number(n) => *n != 0.0,
1153            _ => true,
1154        }
1155    } else {
1156        true
1157    };
1158    let (log_base, log_intercept) = if use_intercept {
1159        simple_linear_regression(&xs, &log_y)
1160    } else {
1161        // Force intercept = 0: slope = sum(x*lny)/sum(x^2)
1162        let sum_xy: f64 = xs.iter().zip(log_y.iter()).map(|(x, ly)| x * ly).sum();
1163        let sum_xx: f64 = xs.iter().map(|x| x * x).sum();
1164        let slope = if sum_xx.abs() < 1e-15 { 0.0 } else { sum_xy / sum_xx };
1165        (slope, 0.0)
1166    };
1167    let result: Vec<Value> = new_xs
1168        .iter()
1169        .map(|&x| Value::Number(libm::exp(log_base * x + log_intercept)))
1170        .collect();
1171    Value::Array(result)
1172}
1173
1174// ── Higher-order functions (LazyFn) ───────────────────────────────────────────
1175
1176/// Apply a LAMBDA expression with bound parameter values.
1177/// `lambda_expr` should be `Expr::FunctionCall { name: "LAMBDA", args: [p1, ..., body] }`
1178/// `bound_args` are the Values to bind to p1, p2, ...
1179///
1180/// Every higher-order function (MAP, REDUCE, BYROW, BYCOL, SCAN, MAKEARRAY)
1181/// funnels its per-invocation lambda call through here, so this is also
1182/// where each invocation's parameter-binding [`EvalOp::Variable`] events fire
1183/// (issue #740 follow-up) — see the doc comment on [`crate::eval::EvalHook`]
1184/// for what these events mean and why they exist.
1185fn apply_lambda(lambda_expr: &Expr, bound_args: &[Value], ctx: &mut EvalCtx<'_>) -> Option<Value> {
1186    match lambda_expr {
1187        Expr::FunctionCall { name, args, .. } if name == "LAMBDA" => {
1188            if args.is_empty() {
1189                return None;
1190            }
1191            let body = &args[args.len() - 1];
1192            let params = &args[..args.len() - 1];
1193            if params.len() != bound_args.len() {
1194                return None;
1195            }
1196            // Bind each parameter in context
1197            let mut saved: Vec<(String, Value)> = Vec::new();
1198            for (param_expr, val) in params.iter().zip(bound_args.iter()) {
1199                if let Expr::Variable(name, span) = param_expr {
1200                    // Parameter-binding event, mirroring standalone
1201                    // `LAMBDA(...)(...)` (`eval_apply`, issue #740): fires once
1202                    // per parameter for *this* invocation, carrying the
1203                    // parameter's own span (same span across every
1204                    // invocation of a given HOF call — e.g. all N elements of
1205                    // a MAP) and the value bound for this element/row/col/
1206                    // accumulator step. Not deduped by design: a trace
1207                    // consumer sees one event per invocation, exactly like
1208                    // repeated reads of the same cell reference.
1209                    if let Some(hook) = ctx.hook.as_deref_mut() {
1210                        hook.on_node(EvalOp::Variable(name), *span, val);
1211                    }
1212                    // Strip `$` and uppercase for the same reason as
1213                    // `eval_apply`: a $-shaped bare token is syntactically
1214                    // legal (issue #708) but must bind/read under the same
1215                    // normalized key, so a $- or case-variant lambda param
1216                    // behaves identically whether reached via a HOF
1217                    // (MAP/REDUCE/BYROW/BYCOL/SCAN/MAKEARRAY) or standalone
1218                    // `LAMBDA(...)(...)`.
1219                    let bind_key = name.to_uppercase().replace('$', "");
1220                    let old = ctx.ctx.get(&bind_key);
1221                    saved.push((bind_key.clone(), old));
1222                    ctx.ctx.set(bind_key, val.clone());
1223                } else {
1224                    return None;
1225                }
1226            }
1227            let result = evaluate_expr(body, ctx);
1228            // Restore context
1229            for (name, old_val) in saved {
1230                ctx.ctx.set(name, old_val);
1231            }
1232            Some(result)
1233        }
1234        _ => None,
1235    }
1236}
1237
1238// ── BYROW ─────────────────────────────────────────────────────────────────────
1239
1240pub fn byrow_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1241    if let Some(e) = check_arity_len(args.len(), 2, 2) {
1242        return e;
1243    }
1244    let arr_val = evaluate_expr(&args[0], ctx);
1245    if arr_val.is_error() {
1246        return arr_val;
1247    }
1248    let grid = to_2d(&arr_val);
1249    let lambda_expr = &args[1];
1250    let mut results: Vec<Value> = Vec::with_capacity(grid.len());
1251    for row in &grid {
1252        let row_val = Value::Array(row.clone());
1253        match apply_lambda(lambda_expr, &[row_val], ctx) {
1254            Some(v) => results.push(v),
1255            None => return Value::Error(ErrorKind::NA),
1256        }
1257    }
1258    // Return as column vector (one result per row)
1259    let col: Vec<Vec<Value>> = results.into_iter().map(|v| vec![v]).collect();
1260    from_2d(col)
1261}
1262
1263// ── BYCOL ─────────────────────────────────────────────────────────────────────
1264
1265pub fn bycol_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1266    if let Some(e) = check_arity_len(args.len(), 2, 2) {
1267        return e;
1268    }
1269    let arr_val = evaluate_expr(&args[0], ctx);
1270    if arr_val.is_error() {
1271        return arr_val;
1272    }
1273    let grid = to_2d(&arr_val);
1274    let ncols = grid.first().map(|r| r.len()).unwrap_or(0);
1275    // Build columns first to avoid range-loop indexing
1276    let columns: Vec<Vec<Value>> = (0..ncols)
1277        .map(|c| grid.iter().map(|row| row[c].clone()).collect())
1278        .collect();
1279    let lambda_expr = &args[1];
1280    let mut results: Vec<Value> = Vec::with_capacity(ncols);
1281    for col in columns {
1282        // Pass flat array so SUM/MAX/MIN etc can iterate over elements
1283        let col_val = Value::Array(col);
1284        match apply_lambda(lambda_expr, &[col_val], ctx) {
1285            Some(v) => results.push(v),
1286            None => return Value::Error(ErrorKind::NA),
1287        }
1288    }
1289    // Return as row vector (one result per col)
1290    Value::Array(results)
1291}
1292
1293// ── MAP ───────────────────────────────────────────────────────────────────────
1294
1295pub fn map_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1296    if let Some(e) = check_arity_len(args.len(), 2, usize::MAX) {
1297        return e;
1298    }
1299    // Last arg is LAMBDA, all prior are arrays
1300    let lambda_expr = &args[args.len() - 1];
1301    let arr_count = args.len() - 1;
1302    // Evaluate each array argument once, keeping the un-flattened value of
1303    // the first one around to read its shape later (avoids a redundant
1304    // second `evaluate_expr(&args[0], ctx)` — that used to double-evaluate
1305    // the argument purely to reshape the result). Mirrors REDUCE/SCAN:
1306    // check `is_error()` on each evaluated array before flattening, so an
1307    // errored array argument propagates instead of silently flattening the
1308    // error into the data.
1309    let mut first_shape_val: Option<Value> = None;
1310    let mut arrays: Vec<Vec<Value>> = Vec::with_capacity(arr_count);
1311    for (i, a) in args[..arr_count].iter().enumerate() {
1312        let v = evaluate_expr(a, ctx);
1313        if v.is_error() {
1314            return v;
1315        }
1316        if i == 0 {
1317            first_shape_val = Some(v.clone());
1318        }
1319        arrays.push(flatten_val(&v));
1320    }
1321    let len = arrays[0].len();
1322    for arr in &arrays[1..] {
1323        if arr.len() != len {
1324            return Value::Error(ErrorKind::Value);
1325        }
1326    }
1327    let mut results: Vec<Value> = Vec::with_capacity(len);
1328    for i in 0..len {
1329        let bound: Vec<Value> = arrays.iter().map(|a| a[i].clone()).collect();
1330        match apply_lambda(lambda_expr, &bound, ctx) {
1331            Some(v) => results.push(v),
1332            None => return Value::Error(ErrorKind::NA),
1333        }
1334    }
1335    // Preserve shape of first array (reuse the value evaluated above).
1336    let first_grid = to_2d(
1337        first_shape_val
1338            .as_ref()
1339            .expect("arr_count >= 1 (check_arity_len enforces at least 2 args)"),
1340    );
1341    if first_grid.len() > 1 {
1342        // 2D → reshape results
1343        let ncols = first_grid[0].len();
1344        let nrows = first_grid.len();
1345        let grid: Vec<Vec<Value>> = (0..nrows)
1346            .map(|r| (0..ncols).map(|c| results[r * ncols + c].clone()).collect())
1347            .collect();
1348        from_2d(grid)
1349    } else {
1350        Value::Array(results)
1351    }
1352}
1353
1354// ── REDUCE ────────────────────────────────────────────────────────────────────
1355
1356pub fn reduce_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1357    if let Some(e) = check_arity_len(args.len(), 3, 3) {
1358        return e;
1359    }
1360    let initial = evaluate_expr(&args[0], ctx);
1361    if initial.is_error() {
1362        return initial;
1363    }
1364    let arr_val = evaluate_expr(&args[1], ctx);
1365    if arr_val.is_error() {
1366        return arr_val;
1367    }
1368    let items = flatten_val(&arr_val);
1369    if items.is_empty() {
1370        return Value::Error(ErrorKind::Ref);
1371    }
1372    let lambda_expr = &args[2];
1373    let mut acc = initial;
1374    for item in &items {
1375        match apply_lambda(lambda_expr, &[acc.clone(), item.clone()], ctx) {
1376            Some(v) => acc = v,
1377            None => return Value::Error(ErrorKind::NA),
1378        }
1379    }
1380    acc
1381}
1382
1383// ── SCAN ──────────────────────────────────────────────────────────────────────
1384
1385pub fn scan_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1386    if let Some(e) = check_arity_len(args.len(), 3, 3) {
1387        return e;
1388    }
1389    let initial = evaluate_expr(&args[0], ctx);
1390    if initial.is_error() {
1391        return initial;
1392    }
1393    let arr_val = evaluate_expr(&args[1], ctx);
1394    if arr_val.is_error() {
1395        return arr_val;
1396    }
1397    let grid = to_2d(&arr_val);
1398    let items = flatten_val(&arr_val);
1399    let lambda_expr = &args[2];
1400    let mut acc = initial;
1401    let mut results: Vec<Value> = Vec::with_capacity(items.len());
1402    for item in &items {
1403        match apply_lambda(lambda_expr, &[acc.clone(), item.clone()], ctx) {
1404            Some(v) => {
1405                acc = v.clone();
1406                results.push(v);
1407            }
1408            None => return Value::Error(ErrorKind::NA),
1409        }
1410    }
1411    // Preserve shape of input array
1412    if grid.len() > 1 {
1413        let ncols = grid[0].len();
1414        let nrows = grid.len();
1415        let result_grid: Vec<Vec<Value>> = (0..nrows)
1416            .map(|r| (0..ncols).map(|c| results[r * ncols + c].clone()).collect())
1417            .collect();
1418        from_2d(result_grid)
1419    } else {
1420        Value::Array(results)
1421    }
1422}
1423
1424// ── MAKEARRAY ─────────────────────────────────────────────────────────────────
1425
1426pub fn makearray_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1427    if let Some(e) = check_arity_len(args.len(), 3, 3) {
1428        return e;
1429    }
1430    let rows_val = evaluate_expr(&args[0], ctx);
1431    let cols_val = evaluate_expr(&args[1], ctx);
1432    if rows_val.is_error() {
1433        return rows_val;
1434    }
1435    if cols_val.is_error() {
1436        return cols_val;
1437    }
1438    let nrows = match to_f64(&rows_val) {
1439        Some(n) if n >= 1.0 => n as usize,
1440        _ => return Value::Error(ErrorKind::Value),
1441    };
1442    let ncols = match to_f64(&cols_val) {
1443        Some(n) if n >= 1.0 => n as usize,
1444        _ => return Value::Error(ErrorKind::Value),
1445    };
1446    let lambda_expr = &args[2];
1447    let mut grid: Vec<Vec<Value>> = Vec::with_capacity(nrows);
1448    for r in 1..=nrows {
1449        let mut row = Vec::with_capacity(ncols);
1450        for c in 1..=ncols {
1451            let rv = Value::Number(r as f64);
1452            let cv = Value::Number(c as f64);
1453            match apply_lambda(lambda_expr, &[rv, cv], ctx) {
1454                Some(Value::Array(_)) => return Value::Error(ErrorKind::Value),
1455                Some(v) => row.push(v),
1456                None => return Value::Error(ErrorKind::NA),
1457            }
1458        }
1459        grid.push(row);
1460    }
1461    if nrows == 1 && ncols == 1 {
1462        return grid[0][0].clone();
1463    }
1464    from_2d(grid)
1465}
1466
1467// ── Registration ─────────────────────────────────────────────────────────────
1468
1469
1470/// `ARRAYFORMULA(array_formula)` — evaluate an array formula.
1471///
1472/// Native operators (`+`, `*`, ...) already broadcast over `Value::Array`
1473/// operands via `eval_binary`, so a plain pass-through is correct for those.
1474/// But a normally-scalar function (`UPPER`, `LEN`, `ISNUMBER`, `IF`, ...)
1475/// either errors or collapses to its top-left element when given an array
1476/// argument outside of ARRAYFORMULA. Google Sheets forces per-cell
1477/// evaluation for exactly these cases, so `ARRAYFORMULA` broadcasts them
1478/// here instead of relying on the wrapped function's own (scalar) coercion.
1479pub fn arrayformula_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1480    if args.len() != 1 {
1481        return Value::Error(ErrorKind::NA);
1482    }
1483    broadcast_expr(&args[0], ctx)
1484}
1485
1486fn broadcast_expr(expr: &Expr, ctx: &mut EvalCtx<'_>) -> Value {
1487    match expr {
1488        // IF's condition already broadcasts correctly (e.g. `{1,2,3}>2`
1489        // evaluates to an array via `eval_binary`), but `if_fn` collapses
1490        // that array to its top-left element before branching. Re-run the
1491        // branch selection here per element instead.
1492        Expr::FunctionCall { name, args: if_args, .. }
1493            if name == "IF" && (if_args.len() == 2 || if_args.len() == 3) =>
1494        {
1495            let cond = evaluate_expr(&if_args[0], ctx);
1496            if !matches!(cond, Value::Array(_)) {
1497                return evaluate_expr(expr, ctx);
1498            }
1499            let true_val = evaluate_expr(&if_args[1], ctx);
1500            let false_val = if if_args.len() == 3 {
1501                evaluate_expr(&if_args[2], ctx)
1502            } else {
1503                Value::Bool(false)
1504            };
1505            broadcast_if(&cond, &true_val, &false_val)
1506        }
1507        // ISNUMBER is registered as a lazy fn (so it can inspect array
1508        // arguments for implicit-intersection outside ARRAYFORMULA), which
1509        // means it's invisible to the generic `FunctionKind::Eager` branch
1510        // below. Its scalar-check logic already exists as a plain eager fn
1511        // (`isnumber_fn`, used today only by unit tests) — reuse it here.
1512        Expr::FunctionCall { name, args: inner_args, .. }
1513            if name == "ISNUMBER" && inner_args.len() == 1 =>
1514        {
1515            let v = evaluate_expr(&inner_args[0], ctx);
1516            if v.is_error() {
1517                return v;
1518            }
1519            if matches!(v, Value::Array(_)) {
1520                broadcast_eager(super::logical::is_checks::isnumber_fn, &[v])
1521            } else {
1522                super::logical::is_checks::isnumber_fn(&[v])
1523            }
1524        }
1525        // Confirmed-broadcastable scalar functions only. Most eager
1526        // functions (SUM, MMULT, ...) legitimately want the whole array as
1527        // one argument — blanket-broadcasting any eager fn with an array
1528        // argument breaks those (e.g. `ARRAYFORMULA(SUM({1,2,3}))` must
1529        // stay `6`, not become `{1,2,3}`). Scope this to the specific
1530        // per-cell functions confirmed against live Google Sheets to need
1531        // element-wise broadcasting under ARRAYFORMULA.
1532        Expr::FunctionCall { name, args: inner_args, .. }
1533            if matches!(name.as_str(), "LEN" | "UPPER") =>
1534        {
1535            match ctx.registry.get(name) {
1536                Some(FunctionKind::Eager(f)) => {
1537                    let f: EagerFn = *f;
1538                    let mut evaluated = Vec::with_capacity(inner_args.len());
1539                    for a in inner_args {
1540                        let v = evaluate_expr(a, ctx);
1541                        if v.is_error() {
1542                            return v;
1543                        }
1544                        evaluated.push(v);
1545                    }
1546                    if evaluated.iter().any(|v| matches!(v, Value::Array(_))) {
1547                        broadcast_eager(f, &evaluated)
1548                    } else {
1549                        f(&evaluated)
1550                    }
1551                }
1552                _ => evaluate_expr(expr, ctx),
1553            }
1554        }
1555        _ => evaluate_expr(expr, ctx),
1556    }
1557}
1558
1559/// Returns the common (rows, cols) shape of every `Value::Array` in
1560/// `values`, or `None` if two arrays disagree on shape.
1561fn broadcast_shape(values: &[Value]) -> Option<(usize, usize)> {
1562    let mut shape = None;
1563    for v in values {
1564        if matches!(v, Value::Array(_)) {
1565            let grid = to_2d(v);
1566            let nr = grid.len();
1567            let nc = grid.first().map(Vec::len).unwrap_or(0);
1568            match shape {
1569                None => shape = Some((nr, nc)),
1570                Some((r, c)) if r == nr && c == nc => {}
1571                Some(_) => return None,
1572            }
1573        }
1574    }
1575    shape
1576}
1577
1578/// Calls an eager function once per element of a shared array shape,
1579/// broadcasting any scalar arguments across every position.
1580fn broadcast_eager(f: EagerFn, evaluated: &[Value]) -> Value {
1581    let (nrows, ncols) = match broadcast_shape(evaluated) {
1582        Some(s) => s,
1583        None => return Value::Error(ErrorKind::Value),
1584    };
1585    let grids: Vec<Option<Vec<Vec<Value>>>> = evaluated
1586        .iter()
1587        .map(|v| matches!(v, Value::Array(_)).then(|| to_2d(v)))
1588        .collect();
1589    let mut out = Vec::with_capacity(nrows);
1590    for r in 0..nrows {
1591        let mut row = Vec::with_capacity(ncols);
1592        for c in 0..ncols {
1593            let per_pos: Vec<Value> = evaluated
1594                .iter()
1595                .enumerate()
1596                .map(|(i, v)| match &grids[i] {
1597                    Some(g) => g[r][c].clone(),
1598                    None => v.clone(),
1599                })
1600                .collect();
1601            row.push(f(&per_pos));
1602        }
1603        out.push(row);
1604    }
1605    from_2d(out)
1606}
1607
1608/// Broadcasts `IF(cond, true_val, false_val)` element-wise over `cond`'s
1609/// shape, indexing into `true_val`/`false_val` at the same position when
1610/// they are themselves arrays, or reusing them as a scalar otherwise.
1611fn broadcast_if(cond: &Value, true_val: &Value, false_val: &Value) -> Value {
1612    let cond_grid = to_2d(cond);
1613    let true_grid = matches!(true_val, Value::Array(_)).then(|| to_2d(true_val));
1614    let false_grid = matches!(false_val, Value::Array(_)).then(|| to_2d(false_val));
1615    let nrows = cond_grid.len();
1616    let ncols = cond_grid.first().map(Vec::len).unwrap_or(0);
1617    let mut out = Vec::with_capacity(nrows);
1618    for (r, cond_row) in cond_grid.iter().enumerate() {
1619        let mut row = Vec::with_capacity(ncols);
1620        for (c, cond_cell) in cond_row.iter().enumerate() {
1621            let branch_val = match to_bool(cond_cell.clone()) {
1622                Ok(true) => match &true_grid {
1623                    Some(g) => g
1624                        .get(r)
1625                        .and_then(|row| row.get(c))
1626                        .cloned()
1627                        .unwrap_or(Value::Error(ErrorKind::Value)),
1628                    None => true_val.clone(),
1629                },
1630                Ok(false) => match &false_grid {
1631                    Some(g) => g
1632                        .get(r)
1633                        .and_then(|row| row.get(c))
1634                        .cloned()
1635                        .unwrap_or(Value::Error(ErrorKind::Value)),
1636                    None => false_val.clone(),
1637                },
1638                Err(e) => e,
1639            };
1640            row.push(branch_val);
1641        }
1642        out.push(row);
1643    }
1644    from_2d(out)
1645}
1646
1647pub fn register_array(registry: &mut Registry) {
1648    registry.register_eager("ROWS", rows_fn, FunctionMeta {
1649        category: "array",
1650        signature: "ROWS(array)",
1651        description: "Returns the number of rows in an array or range",
1652    });
1653    registry.register_eager("COLUMNS", columns_fn, FunctionMeta {
1654        category: "array",
1655        signature: "COLUMNS(array)",
1656        description: "Returns the number of columns in an array or range",
1657    });
1658    registry.register_eager("TRANSPOSE", transpose_fn, FunctionMeta {
1659        category: "array",
1660        signature: "TRANSPOSE(array)",
1661        description: "Transposes the rows and columns of an array",
1662    });
1663    registry.register_eager("ARRAY_CONSTRAIN", array_constrain_fn, FunctionMeta {
1664        category: "array",
1665        signature: "ARRAY_CONSTRAIN(input, num_rows, num_cols)",
1666        description: "Constrains an array to a given number of rows and columns",
1667    });
1668    registry.register_eager("CHOOSECOLS", choosecols_fn, FunctionMeta {
1669        category: "array",
1670        signature: "CHOOSECOLS(array, col_num1, ...)",
1671        description: "Returns selected columns from an array",
1672    });
1673    registry.register_eager("CHOOSEROWS", chooserows_fn, FunctionMeta {
1674        category: "array",
1675        signature: "CHOOSEROWS(array, row_num1, ...)",
1676        description: "Returns selected rows from an array",
1677    });
1678    registry.register_eager("FLATTEN", flatten_fn, FunctionMeta {
1679        category: "array",
1680        signature: "FLATTEN(array)",
1681        description: "Flattens an array into a single column",
1682    });
1683    registry.register_eager("HSTACK", hstack_fn, FunctionMeta {
1684        category: "array",
1685        signature: "HSTACK(array1, ...)",
1686        description: "Horizontally stacks arrays",
1687    });
1688    registry.register_eager("VSTACK", vstack_fn, FunctionMeta {
1689        category: "array",
1690        signature: "VSTACK(array1, ...)",
1691        description: "Vertically stacks arrays",
1692    });
1693    registry.register_eager("TOCOL", tocol_fn, FunctionMeta {
1694        category: "array",
1695        signature: "TOCOL(array, [ignore], [scan_by_col])",
1696        description: "Converts an array to a single column",
1697    });
1698    registry.register_eager("TOROW", torow_fn, FunctionMeta {
1699        category: "array",
1700        signature: "TOROW(array, [ignore], [scan_by_col])",
1701        description: "Converts an array to a single row",
1702    });
1703    registry.register_eager("WRAPCOLS", wrapcols_fn, FunctionMeta {
1704        category: "array",
1705        signature: "WRAPCOLS(vector, wrap_count, [pad_with])",
1706        description: "Wraps a vector into columns of the given length",
1707    });
1708    registry.register_eager("WRAPROWS", wraprows_fn, FunctionMeta {
1709        category: "array",
1710        signature: "WRAPROWS(vector, wrap_count, [pad_with])",
1711        description: "Wraps a vector into rows of the given length",
1712    });
1713    registry.register_eager("SORT", sort_fn, FunctionMeta {
1714        category: "array",
1715        signature: "SORT(array, [sort_index], [sort_order], [by_col])",
1716        description: "Sorts an array",
1717    });
1718    registry.register_eager("SORTBY", sortby_fn, FunctionMeta {
1719        category: "array",
1720        signature: "SORTBY(array, by_array1, [sort_order1], ...)",
1721        description: "Sorts an array based on the values in corresponding arrays",
1722    });
1723    registry.register_eager("UNIQUE", unique_fn, FunctionMeta {
1724        category: "array",
1725        signature: "UNIQUE(array, [by_col], [exactly_once])",
1726        description: "Returns unique rows or columns from an array",
1727    });
1728    registry.register_eager("SUMPRODUCT", sumproduct_fn, FunctionMeta {
1729        category: "array",
1730        signature: "SUMPRODUCT(array1, [array2], ...)",
1731        description: "Returns the sum of products of corresponding elements",
1732    });
1733    registry.register_eager("SUMXMY2", sumxmy2_fn, FunctionMeta {
1734        category: "array",
1735        signature: "SUMXMY2(array_x, array_y)",
1736        description: "Returns sum of squares of differences",
1737    });
1738    registry.register_eager("SUMX2MY2", sumx2my2_fn, FunctionMeta {
1739        category: "array",
1740        signature: "SUMX2MY2(array_x, array_y)",
1741        description: "Returns sum of (x^2 - y^2)",
1742    });
1743    registry.register_eager("SUMX2PY2", sumx2py2_fn, FunctionMeta {
1744        category: "array",
1745        signature: "SUMX2PY2(array_x, array_y)",
1746        description: "Returns sum of (x^2 + y^2)",
1747    });
1748    registry.register_eager("MMULT", mmult_fn, FunctionMeta {
1749        category: "array",
1750        signature: "MMULT(array1, array2)",
1751        description: "Returns the matrix product of two arrays",
1752    });
1753    registry.register_eager("MDETERM", mdeterm_fn, FunctionMeta {
1754        category: "array",
1755        signature: "MDETERM(array)",
1756        description: "Returns the matrix determinant",
1757    });
1758    registry.register_eager("MINVERSE", minverse_fn, FunctionMeta {
1759        category: "array",
1760        signature: "MINVERSE(array)",
1761        description: "Returns the matrix inverse",
1762    });
1763    registry.register_eager("FREQUENCY", frequency_fn, FunctionMeta {
1764        category: "array",
1765        signature: "FREQUENCY(data, bins)",
1766        description: "Calculates the frequency distribution of values",
1767    });
1768    registry.register_eager("LINEST", linest_fn, FunctionMeta {
1769        category: "array",
1770        signature: "LINEST(known_y, [known_x], [const], [stats])",
1771        description: "Returns linear regression statistics",
1772    });
1773    registry.register_eager("LOGEST", logest_fn, FunctionMeta {
1774        category: "array",
1775        signature: "LOGEST(known_y, [known_x], [const], [stats])",
1776        description: "Returns exponential regression statistics",
1777    });
1778    registry.register_eager("TREND", trend_fn, FunctionMeta {
1779        category: "array",
1780        signature: "TREND(known_y, [known_x], [new_x], [const])",
1781        description: "Returns values along a linear trend",
1782    });
1783    registry.register_eager("GROWTH", growth_fn, FunctionMeta {
1784        category: "array",
1785        signature: "GROWTH(known_y, [known_x], [new_x], [const])",
1786        description: "Returns values along an exponential trend",
1787    });
1788    registry.register_lazy("BYROW", byrow_lazy_fn, FunctionMeta {
1789        category: "array",
1790        signature: "BYROW(array, lambda)",
1791        description: "Applies a LAMBDA to each row of an array",
1792    });
1793    registry.register_lazy("BYCOL", bycol_lazy_fn, FunctionMeta {
1794        category: "array",
1795        signature: "BYCOL(array, lambda)",
1796        description: "Applies a LAMBDA to each column of an array",
1797    });
1798    registry.register_lazy("MAP", map_lazy_fn, FunctionMeta {
1799        category: "array",
1800        signature: "MAP(array1, [array2, ...], lambda)",
1801        description: "Maps a LAMBDA over one or more arrays",
1802    });
1803    registry.register_lazy("REDUCE", reduce_lazy_fn, FunctionMeta {
1804        category: "array",
1805        signature: "REDUCE(initial_value, array, lambda)",
1806        description: "Reduces an array to a single value using a LAMBDA",
1807    });
1808    registry.register_lazy("SCAN", scan_lazy_fn, FunctionMeta {
1809        category: "array",
1810        signature: "SCAN(initial_value, array, lambda)",
1811        description: "Returns running accumulation using a LAMBDA",
1812    });
1813    registry.register_lazy("MAKEARRAY", makearray_lazy_fn, FunctionMeta {
1814        category: "array",
1815        signature: "MAKEARRAY(rows, cols, lambda)",
1816        description: "Creates an array using a LAMBDA for each cell value",
1817    });
1818    registry.register_lazy("ARRAYFORMULA", arrayformula_lazy_fn, FunctionMeta {
1819        category: "array",
1820        signature: "ARRAYFORMULA(array_formula)",
1821        description: "Evaluates a formula as an array formula",
1822    });
1823}
1824
1825#[cfg(test)]
1826mod tests;