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