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