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, 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 = matches!(v, Value::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 = matches!(v, Value::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, ...
1170fn apply_lambda(lambda_expr: &Expr, bound_args: &[Value], ctx: &mut EvalCtx<'_>) -> Option<Value> {
1171    match lambda_expr {
1172        Expr::FunctionCall { name, args, .. } if name == "LAMBDA" => {
1173            if args.is_empty() {
1174                return None;
1175            }
1176            let body = &args[args.len() - 1];
1177            let params = &args[..args.len() - 1];
1178            if params.len() != bound_args.len() {
1179                return None;
1180            }
1181            // Bind each parameter in context
1182            let mut saved: Vec<(String, Value)> = Vec::new();
1183            for (param_expr, val) in params.iter().zip(bound_args.iter()) {
1184                if let Expr::Variable(name, _) = param_expr {
1185                    let old = ctx.ctx.get(name);
1186                    saved.push((name.clone(), old));
1187                    ctx.ctx.set(name.clone(), val.clone());
1188                } else {
1189                    return None;
1190                }
1191            }
1192            let result = evaluate_expr(body, ctx);
1193            // Restore context
1194            for (name, old_val) in saved {
1195                ctx.ctx.set(name, old_val);
1196            }
1197            Some(result)
1198        }
1199        _ => None,
1200    }
1201}
1202
1203// ── BYROW ─────────────────────────────────────────────────────────────────────
1204
1205pub fn byrow_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1206    if let Some(e) = check_arity_len(args.len(), 2, 2) {
1207        return e;
1208    }
1209    let arr_val = evaluate_expr(&args[0], ctx);
1210    if matches!(arr_val, Value::Error(_)) {
1211        return arr_val;
1212    }
1213    let grid = to_2d(&arr_val);
1214    let lambda_expr = &args[1];
1215    let mut results: Vec<Value> = Vec::with_capacity(grid.len());
1216    for row in &grid {
1217        let row_val = Value::Array(row.clone());
1218        match apply_lambda(lambda_expr, &[row_val], ctx) {
1219            Some(v) => results.push(v),
1220            None => return Value::Error(ErrorKind::NA),
1221        }
1222    }
1223    // Return as column vector (one result per row)
1224    let col: Vec<Vec<Value>> = results.into_iter().map(|v| vec![v]).collect();
1225    from_2d(col)
1226}
1227
1228// ── BYCOL ─────────────────────────────────────────────────────────────────────
1229
1230pub fn bycol_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1231    if let Some(e) = check_arity_len(args.len(), 2, 2) {
1232        return e;
1233    }
1234    let arr_val = evaluate_expr(&args[0], ctx);
1235    if matches!(arr_val, Value::Error(_)) {
1236        return arr_val;
1237    }
1238    let grid = to_2d(&arr_val);
1239    let ncols = grid.first().map(|r| r.len()).unwrap_or(0);
1240    // Build columns first to avoid range-loop indexing
1241    let columns: Vec<Vec<Value>> = (0..ncols)
1242        .map(|c| grid.iter().map(|row| row[c].clone()).collect())
1243        .collect();
1244    let lambda_expr = &args[1];
1245    let mut results: Vec<Value> = Vec::with_capacity(ncols);
1246    for col in columns {
1247        // Pass flat array so SUM/MAX/MIN etc can iterate over elements
1248        let col_val = Value::Array(col);
1249        match apply_lambda(lambda_expr, &[col_val], ctx) {
1250            Some(v) => results.push(v),
1251            None => return Value::Error(ErrorKind::NA),
1252        }
1253    }
1254    // Return as row vector (one result per col)
1255    Value::Array(results)
1256}
1257
1258// ── MAP ───────────────────────────────────────────────────────────────────────
1259
1260pub fn map_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1261    if let Some(e) = check_arity_len(args.len(), 2, usize::MAX) {
1262        return e;
1263    }
1264    // Last arg is LAMBDA, all prior are arrays
1265    let lambda_expr = &args[args.len() - 1];
1266    let arr_count = args.len() - 1;
1267    let arrays: Vec<Vec<Value>> = args[..arr_count]
1268        .iter()
1269        .map(|a| {
1270            let v = evaluate_expr(a, ctx);
1271            flatten_val(&v)
1272        })
1273        .collect();
1274    let len = arrays[0].len();
1275    for arr in &arrays[1..] {
1276        if arr.len() != len {
1277            return Value::Error(ErrorKind::Value);
1278        }
1279    }
1280    let mut results: Vec<Value> = Vec::with_capacity(len);
1281    for i in 0..len {
1282        let bound: Vec<Value> = arrays.iter().map(|a| a[i].clone()).collect();
1283        match apply_lambda(lambda_expr, &bound, ctx) {
1284            Some(v) => results.push(v),
1285            None => return Value::Error(ErrorKind::NA),
1286        }
1287    }
1288    // Preserve shape of first array
1289    let first_grid = to_2d(&evaluate_expr(&args[0], ctx));
1290    if first_grid.len() > 1 {
1291        // 2D → reshape results
1292        let ncols = first_grid[0].len();
1293        let nrows = first_grid.len();
1294        let grid: Vec<Vec<Value>> = (0..nrows)
1295            .map(|r| (0..ncols).map(|c| results[r * ncols + c].clone()).collect())
1296            .collect();
1297        from_2d(grid)
1298    } else {
1299        Value::Array(results)
1300    }
1301}
1302
1303// ── REDUCE ────────────────────────────────────────────────────────────────────
1304
1305pub fn reduce_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1306    if let Some(e) = check_arity_len(args.len(), 3, 3) {
1307        return e;
1308    }
1309    let initial = evaluate_expr(&args[0], ctx);
1310    if matches!(initial, Value::Error(_)) {
1311        return initial;
1312    }
1313    let arr_val = evaluate_expr(&args[1], ctx);
1314    if matches!(arr_val, Value::Error(_)) {
1315        return arr_val;
1316    }
1317    let items = flatten_val(&arr_val);
1318    if items.is_empty() {
1319        return Value::Error(ErrorKind::Ref);
1320    }
1321    let lambda_expr = &args[2];
1322    let mut acc = initial;
1323    for item in &items {
1324        match apply_lambda(lambda_expr, &[acc.clone(), item.clone()], ctx) {
1325            Some(v) => acc = v,
1326            None => return Value::Error(ErrorKind::NA),
1327        }
1328    }
1329    acc
1330}
1331
1332// ── SCAN ──────────────────────────────────────────────────────────────────────
1333
1334pub fn scan_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1335    if let Some(e) = check_arity_len(args.len(), 3, 3) {
1336        return e;
1337    }
1338    let initial = evaluate_expr(&args[0], ctx);
1339    if matches!(initial, Value::Error(_)) {
1340        return initial;
1341    }
1342    let arr_val = evaluate_expr(&args[1], ctx);
1343    if matches!(arr_val, Value::Error(_)) {
1344        return arr_val;
1345    }
1346    let grid = to_2d(&arr_val);
1347    let items = flatten_val(&arr_val);
1348    let lambda_expr = &args[2];
1349    let mut acc = initial;
1350    let mut results: Vec<Value> = Vec::with_capacity(items.len());
1351    for item in &items {
1352        match apply_lambda(lambda_expr, &[acc.clone(), item.clone()], ctx) {
1353            Some(v) => {
1354                acc = v.clone();
1355                results.push(v);
1356            }
1357            None => return Value::Error(ErrorKind::NA),
1358        }
1359    }
1360    // Preserve shape of input array
1361    if grid.len() > 1 {
1362        let ncols = grid[0].len();
1363        let nrows = grid.len();
1364        let result_grid: Vec<Vec<Value>> = (0..nrows)
1365            .map(|r| (0..ncols).map(|c| results[r * ncols + c].clone()).collect())
1366            .collect();
1367        from_2d(result_grid)
1368    } else {
1369        Value::Array(results)
1370    }
1371}
1372
1373// ── MAKEARRAY ─────────────────────────────────────────────────────────────────
1374
1375pub fn makearray_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1376    if let Some(e) = check_arity_len(args.len(), 3, 3) {
1377        return e;
1378    }
1379    let rows_val = evaluate_expr(&args[0], ctx);
1380    let cols_val = evaluate_expr(&args[1], ctx);
1381    if matches!(rows_val, Value::Error(_)) {
1382        return rows_val;
1383    }
1384    if matches!(cols_val, Value::Error(_)) {
1385        return cols_val;
1386    }
1387    let nrows = match to_f64(&rows_val) {
1388        Some(n) if n >= 1.0 => n as usize,
1389        _ => return Value::Error(ErrorKind::Value),
1390    };
1391    let ncols = match to_f64(&cols_val) {
1392        Some(n) if n >= 1.0 => n as usize,
1393        _ => return Value::Error(ErrorKind::Value),
1394    };
1395    let lambda_expr = &args[2];
1396    let mut grid: Vec<Vec<Value>> = Vec::with_capacity(nrows);
1397    for r in 1..=nrows {
1398        let mut row = Vec::with_capacity(ncols);
1399        for c in 1..=ncols {
1400            let rv = Value::Number(r as f64);
1401            let cv = Value::Number(c as f64);
1402            match apply_lambda(lambda_expr, &[rv, cv], ctx) {
1403                Some(Value::Array(_)) => return Value::Error(ErrorKind::Value),
1404                Some(v) => row.push(v),
1405                None => return Value::Error(ErrorKind::NA),
1406            }
1407        }
1408        grid.push(row);
1409    }
1410    if nrows == 1 && ncols == 1 {
1411        return grid[0][0].clone();
1412    }
1413    from_2d(grid)
1414}
1415
1416// ── Registration ─────────────────────────────────────────────────────────────
1417
1418
1419/// `ARRAYFORMULA(array_formula)` — evaluate an array formula.
1420///
1421/// Native operators (`+`, `*`, ...) already broadcast over `Value::Array`
1422/// operands via `eval_binary`, so a plain pass-through is correct for those.
1423/// But a normally-scalar function (`UPPER`, `LEN`, `ISNUMBER`, `IF`, ...)
1424/// either errors or collapses to its top-left element when given an array
1425/// argument outside of ARRAYFORMULA. Google Sheets forces per-cell
1426/// evaluation for exactly these cases, so `ARRAYFORMULA` broadcasts them
1427/// here instead of relying on the wrapped function's own (scalar) coercion.
1428pub fn arrayformula_lazy_fn(args: &[Expr], ctx: &mut EvalCtx<'_>) -> Value {
1429    if args.len() != 1 {
1430        return Value::Error(ErrorKind::NA);
1431    }
1432    broadcast_expr(&args[0], ctx)
1433}
1434
1435fn broadcast_expr(expr: &Expr, ctx: &mut EvalCtx<'_>) -> Value {
1436    match expr {
1437        // IF's condition already broadcasts correctly (e.g. `{1,2,3}>2`
1438        // evaluates to an array via `eval_binary`), but `if_fn` collapses
1439        // that array to its top-left element before branching. Re-run the
1440        // branch selection here per element instead.
1441        Expr::FunctionCall { name, args: if_args, .. }
1442            if name == "IF" && (if_args.len() == 2 || if_args.len() == 3) =>
1443        {
1444            let cond = evaluate_expr(&if_args[0], ctx);
1445            if !matches!(cond, Value::Array(_)) {
1446                return evaluate_expr(expr, ctx);
1447            }
1448            let true_val = evaluate_expr(&if_args[1], ctx);
1449            let false_val = if if_args.len() == 3 {
1450                evaluate_expr(&if_args[2], ctx)
1451            } else {
1452                Value::Bool(false)
1453            };
1454            broadcast_if(&cond, &true_val, &false_val)
1455        }
1456        // ISNUMBER is registered as a lazy fn (so it can inspect array
1457        // arguments for implicit-intersection outside ARRAYFORMULA), which
1458        // means it's invisible to the generic `FunctionKind::Eager` branch
1459        // below. Its scalar-check logic already exists as a plain eager fn
1460        // (`isnumber_fn`, used today only by unit tests) — reuse it here.
1461        Expr::FunctionCall { name, args: inner_args, .. }
1462            if name == "ISNUMBER" && inner_args.len() == 1 =>
1463        {
1464            let v = evaluate_expr(&inner_args[0], ctx);
1465            if matches!(v, Value::Error(_)) {
1466                return v;
1467            }
1468            if matches!(v, Value::Array(_)) {
1469                broadcast_eager(super::logical::is_checks::isnumber_fn, &[v])
1470            } else {
1471                super::logical::is_checks::isnumber_fn(&[v])
1472            }
1473        }
1474        // Confirmed-broadcastable scalar functions only. Most eager
1475        // functions (SUM, MMULT, ...) legitimately want the whole array as
1476        // one argument — blanket-broadcasting any eager fn with an array
1477        // argument breaks those (e.g. `ARRAYFORMULA(SUM({1,2,3}))` must
1478        // stay `6`, not become `{1,2,3}`). Scope this to the specific
1479        // per-cell functions confirmed against live Google Sheets to need
1480        // element-wise broadcasting under ARRAYFORMULA.
1481        Expr::FunctionCall { name, args: inner_args, .. }
1482            if matches!(name.as_str(), "LEN" | "UPPER") =>
1483        {
1484            match ctx.registry.get(name) {
1485                Some(FunctionKind::Eager(f)) => {
1486                    let f: EagerFn = *f;
1487                    let mut evaluated = Vec::with_capacity(inner_args.len());
1488                    for a in inner_args {
1489                        let v = evaluate_expr(a, ctx);
1490                        if matches!(v, Value::Error(_)) {
1491                            return v;
1492                        }
1493                        evaluated.push(v);
1494                    }
1495                    if evaluated.iter().any(|v| matches!(v, Value::Array(_))) {
1496                        broadcast_eager(f, &evaluated)
1497                    } else {
1498                        f(&evaluated)
1499                    }
1500                }
1501                _ => evaluate_expr(expr, ctx),
1502            }
1503        }
1504        _ => evaluate_expr(expr, ctx),
1505    }
1506}
1507
1508/// Returns the common (rows, cols) shape of every `Value::Array` in
1509/// `values`, or `None` if two arrays disagree on shape.
1510fn broadcast_shape(values: &[Value]) -> Option<(usize, usize)> {
1511    let mut shape = None;
1512    for v in values {
1513        if matches!(v, Value::Array(_)) {
1514            let grid = to_2d(v);
1515            let nr = grid.len();
1516            let nc = grid.first().map(Vec::len).unwrap_or(0);
1517            match shape {
1518                None => shape = Some((nr, nc)),
1519                Some((r, c)) if r == nr && c == nc => {}
1520                Some(_) => return None,
1521            }
1522        }
1523    }
1524    shape
1525}
1526
1527/// Calls an eager function once per element of a shared array shape,
1528/// broadcasting any scalar arguments across every position.
1529fn broadcast_eager(f: EagerFn, evaluated: &[Value]) -> Value {
1530    let (nrows, ncols) = match broadcast_shape(evaluated) {
1531        Some(s) => s,
1532        None => return Value::Error(ErrorKind::Value),
1533    };
1534    let grids: Vec<Option<Vec<Vec<Value>>>> = evaluated
1535        .iter()
1536        .map(|v| matches!(v, Value::Array(_)).then(|| to_2d(v)))
1537        .collect();
1538    let mut out = Vec::with_capacity(nrows);
1539    for r in 0..nrows {
1540        let mut row = Vec::with_capacity(ncols);
1541        for c in 0..ncols {
1542            let per_pos: Vec<Value> = evaluated
1543                .iter()
1544                .enumerate()
1545                .map(|(i, v)| match &grids[i] {
1546                    Some(g) => g[r][c].clone(),
1547                    None => v.clone(),
1548                })
1549                .collect();
1550            row.push(f(&per_pos));
1551        }
1552        out.push(row);
1553    }
1554    from_2d(out)
1555}
1556
1557/// Broadcasts `IF(cond, true_val, false_val)` element-wise over `cond`'s
1558/// shape, indexing into `true_val`/`false_val` at the same position when
1559/// they are themselves arrays, or reusing them as a scalar otherwise.
1560fn broadcast_if(cond: &Value, true_val: &Value, false_val: &Value) -> Value {
1561    let cond_grid = to_2d(cond);
1562    let true_grid = matches!(true_val, Value::Array(_)).then(|| to_2d(true_val));
1563    let false_grid = matches!(false_val, Value::Array(_)).then(|| to_2d(false_val));
1564    let nrows = cond_grid.len();
1565    let ncols = cond_grid.first().map(Vec::len).unwrap_or(0);
1566    let mut out = Vec::with_capacity(nrows);
1567    for (r, cond_row) in cond_grid.iter().enumerate() {
1568        let mut row = Vec::with_capacity(ncols);
1569        for (c, cond_cell) in cond_row.iter().enumerate() {
1570            let branch_val = match to_bool(cond_cell.clone()) {
1571                Ok(true) => match &true_grid {
1572                    Some(g) => g
1573                        .get(r)
1574                        .and_then(|row| row.get(c))
1575                        .cloned()
1576                        .unwrap_or(Value::Error(ErrorKind::Value)),
1577                    None => true_val.clone(),
1578                },
1579                Ok(false) => match &false_grid {
1580                    Some(g) => g
1581                        .get(r)
1582                        .and_then(|row| row.get(c))
1583                        .cloned()
1584                        .unwrap_or(Value::Error(ErrorKind::Value)),
1585                    None => false_val.clone(),
1586                },
1587                Err(e) => e,
1588            };
1589            row.push(branch_val);
1590        }
1591        out.push(row);
1592    }
1593    from_2d(out)
1594}
1595
1596pub fn register_array(registry: &mut Registry) {
1597    registry.register_eager("ROWS", rows_fn, FunctionMeta {
1598        category: "array",
1599        signature: "ROWS(array)",
1600        description: "Returns the number of rows in an array or range",
1601    });
1602    registry.register_eager("COLUMNS", columns_fn, FunctionMeta {
1603        category: "array",
1604        signature: "COLUMNS(array)",
1605        description: "Returns the number of columns in an array or range",
1606    });
1607    registry.register_eager("TRANSPOSE", transpose_fn, FunctionMeta {
1608        category: "array",
1609        signature: "TRANSPOSE(array)",
1610        description: "Transposes the rows and columns of an array",
1611    });
1612    registry.register_eager("ARRAY_CONSTRAIN", array_constrain_fn, FunctionMeta {
1613        category: "array",
1614        signature: "ARRAY_CONSTRAIN(input, num_rows, num_cols)",
1615        description: "Constrains an array to a given number of rows and columns",
1616    });
1617    registry.register_eager("CHOOSECOLS", choosecols_fn, FunctionMeta {
1618        category: "array",
1619        signature: "CHOOSECOLS(array, col_num1, ...)",
1620        description: "Returns selected columns from an array",
1621    });
1622    registry.register_eager("CHOOSEROWS", chooserows_fn, FunctionMeta {
1623        category: "array",
1624        signature: "CHOOSEROWS(array, row_num1, ...)",
1625        description: "Returns selected rows from an array",
1626    });
1627    registry.register_eager("FLATTEN", flatten_fn, FunctionMeta {
1628        category: "array",
1629        signature: "FLATTEN(array)",
1630        description: "Flattens an array into a single column",
1631    });
1632    registry.register_eager("HSTACK", hstack_fn, FunctionMeta {
1633        category: "array",
1634        signature: "HSTACK(array1, ...)",
1635        description: "Horizontally stacks arrays",
1636    });
1637    registry.register_eager("VSTACK", vstack_fn, FunctionMeta {
1638        category: "array",
1639        signature: "VSTACK(array1, ...)",
1640        description: "Vertically stacks arrays",
1641    });
1642    registry.register_eager("TOCOL", tocol_fn, FunctionMeta {
1643        category: "array",
1644        signature: "TOCOL(array, [ignore], [scan_by_col])",
1645        description: "Converts an array to a single column",
1646    });
1647    registry.register_eager("TOROW", torow_fn, FunctionMeta {
1648        category: "array",
1649        signature: "TOROW(array, [ignore], [scan_by_col])",
1650        description: "Converts an array to a single row",
1651    });
1652    registry.register_eager("WRAPCOLS", wrapcols_fn, FunctionMeta {
1653        category: "array",
1654        signature: "WRAPCOLS(vector, wrap_count, [pad_with])",
1655        description: "Wraps a vector into columns of the given length",
1656    });
1657    registry.register_eager("WRAPROWS", wraprows_fn, FunctionMeta {
1658        category: "array",
1659        signature: "WRAPROWS(vector, wrap_count, [pad_with])",
1660        description: "Wraps a vector into rows of the given length",
1661    });
1662    registry.register_eager("SORT", sort_fn, FunctionMeta {
1663        category: "array",
1664        signature: "SORT(array, [sort_index], [sort_order], [by_col])",
1665        description: "Sorts an array",
1666    });
1667    registry.register_eager("SORTBY", sortby_fn, FunctionMeta {
1668        category: "array",
1669        signature: "SORTBY(array, by_array1, [sort_order1], ...)",
1670        description: "Sorts an array based on the values in corresponding arrays",
1671    });
1672    registry.register_eager("UNIQUE", unique_fn, FunctionMeta {
1673        category: "array",
1674        signature: "UNIQUE(array, [by_col], [exactly_once])",
1675        description: "Returns unique rows or columns from an array",
1676    });
1677    registry.register_eager("SUMPRODUCT", sumproduct_fn, FunctionMeta {
1678        category: "array",
1679        signature: "SUMPRODUCT(array1, [array2], ...)",
1680        description: "Returns the sum of products of corresponding elements",
1681    });
1682    registry.register_eager("SUMXMY2", sumxmy2_fn, FunctionMeta {
1683        category: "array",
1684        signature: "SUMXMY2(array_x, array_y)",
1685        description: "Returns sum of squares of differences",
1686    });
1687    registry.register_eager("SUMX2MY2", sumx2my2_fn, FunctionMeta {
1688        category: "array",
1689        signature: "SUMX2MY2(array_x, array_y)",
1690        description: "Returns sum of (x^2 - y^2)",
1691    });
1692    registry.register_eager("SUMX2PY2", sumx2py2_fn, FunctionMeta {
1693        category: "array",
1694        signature: "SUMX2PY2(array_x, array_y)",
1695        description: "Returns sum of (x^2 + y^2)",
1696    });
1697    registry.register_eager("MMULT", mmult_fn, FunctionMeta {
1698        category: "array",
1699        signature: "MMULT(array1, array2)",
1700        description: "Returns the matrix product of two arrays",
1701    });
1702    registry.register_eager("MDETERM", mdeterm_fn, FunctionMeta {
1703        category: "array",
1704        signature: "MDETERM(array)",
1705        description: "Returns the matrix determinant",
1706    });
1707    registry.register_eager("MINVERSE", minverse_fn, FunctionMeta {
1708        category: "array",
1709        signature: "MINVERSE(array)",
1710        description: "Returns the matrix inverse",
1711    });
1712    registry.register_eager("FREQUENCY", frequency_fn, FunctionMeta {
1713        category: "array",
1714        signature: "FREQUENCY(data, bins)",
1715        description: "Calculates the frequency distribution of values",
1716    });
1717    registry.register_eager("LINEST", linest_fn, FunctionMeta {
1718        category: "array",
1719        signature: "LINEST(known_y, [known_x], [const], [stats])",
1720        description: "Returns linear regression statistics",
1721    });
1722    registry.register_eager("LOGEST", logest_fn, FunctionMeta {
1723        category: "array",
1724        signature: "LOGEST(known_y, [known_x], [const], [stats])",
1725        description: "Returns exponential regression statistics",
1726    });
1727    registry.register_eager("TREND", trend_fn, FunctionMeta {
1728        category: "array",
1729        signature: "TREND(known_y, [known_x], [new_x], [const])",
1730        description: "Returns values along a linear trend",
1731    });
1732    registry.register_eager("GROWTH", growth_fn, FunctionMeta {
1733        category: "array",
1734        signature: "GROWTH(known_y, [known_x], [new_x], [const])",
1735        description: "Returns values along an exponential trend",
1736    });
1737    registry.register_lazy("BYROW", byrow_lazy_fn, FunctionMeta {
1738        category: "array",
1739        signature: "BYROW(array, lambda)",
1740        description: "Applies a LAMBDA to each row of an array",
1741    });
1742    registry.register_lazy("BYCOL", bycol_lazy_fn, FunctionMeta {
1743        category: "array",
1744        signature: "BYCOL(array, lambda)",
1745        description: "Applies a LAMBDA to each column of an array",
1746    });
1747    registry.register_lazy("MAP", map_lazy_fn, FunctionMeta {
1748        category: "array",
1749        signature: "MAP(array1, [array2, ...], lambda)",
1750        description: "Maps a LAMBDA over one or more arrays",
1751    });
1752    registry.register_lazy("REDUCE", reduce_lazy_fn, FunctionMeta {
1753        category: "array",
1754        signature: "REDUCE(initial_value, array, lambda)",
1755        description: "Reduces an array to a single value using a LAMBDA",
1756    });
1757    registry.register_lazy("SCAN", scan_lazy_fn, FunctionMeta {
1758        category: "array",
1759        signature: "SCAN(initial_value, array, lambda)",
1760        description: "Returns running accumulation using a LAMBDA",
1761    });
1762    registry.register_lazy("MAKEARRAY", makearray_lazy_fn, FunctionMeta {
1763        category: "array",
1764        signature: "MAKEARRAY(rows, cols, lambda)",
1765        description: "Creates an array using a LAMBDA for each cell value",
1766    });
1767    registry.register_lazy("ARRAYFORMULA", arrayformula_lazy_fn, FunctionMeta {
1768        category: "array",
1769        signature: "ARRAYFORMULA(array_formula)",
1770        description: "Evaluates a formula as an array formula",
1771    });
1772}
1773
1774#[cfg(test)]
1775mod tests;